Bug Fixes to TADC Main and TADC Cargo Systems

This commit is contained in:
iTracerFacer 2025-10-19 15:47:22 -05:00
parent a3da2a6b8b
commit ed662a6289
8 changed files with 454 additions and 77 deletions

View File

@ -133,6 +133,20 @@ log("[DEBUG] The Lakes zone initialization complete")
-- Helper functions for tactical information -- Helper functions for tactical information
-- Global cached unit set - created once and maintained automatically by MOOSE
local CachedUnitSet = nil
-- Initialize the cached unit set once
local function InitializeCachedUnitSet()
if not CachedUnitSet then
CachedUnitSet = SET_UNIT:New()
:FilterCategories({"ground", "plane", "helicopter"}) -- Only scan relevant unit types
:FilterOnce() -- Don't filter continuously, we'll use the live set
log("[PERFORMANCE] Initialized cached unit set for zone scanning")
end
end
local function GetZoneForceStrengths(ZoneCapture) local function GetZoneForceStrengths(ZoneCapture)
local zone = ZoneCapture:GetZone() local zone = ZoneCapture:GetZone()
if not zone then return {red = 0, blue = 0, neutral = 0} end if not zone then return {red = 0, blue = 0, neutral = 0} end
@ -141,15 +155,15 @@ local function GetZoneForceStrengths(ZoneCapture)
local blueCount = 0 local blueCount = 0
local neutralCount = 0 local neutralCount = 0
-- Simple approach: scan all units and check if they're in the zone -- Use MOOSE's optimized zone scanning instead of manual distance checks
local coord = zone:GetCoordinate() local success, scannedUnits = pcall(function()
local radius = zone:GetRadius() or 1000 return zone:GetScannedUnits()
end)
local allUnits = SET_UNIT:New():FilterStart() if success and scannedUnits then
allUnits:ForEachUnit(function(unit) -- Use MOOSE's built-in scanned units (much faster)
if unit and unit:IsAlive() then for _, unit in pairs(scannedUnits) do
local unitCoord = unit:GetCoordinate() if unit and unit:IsAlive() then
if unitCoord and coord:Get2DDistance(unitCoord) <= radius then
local unitCoalition = unit:GetCoalition() local unitCoalition = unit:GetCoalition()
if unitCoalition == coalition.side.RED then if unitCoalition == coalition.side.RED then
redCount = redCount + 1 redCount = redCount + 1
@ -160,7 +174,32 @@ local function GetZoneForceStrengths(ZoneCapture)
end end
end end
end end
end) else
-- Fallback: Use zone's built-in scanning with the cached set
InitializeCachedUnitSet()
-- Use zone radius to limit search area
local coord = zone:GetCoordinate()
local radius = zone:GetRadius() or 1000
-- Only scan units within a reasonable distance of the zone
local nearbyUnits = coord:ScanUnits(radius)
if nearbyUnits then
for _, unitData in pairs(nearbyUnits) do
local unit = unitData -- ScanUnits returns unit objects
if unit and type(unit.IsAlive) == "function" and unit:IsAlive() then
local unitCoalition = unit:GetCoalition()
if unitCoalition == coalition.side.RED then
redCount = redCount + 1
elseif unitCoalition == coalition.side.BLUE then
blueCount = blueCount + 1
elseif unitCoalition == coalition.side.NEUTRAL then
neutralCount = neutralCount + 1
end
end
end
end
end
log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d", log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d",
ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount)) ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount))
@ -179,62 +218,57 @@ local function GetRedUnitMGRSCoords(ZoneCapture)
local coords = {} local coords = {}
local units = nil local units = nil
-- Try multiple methods to get units (same as GetZoneForceStrengths) -- Optimized: Try MOOSE's built-in zone scanning first (fastest method)
local success1 = pcall(function() local success1 = pcall(function()
units = zone:GetScannedUnits() units = zone:GetScannedUnits()
end) end)
-- Fallback: Use coordinate-based scanning (much faster than SET_UNIT filtering)
if not success1 or not units then if not success1 or not units then
local coord = zone:GetCoordinate()
local radius = zone:GetRadius() or 1000
local success2 = pcall(function() local success2 = pcall(function()
local unitSet = SET_UNIT:New():FilterZones({zone}):FilterStart() units = coord:ScanUnits(radius)
units = {}
unitSet:ForEachUnit(function(unit)
if unit then
units[unit:GetName()] = unit
end
end)
end) end)
-- Last resort: Manual zone check with cached unit set
if not success2 or not units then
InitializeCachedUnitSet()
units = {}
if CachedUnitSet then
CachedUnitSet:ForEachUnit(function(unit)
if unit and unit:IsAlive() and unit:IsInZone(zone) then
units[unit:GetName()] = unit
end
end)
end
end
end end
if not units then -- Extract RED unit coordinates with optimized error handling
local success3 = pcall(function()
units = {}
local unitSet = SET_UNIT:New():FilterZones({zone}):FilterStart()
unitSet:ForEachUnit(function(unit)
if unit and unit:IsInZone(zone) then
units[unit:GetName()] = unit
end
end)
end)
end
-- Extract RED unit coordinates
if units then if units then
for unitName, unit in pairs(units) do for unitName, unit in pairs(units) do
-- Enhanced nil checking and safe method calls -- Streamlined nil checking
if unit and type(unit) == "table" and unit.IsAlive then if unit and type(unit) == "table" then
local isAlive = false local success, isAlive = pcall(function() return unit:IsAlive() end)
local success_alive = pcall(function()
isAlive = unit:IsAlive()
end)
if success_alive and isAlive then if success and isAlive then
local coalition_side = nil local success_coalition, coalition_side = pcall(function() return unit:GetCoalition() end)
local success_coalition = pcall(function()
coalition_side = unit:GetCoalition()
end)
if success_coalition and coalition_side == coalition.side.RED then if success_coalition and coalition_side == coalition.side.RED then
local coord = unit:GetCoordinate() local success_coord, coord = pcall(function() return unit:GetCoordinate() end)
if coord then
local success, mgrs = pcall(function() if success_coord and coord then
local success_mgrs, mgrs = pcall(function()
return coord:ToStringMGRS(5) -- 5-digit precision return coord:ToStringMGRS(5) -- 5-digit precision
end) end)
if success and mgrs then if success_mgrs and mgrs then
local success_type, unitType = pcall(function() return unit:GetTypeName() end)
table.insert(coords, { table.insert(coords, {
name = unit:GetName(), name = unit:GetName(),
type = unit:GetTypeName(), type = success_type and unitType or "Unknown",
mgrs = mgrs mgrs = mgrs
}) })
end end
@ -673,21 +707,37 @@ end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minute
local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
log("[ZONE COLORS] Running periodic zone color verification...") log("[ZONE COLORS] Running periodic zone color verification...")
-- Verify each zone's visual marker matches its coalition -- Verify each zone's visual marker matches its CURRENT STATE (not just coalition)
for i, zoneCapture in ipairs(zoneCaptureObjects) do for i, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture then if zoneCapture then
local zoneCoalition = zoneCapture:GetCoalition() local zoneCoalition = zoneCapture:GetCoalition()
local zoneName = zoneNames[i] or ("Zone " .. i) local zoneName = zoneNames[i] or ("Zone " .. i)
local currentState = zoneCapture:GetCurrentState()
-- Force redraw the zone with correct color (helps prevent desync issues) -- Force redraw the zone with correct color based on CURRENT STATE
zoneCapture:UndrawZone() zoneCapture:UndrawZone()
if zoneCoalition == coalition.side.BLUE then -- Color priority: State (Attacked/Empty) overrides coalition ownership
zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true) -- Blue if currentState == "Attacked" then
-- Orange for contested zones (highest priority)
zoneCapture:DrawZone(-1, {1, 0.5, 0}, 0.5, {1, 0.5, 0}, 0.2, 2, true)
log(string.format("[ZONE COLORS] %s: Set to ORANGE (Attacked)", zoneName))
elseif currentState == "Empty" then
-- Green for neutral/empty zones
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true)
log(string.format("[ZONE COLORS] %s: Set to GREEN (Empty)", zoneName))
elseif zoneCoalition == coalition.side.BLUE then
-- Blue for BLUE-owned zones (Guarded or Captured state)
zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true)
log(string.format("[ZONE COLORS] %s: Set to BLUE (Owned)", zoneName))
elseif zoneCoalition == coalition.side.RED then elseif zoneCoalition == coalition.side.RED then
zoneCapture:DrawZone(-1, {1, 0, 0}, 0.5, {1, 0, 0}, 0.2, 2, true) -- Red -- Red for RED-owned zones (Guarded or Captured state)
zoneCapture:DrawZone(-1, {1, 0, 0}, 0.5, {1, 0, 0}, 0.2, 2, true)
log(string.format("[ZONE COLORS] %s: Set to RED (Owned)", zoneName))
else else
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true) -- Green (neutral) -- Fallback to green for any other state
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true)
log(string.format("[ZONE COLORS] %s: Set to GREEN (Fallback)", zoneName))
end end
end end
end end
@ -713,22 +763,29 @@ local function RefreshAllZoneColors()
for i, zoneCapture in ipairs(zoneCaptureObjects) do for i, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture then if zoneCapture then
local coalition = zoneCapture:GetCoalition() local zoneCoalition = zoneCapture:GetCoalition()
local zoneName = zoneNames[i] or ("Zone " .. i) local zoneName = zoneNames[i] or ("Zone " .. i)
local currentState = zoneCapture:GetCurrentState()
-- Clear existing drawings -- Clear existing drawings
zoneCapture:UndrawZone() zoneCapture:UndrawZone()
-- Redraw with correct color based on current coalition -- Redraw with correct color based on CURRENT STATE (priority over coalition)
if coalition == coalition.side.BLUE then if currentState == "Attacked" then
zoneCapture:DrawZone(-1, {1, 0.5, 0}, 0.5, {1, 0.5, 0}, 0.2, 2, true) -- Orange
log(string.format("[ZONE COLORS] %s: Set to ORANGE (Attacked)", zoneName))
elseif currentState == "Empty" then
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true) -- Green
log(string.format("[ZONE COLORS] %s: Set to GREEN (Empty)", zoneName))
elseif zoneCoalition == coalition.side.BLUE then
zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true) -- Blue zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true) -- Blue
log(string.format("[ZONE COLORS] %s: Set to BLUE", zoneName)) log(string.format("[ZONE COLORS] %s: Set to BLUE (Owned)", zoneName))
elseif coalition == coalition.side.RED then elseif zoneCoalition == coalition.side.RED then
zoneCapture:DrawZone(-1, {1, 0, 0}, 0.5, {1, 0, 0}, 0.2, 2, true) -- Red zoneCapture:DrawZone(-1, {1, 0, 0}, 0.5, {1, 0, 0}, 0.2, 2, true) -- Red
log(string.format("[ZONE COLORS] %s: Set to RED", zoneName)) log(string.format("[ZONE COLORS] %s: Set to RED (Owned)", zoneName))
else else
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true) -- Green (neutral) zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true) -- Green (neutral)
log(string.format("[ZONE COLORS] %s: Set to NEUTRAL/GREEN", zoneName)) log(string.format("[ZONE COLORS] %s: Set to NEUTRAL/GREEN (Fallback)", zoneName))
end end
end end
end end
@ -773,6 +830,10 @@ end
-- Initialize zone status monitoring -- Initialize zone status monitoring
SCHEDULER:New( nil, function() SCHEDULER:New( nil, function()
log("[VICTORY SYSTEM] Initializing zone monitoring system...") log("[VICTORY SYSTEM] Initializing zone monitoring system...")
-- Initialize performance optimization caches
InitializeCachedUnitSet()
SetupZoneStatusCommands() SetupZoneStatusCommands()
-- Initial status report -- Initial status report

View File

@ -212,13 +212,16 @@ end
hasActiveCargoMission(coalitionKey, airbaseName) hasActiveCargoMission(coalitionKey, airbaseName)
-------------------------------------------------------------------------- --------------------------------------------------------------------------
Returns true if there is an active (not completed/failed) cargo mission for the given airbase. Returns true if there is an active (not completed/failed) cargo mission for the given airbase.
Failed missions are immediately removed from tracking to allow retries.
]] ]]
local function hasActiveCargoMission(coalitionKey, airbaseName) local function hasActiveCargoMission(coalitionKey, airbaseName)
for _, mission in pairs(cargoMissions[coalitionKey]) do for i = #cargoMissions[coalitionKey], 1, -1 do
local mission = cargoMissions[coalitionKey][i]
if mission.destination == airbaseName then if mission.destination == airbaseName then
-- Ignore completed or failed missions -- Remove completed or failed missions immediately to allow retries
if mission.status == "completed" or mission.status == "failed" then if mission.status == "completed" or mission.status == "failed" then
-- not active log("Removing " .. mission.status .. " cargo mission for " .. airbaseName .. " from tracking")
table.remove(cargoMissions[coalitionKey], i)
else else
-- Consider mission active only if the group is alive OR we're still within the grace window -- Consider mission active only if the group is alive OR we're still within the grace window
local stillActive = false local stillActive = false
@ -283,12 +286,25 @@ local function dispatchCargo(squadron, coalitionKey)
local origin local origin
local attempts = 0 local attempts = 0
local maxAttempts = 10 local maxAttempts = 10
local coalitionSide = getCoalitionSide(coalitionKey)
repeat repeat
origin = selectRandomAirfield(config.supplyAirfields) origin = selectRandomAirfield(config.supplyAirfields)
attempts = attempts + 1 attempts = attempts + 1
-- Ensure origin is not the same as destination -- Ensure origin is not the same as destination
if origin == squadron.airbaseName then if origin == squadron.airbaseName then
origin = nil origin = nil
else
-- Validate that origin airbase exists and is controlled by correct coalition
local originAirbase = AIRBASE:FindByName(origin)
if not originAirbase then
log("WARNING: Origin airbase '" .. tostring(origin) .. "' does not exist. Trying another...")
origin = nil
elseif originAirbase:GetCoalition() ~= coalitionSide then
log("WARNING: Origin airbase '" .. tostring(origin) .. "' is not controlled by " .. coalitionKey .. " coalition. Trying another...")
origin = nil
end
end end
until origin or attempts >= maxAttempts until origin or attempts >= maxAttempts
@ -372,14 +388,45 @@ local function dispatchCargo(squadron, coalitionKey)
local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999)) local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999))
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true) log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true)
-- Check if destination airbase is still controlled by the correct coalition -- Validate destination airbase: RAT's "Airbase doesn't exist" error actually means
-- "Airbase not found OR not owned by the correct coalition" because RAT filters by coalition internally.
-- We perform the same validation here to fail fast with better error messages.
local destAirbase = AIRBASE:FindByName(destination) local destAirbase = AIRBASE:FindByName(destination)
local coalitionSide = getCoalitionSide(coalitionKey)
if not destAirbase then if not destAirbase then
log("ERROR: Destination airbase '" .. destination .. "' does not exist. Skipping dispatch.") log("ERROR: Destination airbase '" .. destination .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (airbase not found on map)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return return
end end
if destAirbase:GetCoalition() ~= getCoalitionSide(coalitionKey) then
log("ERROR: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition. Skipping dispatch.") local destCoalition = destAirbase:GetCoalition()
if destCoalition ~= coalitionSide then
log("WARNING: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(destCoalition) .. "). This will cause RAT to fail with 'Airbase doesn't exist' error. Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (airbase captured by enemy)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return
end
-- Validate origin airbase with same coalition filtering logic
local originAirbase = AIRBASE:FindByName(origin)
if not originAirbase then
log("ERROR: Origin airbase '" .. origin .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (airbase not found on map)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return
end
local originCoalition = originAirbase:GetCoalition()
if originCoalition ~= coalitionSide then
log("WARNING: Origin airbase '" .. origin .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(originCoalition) .. "). This will cause RAT to fail. Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (origin airbase captured by enemy)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return return
end end
@ -390,6 +437,8 @@ local function dispatchCargo(squadron, coalitionKey)
log("TRACEBACK: " .. tostring(debug.traceback(rat)), true) log("TRACEBACK: " .. tostring(debug.traceback(rat)), true)
end end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!") announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!")
-- Mark mission as failed and cleanup immediately - do NOT track failed RAT spawns
mission.status = "failed"
return return
end end
@ -478,6 +527,9 @@ local function dispatchCargo(squadron, coalitionKey)
if debug and debug.traceback then if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true) log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true)
end end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn error)!")
-- Mark mission as failed and cleanup immediately - do NOT track failed spawns
mission.status = "failed"
return return
end end
end end
@ -629,6 +681,112 @@ log("═════════════════════════
-- End Moose_TDAC_CargoDispatcher.lua -- End Moose_TDAC_CargoDispatcher.lua
--[[
DIAGNOSTIC CONSOLE HELPERS
--------------------------------------------------------------------------
Functions you can call from the DCS Lua console (F12) to debug issues.
]]
-- Check airbase coalition ownership for all configured supply airbases
-- Usage: _G.TDAC_CheckAirbaseOwnership()
function _G.TDAC_CheckAirbaseOwnership()
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
env.info("[TDAC DIAGNOSTIC] Checking Coalition Ownership of All Supply Airbases")
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
for _, coalitionKey in ipairs({"red", "blue"}) do
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
local expectedCoalition = getCoalitionSide(coalitionKey)
env.info(string.format("[TDAC DIAGNOSTIC] %s COALITION (expected coalition ID: %s)", coalitionKey:upper(), tostring(expectedCoalition)))
if config and config.supplyAirfields then
for _, airbaseName in ipairs(config.supplyAirfields) do
local airbase = AIRBASE:FindByName(airbaseName)
if not airbase then
env.info(string.format("[TDAC DIAGNOSTIC] ✗ %-30s - NOT FOUND (invalid name or not on this map)", airbaseName))
else
local actualCoalition = airbase:GetCoalition()
local coalitionName = "UNKNOWN"
local status = ""
if actualCoalition == coalition.side.NEUTRAL then
coalitionName = "NEUTRAL"
elseif actualCoalition == coalition.side.RED then
coalitionName = "RED"
elseif actualCoalition == coalition.side.BLUE then
coalitionName = "BLUE"
end
if actualCoalition == expectedCoalition then
status = ""
end
env.info(string.format("[TDAC DIAGNOSTIC] %s %-30s - %s (coalition ID: %s)", status, airbaseName, coalitionName, tostring(actualCoalition)))
end
end
else
env.info("[TDAC DIAGNOSTIC] ERROR: No supply airfields configured!")
end
env.info("[TDAC DIAGNOSTIC] ───────────────────────────────────────")
end
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
env.info("[TDAC DIAGNOSTIC] Check complete. ✓ = Owned by correct coalition, ✗ = Wrong coalition or not found")
return true
end
-- Check specific airbase coalition ownership
-- Usage: _G.TDAC_CheckAirbase("Olenya")
function _G.TDAC_CheckAirbase(airbaseName)
if type(airbaseName) ~= 'string' then
env.info("[TDAC DIAGNOSTIC] ERROR: airbaseName must be a string")
return false
end
local airbase = AIRBASE:FindByName(airbaseName)
if not airbase then
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' NOT FOUND (invalid name or not on this map)", airbaseName))
return false, "not_found"
end
local actualCoalition = airbase:GetCoalition()
local coalitionName = "UNKNOWN"
if actualCoalition == coalition.side.NEUTRAL then
coalitionName = "NEUTRAL"
elseif actualCoalition == coalition.side.RED then
coalitionName = "RED"
elseif actualCoalition == coalition.side.BLUE then
coalitionName = "BLUE"
end
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' - Coalition: %s (ID: %s)", airbaseName, coalitionName, tostring(actualCoalition)))
env.info(string.format("[TDAC DIAGNOSTIC] IsAlive: %s", tostring(airbase:IsAlive())))
-- Check parking spots
local function spotsFor(term, termName)
local ok, n = pcall(function() return airbase:GetParkingSpotsNumber(term) end)
if ok and n then
env.info(string.format("[TDAC DIAGNOSTIC] Parking %-15s: %d spots", termName, n))
end
end
spotsFor(AIRBASE.TerminalType.OpenBig, "OpenBig")
spotsFor(AIRBASE.TerminalType.OpenMed, "OpenMed")
spotsFor(AIRBASE.TerminalType.OpenMedOrBig, "OpenMedOrBig")
spotsFor(AIRBASE.TerminalType.Runway, "Runway")
return true, coalitionName, actualCoalition
end
env.info("[TDAC DIAGNOSTIC] Console helpers loaded:")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbaseOwnership() - Check all supply airbases")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbase('Olenya') - Check specific airbase")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_RunConfigCheck() - Validate dispatcher config")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_LogAirbaseParking('Olenya') - Check parking availability")
-- Diagnostic helper: call from DCS console to test spawn-by-name and routing. -- Diagnostic helper: call from DCS console to test spawn-by-name and routing.
-- Example (paste into DCS Lua console): -- Example (paste into DCS Lua console):
-- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga") -- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga")

