mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
862 lines
41 KiB
Lua
862 lines
41 KiB
Lua
--[[
|
|
═══════════════════════════════════════════════════════════════════════════════
|
|
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 = { "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
|
|
--------------------------------------------------------------------------
|
|
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 = "[TADC 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.
|
|
Failed missions are immediately removed from tracking to allow retries.
|
|
]]
|
|
local function hasActiveCargoMission(coalitionKey, airbaseName)
|
|
for i = #cargoMissions[coalitionKey], 1, -1 do
|
|
local mission = cargoMissions[coalitionKey][i]
|
|
if mission.destination == airbaseName then
|
|
-- Remove completed or failed missions immediately to allow retries
|
|
if mission.status == "completed" or mission.status == "failed" then
|
|
log("Removing " .. mission.status .. " cargo mission for " .. airbaseName .. " from tracking")
|
|
table.remove(cargoMissions[coalitionKey], i)
|
|
else
|
|
-- Consider mission active only if the group is alive OR we're still within the grace window
|
|
local stillActive = false
|
|
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
|
|
local coalitionSide = getCoalitionSide(coalitionKey)
|
|
|
|
repeat
|
|
origin = selectRandomAirfield(config.supplyAirfields)
|
|
attempts = attempts + 1
|
|
|
|
-- Ensure origin is not the same as destination
|
|
if origin == squadron.airbaseName then
|
|
origin = nil
|
|
else
|
|
-- Validate that origin airbase exists and is controlled by correct coalition
|
|
local originAirbase = AIRBASE:FindByName(origin)
|
|
if not originAirbase then
|
|
log("WARNING: Origin airbase '" .. tostring(origin) .. "' does not exist. Trying another...")
|
|
origin = nil
|
|
elseif originAirbase:GetCoalition() ~= coalitionSide then
|
|
log("WARNING: Origin airbase '" .. tostring(origin) .. "' is not controlled by " .. coalitionKey .. " coalition. Trying another...")
|
|
origin = nil
|
|
end
|
|
end
|
|
until origin or attempts >= maxAttempts
|
|
|
|
-- 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)
|
|
|
|
-- Validate destination airbase: RAT's "Airbase doesn't exist" error actually means
|
|
-- "Airbase not found OR not owned by the correct coalition" because RAT filters by coalition internally.
|
|
-- We perform the same validation here to fail fast with better error messages.
|
|
local destAirbase = AIRBASE:FindByName(destination)
|
|
local coalitionSide = getCoalitionSide(coalitionKey)
|
|
|
|
if not destAirbase then
|
|
log("ERROR: Destination airbase '" .. destination .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
|
|
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (airbase not found on map)!")
|
|
-- Mark mission as failed and cleanup immediately
|
|
mission.status = "failed"
|
|
return
|
|
end
|
|
|
|
local destCoalition = destAirbase:GetCoalition()
|
|
if destCoalition ~= coalitionSide then
|
|
log("INFO: Destination airbase '" .. destination .. "' captured by enemy - cargo dispatch skipped (normal mission state).", true)
|
|
-- No announcement to coalition - this is expected behavior when base is captured
|
|
-- Mark mission as failed and cleanup immediately
|
|
mission.status = "failed"
|
|
return
|
|
end
|
|
|
|
-- Validate origin airbase with same coalition filtering logic
|
|
local originAirbase = AIRBASE:FindByName(origin)
|
|
if not originAirbase then
|
|
log("ERROR: Origin airbase '" .. origin .. "' does not exist in DCS (invalid name or not on this map). Skipping dispatch.")
|
|
announceToCoalition(coalitionKey, "Resupply mission from " .. origin .. " failed (airbase not found on map)!")
|
|
-- Mark mission as failed and cleanup immediately
|
|
mission.status = "failed"
|
|
return
|
|
end
|
|
|
|
local originCoalition = originAirbase:GetCoalition()
|
|
if originCoalition ~= coalitionSide then
|
|
log("INFO: Origin airbase '" .. origin .. "' captured by enemy - trying another supply source.", true)
|
|
-- Don't announce or mark as failed - the dispatcher will try another origin
|
|
return
|
|
end
|
|
|
|
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)!")
|
|
-- Mark mission as failed and cleanup immediately - do NOT track failed RAT spawns
|
|
mission.status = "failed"
|
|
return
|
|
end
|
|
|
|
-- Configure RAT for a single, non-respawning dispatch
|
|
rat:SetDeparture(origin)
|
|
rat:SetDestination(destination)
|
|
rat:NoRespawn()
|
|
rat:SetSpawnLimit(1)
|
|
rat:SetSpawnDelay(1)
|
|
|
|
-- CRITICAL: Force takeoff from runway to prevent aircraft getting stuck at parking
|
|
-- SetTakeoffRunway() ensures aircraft spawn directly on runway and take off immediately
|
|
if rat.SetTakeoffRunway then
|
|
rat:SetTakeoffRunway()
|
|
log("DEBUG: Configured cargo to take off from runway at " .. origin, true)
|
|
else
|
|
log("WARNING: SetTakeoffRunway() not available - falling back to SetTakeoffHot()", true)
|
|
if rat.SetTakeoffHot then rat:SetTakeoffHot() end
|
|
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
|
|
announceToCoalition(coalitionKey, "Resupply mission to " .. destination .. " failed (spawn error)!")
|
|
-- Mark mission as failed and cleanup immediately - do NOT track failed spawns
|
|
mission.status = "failed"
|
|
return
|
|
end
|
|
end
|
|
|
|
|
|
-- 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.
|
|
Skips squadrons that are captured or not operational.
|
|
]]
|
|
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
|
|
-- Skip non-operational squadrons (captured, destroyed, etc.)
|
|
if squadron.state and squadron.state ~= "operational" then
|
|
log("Squadron " .. squadron.displayName .. " (" .. coalitionKey .. ") is " .. squadron.state .. " - skipping cargo dispatch", true)
|
|
else
|
|
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
|
|
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 CONSOLE HELPERS
|
|
--------------------------------------------------------------------------
|
|
Functions you can call from the DCS Lua console (F12) to debug issues.
|
|
]]
|
|
|
|
-- Check airbase coalition ownership for all configured supply airbases
|
|
-- Usage: _G.TDAC_CheckAirbaseOwnership()
|
|
function _G.TDAC_CheckAirbaseOwnership()
|
|
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
|
|
env.info("[TDAC DIAGNOSTIC] Checking Coalition Ownership of All Supply Airbases")
|
|
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
|
|
|
|
for _, coalitionKey in ipairs({"red", "blue"}) do
|
|
local config = CARGO_SUPPLY_CONFIG[coalitionKey]
|
|
local expectedCoalition = getCoalitionSide(coalitionKey)
|
|
|
|
env.info(string.format("[TDAC DIAGNOSTIC] %s COALITION (expected coalition ID: %s)", coalitionKey:upper(), tostring(expectedCoalition)))
|
|
|
|
if config and config.supplyAirfields then
|
|
for _, airbaseName in ipairs(config.supplyAirfields) do
|
|
local airbase = AIRBASE:FindByName(airbaseName)
|
|
if not airbase then
|
|
env.info(string.format("[TDAC DIAGNOSTIC] ✗ %-30s - NOT FOUND (invalid name or not on this map)", airbaseName))
|
|
else
|
|
local actualCoalition = airbase:GetCoalition()
|
|
local coalitionName = "UNKNOWN"
|
|
local status = "✗"
|
|
|
|
if actualCoalition == coalition.side.NEUTRAL then
|
|
coalitionName = "NEUTRAL"
|
|
elseif actualCoalition == coalition.side.RED then
|
|
coalitionName = "RED"
|
|
elseif actualCoalition == coalition.side.BLUE then
|
|
coalitionName = "BLUE"
|
|
end
|
|
|
|
if actualCoalition == expectedCoalition then
|
|
status = "✓"
|
|
end
|
|
|
|
env.info(string.format("[TDAC DIAGNOSTIC] %s %-30s - %s (coalition ID: %s)", status, airbaseName, coalitionName, tostring(actualCoalition)))
|
|
end
|
|
end
|
|
else
|
|
env.info("[TDAC DIAGNOSTIC] ERROR: No supply airfields configured!")
|
|
end
|
|
env.info("[TDAC DIAGNOSTIC] ───────────────────────────────────────")
|
|
end
|
|
|
|
env.info("[TDAC DIAGNOSTIC] ═══════════════════════════════════════")
|
|
env.info("[TDAC DIAGNOSTIC] Check complete. ✓ = Owned by correct coalition, ✗ = Wrong coalition or not found")
|
|
return true
|
|
end
|
|
|
|
-- Check specific airbase coalition ownership
|
|
-- Usage: _G.TDAC_CheckAirbase("Olenya")
|
|
function _G.TDAC_CheckAirbase(airbaseName)
|
|
if type(airbaseName) ~= 'string' then
|
|
env.info("[TDAC DIAGNOSTIC] ERROR: airbaseName must be a string")
|
|
return false
|
|
end
|
|
|
|
local airbase = AIRBASE:FindByName(airbaseName)
|
|
if not airbase then
|
|
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' NOT FOUND (invalid name or not on this map)", airbaseName))
|
|
return false, "not_found"
|
|
end
|
|
|
|
local actualCoalition = airbase:GetCoalition()
|
|
local coalitionName = "UNKNOWN"
|
|
|
|
if actualCoalition == coalition.side.NEUTRAL then
|
|
coalitionName = "NEUTRAL"
|
|
elseif actualCoalition == coalition.side.RED then
|
|
coalitionName = "RED"
|
|
elseif actualCoalition == coalition.side.BLUE then
|
|
coalitionName = "BLUE"
|
|
end
|
|
|
|
env.info(string.format("[TDAC DIAGNOSTIC] Airbase '%s' - Coalition: %s (ID: %s)", airbaseName, coalitionName, tostring(actualCoalition)))
|
|
env.info(string.format("[TDAC DIAGNOSTIC] IsAlive: %s", tostring(airbase:IsAlive())))
|
|
|
|
-- Check parking spots
|
|
local function spotsFor(term, termName)
|
|
local ok, n = pcall(function() return airbase:GetParkingSpotsNumber(term) end)
|
|
if ok and n then
|
|
env.info(string.format("[TDAC DIAGNOSTIC] Parking %-15s: %d spots", termName, n))
|
|
end
|
|
end
|
|
|
|
spotsFor(AIRBASE.TerminalType.OpenBig, "OpenBig")
|
|
spotsFor(AIRBASE.TerminalType.OpenMed, "OpenMed")
|
|
spotsFor(AIRBASE.TerminalType.OpenMedOrBig, "OpenMedOrBig")
|
|
spotsFor(AIRBASE.TerminalType.Runway, "Runway")
|
|
|
|
return true, coalitionName, actualCoalition
|
|
end
|
|
|
|
env.info("[TDAC DIAGNOSTIC] Console helpers loaded:")
|
|
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbaseOwnership() - Check all supply airbases")
|
|
env.info("[TDAC DIAGNOSTIC] _G.TDAC_CheckAirbase('Olenya') - Check specific airbase")
|
|
env.info("[TDAC DIAGNOSTIC] _G.TDAC_RunConfigCheck() - Validate dispatcher config")
|
|
env.info("[TDAC DIAGNOSTIC] _G.TDAC_LogAirbaseParking('Olenya') - Check parking availability")
|
|
|
|
|
|
-- Diagnostic helper: call from DCS console to test spawn-by-name and routing.
|
|
-- Example (paste into DCS Lua console):
|
|
-- _G.TDAC_CargoDispatcher_TestSpawn("CARGO_BLUE_C130_TEMPLATE", "Kittila", "Luostari Pechenga")
|
|
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
|
|
|