TADC & TADC Cargo Supply System Updaetd - Fixed all spawning and routing issues and made most of the logging optional for debugging.

This commit is contained in:
iTracerFacer 2025-10-17 17:43:02 -05:00
parent f564d556dc
commit 55d217e291
8 changed files with 2296 additions and 1221 deletions

View File

@ -22,95 +22,113 @@ do -- Missions
end
-- Logging configuration: toggle logging behavior for this module
-- Set `CAPTURE_ZONE_LOGGING.enabled = false` to silence module logs
if not CAPTURE_ZONE_LOGGING then
CAPTURE_ZONE_LOGGING = { enabled = false, prefix = "[CAPTURE Module]" }
end
local function log(message, detailed)
if CAPTURE_ZONE_LOGGING.enabled then
-- Preserve the previous prefixing used across the module
if CAPTURE_ZONE_LOGGING.prefix then
env.info(tostring(CAPTURE_ZONE_LOGGING.prefix) .. " " .. tostring(message))
else
env.info(tostring(message))
end
end
end
-- Red Airbases (from TADC configuration)
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Kilpyavr")
log("[DEBUG] Initializing Capture Zone: Kilpyavr")
CaptureZone_Kilpyavr = ZONE:New( "Capture Kilpyavr" )
ZoneCapture_Kilpyavr = ZONE_CAPTURE_COALITION:New( CaptureZone_Kilpyavr, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Kilpyavr:__Guard( 1 )
ZoneCapture_Kilpyavr:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Kilpyavr zone initialization complete")
log("[DEBUG] Kilpyavr zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Severomorsk-1")
log("[DEBUG] Initializing Capture Zone: Severomorsk-1")
CaptureZone_Severomorsk_1 = ZONE:New( "Capture Severomorsk-1" )
ZoneCapture_Severomorsk_1 = ZONE_CAPTURE_COALITION:New( CaptureZone_Severomorsk_1, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Severomorsk_1:__Guard( 1 )
ZoneCapture_Severomorsk_1:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Severomorsk-1 zone initialization complete")
log("[DEBUG] Severomorsk-1 zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Severomorsk-3")
log("[DEBUG] Initializing Capture Zone: Severomorsk-3")
CaptureZone_Severomorsk_3 = ZONE:New( "Capture Severomorsk-3" )
ZoneCapture_Severomorsk_3 = ZONE_CAPTURE_COALITION:New( CaptureZone_Severomorsk_3, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Severomorsk_3:__Guard( 1 )
ZoneCapture_Severomorsk_3:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Severomorsk-3 zone initialization complete")
log("[DEBUG] Severomorsk-3 zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Murmansk International")
log("[DEBUG] Initializing Capture Zone: Murmansk International")
CaptureZone_Murmansk_International = ZONE:New( "Capture Murmansk International" )
ZoneCapture_Murmansk_International = ZONE_CAPTURE_COALITION:New( CaptureZone_Murmansk_International, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Murmansk_International:__Guard( 1 )
ZoneCapture_Murmansk_International:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Murmansk International zone initialization complete")
log("[DEBUG] Murmansk International zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Monchegorsk")
log("[DEBUG] Initializing Capture Zone: Monchegorsk")
CaptureZone_Monchegorsk = ZONE:New( "Capture Monchegorsk" )
ZoneCapture_Monchegorsk = ZONE_CAPTURE_COALITION:New( CaptureZone_Monchegorsk, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Monchegorsk:__Guard( 1 )
ZoneCapture_Monchegorsk:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Monchegorsk zone initialization complete")
log("[DEBUG] Monchegorsk zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Olenya")
log("[DEBUG] Initializing Capture Zone: Olenya")
CaptureZone_Olenya = ZONE:New( "Capture Olenya" )
ZoneCapture_Olenya = ZONE_CAPTURE_COALITION:New( CaptureZone_Olenya, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Olenya:__Guard( 1 )
ZoneCapture_Olenya:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Olenya zone initialization complete")
log("[DEBUG] Olenya zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: Afrikanda")
log("[DEBUG] Initializing Capture Zone: Afrikanda")
CaptureZone_Afrikanda = ZONE:New( "Capture Afrikanda" )
ZoneCapture_Afrikanda = ZONE_CAPTURE_COALITION:New( CaptureZone_Afrikanda, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_Afrikanda:__Guard( 1 )
ZoneCapture_Afrikanda:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] Afrikanda zone initialization complete")
log("[DEBUG] Afrikanda zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: The Mountain")
log("[DEBUG] Initializing Capture Zone: The Mountain")
CaptureZone_The_Mountain = ZONE:New( "Capture The Mountain" )
ZoneCapture_The_Mountain = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Mountain, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_The_Mountain:__Guard( 1 )
ZoneCapture_The_Mountain:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] The Mountain zone initialization complete")
log("[DEBUG] The Mountain zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: The River")
log("[DEBUG] Initializing Capture Zone: The River")
CaptureZone_The_River = ZONE:New( "Capture The River" )
ZoneCapture_The_River = ZONE_CAPTURE_COALITION:New( CaptureZone_The_River, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_The_River:__Guard( 1 )
ZoneCapture_The_River:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] The River zone initialization complete")
log("[DEBUG] The River zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: The Gulf")
log("[DEBUG] Initializing Capture Zone: The Gulf")
CaptureZone_The_Gulf = ZONE:New( "Capture The Gulf" )
ZoneCapture_The_Gulf = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Gulf, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_The_Gulf:__Guard( 1 )
ZoneCapture_The_Gulf:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] The Gulf zone initialization complete")
log("[DEBUG] The Gulf zone initialization complete")
env.info("[CAPTURE Module] [DEBUG] Initializing Capture Zone: The Lakes")
log("[DEBUG] Initializing Capture Zone: The Lakes")
CaptureZone_The_Lakes = ZONE:New( "Capture The Lakes" )
ZoneCapture_The_Lakes = ZONE_CAPTURE_COALITION:New( CaptureZone_The_Lakes, coalition.side.RED )
-- SetMarkReadOnly method not available in this MOOSE version - feature disabled
ZoneCapture_The_Lakes:__Guard( 1 )
ZoneCapture_The_Lakes:Start( 30, 30 )
env.info("[CAPTURE Module] [DEBUG] The Lakes zone initialization complete")
log("[DEBUG] The Lakes zone initialization complete")
@ -144,7 +162,7 @@ local function GetZoneForceStrengths(ZoneCapture)
end
end)
env.info(string.format("[CAPTURE Module] [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))
return {
@ -227,7 +245,7 @@ local function GetRedUnitMGRSCoords(ZoneCapture)
end
end
env.info(string.format("[CAPTURE Module] [TACTICAL] Found %d RED units with coordinates in %s",
log(string.format("[TACTICAL] Found %d RED units with coordinates in %s",
#coords, ZoneCapture:GetZoneName()))
return coords
@ -274,7 +292,7 @@ local function CreateTacticalInfoMarker(ZoneCapture)
-- Remove any existing tactical marker first
if ZoneCapture.TacticalMarkerID then
env.info(string.format("[CAPTURE Module] [TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName))
log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName))
-- Try multiple removal methods
local success1 = pcall(function()
offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID)
@ -306,9 +324,9 @@ local function CreateTacticalInfoMarker(ZoneCapture)
offsetCoord:SetMarkReadOnly(markerID, true)
end)
env.info(string.format("[CAPTURE Module] [TACTICAL] Created read-only marker for %s with %d RED, %d BLUE units", zoneName, forces.red, forces.blue))
log(string.format("[TACTICAL] Created read-only marker for %s with %d RED, %d BLUE units", zoneName, forces.red, forces.blue))
else
env.info(string.format("[CAPTURE Module] [TACTICAL] Failed to create marker for %s", zoneName))
log(string.format("[TACTICAL] Failed to create marker for %s", zoneName))
end
end
end
@ -391,11 +409,11 @@ local function CheckVictoryCondition()
end
end
env.info(string.format("[CAPTURE Module] [VICTORY CHECK] Blue owns %d/%d zones", blueZonesCount, totalZones))
log(string.format("[VICTORY CHECK] Blue owns %d/%d zones", blueZonesCount, totalZones))
if blueZonesCount >= totalZones then
-- All zones captured by BLUE - trigger victory condition
env.info("[CAPTURE Module] [VICTORY] All zones captured by BLUE! Triggering victory sequence...")
log("[VICTORY] All zones captured by BLUE! Triggering victory sequence...")
-- Victory messages
US_CC:MessageTypeToCoalition(
@ -425,7 +443,7 @@ local function CheckVictoryCondition()
-- Schedule mission end after 60 seconds
SCHEDULER:New( nil, function()
env.info("[CAPTURE Module] [VICTORY] Ending mission due to complete zone capture by BLUE")
log("[VICTORY] Ending mission due to complete zone capture by BLUE")
-- You can trigger specific end-mission logic here
-- For example: trigger.action.setUserFlag("MissionComplete", 1)
-- Or call specific mission ending functions
@ -494,7 +512,7 @@ for i, zoneCapture in ipairs(zoneCaptureObjects) do
-- Debug: Check if the underlying zone exists
local success, zone = pcall(function() return zoneCapture:GetZone() end)
if success and zone then
env.info("[CAPTURE Module] ✓ Zone 'Capture " .. zoneName .. "' successfully created and linked")
log("✓ Zone 'Capture " .. zoneName .. "' successfully created and linked")
-- Initialize zone borders with initial RED color (all zones start as RED coalition)
local drawSuccess, drawError = pcall(function()
@ -502,46 +520,46 @@ for i, zoneCapture in ipairs(zoneCaptureObjects) do
end)
if not drawSuccess then
env.info("[CAPTURE Module] ⚠ Zone 'Capture " .. zoneName .. "' border drawing failed: " .. tostring(drawError))
log("⚠ Zone 'Capture " .. zoneName .. "' border drawing failed: " .. tostring(drawError))
-- Alternative: Try simpler zone marking
pcall(function()
zone:SmokeZone(SMOKECOLOR.Red, 30)
end)
else
env.info("[CAPTURE Module] ✓ Zone 'Capture " .. zoneName .. "' border drawn successfully with RED initial color")
log("✓ Zone 'Capture " .. zoneName .. "' border drawn successfully with RED initial color")
end
else
env.info("[CAPTURE Module] ✗ ERROR: Zone 'Capture " .. zoneName .. "' not found in mission editor!")
env.info("[CAPTURE Module] Make sure you have a trigger zone named exactly: 'Capture " .. zoneName .. "'")
log("✗ ERROR: Zone 'Capture " .. zoneName .. "' not found in mission editor!")
log(" Make sure you have a trigger zone named exactly: 'Capture " .. zoneName .. "'")
end
else
env.info("[CAPTURE Module] ✗ ERROR: Zone capture object " .. i .. " (" .. (zoneNames[i] or "Unknown") .. ") is nil!")
log("✗ ERROR: Zone capture object " .. i .. " (" .. (zoneNames[i] or "Unknown") .. ") is nil!")
end
end
-- Additional specific check for Olenya
env.info("[CAPTURE Module] === OLENYA SPECIFIC DEBUG ===")
log("=== OLENYA SPECIFIC DEBUG ===")
if ZoneCapture_Olenya then
env.info("[CAPTURE Module] ✓ ZoneCapture_Olenya object exists")
log("✓ ZoneCapture_Olenya object exists")
local success, result = pcall(function() return ZoneCapture_Olenya:GetZoneName() end)
if success then
env.info("[CAPTURE Module] ✓ Zone name: " .. tostring(result))
log("✓ Zone name: " .. tostring(result))
else
env.info("[CAPTURE Module] ✗ Could not get zone name: " .. tostring(result))
log("✗ Could not get zone name: " .. tostring(result))
end
local success2, zone = pcall(function() return ZoneCapture_Olenya:GetZone() end)
if success2 and zone then
env.info("[CAPTURE Module] ✓ Underlying zone object exists")
log("✓ Underlying zone object exists")
local coord = zone:GetCoordinate()
if coord then
env.info("[CAPTURE Module] ✓ Zone coordinate: " .. coord:ToStringLLDMS())
log("✓ Zone coordinate: " .. coord:ToStringLLDMS())
end
else
env.info("[CAPTURE Module] ✗ Underlying zone object missing: " .. tostring(zone))
log("✗ Underlying zone object missing: " .. tostring(zone))
end
else
env.info("[CAPTURE Module] ✗ ZoneCapture_Olenya object is nil!")
log("✗ ZoneCapture_Olenya object is nil!")
end
-- ==========================================
@ -625,7 +643,7 @@ local function BroadcastZoneStatus()
US_CC:MessageTypeToCoalition( fullMessage, MESSAGE.Type.Information, 15 )
env.info("[CAPTURE Module] [ZONE STATUS] " .. reportMessage:gsub("\n", " | "))
log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | "))
return status
end
@ -653,7 +671,7 @@ end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minute
-- Periodic zone color verification system (every 2 minutes)
local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
env.info("[CAPTURE Module] [ZONE COLORS] Running periodic zone color verification...")
log("[ZONE COLORS] Running periodic zone color verification...")
-- Verify each zone's visual marker matches its coalition
for i, zoneCapture in ipairs(zoneCaptureObjects) do
@ -678,7 +696,7 @@ end, {}, 60, 120 ) -- Start after 60 seconds, repeat every 120 seconds (2 minute
-- Periodic tactical marker update system (every 1 minute)
local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function()
env.info("[CAPTURE Module] [TACTICAL] Running periodic tactical marker update...")
log("[TACTICAL] Running periodic tactical marker update...")
-- Update tactical markers for all zones
for i, zoneCapture in ipairs(zoneCaptureObjects) do
@ -691,7 +709,7 @@ end, {}, 30, 60 ) -- Start after 30 seconds, repeat every 60 seconds (1 minute)
-- Function to refresh all zone colors based on current ownership
local function RefreshAllZoneColors()
env.info("[CAPTURE Module] [ZONE COLORS] Refreshing all zone visual markers...")
log("[ZONE COLORS] Refreshing all zone visual markers...")
for i, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture then
@ -704,13 +722,13 @@ local function RefreshAllZoneColors()
-- Redraw with correct color based on current coalition
if coalition == coalition.side.BLUE then
zoneCapture:DrawZone(-1, {0, 0, 1}, 0.5, {0, 0, 1}, 0.2, 2, true) -- Blue
env.info(string.format("[CAPTURE Module] [ZONE COLORS] %s: Set to BLUE", zoneName))
log(string.format("[ZONE COLORS] %s: Set to BLUE", zoneName))
elseif coalition == coalition.side.RED then
zoneCapture:DrawZone(-1, {1, 0, 0}, 0.5, {1, 0, 0}, 0.2, 2, true) -- Red
env.info(string.format("[CAPTURE Module] [ZONE COLORS] %s: Set to RED", zoneName))
log(string.format("[ZONE COLORS] %s: Set to RED", zoneName))
else
zoneCapture:DrawZone(-1, {0, 1, 0}, 0.5, {0, 1, 0}, 0.2, 2, true) -- Green (neutral)
env.info(string.format("[CAPTURE Module] [ZONE COLORS] %s: Set to NEUTRAL/GREEN", zoneName))
log(string.format("[ZONE COLORS] %s: Set to NEUTRAL/GREEN", zoneName))
end
end
end
@ -754,16 +772,16 @@ end
-- Initialize zone status monitoring
SCHEDULER:New( nil, function()
env.info("[CAPTURE Module] [VICTORY SYSTEM] Initializing zone monitoring system...")
log("[VICTORY SYSTEM] Initializing zone monitoring system...")
SetupZoneStatusCommands()
-- Initial status report
SCHEDULER:New( nil, function()
env.info("[CAPTURE Module] [VICTORY SYSTEM] Broadcasting initial zone status...")
log("[VICTORY SYSTEM] Broadcasting initial zone status...")
BroadcastZoneStatus()
end, {}, 30 ) -- Initial report after 30 seconds
end, {}, 5 ) -- Initialize after 5 seconds
env.info("[CAPTURE Module] [VICTORY SYSTEM] Zone capture victory monitoring system loaded successfully!")
log("[VICTORY SYSTEM] Zone capture victory monitoring system loaded successfully!")

View File

@ -5,7 +5,7 @@
DESCRIPTION:
This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It tracks each supply mission, announces key stages to players, and prevents duplicate or spam missions. The system integrates with TADC's existing cargo landing logic for replenishment.
This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system.
CONFIGURATION:
- Update static templates and airfield lists as needed for your mission.
@ -40,6 +40,26 @@ if DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE == nil then
DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE = false
end
--[[
CARGO SUPPLY CONFIGURATION
--------------------------------------------------------------------------
Set supply airfields, cargo template names, and resupply thresholds for each coalition.
]]
local CARGO_SUPPLY_CONFIG = {
red = {
supplyAirfields = { "Afrikanda", "Kalevala", "Poduzhemye", "Severomorsk-1", "Severomorsk-3", "Murmansk International", "Kilpyavr", "Olenya", "Monchegorsk" }, -- replace with your RED supply airbase names
cargoTemplate = "CARGO_RED_AN26", -- replace with your RED cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
},
blue = {
supplyAirfields = { "Banak", "Kittila", "Alta", "Sodankyla", "Enontekio", "Kirkenes", "Ivalo", "Luostari Pechenga", "Koshka Yavr" }, -- replace with your BLUE supply airbase names
cargoTemplate = "CARGO_BLUE_C130", -- replace with your BLUE cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
}
}
--[[
UTILITY STUBS
--------------------------------------------------------------------------
@ -71,10 +91,22 @@ end
Advanced logging configuration and helper function for debug output.
]]
local ADVANCED_LOGGING = {
enableDetailedLogging = true,
enableDetailedLogging = false,
logPrefix = "[TDAC Cargo]"
}
-- Logging function (must be defined before any log() calls)
local function log(message, detailed)
if not detailed or ADVANCED_LOGGING.enableDetailedLogging then
env.info(ADVANCED_LOGGING.logPrefix .. " " .. message)
end
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
log("Moose_TDAC_CargoDispatcher.lua loaded.", true)
log("═══════════════════════════════════════════════════════════════════════════════", true)
-- Provide a safe deepCopy if MIST is not available
local function deepCopy(obj)
if type(obj) ~= 'table' then return obj end
@ -99,35 +131,62 @@ local function getCoalitionSide(coalitionKey)
return nil
end
-- Logging function (mimics Moose_TADC_Load2nd.lua)
local function log(message, detailed)
if not detailed or ADVANCED_LOGGING.enableDetailedLogging then
env.info(ADVANCED_LOGGING.logPrefix .. " " .. message)
-- Forward-declare parking check helper so functions defined earlier can call it
local destinationHasSuitableParking
-- Validate dispatcher configuration: check that supply airfields exist and templates appear valid
local function validateDispatcherConfig()
local problems = {}
-- Check supply airfields exist
for coalitionKey, cfg in pairs(CARGO_SUPPLY_CONFIG) do
if cfg and cfg.supplyAirfields and type(cfg.supplyAirfields) == 'table' then
for _, abName in ipairs(cfg.supplyAirfields) do
local ok, ab = pcall(function() return AIRBASE:FindByName(abName) end)
if not ok or not ab then
table.insert(problems, string.format("Missing airbase for %s supply list: '%s'", tostring(coalitionKey), tostring(abName)))
end
end
else
table.insert(problems, string.format("Missing or invalid supplyAirfields for coalition '%s'", tostring(coalitionKey)))
end
-- Check cargo template presence (best-effort using SPAWN:New if available)
if cfg and cfg.cargoTemplate and type(cfg.cargoTemplate) == 'string' and cfg.cargoTemplate ~= '' then
local okSpawn, spawnObj = pcall(function() return SPAWN:New(cfg.cargoTemplate) end)
if not okSpawn or not spawnObj then
-- SPAWN:New may not be available at load time; warn but don't fail hard
table.insert(problems, string.format("Cargo template suspicious or missing: '%s' (coalition: %s)", tostring(cfg.cargoTemplate), tostring(coalitionKey)))
end
else
table.insert(problems, string.format("Missing cargoTemplate for coalition '%s'", tostring(coalitionKey)))
end
end
if #problems == 0 then
log("TDAC Dispatcher config validation passed ✓", true)
MESSAGE:New("TDAC Dispatcher config validation passed ✓", 15):ToAll()
return true, {}
else
log("TDAC Dispatcher config validation found issues:", true)
MESSAGE:New("TDAC Dispatcher config validation found issues:" .. table.concat(problems, ", "), 15):ToAll()
for _, p in ipairs(problems) do
log("" .. p, true)
end
return false, problems
end
end
-- Expose console helper to run the check manually
function _G.TDAC_RunConfigCheck()
local ok, problems = validateDispatcherConfig()
if ok then
return true, "OK"
else
return false, problems
end
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
log("Moose_TDAC_CargoDispatcher.lua loaded.", true)
log("═══════════════════════════════════════════════════════════════════════════════", true)
--[[
CARGO SUPPLY CONFIGURATION
--------------------------------------------------------------------------
Set supply airfields, cargo template names, and resupply thresholds for each coalition.
]]
local CARGO_SUPPLY_CONFIG = {
red = {
supplyAirfields = { "Kuusamo", "Kalevala", "Vuojarvi", "Kalevala", "Poduzhemye", "Kirkenes" }, -- replace with your RED supply airbase names
cargoTemplate = "CARGO_RED_AN26", -- replace with your RED cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
},
blue = {
supplyAirfields = { "Banak", "Kittila", "Alta", "Sodankyla", "Vuojarvi", "Enontekio" }, -- replace with your BLUE supply airbase names
cargoTemplate = "CARGO_BLUE_C130", -- replace with your BLUE cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
}
}
--[[
@ -196,7 +255,7 @@ end
--[[
cleanupCargoMissions()
--------------------------------------------------------------------------
Removes completed or failed cargo missions from the tracking table if their group is no longer alive.
Removes failed cargo missions from the tracking table if their group is no longer alive.
]]
local function cleanupCargoMissions()
for _, coalitionKey in ipairs({"red", "blue"}) do
@ -207,11 +266,6 @@ local function cleanupCargoMissions()
log("Cleaning up failed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: failed")
table.remove(cargoMissions[coalitionKey], i)
end
elseif m.status == "completed" then
if not (m.group and m.group:IsAlive()) then
log("Cleaning up completed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: completed")
table.remove(cargoMissions[coalitionKey], i)
end
end
end
end
@ -226,7 +280,18 @@ end
]]
local function dispatchCargo(squadron, coalitionKey)
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
local origin = selectRandomAirfield(config.supplyAirfields)
local origin
local attempts = 0
local maxAttempts = 10
repeat
origin = selectRandomAirfield(config.supplyAirfields)
attempts = attempts + 1
-- Ensure origin is not the same as destination
if origin == squadron.airbaseName then
origin = nil
end
until origin or attempts >= maxAttempts
-- enforce cooldown per destination to avoid immediate retries
lastDispatchAttempt[coalitionKey] = lastDispatchAttempt[coalitionKey] or {}
local last = lastDispatchAttempt[coalitionKey][squadron.airbaseName]
@ -235,32 +300,33 @@ local function dispatchCargo(squadron, coalitionKey)
return
end
if not origin then
log("No origin airfield found for cargo dispatch.")
log("No valid origin airfield found for cargo dispatch to " .. squadron.airbaseName .. " (avoiding same origin/destination)")
return
end
local destination = squadron.airbaseName
local cargoTemplate = config.cargoTemplate
-- Safety: check if destination has suitable parking for larger transports. If not, warn in log.
local okParking = true
-- Only check for likely large transports (C-130 / An-26 are large-ish) — keep conservative
if cargoTemplate and (string.find(cargoTemplate:upper(), "C130") or string.find(cargoTemplate:upper(), "C-17") or string.find(cargoTemplate:upper(), "C17") or string.find(cargoTemplate:upper(), "AN26") ) then
okParking = destinationHasSuitableParking(destination)
if not okParking then
log("WARNING: Destination '" .. tostring(destination) .. "' may not have suitable parking for " .. tostring(cargoTemplate) .. ". This can cause immediate despawn on landing.")
log("WARNING: Destination '" .. tostring(destination) .. "' may not have suitable parking for " .. tostring(cargoTemplate) .. ". Skipping dispatch to prevent despawn.")
return
end
end
local cargoTemplate = config.cargoTemplate
local groupName = cargoTemplate .. "_to_" .. destination .. "_" .. math.random(1000,9999)
log("Dispatching cargo: " .. groupName .. " from " .. origin .. " to " .. destination)
-- Spawn cargo aircraft at origin using the template name ONLY for SPAWN
-- Note: cargoTemplate is a config string; script uses in-file Lua template tables (CARGO_AIRCRAFT_TEMPLATE_*)
log("DEBUG: Attempting spawn for group: '" .. groupName .. "' at airbase: '" .. origin .. "' (using in-file Lua template)")
log("DEBUG: Attempting spawn for group: '" .. groupName .. "' at airbase: '" .. origin .. "' (using in-file Lua template)", true)
local airbaseObj = AIRBASE:FindByName(origin)
if not airbaseObj then
log("ERROR: AIRBASE:FindByName failed for '" .. tostring(origin) .. "'. Airbase object is nil!")
else
log("DEBUG: AIRBASE object found for '" .. origin .. "'. Proceeding with spawn.")
log("DEBUG: AIRBASE object found for '" .. origin .. "'. Proceeding with spawn.", true)
end
-- Select the correct template based on coalition
local templateBase, uniqueGroupName
@ -304,13 +370,13 @@ local function dispatchCargo(squadron, coalitionKey)
-- Use a per-dispatch RAT object to spawn and route cargo aircraft.
-- Create a unique alias to avoid naming collisions and let RAT handle routing/landing.
local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999))
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'")
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true)
local okNew, rat = pcall(function() return RAT:New(cargoTemplate, alias) end)
if not okNew or not rat then
log("ERROR: RAT:New failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(rat))
if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(rat)))
log("TRACEBACK: " .. tostring(debug.traceback(rat)), true)
end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!")
return
@ -348,9 +414,9 @@ local function dispatchCargo(squadron, coalitionKey)
log("RAT spawned cargo aircraft group: " .. tostring(spawnedGroup:GetName()))
-- Temporary debug: log group state every 5s for 60s to trace landing/parking behavior
local debugChecks = 12
local checkInterval = 5
-- Temporary debug: log group state every 10s for 10 minutes to trace landing/parking behavior
local debugChecks = 60 -- 60 * 10s = 10 minutes
local checkInterval = 10
local function debugLogState(iter)
if iter > debugChecks then return end
local ok, err = pcall(function()
@ -364,6 +430,7 @@ local function dispatchCargo(squadron, coalitionKey)
-- Use dot accessor to test for function existence; colon-call to invoke
local vel = (u.getVelocity and u:getVelocity()) or {x=0,y=0,z=0}
local speed = math.sqrt((vel.x or 0)^2 + (vel.y or 0)^2 + (vel.z or 0)^2)
local controller = dcs:getController()
local airbaseObj = AIRBASE:FindByName(destination)
local dist = nil
if airbaseObj then
@ -372,16 +439,16 @@ local function dispatchCargo(squadron, coalitionKey)
local dz = pos.z - dest.y
dist = math.sqrt(dx*dx + dz*dz)
end
log(string.format("[TDAC DEBUG] %s state check %d: alive=%s pos=(%.1f,%.1f) speed=%.2f m/s distToDest=%s", name, iter, tostring(spawnedGroup:IsAlive()), pos.x or 0, pos.z or 0, speed, tostring(dist)))
log(string.format("[TDAC DEBUG] %s state check %d: alive=%s pos=(%.1f,%.1f) speed=%.2f m/s distToDest=%s", name, iter, tostring(spawnedGroup:IsAlive()), pos.x or 0, pos.z or 0, speed, tostring(dist)), true)
else
log(string.format("[TDAC DEBUG] %s state check %d: DCS group has no units", tostring(spawnedGroup:GetName()), iter))
log(string.format("[TDAC DEBUG] %s state check %d: DCS group has no units", tostring(spawnedGroup:GetName()), iter), true)
end
else
log(string.format("[TDAC DEBUG] %s state check %d: no DCS group object", tostring(spawnedGroup:GetName()), iter))
log(string.format("[TDAC DEBUG] %s state check %d: no DCS group object", tostring(spawnedGroup:GetName()), iter), true)
end
end)
if not ok then
log("[TDAC DEBUG] Error during debugLogState: " .. tostring(err))
log("[TDAC DEBUG] Error during debugLogState: " .. tostring(err), true)
end
timer.scheduleFunction(function() debugLogState(iter + 1) end, {}, timer.getTime() + checkInterval)
end
@ -389,149 +456,19 @@ local function dispatchCargo(squadron, coalitionKey)
-- RAT should handle routing/taxi/parking. Finalize mission tracking now.
finalizeMissionAfterSpawn(spawnedGroup, spawnPos)
mission.status = "enroute"
mission._pendingStartTime = timer.getTime()
announceToCoalition(coalitionKey, "CARGO aircraft departing (airborne) for " .. destination .. ". Defend it!")
end)
local okSpawn, errSpawn = pcall(function() rat:Spawn(1) end)
if not okSpawn then
log("ERROR: rat:Spawn() failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(errSpawn))
if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)))
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true)
end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn error)!")
return
end
-- Assign route to destination using DCS-native AI tasking, with retries to handle slow registration
local function assignRouteWithRetries(attempt, maxAttempts)
attempt = attempt or 1
maxAttempts = maxAttempts or 6
if not (mission.group and mission.group:IsAlive()) then
log(string.format("assignRouteWithRetries: mission.group invalid or dead (attempt %d/%d)", attempt, maxAttempts))
if attempt >= maxAttempts then
mission.status = "failed"
log("Cargo mission failed: spawned group never registered/alive for mission to " .. destination)
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn issue)!")
return
end
-- retry after backoff
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
return
end
local destAirbase = AIRBASE:FindByName(destination)
if not destAirbase then
log("assignRouteWithRetries: Destination airbase not found: " .. tostring(destination))
mission.status = "failed"
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no airbase)!")
return
end
local dcsGroup = mission.group:GetDCSObject()
if not dcsGroup then
log("assignRouteWithRetries: DCS group object not available yet (attempt " .. attempt .. ")")
if attempt >= maxAttempts then
mission.status = "failed"
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no DCS group)!")
return
end
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
return
end
local controller = dcsGroup:getController()
if not controller then
log("assignRouteWithRetries: Controller not available yet (attempt " .. attempt .. ")")
if attempt >= maxAttempts then
mission.status = "failed"
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no controller)!")
return
end
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
return
end
-- Build route now that we have positions. Use the spawn position captured earlier if available,
-- otherwise read the current unit position from the DCS group.
local cruiseAlt = 6096 -- 20,000 feet in meters
local destCoord = destAirbase:GetCoordinate():GetVec2()
local destElevation = destAirbase:GetCoordinate():GetLandHeight() or 0
local landingAlt = destElevation + 10 -- 10m above ground
local airdromeId = destAirbase:GetID() or 0
local destX = destCoord.x
local destZ = destCoord.y
local pos = mission._spawnPos
if not pos then
local units = dcsGroup:getUnits()
if units and #units > 0 then
pos = units[1]:getPoint()
end
end
if not pos or not pos.x or not pos.z then
log("assignRouteWithRetries: Could not determine spawn position for route assignment (attempt " .. attempt .. ")")
if attempt >= maxAttempts then
mission.status = "failed"
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (no position)!")
return
end
timer.scheduleFunction(function() assignRouteWithRetries(attempt + 1, maxAttempts) end, {}, timer.getTime() + (attempt * 2))
return
end
local route = {
{
x = pos.x,
z = pos.z,
alt = cruiseAlt,
type = "Turning Point",
action = "Turning Point",
speed = 330
},
{
x = destX,
z = destZ,
alt = cruiseAlt,
type = "Turning Point",
action = "Turning Point",
speed = 330
},
{
x = destX,
z = destZ,
alt = landingAlt,
type = "Land",
action = "Landing",
speed = 70,
airdromeId = airdromeId
},
}
log("DEBUG: Route table assigned:")
for i, wp in ipairs(route) do
log(string.format(" WP%d: x=%.1f z=%.1f alt=%.1f type=%s action=%s speed=%.1f", i, wp.x, wp.z, wp.alt, wp.type, wp.action or "", wp.speed or 0))
end
local okSet, errSet = pcall(function()
controller:setTask({ id = 'Mission', params = { route = { points = route } } })
end)
if not okSet then
log("ERROR: controller:setTask failed: " .. tostring(errSet))
if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(errSet)))
end
end
log("Assigned custom route to airbase: " .. destination)
if mission.group and mission.group:IsAlive() then
mission.status = "enroute"
mission._pendingStartTime = timer.getTime()
announceToCoalition(coalitionKey, "CARGO aircraft departing (airborne) for " .. destination .. ". Defend it!")
else
mission.status = "failed"
log("Cargo mission failed after route assignment: group not alive: " .. tostring(destination))
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed after assignment!")
end
end
-- Start first attempt after short delay
timer.scheduleFunction(function() assignRouteWithRetries(1, 5) end, {}, timer.getTime() + 2)
end
@ -562,7 +499,7 @@ end
-- Pre-dispatch safety check: ensure destination can accommodate larger transport types
local function destinationHasSuitableParking(destination, preferredTermTypes)
destinationHasSuitableParking = function(destination, preferredTermTypes)
local base = AIRBASE:FindByName(destination)
if not base then return false end
preferredTermTypes = preferredTermTypes or { AIRBASE.TerminalType.OpenBig, AIRBASE.TerminalType.OpenMedOrBig, AIRBASE.TerminalType.OpenMed }
@ -601,27 +538,27 @@ end
--[[
monitorCargoMissions()
--------------------------------------------------------------------------
Monitors all cargo missions, updates their status, and cleans up completed/failed ones.
Handles mission failure after a grace period and mission completion when the group is near the destination airbase.
Monitors all cargo missions, updates their status, and cleans up failed ones.
Handles mission failure after a grace period.
]]
local function monitorCargoMissions()
for _, coalitionKey in ipairs({"red", "blue"}) do
for _, mission in ipairs(cargoMissions[coalitionKey]) do
if mission.group == nil then
log("[DEBUG] Mission group object is nil for mission to " .. tostring(mission.destination))
log("[DEBUG] Mission group object is nil for mission to " .. tostring(mission.destination), true)
else
log("[DEBUG] Mission group: " .. tostring(mission.group:GetName()) .. ", IsAlive(): " .. tostring(mission.group:IsAlive()))
log("[DEBUG] Mission group: " .. tostring(mission.group:GetName()) .. ", IsAlive(): " .. tostring(mission.group:IsAlive()), true)
local dcsGroup = mission.group:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
local pos = units[1]:getPoint()
log(string.format("[DEBUG] Group position: x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z))
log(string.format("[DEBUG] Group position: x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z), true)
else
log("[DEBUG] No units found in DCS group for mission to " .. tostring(mission.destination))
log("[DEBUG] No units found in DCS group for mission to " .. tostring(mission.destination), true)
end
else
log("[DEBUG] DCS group object is nil for mission to " .. tostring(mission.destination))
log("[DEBUG] DCS group object is nil for mission to " .. tostring(mission.destination), true)
end
end
@ -643,41 +580,7 @@ local function monitorCargoMissions()
log("Cargo mission failed (after grace period): " .. (mission.group and mission.group:GetName() or "nil group") .. " to " .. mission.destination)
announceToCoalition(coalitionKey, "Resupply mission to " .. mission.destination .. " failed!")
else
log("DEBUG: Mission appears to still have DCS units despite IsAlive=false; skipping failure for " .. tostring(mission.destination))
end
end
-- Mission completion logic (unchanged)
if mission.status == "enroute" and mission.group and mission.group:IsAlive() then
local destAirbase = AIRBASE:FindByName(mission.destination)
local reached = false
if destAirbase then
-- Prefer native MOOSE helper if available
if mission.group.IsNearAirbase and type(mission.group.IsNearAirbase) == "function" then
reached = mission.group:IsNearAirbase(destAirbase, 3000)
else
-- Fallback: compute distance between group's first unit and airbase coordinate
local dcsGroup = mission.group and mission.group.GetDCSObject and mission.group:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
local pos = units[1]:getPoint()
local destCoord = destAirbase:GetCoordinate():GetVec2()
local dx = pos.x - destCoord.x
local dz = pos.z - destCoord.y
local dist = math.sqrt(dx * dx + dz * dz)
if dist <= 3000 then
reached = true
end
end
end
end
end
if reached then
mission.status = "completed"
local name = (mission.group and (mission.group.GetName and mission.group:GetName() or tostring(mission.group)) ) or "unknown"
log("Cargo mission completed: " .. name .. " delivered to " .. mission.destination)
announceToCoalition(coalitionKey, "Resupply delivered to " .. mission.destination .. "!")
log("DEBUG: Mission appears to still have DCS units despite IsAlive=false; skipping failure for " .. tostring(mission.destination), true)
end
end
end
@ -719,7 +622,7 @@ log("═════════════════════════
-- Example (paste into DCS Lua console):
-- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga")
function _G.TDAC_CargoDispatcher_TestSpawn(templateName, originAirbase, destinationAirbase)
env.info("[TDAC TEST] Starting test spawn for template: " .. tostring(templateName))
log("[TDAC TEST] Starting test spawn for template: " .. tostring(templateName), true)
local ok, err
if type(templateName) ~= 'string' then
env.info("[TDAC TEST] templateName must be a string")
@ -728,19 +631,19 @@ function _G.TDAC_CargoDispatcher_TestSpawn(templateName, originAirbase, destinat
local spawnByName = nil
ok, spawnByName = pcall(function() return SPAWN:New(templateName) end)
if not ok or not spawnByName then
env.info("[TDAC TEST] SPAWN:New failed for template " .. tostring(templateName) .. ". Error: " .. tostring(spawnByName))
if debug and debug.traceback then env.info(debug.traceback(tostring(spawnByName))) end
log("[TDAC TEST] SPAWN:New failed for template " .. tostring(templateName) .. ". Error: " .. tostring(spawnByName), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(spawnByName))), true) end
return false, "spawn_new_failed"
end
spawnByName:OnSpawnGroup(function(spawnedGroup)
env.info("[TDAC TEST] OnSpawnGroup called for: " .. tostring(spawnedGroup:GetName()))
log("[TDAC TEST] OnSpawnGroup called for: " .. tostring(spawnedGroup:GetName()), true)
local dcsGroup = spawnedGroup:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
local pos = units[1]:getPoint()
env.info(string.format("[TDAC TEST] Spawned pos x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z))
log(string.format("[TDAC TEST] Spawned pos x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z), true)
end
end
if destinationAirbase then
@ -748,59 +651,29 @@ function _G.TDAC_CargoDispatcher_TestSpawn(templateName, originAirbase, destinat
local base = AIRBASE:FindByName(destinationAirbase)
if base and spawnedGroup and spawnedGroup.RouteToAirbase then
spawnedGroup:RouteToAirbase(base, AI_Task_Land.Runway)
env.info("[TDAC TEST] RouteToAirbase assigned to " .. tostring(destinationAirbase))
log("[TDAC TEST] RouteToAirbase assigned to " .. tostring(destinationAirbase), true)
else
env.info("[TDAC TEST] RouteToAirbase not available or base not found")
log("[TDAC TEST] RouteToAirbase not available or base not found", true)
end
end)
if not okAssign then
env.info("[TDAC TEST] RouteToAirbase pcall failed: " .. tostring(errAssign))
if debug and debug.traceback then env.info(debug.traceback(tostring(errAssign))) end
log("[TDAC TEST] RouteToAirbase pcall failed: " .. tostring(errAssign), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(errAssign))), true) end
end
end
end)
ok, err = pcall(function() spawnByName:Spawn() end)
if not ok then
env.info("[TDAC TEST] spawnByName:Spawn() failed: " .. tostring(err))
if debug and debug.traceback then env.info(debug.traceback(tostring(err))) end
log("[TDAC TEST] spawnByName:Spawn() failed: " .. tostring(err), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(err))), true) end
return false, "spawn_failed"
end
env.info("[TDAC TEST] spawnByName:Spawn() returned successfully")
log("[TDAC TEST] spawnByName:Spawn() returned successfully", true)
return true
end
-- Global notify API: allow external scripts (e.g. Load2nd) to mark a cargo mission as delivered.
-- Usage: _G.TDAC_CargoDelivered(groupName, destination, coalitionKey)
function _G.TDAC_CargoDelivered(groupName, destination, coalitionKey)
local ok, err = pcall(function()
if type(groupName) ~= 'string' or type(destination) ~= 'string' or type(coalitionKey) ~= 'string' then
log("TDAC notify: invalid parameters to _G.TDAC_CargoDelivered", true)
return false
end
coalitionKey = coalitionKey:lower()
if not cargoMissions or not cargoMissions[coalitionKey] then
log("TDAC notify: no cargoMissions table for coalition '" .. tostring(coalitionKey) .. "'", true)
return false
end
-- Find any mission matching destination and group name (or group name substring) and mark completed.
for _, mission in ipairs(cargoMissions[coalitionKey]) do
local mname = mission.group and (mission.group.GetName and mission.group:GetName() or tostring(mission.group)) or nil
if mission.destination == destination then
if mname and string.find(mname:upper(), groupName:upper(), 1, true) then
mission.status = 'completed'
log("TDAC notify: marked mission " .. tostring(mname) .. " as completed for " .. destination .. " (" .. coalitionKey .. ")")
return true
end
end
end
log("TDAC notify: no matching mission found for group='" .. tostring(groupName) .. "' dest='" .. tostring(destination) .. "' coal='" .. tostring(coalitionKey) .. "'")
return false
end)
if not ok then
log("ERROR: _G.TDAC_CargoDelivered failed: " .. tostring(err), true)
return false
end
return true
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
-- End Moose_TDAC_CargoDispatcher.lua

View File

@ -215,6 +215,9 @@ local ADVANCED_SETTINGS = {
-- Distance from airbase to consider cargo "delivered" via flyover (meters)
-- Aircraft flying within this range will count as supply delivery (no landing required)
cargoLandingDistance = 3000,
-- Distance from airbase to consider a landing as delivered (wheel touchdown)
-- Use a slightly larger radius than 1000m to account for runway offsets from airbase center
cargoLandingEventRadius = 2000,
-- Velocity below which aircraft is considered "landed" (km/h)
cargoLandedVelocity = 5,
@ -224,8 +227,10 @@ local ADVANCED_SETTINGS = {
rtbSpeed = 430, -- Return to base speed (knots)
-- Logging settings
enableDetailedLogging = true, -- Set to false to reduce log spam
enableDetailedLogging = false, -- Set to false to reduce log spam
logPrefix = "[Universal TADC]", -- Prefix for all log messages
-- Proxy/raw-fallback verbose logging (set true to debug proxy behavior)
verboseProxyLogging = false,
}
--[[
@ -259,6 +264,18 @@ squadronAircraftCounts = {
blue = {}
}
-- Aircraft spawn tracking for stuck detection
local aircraftSpawnTracking = {
red = {}, -- groupName -> {spawnPos, spawnTime, squadron, airbase}
blue = {}
}
-- Airbase health status
local airbaseHealthStatus = {
red = {}, -- airbaseName -> "operational"|"stuck-aircraft"|"unusable"
blue = {}
}
-- Logging function
local function log(message, detailed)
if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then
@ -642,44 +659,51 @@ end
-- Process cargo delivery for a squadron
local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coalitionKey)
-- Initialize processed deliveries table
-- Simple delivery processor: dedupe by group ID and credit supplies directly.
if not _G.processedDeliveries then
_G.processedDeliveries = {}
end
-- Create unique delivery key including timestamp to prevent race conditions
-- Note: Key doesn't include airbase to prevent double-counting if aircraft moves between airbases
local deliveryKey = cargoGroup:GetName() .. "_" .. coalitionKey:upper() .. "_" .. cargoGroup:GetID()
if not _G.processedDeliveries[deliveryKey] then
-- Mark delivery as processed immediately to prevent race conditions
_G.processedDeliveries[deliveryKey] = timer.getTime()
-- Process replenishment
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
local maxCount = squadron.aircraft
local newCount = math.min(currentCount + TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
" aircraft to " .. squadron.displayName ..
" (" .. newCount .. "/" .. maxCount .. ")"
log(msg)
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
-- Notify dispatcher (if available) so it can mark the matching mission completed immediately
if type(_G.TDAC_CargoDelivered) == 'function' then
pcall(function()
_G.TDAC_CargoDelivered(cargoGroup:GetName(), squadron.airbaseName, coalitionKey)
end)
end
else
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity"
log(msg, true)
MESSAGE:New(msg, 15):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end
-- Use group ID + squadron airbase as dedupe key to avoid double crediting when the same group
-- triggers multiple events or moves between airbases rapidly.
local okId, grpId = pcall(function() return cargoGroup and cargoGroup.GetID and cargoGroup:GetID() end)
local groupIdStr = (okId and grpId) and tostring(grpId) or "<no-id>"
local deliveryKey = groupIdStr .. "_" .. tostring(squadron.airbaseName)
-- Diagnostic log: show group name, id, and delivery key when processor invoked
local okName, grpName = pcall(function() return cargoGroup and cargoGroup.GetName and cargoGroup:GetName() end)
local groupNameStr = (okName and grpName) and tostring(grpName) or "<no-name>"
log("PROCESS CARGO: invoked for group=" .. groupNameStr .. " id=" .. groupIdStr .. " targetAirbase=" .. tostring(squadron.airbaseName) .. " deliveryKey=" .. deliveryKey, true)
if _G.processedDeliveries[deliveryKey] then
-- Already processed recently, ignore
log("PROCESS CARGO: deliveryKey " .. deliveryKey .. " already processed at " .. tostring(_G.processedDeliveries[deliveryKey]), true)
return
end
-- Mark processed immediately
_G.processedDeliveries[deliveryKey] = timer.getTime()
-- Credit the squadron
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
local maxCount = squadron.aircraft or 0
local addAmount = TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount or 0
local newCount = math.min(currentCount + addAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
" aircraft to " .. (squadron.displayName or squadron.templateName) ..
" (" .. newCount .. "/" .. maxCount .. ")"
log(msg)
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
else
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. (squadron.displayName or squadron.templateName) .. " already at max capacity"
log(msg, true)
MESSAGE:New(msg, 10):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end
end
@ -688,141 +712,582 @@ local cargoEventHandler = {}
function cargoEventHandler:onEvent(event)
if event.id == world.event.S_EVENT_LAND then
local unit = event.initiator
-- Safe unit name retrieval
local unitName = "unknown"
if unit and type(unit) == "table" then
local ok, name = pcall(function() return unit:GetName() end)
if ok and name then
unitName = name
end
end
log("LANDING EVENT: Received S_EVENT_LAND for unit: " .. unitName, true)
if unit and type(unit) == "table" and unit.IsAlive and unit:IsAlive() then
local group = unit:GetGroup()
if group and type(group) == "table" and group.IsAlive and group:IsAlive() then
local cargoName = group:GetName():upper()
-- Safe group name retrieval
local cargoName = "unknown"
local ok, name = pcall(function() return group:GetName():upper() end)
if ok and name then
cargoName = name
end
log("LANDING EVENT: Processing group: " .. cargoName, true)
local isCargoAircraft = false
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
log("LANDING EVENT: Matched cargo pattern '" .. pattern .. "' for " .. cargoName, true)
break
end
end
if isCargoAircraft then
local cargoCoord = unit:GetCoordinate()
local coalitionSide = unit:GetCoalition()
local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue"
-- Safe coordinate and coalition retrieval
local cargoCoord = nil
local ok, coord = pcall(function() return unit:GetCoordinate() end)
if ok and coord then
cargoCoord = coord
end
-- Check which airbase it's near
local squadronConfig = getSquadronConfig(coalitionSide)
for _, squadron in pairs(squadronConfig) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalitionSide then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- If within configured distance of airbase, consider it a landing delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("LANDING DELIVERY: " .. cargoName .. " landed and delivered at " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m)")
processCargoDelivery(group, squadron, coalitionSide, coalitionKey)
log("LANDING EVENT: Cargo aircraft " .. cargoName .. " at coord: " .. tostring(cargoCoord), true)
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
-- Search RED squadron configs
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT: Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
-- Search BLUE squadron configs
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT: Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
if closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
log("LANDING DELIVERY: " .. cargoName .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(group, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED: " .. cargoName .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING DETECTED: " .. cargoName .. " landed but no configured squadron airbases available to check", true)
end
else
log("LANDING EVENT: Could not get coordinates for cargo aircraft " .. cargoName, true)
end
else
log("LANDING EVENT: " .. cargoName .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Group is nil or not alive", true)
end
else
-- Fallback: unit was nil or not alive (race/despawn). Try to retrieve group and name safely
log("LANDING EVENT: Unit is nil or not alive - attempting fallback group retrieval", true)
local fallbackGroup = nil
local okGetGroup, grp = pcall(function()
if unit and type(unit) == "table" and unit.GetGroup then
return unit:GetGroup()
end
-- Try event.initiator (may be raw DCS object)
if event and event.initiator and type(event.initiator) == 'table' and event.initiator.GetGroup then
return event.initiator:GetGroup()
end
return nil
end)
if okGetGroup and grp then
fallbackGroup = grp
end
if fallbackGroup then
-- Try to get group name even if group:IsAlive() is false
local okName, gname = pcall(function() return fallbackGroup:GetName():upper() end)
local cargoName = "unknown"
if okName and gname then
cargoName = gname
end
log("LANDING EVENT (fallback): Processing group: " .. cargoName, true)
local isCargoAircraft = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
log("LANDING EVENT (fallback): Matched cargo pattern '" .. pattern .. "' for " .. cargoName, true)
break
end
end
if isCargoAircraft then
-- Try to get coordinate and coalition via multiple safe methods
local cargoCoord = nil
local okCoord, coord = pcall(function()
if unit and unit.GetCoordinate then return unit:GetCoordinate() end
if fallbackGroup and fallbackGroup.GetCoordinate then return fallbackGroup:GetCoordinate() end
return nil
end)
if okCoord and coord then cargoCoord = coord end
log("LANDING EVENT (fallback): Cargo aircraft " .. cargoName .. " at coord: " .. tostring(cargoCoord), true)
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT (fallback): Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT (fallback): Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
if closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
log("LANDING DELIVERY (fallback): " .. cargoName .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(fallbackGroup, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED (fallback): " .. cargoName .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING EVENT (fallback): No configured squadron airbases available to check", true)
end
else
log("LANDING EVENT (fallback): Could not get coordinates for cargo aircraft " .. cargoName, true)
end
else
log("LANDING EVENT (fallback): " .. cargoName .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Fallback group retrieval failed", true)
-- Additional fallback: try raw DCS object methods (lowercase) and resolve by name
local okRaw, rawGroup = pcall(function()
if event and event.initiator and type(event.initiator) == 'table' and event.initiator.getGroup then
return event.initiator:getGroup()
end
return nil
end)
if okRaw and rawGroup then
-- Try to get raw group name
local okRawName, rawName = pcall(function()
if rawGroup.getName then return rawGroup:getName() end
return nil
end)
if okRawName and rawName then
local rawNameUp = tostring(rawName):upper()
log("LANDING EVENT: Resolved raw DCS group name: " .. rawNameUp, true)
-- Try to find MOOSE GROUP by that name
local okFind, mooseGroup = pcall(function() return GROUP:FindByName(rawNameUp) end)
if okFind and mooseGroup and type(mooseGroup) == 'table' then
log("LANDING EVENT: Found MOOSE GROUP for raw name: " .. rawNameUp, true)
-- Reuse the fallback logic using mooseGroup
local cargoName = rawNameUp
local isCargoAircraft = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
-- Try to get coordinate from raw group if possible
local cargoCoord = nil
local okPoint, point = pcall(function()
if rawGroup.getController then
-- Raw DCS unit list -> first unit point
local dcs = rawGroup
if dcs.getUnits then
local units = dcs:getUnits()
if units and #units > 0 and units[1].getPoint then
return units[1]:getPoint()
end
end
end
return nil
end)
if okPoint and point then cargoCoord = point end
-- If we have a coordinate, find nearest squadron and process
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
if type(cargoCoord) == 'table' and cargoCoord.Get2DDistance then
local okDist, d = pcall(function() return cargoCoord:Get2DDistance(airbase:GetCoordinate()) end)
if okDist and d then distance = d end
else
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
if type(cargoCoord) == 'table' and cargoCoord.Get2DDistance then
local okDist, d = pcall(function() return cargoCoord:Get2DDistance(airbase:GetCoordinate()) end)
if okDist and d then distance = d end
else
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase and closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
log("LANDING DELIVERY (raw-fallback): " .. rawNameUp .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(mooseGroup, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED (raw-fallback): " .. rawNameUp .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING EVENT: Could not extract coordinate from raw DCS group: " .. tostring(rawName), true)
end
else
log("LANDING EVENT: Raw group " .. tostring(rawName) .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Could not find MOOSE GROUP for raw name: " .. tostring(rawName) .. " - attempting raw-group proxy processing", true)
-- Even if we can't find a MOOSE GROUP, try to extract coordinates from the raw DCS group
local okPoint2, point2 = pcall(function()
if rawGroup and rawGroup.getUnits then
local units = rawGroup:getUnits()
if units and #units > 0 and units[1].getPoint then
return units[1]:getPoint()
end
end
return nil
end)
if okPoint2 and point2 then
local cargoCoord = point2
-- Find nearest configured squadron airbase (RED + BLUE)
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase and closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
-- Ensure the raw group name actually looks like a cargo aircraft before crediting
local rawNameUpCheck = tostring(rawName):upper()
local isCargoProxy = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(rawNameUpCheck, pattern) then
isCargoProxy = true
break
end
end
if not isCargoProxy then
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING IGNORED (raw-proxy): " .. tostring(rawName) .. " is not a cargo-type name, skipping delivery proxy", true)
else
log("LANDING IGNORED (raw-proxy): " .. tostring(rawName) .. " is not a cargo-type name, skipping delivery proxy", true)
end
else
-- Build a small proxy object that exposes GetName and GetID so processCargoDelivery can use it
local cargoProxy = {}
function cargoProxy:GetName()
local okn, nm = pcall(function()
if rawGroup and rawGroup.getName then return rawGroup:getName() end
return tostring(rawName)
end)
return (okn and nm) and tostring(nm) or tostring(rawName)
end
function cargoProxy:GetID()
local okid, id = pcall(function()
if rawGroup and rawGroup.getID then return rawGroup:getID() end
if rawGroup and rawGroup.getID == nil and rawGroup.getController then
-- Try to hash name as fallback unique-ish id
return tostring(rawName) .. "_proxy"
end
return nil
end)
return (okid and id) and id or tostring(rawName) .. "_proxy"
end
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING DELIVERY (raw-proxy): " .. tostring(rawName) .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m) - using proxy object", true)
end
processCargoDelivery(cargoProxy, closestSquadron, abCoalition, coalitionKey)
end
else
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING DETECTED (raw-proxy): " .. tostring(rawName) .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")", true)
end
end
else
log("LANDING EVENT: Could not extract coordinate from raw DCS group for proxy processing: " .. tostring(rawName), true)
end
end
else
log("LANDING EVENT: rawGroup:getName() failed", true)
end
else
log("LANDING EVENT: raw DCS group retrieval failed", true)
end
end
end
end
end
-- Monitor cargo aircraft flyovers for squadron replenishment
local function monitorCargoReplenishment()
-- Process RED cargo aircraft
if TADC_SETTINGS.enableRed then
-- Use cached set for performance, create if needed
if not cachedSets.redCargo then
cachedSets.redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart()
end
local redCargo = cachedSets.redCargo
redCargo:ForEach(function(cargoGroup)
if cargoGroup and cargoGroup:IsAlive() then
local cargoName = cargoGroup:GetName():upper()
local isCargoAircraft = false
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
local cargoCoord = cargoGroup:GetCoordinate()
local cargoVelocity = cargoGroup:GetVelocityKMH()
-- DEBUG: log candidate details with timestamp for diagnosis
if ADVANCED_SETTINGS.enableDetailedLogging then
log(string.format("[LOAD2ND DEBUG] Evaluating cargo %s at time=%d vel=%.2f km/h coord=(%.1f,%.1f)",
cargoGroup:GetName(), timer.getTime(), cargoVelocity, cargoCoord:GetVec2().x, cargoCoord:GetVec2().y))
end
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
-- Check which RED airbase it's near
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.RED then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- If within configured distance of airbase, consider it a flyover delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
processCargoDelivery(cargoGroup, squadron, coalition.side.RED, "red")
end
end
end
end
-- Reassign squadron to an alternative airbase when primary airbase has issues
local function reassignSquadronToAlternativeAirbase(squadron, coalitionKey)
local coalitionSide = (coalitionKey == "red") and coalition.side.RED or coalition.side.BLUE
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
local squadronConfig = getSquadronConfig(coalitionSide)
-- Find alternative airbases (other squadrons' airbases that are operational)
local alternativeAirbases = {}
for _, altSquadron in pairs(squadronConfig) do
if altSquadron.airbaseName ~= squadron.airbaseName then
local usable, status = isAirbaseUsable(altSquadron.airbaseName, coalitionSide)
local healthStatus = airbaseHealthStatus[coalitionKey][altSquadron.airbaseName] or "operational"
if usable and healthStatus == "operational" then
table.insert(alternativeAirbases, altSquadron.airbaseName)
end
end)
end
end
-- Process BLUE cargo aircraft
if TADC_SETTINGS.enableBlue then
-- Use cached set for performance, create if needed
if not cachedSets.blueCargo then
cachedSets.blueCargo = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart()
end
local blueCargo = cachedSets.blueCargo
if #alternativeAirbases > 0 then
-- Select random alternative airbase
local newAirbase = alternativeAirbases[math.random(1, #alternativeAirbases)]
blueCargo:ForEach(function(cargoGroup)
if cargoGroup and cargoGroup:IsAlive() then
local cargoName = cargoGroup:GetName():upper()
local isCargoAircraft = false
-- Update squadron configuration (this is a runtime change)
squadron.airbaseName = newAirbase
airbaseHealthStatus[coalitionKey][squadron.airbaseName] = "operational" -- Reset health for new assignment
log("REASSIGNED: " .. coalitionName .. " Squadron " .. squadron.displayName .. " moved from " .. squadron.airbaseName .. " to " .. newAirbase)
MESSAGE:New(coalitionName .. " Squadron " .. squadron.displayName .. " reassigned to " .. newAirbase .. " due to airbase issues", 20):ToCoalition(coalitionSide)
else
log("WARNING: No alternative airbases available for " .. coalitionName .. " Squadron " .. squadron.displayName)
MESSAGE:New("WARNING: No alternative airbases available for " .. squadron.displayName, 30):ToCoalition(coalitionSide)
end
end
-- Monitor for stuck aircraft at airbases
local function monitorStuckAircraft()
local currentTime = timer.getTime()
local stuckThreshold = 300 -- 5 minutes before considering aircraft stuck
local movementThreshold = 50 -- meters - aircraft must move at least this far to not be considered stuck
for _, coalitionKey in ipairs({"red", "blue"}) do
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
for aircraftName, trackingData in pairs(aircraftSpawnTracking[coalitionKey]) do
if trackingData and trackingData.group and trackingData.group:IsAlive() then
local timeSinceSpawn = currentTime - trackingData.spawnTime
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
local cargoCoord = cargoGroup:GetCoordinate()
local cargoVelocity = cargoGroup:GetVelocityKMH()
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
-- Check which BLUE airbase it's near
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.BLUE then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- Only check aircraft that have been spawned for at least the threshold time
if timeSinceSpawn >= stuckThreshold then
local currentPos = trackingData.group:GetCoordinate()
if currentPos and trackingData.spawnPos then
local distanceMoved = trackingData.spawnPos:Get2DDistance(currentPos)
-- Check if aircraft has moved less than threshold (stuck)
if distanceMoved < movementThreshold then
log("STUCK AIRCRAFT DETECTED: " .. aircraftName .. " at " .. trackingData.airbase ..
" has only moved " .. math.floor(distanceMoved) .. "m in " .. math.floor(timeSinceSpawn/60) .. " minutes")
-- If within configured distance of airbase, consider it a flyover delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
processCargoDelivery(cargoGroup, squadron, coalition.side.BLUE, "blue")
end
-- Mark airbase as having stuck aircraft
airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft"
-- Remove the stuck aircraft
trackingData.group:Destroy()
activeInterceptors[coalitionKey][aircraftName] = nil
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
-- Reassign squadron to alternative airbase
reassignSquadronToAlternativeAirbase(trackingData.squadron, coalitionKey)
MESSAGE:New(coalitionName .. " aircraft stuck at " .. trackingData.airbase .. " - destroyed and squadron reassigned", 15):ToCoalition(coalitionKey == "red" and coalition.side.RED or coalition.side.BLUE)
else
-- Aircraft has moved sufficiently, remove from tracking (no longer needs monitoring)
log("Aircraft " .. aircraftName .. " has moved " .. math.floor(distanceMoved) .. "m - removing from stuck monitoring", true)
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end
end
end
else
-- Clean up dead aircraft from tracking
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end
end)
end
end
end
@ -1046,7 +1511,9 @@ local function findBestSquadron(threatCoord, threatSize, coalitionSide)
return selected.squadron, selected.responseRatio, selected.zoneDescription
end
log("No " .. coalitionName .. " squadron available for threat at coordinates")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available for threat at coordinates")
end
return nil, 0, "no available squadrons"
end
@ -1093,7 +1560,9 @@ local function launchInterceptor(threatGroup, coalitionSide)
local squadron, zoneResponseRatio, zoneDescription = findBestSquadron(threatCoord, threatSize, coalitionSide)
if not squadron then
log("No " .. coalitionName .. " squadron available")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available")
end
return
end
@ -1110,7 +1579,9 @@ local function launchInterceptor(threatGroup, coalitionSide)
end
end
if not squadron then
log("No " .. coalitionName .. " squadron available")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available")
end
return
end
@ -1173,11 +1644,25 @@ local function launchInterceptor(threatGroup, coalitionSide)
displayName = squadron.displayName
}
-- Track spawn position for stuck aircraft detection
local spawnPos = interceptor:GetCoordinate()
if spawnPos then
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = {
spawnPos = spawnPos,
spawnTime = timer.getTime(),
squadron = squadron,
airbase = squadron.airbaseName
}
log("Tracking spawn position for " .. interceptor:GetName() .. " at " .. squadron.airbaseName, true)
end
-- Emergency cleanup (safety net)
SCHEDULER:New(nil, function()
if activeInterceptors[coalitionKey][interceptor:GetName()] then
log("Emergency cleanup of " .. coalitionName .. " " .. interceptor:GetName() .. " (should have RTB'd)")
activeInterceptors[coalitionKey][interceptor:GetName()] = nil
-- Also clean up spawn tracking
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = nil
end
end, {}, coalitionSettings.emergencyCleanupTime)
end
@ -1503,13 +1988,55 @@ local function initializeSystem()
end
-- Start schedulers
-- Set up event handler for cargo landing detection
-- Set up event handler for cargo landing detection (handled via MOOSE EVENTHANDLER wrapper below)
-- Re-register world event handler for robust detection (handles raw DCS initiators and race cases)
world.addEventHandler(cargoEventHandler)
-- MOOSE-style EVENTHANDLER wrapper for readability: logs EventData but does NOT delegate to avoid double-processing
if EVENTHANDLER then
local TADC_CARGO_LANDING_HANDLER = EVENTHANDLER:New()
function TADC_CARGO_LANDING_HANDLER:OnEventLand(EventData)
-- Convert MOOSE EventData to raw world.event format and reuse existing handler logic
if ADVANCED_SETTINGS.enableDetailedLogging then
-- Log presence and types of key fields
local function safeName(obj)
if not obj then return "<nil>" end
local ok, n = pcall(function()
if obj.GetName then return obj:GetName() end
if obj.getName then return obj:getName() end
return nil
end)
return (ok and n) and tostring(n) or "<unavailable>"
end
local iniUnitPresent = EventData.IniUnit ~= nil
local iniGroupPresent = EventData.IniGroup ~= nil
local placePresent = EventData.Place ~= nil
local iniUnitName = safeName(EventData.IniUnit)
local iniGroupName = safeName(EventData.IniGroup)
local placeName = safeName(EventData.Place)
log("MOOSE LAND EVENT: IniUnitPresent=" .. tostring(iniUnitPresent) .. ", IniUnitName=" .. tostring(iniUnitName) .. ", IniGroupPresent=" .. tostring(iniGroupPresent) .. ", IniGroupName=" .. tostring(iniGroupName) .. ", PlacePresent=" .. tostring(placePresent) .. ", PlaceName=" .. tostring(placeName), true)
end
local rawEvent = {
id = world.event.S_EVENT_LAND,
initiator = EventData.IniUnit or EventData.IniGroup or nil,
place = EventData.Place or nil,
-- Provide the original EventData for potential fallback use
_moose_original = EventData
}
-- Log and return; the world event handler `cargoEventHandler` will handle the actual processing.
return
end
-- Register the MOOSE handler
TADC_CARGO_LANDING_HANDLER:HandleEvent(EVENTS.Land)
end
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval)
SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval)
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval)
SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, TADC_SETTINGS.cargoCheckInterval)
SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour
-- Start periodic squadron summary broadcast
@ -1706,5 +2233,37 @@ MENU_MISSION_COMMAND:New("Show TADC System Status", menuRoot, function()
MESSAGE:New(status, 20):ToAll()
end)
-- 10. Check for Stuck Aircraft (manual trigger)
MENU_MISSION_COMMAND:New("Check for Stuck Aircraft", menuRoot, function()
monitorStuckAircraft()
MESSAGE:New("Stuck aircraft check completed", 10):ToAll()
end)
-- 11. Show Airbase Health Status
MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function()
local lines = {"Airbase Health Status:"}
for _, coalitionKey in ipairs({"red", "blue"}) do
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
table.insert(lines, coalitionName .. " Coalition:")
for airbaseName, status in pairs(airbaseHealthStatus[coalitionKey]) do
table.insert(lines, " " .. airbaseName .. ": " .. status)
end
end
MESSAGE:New(table.concat(lines, "\n"), 20):ToAll()
end)
-- Initialize airbase health status for all configured airbases
for _, coalitionKey in ipairs({"red", "blue"}) do
local squadronConfig = getSquadronConfig(coalitionKey == "red" and coalition.side.RED or coalition.side.BLUE)
for _, squadron in pairs(squadronConfig) do
if not airbaseHealthStatus[coalitionKey][squadron.airbaseName] then
airbaseHealthStatus[coalitionKey][squadron.airbaseName] = "operational"
end
end
end
-- Set up periodic stuck aircraft monitoring (every 2 minutes)
SCHEDULER:New(nil, monitorStuckAircraft, {}, 120, 120)

View File

@ -1,613 +0,0 @@
-- Simple TADC - Just Works
-- Detect blue aircraft, launch red fighters, make them intercept
-- Configuration
local TADC_CONFIG = {
checkInterval = 30, -- Check for threats every 30 seconds
maxActiveCAP = 24, -- Max fighters airborne at once
squadronCooldown = 900, -- Squadron cooldown after launch (15 minutes)
interceptRatio = 0.8, -- Launch interceptors per threat (see chart below)
}
--[[
INTERCEPT RATIO CHART - How many interceptors launch per threat aircraft:
Threat Size: 1 2 4 8 12 16 (aircraft)
====================================================================
interceptRatio 0.2: 1 1 1 2 3 4 (conservative)
interceptRatio 0.5: 1 1 2 4 6 8 (light response)
interceptRatio 0.8: 1 2 4 7 10 13 (balanced)
interceptRatio 1.0: 1 2 4 8 12 16 (1:1 parity)
interceptRatio 1.2: 2 3 5 10 15 20 (slight advantage)
interceptRatio 1.4: 2 3 6 12 17 23 (good advantage) <- DEFAULT
interceptRatio 1.6: 2 4 7 13 20 26 (strong response)
interceptRatio 1.8: 2 4 8 15 22 29 (overwhelming)
interceptRatio 2.0: 2 4 8 16 24 32 (overkill)
TACTICAL EFFECTS:
0.2-0.5: Minimal response, may be overwhelmed by large formations
0.8-1.0: Realistic parity, balanced dogfights
1.2-1.4: Red advantage, good for challenging blue players
1.6-1.8: Strong defense, difficult penetration
1.9-2.0: Nearly impenetrable, may exhaust squadron pool quickly
SQUADRON IMPACT:
Low ratios (0.2-0.8): Squadrons available longer, sustained defense
High ratios (1.6-2.0): Rapid squadron depletion, gaps in coverage
Sweet spot (1.0-1.4): Balanced response with good coverage duration
--]]
-- Define squadron configurations with their designated airbases and patrol zones
local squadronConfigs = {
-- Fixed-wing fighters patrol RED BORDER zone
{
templateName = "FIGHTER_SWEEP_RED_Kilpyavr",
displayName = "Kilpyavr CAP",
airbaseName = "Kilpyavr",
aircraft = 12, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 15000,
speed = 300,
patrolTime = 20,
type = "FIGHTER"
},
{
templateName = "FIGHTER_SWEEP_RED_Severomorsk-1",
displayName = "Severomorsk-1 CAP",
airbaseName = "Severomorsk-1",
aircraft = 16, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 20000,
speed = 350,
patrolTime = 25,
type = "FIGHTER"
},
{
templateName = "FIGHTER_SWEEP_RED_Severomorsk-3",
displayName = "Severomorsk-3 CAP",
airbaseName = "Severomorsk-3",
aircraft = 14, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 25000,
speed = 400,
patrolTime = 30,
type = "FIGHTER"
},
{
templateName = "FIGHTER_SWEEP_RED_Murmansk",
displayName = "Murmansk CAP",
airbaseName = "Murmansk International",
aircraft = 18, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 18000,
speed = 320,
patrolTime = 22,
type = "FIGHTER"
},
{
templateName = "FIGHTER_SWEEP_RED_Monchegorsk",
displayName = "Monchegorsk CAP",
airbaseName = "Monchegorsk",
aircraft = 10, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 22000,
speed = 380,
patrolTime = 25,
type = "FIGHTER"
},
{
templateName = "FIGHTER_SWEEP_RED_Olenya",
displayName = "Olenya CAP",
airbaseName = "Olenya",
aircraft = 20, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD,
altitude = 30000,
speed = 450,
patrolTime = 35,
type = "FIGHTER"
},
--[[]
-- Helicopter squadron patrols HELO BORDER zone
{
templateName = "HELO_SWEEP_RED_Afrikanda",
displayName = "Afrikanda Helo CAP",
airbaseName = "Afrikanda",
aircraft = 4,
skill = AI.Skill.GOOD,
altitude = 1000,
speed = 150,
patrolTime = 30,
type = "HELICOPTER"
}
--]]
}
-- Track active missions
local activeInterceptors = {}
local lastLaunchTime = {}
local assignedThreats = {} -- Track which threats already have interceptors assigned
local squadronCooldowns = {} -- Track squadron cooldowns after launch
-- Squadron aircraft tracking
local squadronAircraftCounts = {} -- Current available aircraft per squadron
local cargoReplenishmentAmount = 4 -- Aircraft added per cargo delivery
-- Initialize squadron aircraft counts
for _, squadron in pairs(squadronConfigs) do
squadronAircraftCounts[squadron.templateName] = squadron.aircraft
end
-- Simple logging
local function log(message)
env.info("[Simple TADC] " .. message)
end
-- Monitor cargo aircraft landings for squadron replenishment
local function monitorCargoReplenishment()
-- Find all red cargo aircraft
local redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart()
redCargo:ForEach(function(cargoGroup)
if cargoGroup and cargoGroup:IsAlive() then
-- Check if cargo aircraft contains "CARGO" or "TRANSPORT" in name
local cargoName = cargoGroup:GetName():upper()
if string.find(cargoName, "CARGO") or string.find(cargoName, "TRANSPORT") or
string.find(cargoName, "C130") or string.find(cargoName, "C-130") or
string.find(cargoName, "AN26") or string.find(cargoName, "AN-26") then
-- Check if landed at any squadron airbase
local cargoCoord = cargoGroup:GetCoordinate()
local cargoVelocity = cargoGroup:GetVelocityKMH()
-- Consider aircraft "landed" if velocity is very low
if cargoVelocity < 5 then
-- Check which airbase it's near
for _, squadron in pairs(squadronConfigs) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.RED then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- If within 3km of airbase, consider it a delivery
if distance < 3000 then
-- Check if we haven't already processed this delivery
local deliveryKey = cargoName .. "_" .. squadron.airbaseName
if not _G.processedDeliveries then
_G.processedDeliveries = {}
end
if not _G.processedDeliveries[deliveryKey] then
-- Process replenishment
local currentCount = squadronAircraftCounts[squadron.templateName] or 0
local maxCount = squadron.aircraft
local newCount = math.min(currentCount + cargoReplenishmentAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[squadron.templateName] = newCount
log("CARGO DELIVERY: " .. cargoName .. " delivered " .. actualAdded ..
" aircraft to " .. squadron.displayName ..
" (" .. newCount .. "/" .. maxCount .. ")")
-- Mark delivery as processed
_G.processedDeliveries[deliveryKey] = timer.getTime()
else
log("CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity")
end
end
end
end
end
end
end
end
end)
end
-- Send interceptor back to base
local function sendInterceptorHome(interceptor)
if not interceptor or not interceptor:IsAlive() then
return
end
-- Find nearest friendly airbase
local interceptorCoord = interceptor:GetCoordinate()
local nearestAirbase = nil
local shortestDistance = math.huge
-- Check all squadron airbases to find the nearest one that's still friendly
for _, squadron in pairs(squadronConfigs) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.RED and airbase:IsAlive() then
local airbaseCoord = airbase:GetCoordinate()
local distance = interceptorCoord:Get2DDistance(airbaseCoord)
if distance < shortestDistance then
shortestDistance = distance
nearestAirbase = airbase
end
end
end
if nearestAirbase then
local airbaseCoord = nearestAirbase:GetCoordinate()
local rtbAltitude = 3000 -- RTB at 3000 feet
local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude * 0.3048) -- Convert feet to meters
-- Clear current tasks and route home
interceptor:ClearTasks()
interceptor:RouteAirTo(rtbCoord, 250 * 0.5144, "BARO") -- RTB at 250 knots
log("Sending " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName())
-- Schedule cleanup after they should have landed (give them time to get home)
local flightTime = math.ceil(shortestDistance / (250 * 0.5144)) + 300 -- Flight time + 5 min buffer
SCHEDULER:New(nil, function()
if activeInterceptors[interceptor:GetName()] then
activeInterceptors[interceptor:GetName()] = nil
log("Cleaned up " .. interceptor:GetName() .. " after RTB")
end
end, {}, flightTime)
else
log("No friendly airbase found for " .. interceptor:GetName() .. ", will clean up normally")
end
end
-- Check if airbase is still usable
local function isAirbaseUsable(airbaseName)
local airbase = AIRBASE:FindByName(airbaseName)
if not airbase then
return false, "not found"
elseif airbase:GetCoalition() ~= coalition.side.RED then
return false, "captured by " .. (airbase:GetCoalition() == coalition.side.BLUE and "Blue" or "Neutral")
elseif not airbase:IsAlive() then
return false, "destroyed"
else
return true, "operational"
end
end
-- Count active red fighters
local function countActiveFighters()
local count = 0
for _, interceptorData in pairs(activeInterceptors) do
if interceptorData and interceptorData.group and interceptorData.group:IsAlive() then
count = count + interceptorData.group:GetSize()
end
end
return count
end
-- Find best squadron to launch
local function findBestSquadron(threatCoord)
local bestSquadron = nil
local shortestDistance = math.huge
local currentTime = timer.getTime()
for _, squadron in pairs(squadronConfigs) do
-- Check if squadron is on cooldown
local squadronAvailable = true
if squadronCooldowns[squadron.templateName] then
local cooldownEnd = squadronCooldowns[squadron.templateName]
if currentTime < cooldownEnd then
local timeLeft = math.ceil((cooldownEnd - currentTime) / 60)
log("Squadron " .. squadron.displayName .. " on cooldown for " .. timeLeft .. " more minutes")
squadronAvailable = false
else
-- Cooldown expired, remove it
squadronCooldowns[squadron.templateName] = nil
log("Squadron " .. squadron.displayName .. " cooldown expired, available for launch")
end
end
if squadronAvailable then
-- Check if squadron has available aircraft
local availableAircraft = squadronAircraftCounts[squadron.templateName] or 0
if availableAircraft <= 0 then
log("Squadron " .. squadron.displayName .. " has no aircraft available (" .. availableAircraft .. "/" .. squadron.aircraft .. ")")
squadronAvailable = false
end
end
if squadronAvailable then
-- Check if airbase is still under Red control
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if not airbase then
log("Warning: Airbase " .. squadron.airbaseName .. " not found")
elseif airbase:GetCoalition() ~= coalition.side.RED then
log("Warning: Airbase " .. squadron.airbaseName .. " no longer under Red control")
elseif not airbase:IsAlive() then
log("Warning: Airbase " .. squadron.airbaseName .. " is destroyed")
else
-- Airbase is valid, check if squadron can spawn
local spawn = SPAWN:New(squadron.templateName)
if spawn then
-- Get squadron's airbase
local template = GROUP:FindByName(squadron.templateName)
if template then
local airbaseCoord = template:GetCoordinate()
if airbaseCoord then
local distance = airbaseCoord:Get2DDistance(threatCoord)
if distance < shortestDistance then
shortestDistance = distance
bestSquadron = squadron
end
end
end
end
end
end
end
return bestSquadron
end
-- Launch interceptor
local function launchInterceptor(threatGroup)
if not threatGroup or not threatGroup:IsAlive() then
return
end
local threatCoord = threatGroup:GetCoordinate()
local threatName = threatGroup:GetName()
local threatSize = threatGroup:GetSize() -- Get the number of aircraft in the threat group
-- Check if threat already has interceptors assigned
if assignedThreats[threatName] then
local assignedInterceptors = assignedThreats[threatName]
local aliveCount = 0
-- Check if assigned interceptors are still alive
if type(assignedInterceptors) == "table" then
for _, interceptor in pairs(assignedInterceptors) do
if interceptor and interceptor:IsAlive() then
aliveCount = aliveCount + 1
end
end
else
-- Handle legacy single interceptor assignment
if assignedInterceptors and assignedInterceptors:IsAlive() then
aliveCount = 1
end
end
if aliveCount > 0 then
return -- Still being intercepted
else
-- All interceptors are dead, clear the assignment
assignedThreats[threatName] = nil
end
end
-- Calculate how many interceptors to launch (at least match threat size, up to ratio)
local interceptorsNeeded = math.max(threatSize, math.ceil(threatSize * TADC_CONFIG.interceptRatio))
-- Check if we have capacity
if countActiveFighters() + interceptorsNeeded > TADC_CONFIG.maxActiveCAP then
interceptorsNeeded = TADC_CONFIG.maxActiveCAP - countActiveFighters()
if interceptorsNeeded <= 0 then
log("Max fighters airborne, skipping launch")
return
end
end
-- Find best squadron
local squadron = findBestSquadron(threatCoord)
if not squadron then
log("No squadron available")
return
end
-- Limit interceptors to available aircraft
local availableAircraft = squadronAircraftCounts[squadron.templateName] or 0
interceptorsNeeded = math.min(interceptorsNeeded, availableAircraft)
if interceptorsNeeded <= 0 then
log("Squadron " .. squadron.displayName .. " has no aircraft to launch")
return
end
-- Launch multiple interceptors to match threat
local spawn = SPAWN:New(squadron.templateName)
local interceptors = {}
for i = 1, interceptorsNeeded do
local interceptor = spawn:Spawn()
if interceptor then
table.insert(interceptors, interceptor)
-- Wait a moment for initialization
SCHEDULER:New(nil, function()
if interceptor and interceptor:IsAlive() then
-- Set aggressive AI
interceptor:OptionROEOpenFire()
interceptor:OptionROTVertical()
-- Route to threat
local currentThreatCoord = threatGroup:GetCoordinate()
if currentThreatCoord then
local interceptCoord = currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) -- Convert feet to meters
interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") -- Convert kts to m/s
-- Attack the threat
local attackTask = {
id = 'AttackGroup',
params = {
groupId = threatGroup:GetID(),
weaponType = 'Auto',
attackQtyLimit = 0,
priority = 1
}
}
interceptor:PushTask(attackTask, 1)
end
end
end, {}, 3)
-- Track the interceptor with squadron info
activeInterceptors[interceptor:GetName()] = {
group = interceptor,
squadron = squadron.templateName,
displayName = squadron.displayName
}
-- Emergency cleanup (safety net - should normally RTB before this)
SCHEDULER:New(nil, function()
if activeInterceptors[interceptor:GetName()] then
log("Emergency cleanup of " .. interceptor:GetName() .. " (should have RTB'd)")
activeInterceptors[interceptor:GetName()] = nil
end
end, {}, 7200) -- Emergency cleanup after 2 hours
end
end
-- Log the launch and track assignment
if #interceptors > 0 then
-- Decrement squadron aircraft count
local currentCount = squadronAircraftCounts[squadron.templateName] or 0
squadronAircraftCounts[squadron.templateName] = math.max(0, currentCount - #interceptors)
local remainingCount = squadronAircraftCounts[squadron.templateName]
log("Launched " .. #interceptors .. " x " .. squadron.displayName .. " to intercept " ..
threatSize .. " x " .. threatName .. " (Remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")")
assignedThreats[threatName] = interceptors -- Track which interceptors are assigned to this threat
lastLaunchTime[threatName] = timer.getTime()
-- Apply cooldown immediately when squadron launches
local currentTime = timer.getTime()
squadronCooldowns[squadron.templateName] = currentTime + TADC_CONFIG.squadronCooldown
local cooldownMinutes = TADC_CONFIG.squadronCooldown / 60
log("Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown")
end
end
-- Main threat detection loop
local function detectThreats()
log("Scanning for threats...")
-- Clean up dead threats from tracking
local currentThreats = {}
-- Find all blue aircraft
local blueAircraft = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart()
local threatCount = 0
blueAircraft:ForEach(function(blueGroup)
if blueGroup and blueGroup:IsAlive() then
threatCount = threatCount + 1
currentThreats[blueGroup:GetName()] = true
log("Found threat: " .. blueGroup:GetName() .. " (" .. blueGroup:GetTypeName() .. ")")
-- Launch interceptor for this threat
launchInterceptor(blueGroup)
end
end)
-- Clean up assignments for threats that no longer exist and send interceptors home
for threatName, assignedInterceptors in pairs(assignedThreats) do
if not currentThreats[threatName] then
log("Threat " .. threatName .. " eliminated, sending interceptors home...")
-- Send assigned interceptors back to base
if type(assignedInterceptors) == "table" then
for _, interceptor in pairs(assignedInterceptors) do
if interceptor and interceptor:IsAlive() then
sendInterceptorHome(interceptor)
end
end
else
-- Handle legacy single interceptor assignment
if assignedInterceptors and assignedInterceptors:IsAlive() then
sendInterceptorHome(assignedInterceptors)
end
end
assignedThreats[threatName] = nil
end
end
-- Count assigned threats
local assignedCount = 0
for _ in pairs(assignedThreats) do assignedCount = assignedCount + 1 end
log("Scan complete: " .. threatCount .. " threats, " .. countActiveFighters() .. " active fighters, " ..
assignedCount .. " assigned")
end
-- Monitor interceptor groups for cleanup when destroyed
local function monitorInterceptors()
-- Check all active interceptors for cleanup
for interceptorName, interceptorData in pairs(activeInterceptors) do
if interceptorData and interceptorData.group then
if not interceptorData.group:IsAlive() then
-- Interceptor group is destroyed - just clean up tracking
local displayName = interceptorData.displayName
log("Interceptor from " .. displayName .. " destroyed: " .. interceptorName)
-- Remove from active tracking
activeInterceptors[interceptorName] = nil
end
end
end
end
-- Periodic airbase status check
local function checkAirbaseStatus()
log("=== AIRBASE STATUS REPORT ===")
local usableCount = 0
local currentTime = timer.getTime()
for _, squadron in pairs(squadronConfigs) do
local usable, status = isAirbaseUsable(squadron.airbaseName)
-- Add aircraft count to status
local aircraftCount = squadronAircraftCounts[squadron.templateName] or 0
local maxAircraft = squadron.aircraft
local aircraftStatus = " Aircraft: " .. aircraftCount .. "/" .. maxAircraft
-- Check if squadron is on cooldown
local cooldownStatus = ""
if squadronCooldowns[squadron.templateName] then
local cooldownEnd = squadronCooldowns[squadron.templateName]
if currentTime < cooldownEnd then
local timeLeft = math.ceil((cooldownEnd - currentTime) / 60)
cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)"
end
end
local fullStatus = status .. aircraftStatus .. cooldownStatus
if usable and cooldownStatus == "" and aircraftCount > 0 then
usableCount = usableCount + 1
log("" .. squadron.airbaseName .. " - " .. fullStatus)
else
log("" .. squadron.airbaseName .. " - " .. fullStatus)
end
end
log("Status: " .. usableCount .. "/" .. #squadronConfigs .. " airbases operational")
end
-- Start the system
log("Simple TADC starting...")
log("Squadrons configured: " .. #squadronConfigs)
-- Run detection every interval
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_CONFIG.checkInterval)
-- Run interceptor monitoring every 30 seconds
SCHEDULER:New(nil, monitorInterceptors, {}, 10, 30)
-- Run airbase status check every 2 minutes
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, 120)
-- Monitor cargo aircraft for squadron replenishment every 15 seconds
SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, 15)
log("Simple TADC operational!")
log("Aircraft replenishment: " .. cargoReplenishmentAmount .. " aircraft per cargo delivery")
-- Log initial squadron aircraft counts
for _, squadron in pairs(squadronConfigs) do
local count = squadronAircraftCounts[squadron.templateName]
log("Initial: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft")
end

View File

@ -0,0 +1,679 @@
--[[
Moose_TDAC_CargoDispatcher.lua
Automated Logistics System for TADC Squadron Replenishment
DESCRIPTION:
This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system.
CONFIGURATION:
- Update static templates and airfield lists as needed for your mission.
- Set thresholds and supply airfields in CARGO_SUPPLY_CONFIG.
- Replace static templates with actual group templates from the mission editor for realism.
REQUIRES:
- MOOSE framework (for SPAWN, AIRBASE, etc.)
- Optional: MIST for deep copy of templates
]]
--[[
GLOBAL STATE AND CONFIGURATION
--------------------------------------------------------------------------
Tracks all active cargo missions and dispatcher configuration.
]]
if not cargoMissions then
cargoMissions = { red = {}, blue = {} }
end
-- Dispatcher config (interval in seconds)
if not DISPATCHER_CONFIG then
-- default interval (seconds) and a slightly larger grace period to account for slow servers/networks
DISPATCHER_CONFIG = { interval = 60, gracePeriod = 25 }
end
-- Safety flag: when false, do NOT fall back to spawning from in-memory template tables.
-- Set to true if you understand the tweaked-template warning and accept the risk.
if DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE == nil then
DISPATCHER_CONFIG.ALLOW_FALLBACK_TO_INMEM_TEMPLATE = false
end
--[[
CARGO SUPPLY CONFIGURATION
--------------------------------------------------------------------------
Set supply airfields, cargo template names, and resupply thresholds for each coalition.
]]
local CARGO_SUPPLY_CONFIG = {
red = {
supplyAirfields = { "Sochi-Adler", "Gudauta", "Sukhumi-Babushara", "Nalchik", "Beslan", "Maykop-Khanskaya" }, -- replace with your RED supply airbase names
cargoTemplate = "CARGO_RED_AN26", -- replace with your RED cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
},
blue = {
supplyAirfields = { "Batumi", "Kobuleti", "Senaki-Kolkhi", "Kutaisi", "Soganlug" }, -- replace with your BLUE supply airbase names
cargoTemplate = "CARGO_BLUE_C130", -- replace with your BLUE cargo aircraft template name
threshold = 0.90 -- ratio below which to trigger resupply (testing)
}
}
--[[
UTILITY STUBS
--------------------------------------------------------------------------
selectRandomAirfield: Picks a random airfield from a list.
announceToCoalition: Stub for in-game coalition messaging.
Replace with your own logic as needed.
]]
if not selectRandomAirfield then
function selectRandomAirfield(airfieldList)
if type(airfieldList) == "table" and #airfieldList > 0 then
return airfieldList[math.random(1, #airfieldList)]
end
return nil
end
end
-- Stub for announceToCoalition (replace with your own logic if needed)
if not announceToCoalition then
function announceToCoalition(coalitionKey, message)
-- Replace with actual in-game message logic
env.info("[ANNOUNCE] [" .. tostring(coalitionKey) .. "]: " .. tostring(message))
end
end
--[[
LOGGING
--------------------------------------------------------------------------
Advanced logging configuration and helper function for debug output.
]]
local ADVANCED_LOGGING = {
enableDetailedLogging = false,
logPrefix = "[TDAC Cargo]"
}
-- Logging function (must be defined before any log() calls)
local function log(message, detailed)
if not detailed or ADVANCED_LOGGING.enableDetailedLogging then
env.info(ADVANCED_LOGGING.logPrefix .. " " .. message)
end
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
log("Moose_TDAC_CargoDispatcher.lua loaded.", true)
log("═══════════════════════════════════════════════════════════════════════════════", true)
-- Provide a safe deepCopy if MIST is not available
local function deepCopy(obj)
if type(obj) ~= 'table' then return obj end
local res = {}
for k, v in pairs(obj) do
if type(v) == 'table' then
res[k] = deepCopy(v)
else
res[k] = v
end
end
return res
end
-- Dispatch cooldown per airbase (seconds) to avoid repeated immediate retries
local CARGO_DISPATCH_COOLDOWN = DISPATCHER_CONFIG and DISPATCHER_CONFIG.cooldown or 300 -- default 5 minutes
local lastDispatchAttempt = { red = {}, blue = {} }
local function getCoalitionSide(coalitionKey)
if coalitionKey == 'blue' then return coalition.side.BLUE end
if coalitionKey == 'red' then return coalition.side.RED end
return nil
end
-- Forward-declare parking check helper so functions defined earlier can call it
local destinationHasSuitableParking
-- Validate dispatcher configuration: check that supply airfields exist and templates appear valid
local function validateDispatcherConfig()
local problems = {}
-- Check supply airfields exist
for coalitionKey, cfg in pairs(CARGO_SUPPLY_CONFIG) do
if cfg and cfg.supplyAirfields and type(cfg.supplyAirfields) == 'table' then
for _, abName in ipairs(cfg.supplyAirfields) do
local ok, ab = pcall(function() return AIRBASE:FindByName(abName) end)
if not ok or not ab then
table.insert(problems, string.format("Missing airbase for %s supply list: '%s'", tostring(coalitionKey), tostring(abName)))
end
end
else
table.insert(problems, string.format("Missing or invalid supplyAirfields for coalition '%s'", tostring(coalitionKey)))
end
-- Check cargo template presence (best-effort using SPAWN:New if available)
if cfg and cfg.cargoTemplate and type(cfg.cargoTemplate) == 'string' and cfg.cargoTemplate ~= '' then
local okSpawn, spawnObj = pcall(function() return SPAWN:New(cfg.cargoTemplate) end)
if not okSpawn or not spawnObj then
-- SPAWN:New may not be available at load time; warn but don't fail hard
table.insert(problems, string.format("Cargo template suspicious or missing: '%s' (coalition: %s)", tostring(cfg.cargoTemplate), tostring(coalitionKey)))
end
else
table.insert(problems, string.format("Missing cargoTemplate for coalition '%s'", tostring(coalitionKey)))
end
end
if #problems == 0 then
log("TDAC Dispatcher config validation passed ✓", true)
MESSAGE:New("TDAC Dispatcher config validation passed ✓", 15):ToAll()
return true, {}
else
log("TDAC Dispatcher config validation found issues:", true)
MESSAGE:New("TDAC Dispatcher config validation found issues:" .. table.concat(problems, ", "), 15):ToAll()
for _, p in ipairs(problems) do
log("" .. p, true)
end
return false, problems
end
end
-- Expose console helper to run the check manually
function _G.TDAC_RunConfigCheck()
local ok, problems = validateDispatcherConfig()
if ok then
return true, "OK"
else
return false, problems
end
end
--[[
getSquadronStatus(squadron, coalitionKey)
--------------------------------------------------------------------------
Returns the current, max, and ratio of aircraft for a squadron.
If you track current aircraft in a table, update this logic accordingly.
Returns: currentCount, maxCount, ratio
]]
local function getSquadronStatus(squadron, coalitionKey)
local current = squadron.current or squadron.count or squadron.aircraft or 0
local max = squadron.max or squadron.aircraft or 1
if squadron.templateName and _G.squadronAircraftCounts and _G.squadronAircraftCounts[coalitionKey] then
current = _G.squadronAircraftCounts[coalitionKey][squadron.templateName] or current
end
local ratio = (max > 0) and (current / max) or 0
return current, max, ratio
end
--[[
hasActiveCargoMission(coalitionKey, airbaseName)
--------------------------------------------------------------------------
Returns true if there is an active (not completed/failed) cargo mission for the given airbase.
]]
local function hasActiveCargoMission(coalitionKey, airbaseName)
for _, mission in pairs(cargoMissions[coalitionKey]) do
if mission.destination == airbaseName then
-- Ignore completed or failed missions
if mission.status == "completed" or mission.status == "failed" then
-- not active
else
-- Consider mission active only if the group is alive OR we're still within the grace window
local stillActive = false
if mission.group and mission.group.IsAlive and mission.group:IsAlive() then
stillActive = true
else
local pending = mission._pendingStartTime
local grace = mission._gracePeriod or DISPATCHER_CONFIG.gracePeriod or 8
if pending and (timer.getTime() - pending) <= grace then
stillActive = true
end
end
if stillActive then
log("Active cargo mission found for " .. airbaseName .. " (" .. coalitionKey .. ")")
return true
end
end
end
end
log("No active cargo mission for " .. airbaseName .. " (" .. coalitionKey .. ")")
return false
end
--[[
trackCargoMission(coalitionKey, mission)
--------------------------------------------------------------------------
Adds a new cargo mission to the tracking table and logs it.
]]
local function trackCargoMission(coalitionKey, mission)
table.insert(cargoMissions[coalitionKey], mission)
log("Tracking new cargo mission: " .. (mission.group and mission.group:GetName() or "nil group") .. " from " .. mission.origin .. " to " .. mission.destination)
end
--[[
cleanupCargoMissions()
--------------------------------------------------------------------------
Removes failed cargo missions from the tracking table if their group is no longer alive.
]]
local function cleanupCargoMissions()
for _, coalitionKey in ipairs({"red", "blue"}) do
for i = #cargoMissions[coalitionKey], 1, -1 do
local m = cargoMissions[coalitionKey][i]
if m.status == "failed" then
if not (m.group and m.group:IsAlive()) then
log("Cleaning up failed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: failed")
table.remove(cargoMissions[coalitionKey], i)
end
end
end
end
end
--[[
dispatchCargo(squadron, coalitionKey)
--------------------------------------------------------------------------
Spawns a cargo aircraft from a supply airfield to the destination squadron airbase.
Uses static templates for each coalition, assigns a unique group name, and sets a custom route.
Tracks the mission and schedules route assignment with a delay to ensure group is alive.
]]
local function dispatchCargo(squadron, coalitionKey)
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
local origin
local attempts = 0
local maxAttempts = 10
repeat
origin = selectRandomAirfield(config.supplyAirfields)
attempts = attempts + 1
-- Ensure origin is not the same as destination
if origin == squadron.airbaseName then
origin = nil
end
until origin or attempts >= maxAttempts
-- enforce cooldown per destination to avoid immediate retries
lastDispatchAttempt[coalitionKey] = lastDispatchAttempt[coalitionKey] or {}
local last = lastDispatchAttempt[coalitionKey][squadron.airbaseName]
if last and (timer.getTime() - last) < CARGO_DISPATCH_COOLDOWN then
log("Skipping dispatch to " .. squadron.airbaseName .. " (cooldown active)")
return
end
if not origin then
log("No valid origin airfield found for cargo dispatch to " .. squadron.airbaseName .. " (avoiding same origin/destination)")
return
end
local destination = squadron.airbaseName
local cargoTemplate = config.cargoTemplate
-- Safety: check if destination has suitable parking for larger transports. If not, warn in log.
local okParking = true
-- Only check for likely large transports (C-130 / An-26 are large-ish) — keep conservative
if cargoTemplate and (string.find(cargoTemplate:upper(), "C130") or string.find(cargoTemplate:upper(), "C-17") or string.find(cargoTemplate:upper(), "C17") or string.find(cargoTemplate:upper(), "AN26") ) then
okParking = destinationHasSuitableParking(destination)
if not okParking then
log("WARNING: Destination '" .. tostring(destination) .. "' may not have suitable parking for " .. tostring(cargoTemplate) .. ". Skipping dispatch to prevent despawn.")
return
end
end
local groupName = cargoTemplate .. "_to_" .. destination .. "_" .. math.random(1000,9999)
log("Dispatching cargo: " .. groupName .. " from " .. origin .. " to " .. destination)
-- Spawn cargo aircraft at origin using the template name ONLY for SPAWN
-- Note: cargoTemplate is a config string; script uses in-file Lua template tables (CARGO_AIRCRAFT_TEMPLATE_*)
log("DEBUG: Attempting spawn for group: '" .. groupName .. "' at airbase: '" .. origin .. "' (using in-file Lua template)", true)
local airbaseObj = AIRBASE:FindByName(origin)
if not airbaseObj then
log("ERROR: AIRBASE:FindByName failed for '" .. tostring(origin) .. "'. Airbase object is nil!")
else
log("DEBUG: AIRBASE object found for '" .. origin .. "'. Proceeding with spawn.", true)
end
-- Select the correct template based on coalition
local templateBase, uniqueGroupName
if coalitionKey == "blue" then
templateBase = CARGO_AIRCRAFT_TEMPLATE_BLUE
uniqueGroupName = "CARGO_C130_DYNAMIC_" .. math.random(1000,9999)
else
templateBase = CARGO_AIRCRAFT_TEMPLATE_RED
uniqueGroupName = "CARGO_AN26_DYNAMIC_" .. math.random(1000,9999)
end
-- Clone the template and set the group/unit name
-- Prepare a mission placeholder. We'll set the group and spawnPos after successful spawn.
local mission = {
group = nil,
origin = origin,
destination = destination,
squadron = squadron,
status = "pending",
-- Anchor a pending start time now to avoid the monitor loop expiring a mission
-- before MOOSE has a chance to finalize the OnSpawnGroup callback.
_pendingStartTime = timer.getTime(),
_spawnPos = nil,
_gracePeriod = DISPATCHER_CONFIG.gracePeriod or 8
}
-- Helper to finalize mission after successful spawn
local function finalizeMissionAfterSpawn(spawnedGroup, spawnPos)
mission.group = spawnedGroup
mission._spawnPos = spawnPos
trackCargoMission(coalitionKey, mission)
lastDispatchAttempt[coalitionKey][squadron.airbaseName] = timer.getTime()
end
-- MOOSE-only spawn-by-name flow
if type(cargoTemplate) ~= 'string' or cargoTemplate == '' then
log("ERROR: cargoTemplate for coalition '" .. tostring(coalitionKey) .. "' must be a valid mission template name string. Aborting dispatch.")
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " aborted (invalid cargo template)!")
return
end
-- Use a per-dispatch RAT object to spawn and route cargo aircraft.
-- Create a unique alias to avoid naming collisions and let RAT handle routing/landing.
local alias = cargoTemplate .. "_TO_" .. destination .. "_" .. tostring(math.random(1000,9999))
log("DEBUG: Attempting RAT spawn for template: '" .. cargoTemplate .. "' alias: '" .. alias .. "'", true)
local okNew, rat = pcall(function() return RAT:New(cargoTemplate, alias) end)
if not okNew or not rat then
log("ERROR: RAT:New failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(rat))
if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(rat)), true)
end
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn init error)!")
return
end
-- Configure RAT for a single, non-respawning dispatch
rat:SetDeparture(origin)
rat:SetDestination(destination)
rat:NoRespawn()
rat:SetSpawnLimit(1)
rat:SetSpawnDelay(1)
-- Ensure RAT takes off immediately from the runway (hot start) instead of staying parked
if rat.SetTakeoffHot then rat:SetTakeoffHot() end
-- Ensure RAT will look for parking and not despawn the group immediately on landing.
-- This makes the group taxi to parking and come to a stop so other scripts (e.g. Load2nd)
-- that detect parked/stopped cargo aircraft can register the delivery.
if rat.SetParkingScanRadius then rat:SetParkingScanRadius(80) end
if rat.SetParkingSpotSafeON then rat:SetParkingSpotSafeON() end
if rat.SetDespawnAirOFF then rat:SetDespawnAirOFF() end
-- Check on runway to ensure proper landing behavior (distance in meters)
if rat.CheckOnRunway then rat:CheckOnRunway(true, 75) end
rat:OnSpawnGroup(function(spawnedGroup)
-- Mark the canonical start time when MOOSE reports the group exists
mission._pendingStartTime = timer.getTime()
local spawnPos = nil
local dcsGroup = spawnedGroup:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
spawnPos = units[1]:getPoint()
end
end
log("RAT spawned cargo aircraft group: " .. tostring(spawnedGroup:GetName()))
-- Temporary debug: log group state every 10s for 10 minutes to trace landing/parking behavior
local debugChecks = 60 -- 60 * 10s = 10 minutes
local checkInterval = 10
local function debugLogState(iter)
if iter > debugChecks then return end
local ok, err = pcall(function()
local name = spawnedGroup:GetName()
local dcs = spawnedGroup:GetDCSObject()
if dcs then
local units = dcs:getUnits()
if units and #units > 0 then
local u = units[1]
local pos = u:getPoint()
-- Use dot accessor to test for function existence; colon-call to invoke
local vel = (u.getVelocity and u:getVelocity()) or {x=0,y=0,z=0}
local speed = math.sqrt((vel.x or 0)^2 + (vel.y or 0)^2 + (vel.z or 0)^2)
local controller = dcs:getController()
local airbaseObj = AIRBASE:FindByName(destination)
local dist = nil
if airbaseObj then
local dest = airbaseObj:GetCoordinate():GetVec2()
local dx = pos.x - dest.x
local dz = pos.z - dest.y
dist = math.sqrt(dx*dx + dz*dz)
end
log(string.format("[TDAC DEBUG] %s state check %d: alive=%s pos=(%.1f,%.1f) speed=%.2f m/s distToDest=%s", name, iter, tostring(spawnedGroup:IsAlive()), pos.x or 0, pos.z or 0, speed, tostring(dist)), true)
else
log(string.format("[TDAC DEBUG] %s state check %d: DCS group has no units", tostring(spawnedGroup:GetName()), iter), true)
end
else
log(string.format("[TDAC DEBUG] %s state check %d: no DCS group object", tostring(spawnedGroup:GetName()), iter), true)
end
end)
if not ok then
log("[TDAC DEBUG] Error during debugLogState: " .. tostring(err), true)
end
timer.scheduleFunction(function() debugLogState(iter + 1) end, {}, timer.getTime() + checkInterval)
end
timer.scheduleFunction(function() debugLogState(1) end, {}, timer.getTime() + checkInterval)
-- RAT should handle routing/taxi/parking. Finalize mission tracking now.
finalizeMissionAfterSpawn(spawnedGroup, spawnPos)
mission.status = "enroute"
mission._pendingStartTime = timer.getTime()
announceToCoalition(coalitionKey, "CARGO aircraft departing (airborne) for " .. destination .. ". Defend it!")
end)
local okSpawn, errSpawn = pcall(function() rat:Spawn(1) end)
if not okSpawn then
log("ERROR: rat:Spawn() failed for template '" .. tostring(cargoTemplate) .. "'. Error: " .. tostring(errSpawn))
if debug and debug.traceback then
log("TRACEBACK: " .. tostring(debug.traceback(errSpawn)), true)
end
return
end
end
-- Parking diagnostics helper
-- Call from DCS console: _G.TDAC_LogAirbaseParking("Luostari Pechenga")
function _G.TDAC_LogAirbaseParking(airbaseName)
if type(airbaseName) ~= 'string' then
log("TDAC Parking helper: airbaseName must be a string", true)
return false
end
local base = AIRBASE:FindByName(airbaseName)
if not base then
log("TDAC Parking helper: AIRBASE:FindByName returned nil for '" .. tostring(airbaseName) .. "'", true)
return false
end
local function spotsFor(term)
local ok, n = pcall(function() return base:GetParkingSpotsNumber(term) end)
if not ok then return nil end
return n
end
local openBig = spotsFor(AIRBASE.TerminalType.OpenBig)
local openMed = spotsFor(AIRBASE.TerminalType.OpenMed)
local openMedOrBig = spotsFor(AIRBASE.TerminalType.OpenMedOrBig)
local runway = spotsFor(AIRBASE.TerminalType.Runway)
log(string.format("TDAC Parking: %s -> OpenBig=%s OpenMed=%s OpenMedOrBig=%s Runway=%s", airbaseName, tostring(openBig), tostring(openMed), tostring(openMedOrBig), tostring(runway)), true)
return true
end
-- Pre-dispatch safety check: ensure destination can accommodate larger transport types
destinationHasSuitableParking = function(destination, preferredTermTypes)
local base = AIRBASE:FindByName(destination)
if not base then return false end
preferredTermTypes = preferredTermTypes or { AIRBASE.TerminalType.OpenBig, AIRBASE.TerminalType.OpenMedOrBig, AIRBASE.TerminalType.OpenMed }
for _, term in ipairs(preferredTermTypes) do
local ok, n = pcall(function() return base:GetParkingSpotsNumber(term) end)
if ok and n and n > 0 then
return true
end
end
return false
end
--[[
monitorSquadrons()
--------------------------------------------------------------------------
Checks all squadrons for each coalition. If a squadron is below the resupply threshold and has no active cargo mission,
triggers a supply request and dispatches a cargo aircraft.
]]
local function monitorSquadrons()
for _, coalitionKey in ipairs({"red", "blue"}) do
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
local squadrons = (coalitionKey == "red") and RED_SQUADRON_CONFIG or BLUE_SQUADRON_CONFIG
for _, squadron in ipairs(squadrons) do
local current, max, ratio = getSquadronStatus(squadron, coalitionKey)
log("Squadron status: " .. squadron.displayName .. " (" .. coalitionKey .. ") " .. current .. "/" .. max .. " ratio: " .. string.format("%.2f", ratio))
if ratio <= config.threshold and not hasActiveCargoMission(coalitionKey, squadron.airbaseName) then
log("Supply request triggered for " .. squadron.displayName .. " at " .. squadron.airbaseName)
announceToCoalition(coalitionKey, "Supply requested for " .. squadron.airbaseName .. "! Squadron: " .. squadron.displayName)
dispatchCargo(squadron, coalitionKey)
end
end
end
end
--[[
monitorCargoMissions()
--------------------------------------------------------------------------
Monitors all cargo missions, updates their status, and cleans up failed ones.
Handles mission failure after a grace period.
]]
local function monitorCargoMissions()
for _, coalitionKey in ipairs({"red", "blue"}) do
for _, mission in ipairs(cargoMissions[coalitionKey]) do
if mission.group == nil then
log("[DEBUG] Mission group object is nil for mission to " .. tostring(mission.destination), true)
else
log("[DEBUG] Mission group: " .. tostring(mission.group:GetName()) .. ", IsAlive(): " .. tostring(mission.group:IsAlive()), true)
local dcsGroup = mission.group:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
local pos = units[1]:getPoint()
log(string.format("[DEBUG] Group position: x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z), true)
else
log("[DEBUG] No units found in DCS group for mission to " .. tostring(mission.destination), true)
end
else
log("[DEBUG] DCS group object is nil for mission to " .. tostring(mission.destination), true)
end
end
local graceElapsed = mission._pendingStartTime and (timer.getTime() - mission._pendingStartTime > (mission._gracePeriod or 8))
-- Only allow mission to be failed after grace period, and only if group is truly dead.
-- Some DCS/MOOSE group objects may momentarily report IsAlive() == false while units still exist, so
-- also check DCS object/unit presence before declaring failure.
if (mission.status == "pending" or mission.status == "enroute") and graceElapsed then
local isAlive = mission.group and mission.group:IsAlive()
local dcsGroup = mission.group and mission.group:GetDCSObject()
local unitsPresent = false
if dcsGroup then
local units = dcsGroup:getUnits()
unitsPresent = units and (#units > 0)
end
if not isAlive and not unitsPresent then
mission.status = "failed"
log("Cargo mission failed (after grace period): " .. (mission.group and mission.group:GetName() or "nil group") .. " to " .. mission.destination)
announceToCoalition(coalitionKey, "Resupply mission to " .. mission.destination .. " failed!")
else
log("DEBUG: Mission appears to still have DCS units despite IsAlive=false; skipping failure for " .. tostring(mission.destination), true)
end
end
end
end
cleanupCargoMissions()
end
--[[
MAIN DISPATCHER LOOP
--------------------------------------------------------------------------
Runs the main dispatcher logic on a timer interval.
]]
local function cargoDispatcherMain()
log("═══════════════════════════════════════════════════════════════════════════════", true)
log("Cargo Dispatcher main loop running.", true)
monitorSquadrons()
monitorCargoMissions()
-- Schedule the next run inside a protected call to avoid unhandled errors
timer.scheduleFunction(function()
local ok, err = pcall(cargoDispatcherMain)
if not ok then
log("FATAL: cargoDispatcherMain crashed on scheduled run: " .. tostring(err))
-- do not reschedule to avoid crash loops
end
end, {}, timer.getTime() + DISPATCHER_CONFIG.interval)
end
-- Start the dispatcher
local ok, err = pcall(cargoDispatcherMain)
if not ok then
log("FATAL: cargoDispatcherMain crashed on startup: " .. tostring(err))
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
-- End Moose_TDAC_CargoDispatcher.lua
-- 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")
function _G.TDAC_CargoDispatcher_TestSpawn(templateName, originAirbase, destinationAirbase)
log("[TDAC TEST] Starting test spawn for template: " .. tostring(templateName), true)
local ok, err
if type(templateName) ~= 'string' then
env.info("[TDAC TEST] templateName must be a string")
return false, "invalid templateName"
end
local spawnByName = nil
ok, spawnByName = pcall(function() return SPAWN:New(templateName) end)
if not ok or not spawnByName then
log("[TDAC TEST] SPAWN:New failed for template " .. tostring(templateName) .. ". Error: " .. tostring(spawnByName), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(spawnByName))), true) end
return false, "spawn_new_failed"
end
spawnByName:OnSpawnGroup(function(spawnedGroup)
log("[TDAC TEST] OnSpawnGroup called for: " .. tostring(spawnedGroup:GetName()), true)
local dcsGroup = spawnedGroup:GetDCSObject()
if dcsGroup then
local units = dcsGroup:getUnits()
if units and #units > 0 then
local pos = units[1]:getPoint()
log(string.format("[TDAC TEST] Spawned pos x=%.1f y=%.1f z=%.1f", pos.x, pos.y, pos.z), true)
end
end
if destinationAirbase then
local okAssign, errAssign = pcall(function()
local base = AIRBASE:FindByName(destinationAirbase)
if base and spawnedGroup and spawnedGroup.RouteToAirbase then
spawnedGroup:RouteToAirbase(base, AI_Task_Land.Runway)
log("[TDAC TEST] RouteToAirbase assigned to " .. tostring(destinationAirbase), true)
else
log("[TDAC TEST] RouteToAirbase not available or base not found", true)
end
end)
if not okAssign then
log("[TDAC TEST] RouteToAirbase pcall failed: " .. tostring(errAssign), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(errAssign))), true) end
end
end
end)
ok, err = pcall(function() spawnByName:Spawn() end)
if not ok then
log("[TDAC TEST] spawnByName:Spawn() failed: " .. tostring(err), true)
if debug and debug.traceback then log("TRACEBACK: " .. tostring(debug.traceback(tostring(err))), true) end
return false, "spawn_failed"
end
log("[TDAC TEST] spawnByName:Spawn() returned successfully", true)
return true
end
log("═══════════════════════════════════════════════════════════════════════════════", true)
-- End Moose_TDAC_CargoDispatcher.lua

View File

@ -215,6 +215,9 @@ local ADVANCED_SETTINGS = {
-- Distance from airbase to consider cargo "delivered" via flyover (meters)
-- Aircraft flying within this range will count as supply delivery (no landing required)
cargoLandingDistance = 3000,
-- Distance from airbase to consider a landing as delivered (wheel touchdown)
-- Use a slightly larger radius than 1000m to account for runway offsets from airbase center
cargoLandingEventRadius = 2000,
-- Velocity below which aircraft is considered "landed" (km/h)
cargoLandedVelocity = 5,
@ -224,8 +227,10 @@ local ADVANCED_SETTINGS = {
rtbSpeed = 430, -- Return to base speed (knots)
-- Logging settings
enableDetailedLogging = true, -- Set to false to reduce log spam
enableDetailedLogging = false, -- Set to false to reduce log spam
logPrefix = "[Universal TADC]", -- Prefix for all log messages
-- Proxy/raw-fallback verbose logging (set true to debug proxy behavior)
verboseProxyLogging = false,
}
--[[
@ -259,6 +264,18 @@ squadronAircraftCounts = {
blue = {}
}
-- Aircraft spawn tracking for stuck detection
local aircraftSpawnTracking = {
red = {}, -- groupName -> {spawnPos, spawnTime, squadron, airbase}
blue = {}
}
-- Airbase health status
local airbaseHealthStatus = {
red = {}, -- airbaseName -> "operational"|"stuck-aircraft"|"unusable"
blue = {}
}
-- Logging function
local function log(message, detailed)
if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then
@ -642,44 +659,51 @@ end
-- Process cargo delivery for a squadron
local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coalitionKey)
-- Initialize processed deliveries table
-- Simple delivery processor: dedupe by group ID and credit supplies directly.
if not _G.processedDeliveries then
_G.processedDeliveries = {}
end
-- Create unique delivery key including timestamp to prevent race conditions
-- Note: Key doesn't include airbase to prevent double-counting if aircraft moves between airbases
local deliveryKey = cargoGroup:GetName() .. "_" .. coalitionKey:upper() .. "_" .. cargoGroup:GetID()
if not _G.processedDeliveries[deliveryKey] then
-- Mark delivery as processed immediately to prevent race conditions
_G.processedDeliveries[deliveryKey] = timer.getTime()
-- Process replenishment
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
local maxCount = squadron.aircraft
local newCount = math.min(currentCount + TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
" aircraft to " .. squadron.displayName ..
" (" .. newCount .. "/" .. maxCount .. ")"
log(msg)
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
-- Notify dispatcher (if available) so it can mark the matching mission completed immediately
if type(_G.TDAC_CargoDelivered) == 'function' then
pcall(function()
_G.TDAC_CargoDelivered(cargoGroup:GetName(), squadron.airbaseName, coalitionKey)
end)
end
else
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. squadron.displayName .. " already at max capacity"
log(msg, true)
MESSAGE:New(msg, 15):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end
-- Use group ID + squadron airbase as dedupe key to avoid double crediting when the same group
-- triggers multiple events or moves between airbases rapidly.
local okId, grpId = pcall(function() return cargoGroup and cargoGroup.GetID and cargoGroup:GetID() end)
local groupIdStr = (okId and grpId) and tostring(grpId) or "<no-id>"
local deliveryKey = groupIdStr .. "_" .. tostring(squadron.airbaseName)
-- Diagnostic log: show group name, id, and delivery key when processor invoked
local okName, grpName = pcall(function() return cargoGroup and cargoGroup.GetName and cargoGroup:GetName() end)
local groupNameStr = (okName and grpName) and tostring(grpName) or "<no-name>"
log("PROCESS CARGO: invoked for group=" .. groupNameStr .. " id=" .. groupIdStr .. " targetAirbase=" .. tostring(squadron.airbaseName) .. " deliveryKey=" .. deliveryKey, true)
if _G.processedDeliveries[deliveryKey] then
-- Already processed recently, ignore
log("PROCESS CARGO: deliveryKey " .. deliveryKey .. " already processed at " .. tostring(_G.processedDeliveries[deliveryKey]), true)
return
end
-- Mark processed immediately
_G.processedDeliveries[deliveryKey] = timer.getTime()
-- Credit the squadron
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
local maxCount = squadron.aircraft or 0
local addAmount = TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount or 0
local newCount = math.min(currentCount + addAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
" aircraft to " .. (squadron.displayName or squadron.templateName) ..
" (" .. newCount .. "/" .. maxCount .. ")"
log(msg)
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
else
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. (squadron.displayName or squadron.templateName) .. " already at max capacity"
log(msg, true)
MESSAGE:New(msg, 10):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end
end
@ -688,141 +712,582 @@ local cargoEventHandler = {}
function cargoEventHandler:onEvent(event)
if event.id == world.event.S_EVENT_LAND then
local unit = event.initiator
-- Safe unit name retrieval
local unitName = "unknown"
if unit and type(unit) == "table" then
local ok, name = pcall(function() return unit:GetName() end)
if ok and name then
unitName = name
end
end
log("LANDING EVENT: Received S_EVENT_LAND for unit: " .. unitName, true)
if unit and type(unit) == "table" and unit.IsAlive and unit:IsAlive() then
local group = unit:GetGroup()
if group and type(group) == "table" and group.IsAlive and group:IsAlive() then
local cargoName = group:GetName():upper()
-- Safe group name retrieval
local cargoName = "unknown"
local ok, name = pcall(function() return group:GetName():upper() end)
if ok and name then
cargoName = name
end
log("LANDING EVENT: Processing group: " .. cargoName, true)
local isCargoAircraft = false
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
log("LANDING EVENT: Matched cargo pattern '" .. pattern .. "' for " .. cargoName, true)
break
end
end
if isCargoAircraft then
local cargoCoord = unit:GetCoordinate()
local coalitionSide = unit:GetCoalition()
local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue"
-- Safe coordinate and coalition retrieval
local cargoCoord = nil
local ok, coord = pcall(function() return unit:GetCoordinate() end)
if ok and coord then
cargoCoord = coord
end
-- Check which airbase it's near
local squadronConfig = getSquadronConfig(coalitionSide)
for _, squadron in pairs(squadronConfig) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalitionSide then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- If within configured distance of airbase, consider it a landing delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("LANDING DELIVERY: " .. cargoName .. " landed and delivered at " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m)")
processCargoDelivery(group, squadron, coalitionSide, coalitionKey)
log("LANDING EVENT: Cargo aircraft " .. cargoName .. " at coord: " .. tostring(cargoCoord), true)
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
-- Search RED squadron configs
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT: Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
-- Search BLUE squadron configs
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT: Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
if closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
log("LANDING DELIVERY: " .. cargoName .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(group, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED: " .. cargoName .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING DETECTED: " .. cargoName .. " landed but no configured squadron airbases available to check", true)
end
else
log("LANDING EVENT: Could not get coordinates for cargo aircraft " .. cargoName, true)
end
else
log("LANDING EVENT: " .. cargoName .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Group is nil or not alive", true)
end
else
-- Fallback: unit was nil or not alive (race/despawn). Try to retrieve group and name safely
log("LANDING EVENT: Unit is nil or not alive - attempting fallback group retrieval", true)
local fallbackGroup = nil
local okGetGroup, grp = pcall(function()
if unit and type(unit) == "table" and unit.GetGroup then
return unit:GetGroup()
end
-- Try event.initiator (may be raw DCS object)
if event and event.initiator and type(event.initiator) == 'table' and event.initiator.GetGroup then
return event.initiator:GetGroup()
end
return nil
end)
if okGetGroup and grp then
fallbackGroup = grp
end
if fallbackGroup then
-- Try to get group name even if group:IsAlive() is false
local okName, gname = pcall(function() return fallbackGroup:GetName():upper() end)
local cargoName = "unknown"
if okName and gname then
cargoName = gname
end
log("LANDING EVENT (fallback): Processing group: " .. cargoName, true)
local isCargoAircraft = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
log("LANDING EVENT (fallback): Matched cargo pattern '" .. pattern .. "' for " .. cargoName, true)
break
end
end
if isCargoAircraft then
-- Try to get coordinate and coalition via multiple safe methods
local cargoCoord = nil
local okCoord, coord = pcall(function()
if unit and unit.GetCoordinate then return unit:GetCoordinate() end
if fallbackGroup and fallbackGroup.GetCoordinate then return fallbackGroup:GetCoordinate() end
return nil
end)
if okCoord and coord then cargoCoord = coord end
log("LANDING EVENT (fallback): Cargo aircraft " .. cargoName .. " at coord: " .. tostring(cargoCoord), true)
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT (fallback): Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = cargoCoord:Get2DDistance(airbase:GetCoordinate())
log("LANDING EVENT (fallback): Checking distance to " .. squadron.airbaseName .. ": " .. math.floor(distance) .. "m", true)
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
if closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
log("LANDING DELIVERY (fallback): " .. cargoName .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(fallbackGroup, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED (fallback): " .. cargoName .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING EVENT (fallback): No configured squadron airbases available to check", true)
end
else
log("LANDING EVENT (fallback): Could not get coordinates for cargo aircraft " .. cargoName, true)
end
else
log("LANDING EVENT (fallback): " .. cargoName .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Fallback group retrieval failed", true)
-- Additional fallback: try raw DCS object methods (lowercase) and resolve by name
local okRaw, rawGroup = pcall(function()
if event and event.initiator and type(event.initiator) == 'table' and event.initiator.getGroup then
return event.initiator:getGroup()
end
return nil
end)
if okRaw and rawGroup then
-- Try to get raw group name
local okRawName, rawName = pcall(function()
if rawGroup.getName then return rawGroup:getName() end
return nil
end)
if okRawName and rawName then
local rawNameUp = tostring(rawName):upper()
log("LANDING EVENT: Resolved raw DCS group name: " .. rawNameUp, true)
-- Try to find MOOSE GROUP by that name
local okFind, mooseGroup = pcall(function() return GROUP:FindByName(rawNameUp) end)
if okFind and mooseGroup and type(mooseGroup) == 'table' then
log("LANDING EVENT: Found MOOSE GROUP for raw name: " .. rawNameUp, true)
-- Reuse the fallback logic using mooseGroup
local cargoName = rawNameUp
local isCargoAircraft = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
-- Try to get coordinate from raw group if possible
local cargoCoord = nil
local okPoint, point = pcall(function()
if rawGroup.getController then
-- Raw DCS unit list -> first unit point
local dcs = rawGroup
if dcs.getUnits then
local units = dcs:getUnits()
if units and #units > 0 and units[1].getPoint then
return units[1]:getPoint()
end
end
end
return nil
end)
if okPoint and point then cargoCoord = point end
-- If we have a coordinate, find nearest squadron and process
if cargoCoord then
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
if type(cargoCoord) == 'table' and cargoCoord.Get2DDistance then
local okDist, d = pcall(function() return cargoCoord:Get2DDistance(airbase:GetCoordinate()) end)
if okDist and d then distance = d end
else
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
if type(cargoCoord) == 'table' and cargoCoord.Get2DDistance then
local okDist, d = pcall(function() return cargoCoord:Get2DDistance(airbase:GetCoordinate()) end)
if okDist and d then distance = d end
else
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase and closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
log("LANDING DELIVERY (raw-fallback): " .. rawNameUp .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m)")
processCargoDelivery(mooseGroup, closestSquadron, abCoalition, coalitionKey)
else
log("LANDING DETECTED (raw-fallback): " .. rawNameUp .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")")
end
else
log("LANDING EVENT: Could not extract coordinate from raw DCS group: " .. tostring(rawName), true)
end
else
log("LANDING EVENT: Raw group " .. tostring(rawName) .. " is not a cargo aircraft", true)
end
else
log("LANDING EVENT: Could not find MOOSE GROUP for raw name: " .. tostring(rawName) .. " - attempting raw-group proxy processing", true)
-- Even if we can't find a MOOSE GROUP, try to extract coordinates from the raw DCS group
local okPoint2, point2 = pcall(function()
if rawGroup and rawGroup.getUnits then
local units = rawGroup:getUnits()
if units and #units > 0 and units[1].getPoint then
return units[1]:getPoint()
end
end
return nil
end)
if okPoint2 and point2 then
local cargoCoord = point2
-- Find nearest configured squadron airbase (RED + BLUE)
local closestAirbase = nil
local closestDistance = math.huge
local closestSquadron = nil
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase then
local distance = math.huge
local okVec, aVec = pcall(function() return airbase:GetCoordinate():GetVec2() end)
if okVec and aVec and type(aVec) == 'table' then
local cx, cy
if cargoCoord.x and cargoCoord.z then
cx, cy = cargoCoord.x, cargoCoord.z
elseif cargoCoord.x and cargoCoord.y then
cx, cy = cargoCoord.x, cargoCoord.y
elseif cargoCoord[1] and cargoCoord[3] then
cx, cy = cargoCoord[1], cargoCoord[3]
elseif cargoCoord[1] and cargoCoord[2] then
cx, cy = cargoCoord[1], cargoCoord[2]
end
if cx and cy then
local dx = cx - aVec.x
local dy = cy - aVec.y
distance = math.sqrt(dx*dx + dy*dy)
end
end
if distance < closestDistance then
closestDistance = distance
closestAirbase = airbase
closestSquadron = squadron
end
end
end
if closestAirbase and closestDistance < ADVANCED_SETTINGS.cargoLandingEventRadius then
local abCoalition = closestAirbase:GetCoalition()
local coalitionKey = (abCoalition == coalition.side.RED) and "red" or "blue"
-- Ensure the raw group name actually looks like a cargo aircraft before crediting
local rawNameUpCheck = tostring(rawName):upper()
local isCargoProxy = false
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(rawNameUpCheck, pattern) then
isCargoProxy = true
break
end
end
if not isCargoProxy then
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING IGNORED (raw-proxy): " .. tostring(rawName) .. " is not a cargo-type name, skipping delivery proxy", true)
else
log("LANDING IGNORED (raw-proxy): " .. tostring(rawName) .. " is not a cargo-type name, skipping delivery proxy", true)
end
else
-- Build a small proxy object that exposes GetName and GetID so processCargoDelivery can use it
local cargoProxy = {}
function cargoProxy:GetName()
local okn, nm = pcall(function()
if rawGroup and rawGroup.getName then return rawGroup:getName() end
return tostring(rawName)
end)
return (okn and nm) and tostring(nm) or tostring(rawName)
end
function cargoProxy:GetID()
local okid, id = pcall(function()
if rawGroup and rawGroup.getID then return rawGroup:getID() end
if rawGroup and rawGroup.getID == nil and rawGroup.getController then
-- Try to hash name as fallback unique-ish id
return tostring(rawName) .. "_proxy"
end
return nil
end)
return (okid and id) and id or tostring(rawName) .. "_proxy"
end
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING DELIVERY (raw-proxy): " .. tostring(rawName) .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. math.floor(closestDistance) .. "m) - using proxy object", true)
end
processCargoDelivery(cargoProxy, closestSquadron, abCoalition, coalitionKey)
end
else
if ADVANCED_SETTINGS.verboseProxyLogging then
log("LANDING DETECTED (raw-proxy): " .. tostring(rawName) .. " landed but no valid airbase found within range (closest: " .. (closestDistance and math.floor(closestDistance) .. "m" or "none") .. ")", true)
end
end
else
log("LANDING EVENT: Could not extract coordinate from raw DCS group for proxy processing: " .. tostring(rawName), true)
end
end
else
log("LANDING EVENT: rawGroup:getName() failed", true)
end
else
log("LANDING EVENT: raw DCS group retrieval failed", true)
end
end
end
end
end
-- Monitor cargo aircraft flyovers for squadron replenishment
local function monitorCargoReplenishment()
-- Process RED cargo aircraft
if TADC_SETTINGS.enableRed then
-- Use cached set for performance, create if needed
if not cachedSets.redCargo then
cachedSets.redCargo = SET_GROUP:New():FilterCoalitions("red"):FilterCategoryAirplane():FilterStart()
end
local redCargo = cachedSets.redCargo
redCargo:ForEach(function(cargoGroup)
if cargoGroup and cargoGroup:IsAlive() then
local cargoName = cargoGroup:GetName():upper()
local isCargoAircraft = false
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
local cargoCoord = cargoGroup:GetCoordinate()
local cargoVelocity = cargoGroup:GetVelocityKMH()
-- DEBUG: log candidate details with timestamp for diagnosis
if ADVANCED_SETTINGS.enableDetailedLogging then
log(string.format("[LOAD2ND DEBUG] Evaluating cargo %s at time=%d vel=%.2f km/h coord=(%.1f,%.1f)",
cargoGroup:GetName(), timer.getTime(), cargoVelocity, cargoCoord:GetVec2().x, cargoCoord:GetVec2().y))
end
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
-- Check which RED airbase it's near
for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.RED then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- If within configured distance of airbase, consider it a flyover delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
processCargoDelivery(cargoGroup, squadron, coalition.side.RED, "red")
end
end
end
end
-- Reassign squadron to an alternative airbase when primary airbase has issues
local function reassignSquadronToAlternativeAirbase(squadron, coalitionKey)
local coalitionSide = (coalitionKey == "red") and coalition.side.RED or coalition.side.BLUE
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
local squadronConfig = getSquadronConfig(coalitionSide)
-- Find alternative airbases (other squadrons' airbases that are operational)
local alternativeAirbases = {}
for _, altSquadron in pairs(squadronConfig) do
if altSquadron.airbaseName ~= squadron.airbaseName then
local usable, status = isAirbaseUsable(altSquadron.airbaseName, coalitionSide)
local healthStatus = airbaseHealthStatus[coalitionKey][altSquadron.airbaseName] or "operational"
if usable and healthStatus == "operational" then
table.insert(alternativeAirbases, altSquadron.airbaseName)
end
end)
end
end
-- Process BLUE cargo aircraft
if TADC_SETTINGS.enableBlue then
-- Use cached set for performance, create if needed
if not cachedSets.blueCargo then
cachedSets.blueCargo = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryAirplane():FilterStart()
end
local blueCargo = cachedSets.blueCargo
if #alternativeAirbases > 0 then
-- Select random alternative airbase
local newAirbase = alternativeAirbases[math.random(1, #alternativeAirbases)]
blueCargo:ForEach(function(cargoGroup)
if cargoGroup and cargoGroup:IsAlive() then
local cargoName = cargoGroup:GetName():upper()
local isCargoAircraft = false
-- Update squadron configuration (this is a runtime change)
squadron.airbaseName = newAirbase
airbaseHealthStatus[coalitionKey][squadron.airbaseName] = "operational" -- Reset health for new assignment
log("REASSIGNED: " .. coalitionName .. " Squadron " .. squadron.displayName .. " moved from " .. squadron.airbaseName .. " to " .. newAirbase)
MESSAGE:New(coalitionName .. " Squadron " .. squadron.displayName .. " reassigned to " .. newAirbase .. " due to airbase issues", 20):ToCoalition(coalitionSide)
else
log("WARNING: No alternative airbases available for " .. coalitionName .. " Squadron " .. squadron.displayName)
MESSAGE:New("WARNING: No alternative airbases available for " .. squadron.displayName, 30):ToCoalition(coalitionSide)
end
end
-- Monitor for stuck aircraft at airbases
local function monitorStuckAircraft()
local currentTime = timer.getTime()
local stuckThreshold = 300 -- 5 minutes before considering aircraft stuck
local movementThreshold = 50 -- meters - aircraft must move at least this far to not be considered stuck
for _, coalitionKey in ipairs({"red", "blue"}) do
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
for aircraftName, trackingData in pairs(aircraftSpawnTracking[coalitionKey]) do
if trackingData and trackingData.group and trackingData.group:IsAlive() then
local timeSinceSpawn = currentTime - trackingData.spawnTime
-- Check if aircraft name matches cargo patterns
for _, pattern in pairs(ADVANCED_SETTINGS.cargoPatterns) do
if string.find(cargoName, pattern) then
isCargoAircraft = true
break
end
end
if isCargoAircraft then
local cargoCoord = cargoGroup:GetCoordinate()
local cargoVelocity = cargoGroup:GetVelocityKMH()
-- Check for flyover delivery - aircraft within range of airbase (no landing required)
-- Check which BLUE airbase it's near
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalition.side.BLUE then
local airbaseCoord = airbase:GetCoordinate()
local distance = cargoCoord:Get2DDistance(airbaseCoord)
-- Only check aircraft that have been spawned for at least the threshold time
if timeSinceSpawn >= stuckThreshold then
local currentPos = trackingData.group:GetCoordinate()
if currentPos and trackingData.spawnPos then
local distanceMoved = trackingData.spawnPos:Get2DDistance(currentPos)
-- Check if aircraft has moved less than threshold (stuck)
if distanceMoved < movementThreshold then
log("STUCK AIRCRAFT DETECTED: " .. aircraftName .. " at " .. trackingData.airbase ..
" has only moved " .. math.floor(distanceMoved) .. "m in " .. math.floor(timeSinceSpawn/60) .. " minutes")
-- If within configured distance of airbase, consider it a flyover delivery
if distance < ADVANCED_SETTINGS.cargoLandingDistance then
log("FLYOVER DELIVERY: " .. cargoName .. " delivered supplies to " .. squadron.airbaseName .. " (distance: " .. math.floor(distance) .. "m, altitude: " .. math.floor(cargoCoord.y/0.3048) .. " ft)")
processCargoDelivery(cargoGroup, squadron, coalition.side.BLUE, "blue")
end
-- Mark airbase as having stuck aircraft
airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft"
-- Remove the stuck aircraft
trackingData.group:Destroy()
activeInterceptors[coalitionKey][aircraftName] = nil
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
-- Reassign squadron to alternative airbase
reassignSquadronToAlternativeAirbase(trackingData.squadron, coalitionKey)
MESSAGE:New(coalitionName .. " aircraft stuck at " .. trackingData.airbase .. " - destroyed and squadron reassigned", 15):ToCoalition(coalitionKey == "red" and coalition.side.RED or coalition.side.BLUE)
else
-- Aircraft has moved sufficiently, remove from tracking (no longer needs monitoring)
log("Aircraft " .. aircraftName .. " has moved " .. math.floor(distanceMoved) .. "m - removing from stuck monitoring", true)
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end
end
end
else
-- Clean up dead aircraft from tracking
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end
end)
end
end
end
@ -1046,7 +1511,9 @@ local function findBestSquadron(threatCoord, threatSize, coalitionSide)
return selected.squadron, selected.responseRatio, selected.zoneDescription
end
log("No " .. coalitionName .. " squadron available for threat at coordinates")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available for threat at coordinates")
end
return nil, 0, "no available squadrons"
end
@ -1093,7 +1560,9 @@ local function launchInterceptor(threatGroup, coalitionSide)
local squadron, zoneResponseRatio, zoneDescription = findBestSquadron(threatCoord, threatSize, coalitionSide)
if not squadron then
log("No " .. coalitionName .. " squadron available")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available")
end
return
end
@ -1110,7 +1579,9 @@ local function launchInterceptor(threatGroup, coalitionSide)
end
end
if not squadron then
log("No " .. coalitionName .. " squadron available")
if ADVANCED_SETTINGS.enableDetailedLogging then
log("No " .. coalitionName .. " squadron available")
end
return
end
@ -1173,11 +1644,25 @@ local function launchInterceptor(threatGroup, coalitionSide)
displayName = squadron.displayName
}
-- Track spawn position for stuck aircraft detection
local spawnPos = interceptor:GetCoordinate()
if spawnPos then
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = {
spawnPos = spawnPos,
spawnTime = timer.getTime(),
squadron = squadron,
airbase = squadron.airbaseName
}
log("Tracking spawn position for " .. interceptor:GetName() .. " at " .. squadron.airbaseName, true)
end
-- Emergency cleanup (safety net)
SCHEDULER:New(nil, function()
if activeInterceptors[coalitionKey][interceptor:GetName()] then
log("Emergency cleanup of " .. coalitionName .. " " .. interceptor:GetName() .. " (should have RTB'd)")
activeInterceptors[coalitionKey][interceptor:GetName()] = nil
-- Also clean up spawn tracking
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = nil
end
end, {}, coalitionSettings.emergencyCleanupTime)
end
@ -1503,13 +1988,55 @@ local function initializeSystem()
end
-- Start schedulers
-- Set up event handler for cargo landing detection
-- Set up event handler for cargo landing detection (handled via MOOSE EVENTHANDLER wrapper below)
-- Re-register world event handler for robust detection (handles raw DCS initiators and race cases)
world.addEventHandler(cargoEventHandler)
-- MOOSE-style EVENTHANDLER wrapper for readability: logs EventData but does NOT delegate to avoid double-processing
if EVENTHANDLER then
local TADC_CARGO_LANDING_HANDLER = EVENTHANDLER:New()
function TADC_CARGO_LANDING_HANDLER:OnEventLand(EventData)
-- Convert MOOSE EventData to raw world.event format and reuse existing handler logic
if ADVANCED_SETTINGS.enableDetailedLogging then
-- Log presence and types of key fields
local function safeName(obj)
if not obj then return "<nil>" end
local ok, n = pcall(function()
if obj.GetName then return obj:GetName() end
if obj.getName then return obj:getName() end
return nil
end)
return (ok and n) and tostring(n) or "<unavailable>"
end
local iniUnitPresent = EventData.IniUnit ~= nil
local iniGroupPresent = EventData.IniGroup ~= nil
local placePresent = EventData.Place ~= nil
local iniUnitName = safeName(EventData.IniUnit)
local iniGroupName = safeName(EventData.IniGroup)
local placeName = safeName(EventData.Place)
log("MOOSE LAND EVENT: IniUnitPresent=" .. tostring(iniUnitPresent) .. ", IniUnitName=" .. tostring(iniUnitName) .. ", IniGroupPresent=" .. tostring(iniGroupPresent) .. ", IniGroupName=" .. tostring(iniGroupName) .. ", PlacePresent=" .. tostring(placePresent) .. ", PlaceName=" .. tostring(placeName), true)
end
local rawEvent = {
id = world.event.S_EVENT_LAND,
initiator = EventData.IniUnit or EventData.IniGroup or nil,
place = EventData.Place or nil,
-- Provide the original EventData for potential fallback use
_moose_original = EventData
}
-- Log and return; the world event handler `cargoEventHandler` will handle the actual processing.
return
end
-- Register the MOOSE handler
TADC_CARGO_LANDING_HANDLER:HandleEvent(EVENTS.Land)
end
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval)
SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval)
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval)
SCHEDULER:New(nil, monitorCargoReplenishment, {}, 15, TADC_SETTINGS.cargoCheckInterval)
SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour
-- Start periodic squadron summary broadcast
@ -1706,5 +2233,37 @@ MENU_MISSION_COMMAND:New("Show TADC System Status", menuRoot, function()
MESSAGE:New(status, 20):ToAll()
end)
-- 10. Check for Stuck Aircraft (manual trigger)
MENU_MISSION_COMMAND:New("Check for Stuck Aircraft", menuRoot, function()
monitorStuckAircraft()
MESSAGE:New("Stuck aircraft check completed", 10):ToAll()
end)
-- 11. Show Airbase Health Status
MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function()
local lines = {"Airbase Health Status:"}
for _, coalitionKey in ipairs({"red", "blue"}) do
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
table.insert(lines, coalitionName .. " Coalition:")
for airbaseName, status in pairs(airbaseHealthStatus[coalitionKey]) do
table.insert(lines, " " .. airbaseName .. ": " .. status)
end
end
MESSAGE:New(table.concat(lines, "\n"), 20):ToAll()
end)
-- Initialize airbase health status for all configured airbases
for _, coalitionKey in ipairs({"red", "blue"}) do
local squadronConfig = getSquadronConfig(coalitionKey == "red" and coalition.side.RED or coalition.side.BLUE)
for _, squadron in pairs(squadronConfig) do
if not airbaseHealthStatus[coalitionKey][squadron.airbaseName] then
airbaseHealthStatus[coalitionKey][squadron.airbaseName] = "operational"
end
end
end
-- Set up periodic stuck aircraft monitoring (every 2 minutes)
SCHEDULER:New(nil, monitorStuckAircraft, {}, 120, 120)

Binary file not shown.