View File

@ -147,7 +147,7 @@ local TADC_SETTINGS = {
-- RED Coalition Settings -- RED Coalition Settings
red = { red = {
maxActiveCAP = 24, -- Maximum RED fighters airborne at once maxActiveCAP = 24, -- Maximum RED fighters airborne at once
squadronCooldown = 300, -- RED cooldown after squadron launch (seconds) squadronCooldown = 600, -- RED cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- RED interceptors per threat aircraft interceptRatio = 0.8, -- RED interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) emergencyCleanupTime = 7200, -- RED force cleanup time (seconds)
@ -157,7 +157,7 @@ local TADC_SETTINGS = {
-- BLUE Coalition Settings -- BLUE Coalition Settings
blue = { blue = {
maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once
squadronCooldown = 300, -- BLUE cooldown after squadron launch (seconds) squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- BLUE interceptors per threat aircraft interceptRatio = 0.8, -- BLUE interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds)

View File

@ -212,13 +212,16 @@ end
hasActiveCargoMission(coalitionKey, airbaseName) hasActiveCargoMission(coalitionKey, airbaseName)
-------------------------------------------------------------------------- --------------------------------------------------------------------------
Returns true if there is an active (not completed/failed) cargo mission for the given airbase. Returns true if there is an active (not completed/failed) cargo mission for the given airbase.
Failed missions are immediately removed from tracking to allow retries.
]] ]]
local function hasActiveCargoMission(coalitionKey, airbaseName) local function hasActiveCargoMission(coalitionKey, airbaseName)
for _, mission in pairs(cargoMissions[coalitionKey]) do for i = #cargoMissions[coalitionKey], 1, -1 do
local mission = cargoMissions[coalitionKey][i]
if mission.destination == airbaseName then if mission.destination == airbaseName then
-- Ignore completed or failed missions -- Remove completed or failed missions immediately to allow retries
if mission.status == "completed" or mission.status == "failed" then if mission.status == "completed" or mission.status == "failed" then
-- not active log("Removing " .. mission.status .. " cargo mission for " .. airbaseName .. " from tracking")
table.remove(cargoMissions[coalitionKey], i)
else else
-- Consider mission active only if the group is alive OR we're still within the grace window -- Consider mission active only if the group is alive OR we're still within the grace window
local stillActive = false local stillActive = false
@ -283,12 +286,25 @@ local function dispatchCargo(squadron, coalitionKey)
local origin local origin
local attempts = 0 local attempts = 0
local maxAttempts = 10 local maxAttempts = 10
local coalitionSide = getCoalitionSide(coalitionKey)
repeat repeat
origin = selectRandomAirfield(config.supplyAirfields) origin = selectRandomAirfield(config.supplyAirfields)
attempts = attempts + 1 attempts = attempts + 1
-- Ensure origin is not the same as destination -- Ensure origin is not the same as destination
if origin == squadron.airbaseName then if origin == squadron.airbaseName then
origin = nil origin = nil
else
-- Validate that origin airbase exists and is controlled by correct coalition
local originAirbase = AIRBASE:FindByName(origin)
if not originAirbase then
log("WARNING: Origin airbase '" .. tostring(origin) .. "' does not exist. Trying another...")
origin = nil
elseif originAirbase:GetCoalition() ~= coalitionSide then
log("WARNING: Origin airbase '" .. tostring(origin) .. "' is not controlled by " .. coalitionKey .. " coalition. Trying another...")
origin = nil
end
end end
until origin or attempts >= maxAttempts until origin or attempts >= maxAttempts
@ -372,14 +388,45 @@ local function dispatchCargo(squadron, coalitionKey)
local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999)) local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999))
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true) log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true)
-- Check if destination airbase is still controlled by the correct coalition -- Validate destination airbase: RAT's "Airbase doesn't exist" error actually means
-- "Airbase not found OR not owned by the correct coalition" because RAT filters by coalition internally.
-- We perform the same validation here to fail fast with better error messages.
local destAirbase = AIRBASE:FindByName(destination) local destAirbase = AIRBASE:FindByName(destination)
local coalitionSide = getCoalitionSide(coalitionKey)
if not destAirbase then if not destAirbase then
log("ERROR: Destination airbase '" .. destination .. "' does not exist. Skipping dispatch.") log("ERROR: Destination airbase '" .. destination .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (airbase not found on map)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return return
end end
if destAirbase:GetCoalition() ~= getCoalitionSide(coalitionKey) then
log("ERROR: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition. Skipping dispatch.") local destCoalition = destAirbase:GetCoalition()
if destCoalition ~= coalitionSide then
log("WARNING: Destination airbase '" .. destination .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(destCoalition) .. "). This will cause RAT to fail with 'Airbase doesn't exist' error. Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (airbase captured by enemy)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return
end
-- Validate origin airbase with same coalition filtering logic
local originAirbase = AIRBASE:FindByName(origin)
if not originAirbase then
log("ERROR: Origin airbase '" .. origin .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (airbase not found on map)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return
end
local originCoalition = originAirbase:GetCoalition()
if originCoalition ~= coalitionSide then
log("WARNING: Origin airbase '" .. origin .. "' is not controlled by " .. coalitionKey .. " coalition (currently coalition " .. tostring(originCoalition) .. "). This will cause RAT to fail. Skipping dispatch.")
announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (origin airbase captured by enemy)!")
-- Mark mission as failed and cleanup immediately
mission.status = "failed"
return return
end end
@ -390,6 +437,8 @@ local function dispatchCargo(squadron, coalitionKey)
log("TRACEBACK: " .. tostring(debug.traceback(rat)), true) log("TRACEBACK: " .. tostring(debug.traceback(rat)), true)
end end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!") announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!")
-- Mark mission as failed and cleanup immediately - do NOT track failed RAT spawns
mission.status = "failed"
return return
end end
@ -478,6 +527,9 @@ local function dispatchCargo(squadron, coalitionKey)
if debug and debug.traceback then if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true) log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true)
end end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn error)!")
-- Mark mission as failed and cleanup immediately - do NOT track failed spawns
mission.status = "failed"
return return
end end
end end
@ -629,6 +681,112 @@ log("═════════════════════════
-- End Moose_TDAC_CargoDispatcher.lua -- End Moose_TDAC_CargoDispatcher.lua
--[[
DIAGNOSTIC CONSOLE HELPERS
--------------------------------------------------------------------------
Functions you can call from the DCS Lua console (F12) to debug issues.
]]
-- Check airbase coalition ownership for all configured supply airbases
-- Usage: _G.TDAC_CheckAirbaseOwnership()
function _G.TDAC_CheckAirbaseOwnership()
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
env.info("[TDAC DIAGNOSTIC] Checking Coalition Ownership of All Supply Airbases")
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
for _, coalitionKey in ipairs({"red", "blue"}) do
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
local expectedCoalition = getCoalitionSide(coalitionKey)
env.info(string.format("[TDAC DIAGNOSTIC] %s COALITION (expected coalition ID: %s)", coalitionKey:upper(), tostring(expectedCoalition)))
if config and config.supplyAirfields then
for _, airbaseName in ipairs(config.supplyAirfields) do
local airbase = AIRBASE:FindByName(airbaseName)
if not airbase then
env.info(string.format("[TDAC DIAGNOSTIC] ✗ %-30s - NOT FOUND (invalid name or not on this map)", airbaseName))
else
local actualCoalition = airbase:GetCoalition()
local coalitionName = "UNKNOWN"
local status = ""
if actualCoalition == coalition.side.NEUTRAL then
coalitionName = "NEUTRAL"
elseif actualCoalition == coalition.side.RED then
coalitionName = "RED"
elseif actualCoalition == coalition.side.BLUE then
coalitionName = "BLUE"
end
if actualCoalition == expectedCoalition then
status = ""
end
env.info(string.format("[TDAC DIAGNOSTIC] %s %-30s - %s (coalition ID: %s)", status, airbaseName, coalitionName, tostring(actualCoalition)))
end
end
else
env.info("[TDAC DIAGNOSTIC] ERROR: No supply airfields configured!")
end
env.info("[TDAC DIAGNOSTIC] ───────────────────────────────────────")
end
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
env.info("[TDAC DIAGNOSTIC] Check complete. ✓ = Owned by correct coalition, ✗ = Wrong coalition or not found")
return true
end
-- Check specific airbase coalition ownership
-- Usage: _G.TDAC_CheckAirbase("Olenya")
function _G.TDAC_CheckAirbase(airbaseName)
if type(airbaseName) ~= 'string' then
env.info("[TDAC DIAGNOSTIC] ERROR: airbaseName must be a string")
return false
end
local airbase = AIRBASE:FindByName(airbaseName)
if not airbase then
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' NOT FOUND (invalid name or not on this map)", airbaseName))
return false, "not_found"
end
local actualCoalition = airbase:GetCoalition()
local coalitionName = "UNKNOWN"
if actualCoalition == coalition.side.NEUTRAL then
coalitionName = "NEUTRAL"
elseif actualCoalition == coalition.side.RED then
coalitionName = "RED"
elseif actualCoalition == coalition.side.BLUE then
coalitionName = "BLUE"
end
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' - Coalition: %s (ID: %s)", airbaseName, coalitionName, tostring(actualCoalition)))
env.info(string.format("[TDAC DIAGNOSTIC] IsAlive: %s", tostring(airbase:IsAlive())))
-- Check parking spots
local function spotsFor(term, termName)
local ok, n = pcall(function() return airbase:GetParkingSpotsNumber(term) end)
if ok and n then
env.info(string.format("[TDAC DIAGNOSTIC] Parking %-15s: %d spots", termName, n))
end
end
spotsFor(AIRBASE.TerminalType.OpenBig, "OpenBig")
spotsFor(AIRBASE.TerminalType.OpenMed, "OpenMed")
spotsFor(AIRBASE.TerminalType.OpenMedOrBig, "OpenMedOrBig")
spotsFor(AIRBASE.TerminalType.Runway, "Runway")
return true, coalitionName, actualCoalition
end
env.info("[TDAC DIAGNOSTIC] Console helpers loaded:")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbaseOwnership() - Check all supply airbases")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbase('Olenya') - Check specific airbase")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_RunConfigCheck() - Validate dispatcher config")
env.info("[TDAC DIAGNOSTIC] _G.TDAC_LogAirbaseParking('Olenya') - Check parking availability")
-- Diagnostic helper: call from DCS console to test spawn-by-name and routing. -- Diagnostic helper: call from DCS console to test spawn-by-name and routing.
-- Example (paste into DCS Lua console): -- Example (paste into DCS Lua console):
-- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga") -- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga")

View File

@ -147,7 +147,7 @@ local TADC_SETTINGS = {
-- RED Coalition Settings -- RED Coalition Settings
red = { red = {
maxActiveCAP = 24, -- Maximum RED fighters airborne at once maxActiveCAP = 24, -- Maximum RED fighters airborne at once
squadronCooldown = 300, -- RED cooldown after squadron launch (seconds) squadronCooldown = 600, -- RED cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- RED interceptors per threat aircraft interceptRatio = 0.8, -- RED interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) emergencyCleanupTime = 7200, -- RED force cleanup time (seconds)
@ -157,7 +157,7 @@ local TADC_SETTINGS = {
-- BLUE Coalition Settings -- BLUE Coalition Settings
blue = { blue = {
maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once
squadronCooldown = 300, -- BLUE cooldown after squadron launch (seconds) squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- BLUE interceptors per threat aircraft interceptRatio = 0.8, -- BLUE interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds)

Binary file not shown.