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
-- 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

View File

@ -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")

View File

@ -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)

View File

@ -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")

View File

@ -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)

Binary file not shown.