diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.0.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.0.miz index f2097e4..42e7d5f 100644 Binary files a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.0.miz and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.0.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.1.miz b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.1.miz new file mode 100644 index 0000000..fd3cba2 Binary files /dev/null and b/DCS_Kola/Operation_Polar_Shield/F99th-Operation_Polar_Shield_1.1.1.miz differ diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua b/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua index 734c9b1..3348365 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_CaptureZones.lua @@ -133,6 +133,20 @@ log("[DEBUG] The Lakes zone initialization complete") -- 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 zone = ZoneCapture:GetZone() if not zone then return {red = 0, blue = 0, neutral = 0} end @@ -141,15 +155,15 @@ local function GetZoneForceStrengths(ZoneCapture) local blueCount = 0 local neutralCount = 0 - -- Simple approach: scan all units and check if they're in the zone - local coord = zone:GetCoordinate() - local radius = zone:GetRadius() or 1000 + -- Use MOOSE's optimized zone scanning instead of manual distance checks + local success, scannedUnits = pcall(function() + return zone:GetScannedUnits() + end) - local allUnits = SET_UNIT:New():FilterStart() - allUnits:ForEachUnit(function(unit) - if unit and unit:IsAlive() then - local unitCoord = unit:GetCoordinate() - if unitCoord and coord:Get2DDistance(unitCoord) <= radius then + if success and scannedUnits then + -- Use MOOSE's built-in scanned units (much faster) + for _, unit in pairs(scannedUnits) do + if unit and unit:IsAlive() then local unitCoalition = unit:GetCoalition() if unitCoalition == coalition.side.RED then redCount = redCount + 1 @@ -160,7 +174,32 @@ local function GetZoneForceStrengths(ZoneCapture) 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", ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount)) @@ -179,62 +218,57 @@ local function GetRedUnitMGRSCoords(ZoneCapture) local coords = {} 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() units = zone:GetScannedUnits() end) + -- Fallback: Use coordinate-based scanning (much faster than SET_UNIT filtering) if not success1 or not units then + local coord = zone:GetCoordinate() + local radius = zone:GetRadius() or 1000 + local success2 = pcall(function() - local unitSet = SET_UNIT:New():FilterZones({zone}):FilterStart() - units = {} - unitSet:ForEachUnit(function(unit) - if unit then - units[unit:GetName()] = unit - end - end) + units = coord:ScanUnits(radius) 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 - if not units then - 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 + -- Extract RED unit coordinates with optimized error handling if units then for unitName, unit in pairs(units) do - -- Enhanced nil checking and safe method calls - if unit and type(unit) == "table" and unit.IsAlive then - local isAlive = false - local success_alive = pcall(function() - isAlive = unit:IsAlive() - end) + -- Streamlined nil checking + if unit and type(unit) == "table" then + local success, isAlive = pcall(function() return unit:IsAlive() end) - if success_alive and isAlive then - local coalition_side = nil - local success_coalition = pcall(function() - coalition_side = unit:GetCoalition() - end) + if success and isAlive then + local success_coalition, coalition_side = pcall(function() return unit:GetCoalition() end) if success_coalition and coalition_side == coalition.side.RED then - local coord = unit:GetCoordinate() - if coord then - local success, mgrs = pcall(function() + local success_coord, coord = pcall(function() return unit:GetCoordinate() end) + + if success_coord and coord then + local success_mgrs, mgrs = pcall(function() return coord:ToStringMGRS(5) -- 5-digit precision 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, { name = unit:GetName(), - type = unit:GetTypeName(), + type = success_type and unitType or "Unknown", mgrs = mgrs }) end @@ -673,21 +707,37 @@ end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minute local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() 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 if zoneCapture then local zoneCoalition = zoneCapture:GetCoalition() 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() - if zoneCoalition == coalition.side.BLUE then - zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true) -- Blue + -- Color priority: State (Attacked/Empty) overrides coalition ownership + 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 - 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 - 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 @@ -713,22 +763,29 @@ local function RefreshAllZoneColors() for i, zoneCapture in ipairs(zoneCaptureObjects) do if zoneCapture then - local coalition = zoneCapture:GetCoalition() + local zoneCoalition = zoneCapture:GetCoalition() local zoneName = zoneNames[i] or ("Zone " .. i) + local currentState = zoneCapture:GetCurrentState() -- Clear existing drawings zoneCapture:UndrawZone() - -- Redraw with correct color based on current coalition - if coalition == coalition.side.BLUE then + -- Redraw with correct color based on CURRENT STATE (priority over coalition) + 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 - log(string.format("[ZONE COLORS] %s: Set to BLUE", zoneName)) - elseif coalition == coalition.side.RED then + log(string.format("[ZONE COLORS] %s: Set to BLUE (Owned)", zoneName)) + elseif zoneCoalition == coalition.side.RED then 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 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 @@ -773,6 +830,10 @@ end -- Initialize zone status monitoring SCHEDULER:New( nil, function() log("[VICTORY SYSTEM] Initializing zone monitoring system...") + + -- Initialize performance optimization caches + InitializeCachedUnitSet() + SetupZoneStatusCommands() -- Initial status report diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua index bef4e02..4e8e1ee 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_CargoDispatcher.lua @@ -212,13 +212,16 @@ end hasActiveCargoMission(coalitionKey, airbaseName) -------------------------------------------------------------------------- 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) - for _, mission in pairs(cargoMissions[coalitionKey]) do + for i = #cargoMissions[coalitionKey], 1, -1 do + local mission = cargoMissions[coalitionKey][i] 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 - -- not active + log("Removing " .. mission.status .. " cargo mission for " .. airbaseName .. " from tracking") + table.remove(cargoMissions[coalitionKey], i) else -- Consider mission active only if the group is alive OR we're still within the grace window local stillActive = false @@ -283,12 +286,25 @@ local function dispatchCargo(squadron, coalitionKey) local origin local attempts = 0 local maxAttempts = 10 + local coalitionSide = getCoalitionSide(coalitionKey) + repeat origin = selectRandomAirfield(config.supplyAirfields) attempts = attempts + 1 + -- Ensure origin is not the same as destination if origin == squadron.airbaseName then 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 until origin or attempts >= maxAttempts @@ -372,14 +388,45 @@ local function dispatchCargo(squadron, coalitionKey) local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999)) 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 coalitionSide = getCoalitionSide(coalitionKey) + 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 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 end @@ -390,6 +437,8 @@ local function dispatchCargo(squadron, coalitionKey) log("TRACEBACK: " .. tostring(debug.traceback(rat)), true) end 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 end @@ -478,6 +527,9 @@ local function dispatchCargo(squadron, coalitionKey) if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true) 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 end end @@ -629,6 +681,112 @@ log("═════════════════════════ -- 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. -- Example (paste into DCS Lua console): -- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga") diff --git a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua index 15ae8e0..6a29065 100644 --- a/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua +++ b/DCS_Kola/Operation_Polar_Shield/Moose_TADC_Load2nd.lua @@ -147,7 +147,7 @@ local TADC_SETTINGS = { -- RED Coalition Settings red = { 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 cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) @@ -157,7 +157,7 @@ local TADC_SETTINGS = { -- BLUE Coalition Settings blue = { 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 cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) diff --git a/Moose_TADC/Moose_TADC_CargoDispatcher.lua b/Moose_TADC/Moose_TADC_CargoDispatcher.lua index 768f360..d6b374a 100644 --- a/Moose_TADC/Moose_TADC_CargoDispatcher.lua +++ b/Moose_TADC/Moose_TADC_CargoDispatcher.lua @@ -212,13 +212,16 @@ end hasActiveCargoMission(coalitionKey, airbaseName) -------------------------------------------------------------------------- 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) - for _, mission in pairs(cargoMissions[coalitionKey]) do + for i = #cargoMissions[coalitionKey], 1, -1 do + local mission = cargoMissions[coalitionKey][i] 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 - -- not active + log("Removing " .. mission.status .. " cargo mission for " .. airbaseName .. " from tracking") + table.remove(cargoMissions[coalitionKey], i) else -- Consider mission active only if the group is alive OR we're still within the grace window local stillActive = false @@ -283,12 +286,25 @@ local function dispatchCargo(squadron, coalitionKey) local origin local attempts = 0 local maxAttempts = 10 + local coalitionSide = getCoalitionSide(coalitionKey) + repeat origin = selectRandomAirfield(config.supplyAirfields) attempts = attempts + 1 + -- Ensure origin is not the same as destination if origin == squadron.airbaseName then 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 until origin or attempts >= maxAttempts @@ -372,14 +388,45 @@ local function dispatchCargo(squadron, coalitionKey) local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999)) 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 coalitionSide = getCoalitionSide(coalitionKey) + 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 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 end @@ -390,6 +437,8 @@ local function dispatchCargo(squadron, coalitionKey) log("TRACEBACK: " .. tostring(debug.traceback(rat)), true) end 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 end @@ -478,6 +527,9 @@ local function dispatchCargo(squadron, coalitionKey) if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true) 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 end end @@ -629,6 +681,112 @@ log("═════════════════════════ -- 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. -- Example (paste into DCS Lua console): -- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga") diff --git a/Moose_TADC/Moose_TADC_Load2nd.lua b/Moose_TADC/Moose_TADC_Load2nd.lua index 15ae8e0..6a29065 100644 --- a/Moose_TADC/Moose_TADC_Load2nd.lua +++ b/Moose_TADC/Moose_TADC_Load2nd.lua @@ -147,7 +147,7 @@ local TADC_SETTINGS = { -- RED Coalition Settings red = { 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 cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) @@ -157,7 +157,7 @@ local TADC_SETTINGS = { -- BLUE Coalition Settings blue = { 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 cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) diff --git a/Moose_TADC/TADC_Example.miz b/Moose_TADC/TADC_Example.miz index 2d12ad4..73dbd3a 100644 Binary files a/Moose_TADC/TADC_Example.miz and b/Moose_TADC/TADC_Example.miz differ