commit 38c45e8ef0b2f87f0b17567f575e82887b2d7987 Author: iTracerFacer <134304944+iTracerFacer@users.noreply.github.com> Date: Mon Nov 17 07:34:08 2025 -0600 Inital Commit diff --git a/Cargo_Delivered.ogg b/Cargo_Delivered.ogg new file mode 100644 index 0000000..c223eef Binary files /dev/null and b/Cargo_Delivered.ogg differ diff --git a/Moose_TADC_CargoDispatcher.lua b/Moose_TADC_CargoDispatcher.lua new file mode 100644 index 0000000..908c018 --- /dev/null +++ b/Moose_TADC_CargoDispatcher.lua @@ -0,0 +1,871 @@ +--[[ +═══════════════════════════════════════════════════════════════════════════════ + 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 + +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Single-run guard to prevent duplicate dispatcher loops if script is reloaded +if _G.__TDAC_DISPATCHER_RUNNING then + env.info("[TDAC] CargoDispatcher already running; aborting duplicate load") + return +end +_G.__TDAC_DISPATCHER_RUNNING = true + +--[[ + 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 = "[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" or m.status == "completed" then + if not (m.group and m.group:IsAlive()) then + log("Cleaning up " .. m.status .. " cargo mission: " .. (m.group and m.group:GetName() or "nil group")) + 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:InitUnControlled(false) -- force departing transports to spawn in a controllable state + rat:InitLateActivated(false) + 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 + diff --git a/Moose_TADC_Load2nd.lua b/Moose_TADC_Load2nd.lua new file mode 100644 index 0000000..7b06b50 --- /dev/null +++ b/Moose_TADC_Load2nd.lua @@ -0,0 +1,2617 @@ +--[[ +═══════════════════════════════════════════════════════════════════════════════ + UNIVERSAL TADC + Dual-Coalition Tactical Air Defense Controller + Advanced Zone-Based System +═══════════════════════════════════════════════════════════════════════════════ + +DESCRIPTION: +This script provides a sophisticated automated air defense system for BOTH RED and +BLUE coalitions operating independently. Features advanced zone-based area of +responsibility (AOR) management, allowing squadrons to respond differently based +on threat location and priority levels. Perfect for complex scenarios requiring +realistic air defense behavior and tactical depth. + +CORE FEATURES: +• Dual-coalition support with completely independent operation +• Advanced zone-based area of responsibility system (Primary/Secondary/Tertiary) +• Automatic threat detection with intelligent interceptor allocation +• Multi-squadron management with individual cooldowns and aircraft tracking +• Dynamic cargo aircraft replenishment system +• Configurable intercept ratios with zone-specific response modifiers +• Smart interceptor routing, engagement, and RTB (Return to Base) behavior +• Real-time airbase status monitoring (operational/captured/destroyed) +• Comprehensive configuration validation and error reporting +• Asymmetric warfare support with coalition-specific capabilities +• Emergency cleanup systems and safety nets for mission stability + +ADVANCED ZONE SYSTEM: +Each squadron can be configured with up to three zone types: +• PRIMARY ZONE: Main area of responsibility (full response ratio) +• SECONDARY ZONE: Support area (reduced response, optional low-priority filtering) +• TERTIARY ZONE: Emergency/fallback area (enhanced response when base threatened) +• Squadrons will respond based on threat location relative to their zones +• Zone-specific response modifiers can be configured for each squadron +• Zones may overlap between squadrons for layered defense. + +ADVANCED ZONE SETUP: +• Create zones in the mission editor (MOOSE polygons, circles, etc.) +• Assign zone names to squadrons in the configuration (exact match required) +• Leave zones as nil for global threat response (no zone restrictions) +• Each zone is defined by placing a helicopter group with waypoints outlining the area +• The script will create polygon zones from the helicopter waypoints automatically + +Zone response behaviors include: +• Distance-based engagement limits (max range from airbase) +• Priority thresholds for threat classification (major vs minor threats) +• Fallback conditions (auto-switch to tertiary when squadron weakened) +• Response ratio multipliers per zone type +• Low-priority threat filtering in secondary zones + +REPLENISHMENT SYSTEM: +• Automated cargo aircraft detection system that monitors for transport aircraft + flyovers to replenish squadron aircraft counts (fixed wing only): +• Detects cargo aircraft by name patterns (CARGO, TRANSPORT, C130, C-130, AN26, AN-26) +• Monitors flyover proximity to friendly airbases (no landing required) +• Replenishes squadron aircraft up to maximum capacity per airbase +• Prevents duplicate processing of the same cargo delivery +• Coalition-specific replenishment amounts configurable independently +• Supports sustained operations over extended mission duration + +*** This system does not spawn or manage cargo aircraft - it only detects when +your existing cargo aircraft complete deliveries via flyover. Create and route your own +transport missions to maintain squadron strength. Aircraft can deliver supplies by +flying within 3000m of any configured airbase without needing to land. *** + +INTERCEPT RATIO SYSTEM: +Sophisticated threat response calculation with zone-based modifiers: +• Base intercept ratio (e.g., 0.8 = 8 interceptors per 10 threats) +• Zone-specific multipliers (primary: 1.0, secondary: 0.6, tertiary: 1.4) +• Threat size considerations (larger formations get proportional response) +• Squadron selection based on zone priority and proximity +• Aircraft availability and cooldown status factored into decisions + +SETUP INSTRUCTIONS: +1. Load MOOSE framework in mission before this script +2. Configure Squadrons: Create fighter aircraft GROUP templates for both coalitions in mission editor +3. Configure RED squadrons in RED_SQUADRON_CONFIG section +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG section +5. Optionally create zones in mission editor for area-of-responsibility using helicopter groups with waypoints. +6. Set coalition behavior parameters in TADC_SETTINGS +7. Configure cargo patterns in ADVANCED_SETTINGS if using replenishment +8. Add this script as "DO SCRIPT" trigger at mission start (after MOOSE loaded) +9. Create and manage cargo aircraft missions for replenishment (optional) + +CONFIGURATION VALIDATION: +Built-in validation system checks for: +• Template existence and proper naming +• Airbase name accuracy and coalition control +• Zone existence in mission editor +• Parameter ranges and logical consistency +• Coalition enablement and squadron availability +• Prevents common configuration errors before mission starts + +TACTICAL SCENARIOS SUPPORTED: +• Balanced air warfare with equal capabilities and symmetric response +• Asymmetric scenarios with different coalition strengths and capabilities +• Layered air defense with overlapping squadron zones +• Border/perimeter defense with primary and fallback positions +• Training missions for AI vs AI air combat observation +• Dynamic frontline battles with shifting territorial control +• Long-duration missions with cargo resupply operations +• Emergency response scenarios with threat priority management + +LOGGING AND MONITORING: +• Real-time threat detection and interceptor launch notifications +• Squadron status reports including aircraft counts and cooldown timers +• Airbase operational status with capture/destruction detection +• Cargo delivery tracking and replenishment confirmations +• Zone-based engagement decisions with detailed reasoning +• Configuration validation results and error reporting +• Performance monitoring with emergency cleanup notifications + +REQUIREMENTS: +• MOOSE framework (https://github.com/FlightControl-Master/MOOSE) +• Fighter aircraft GROUP templates (not UNIT templates) for each coalition +• Airbases must exist in mission and be under correct coalition control +• Zone objects in mission editor (if using zone-based features) +• Proper template naming matching squadron configuration + +AUTHOR: +• Based off MOOSE framework by FlightControl-Master +• Developed and customized by Mission Designer "F99th-TracerFacer" + +VERSION: 1.0 +═══════════════════════════════════════════════════════════════════════════════ +]] + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + MAIN SETTINGS +═══════════════════════════════════════════════════════════════════════════════ +]] + +-- Core TADC behavior settings - applies to BOTH coalitions unless overridden +local TADC_SETTINGS = { + -- Enable/Disable coalitions + enableRed = true, -- Set to false to disable RED TADC + enableBlue = true, -- Set to false to disable BLUE TADC + + -- Timing settings (applies to both coalitions) + checkInterval = 30, -- How often to scan for threats (seconds) + monitorInterval = 30, -- How often to check interceptor status (seconds) + statusReportInterval = 1805, -- How often to report airbase status (seconds) + squadronSummaryInterval = 1800, -- How often to broadcast squadron summary (seconds) + cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) + + -- RED Coalition Settings + red = { + maxActiveCAP = 24, -- Maximum RED fighters airborne at once + squadronCooldown = 600, -- RED cooldown after squadron launch (seconds) + interceptRatio = 1.2, -- RED interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) + rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds) + }, + + -- BLUE Coalition Settings + blue = { + maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once + squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds) + interceptRatio = 1.2, -- BLUE interceptors per threat aircraft + cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery + emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) + rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds) + }, +} + + +--[[ +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) <- DEFAULT +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) +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, creates balanced dogfights +• 1.2-1.4: Coalition advantage, challenging for enemy +• 1.6-1.8: Strong defense, difficult penetration missions +• 1.9-2.0: Nearly impenetrable, may exhaust squadrons quickly + +SQUADRON IMPACT: +• Low ratios (0.2-0.8): Squadrons last longer, sustained defense +• High ratios (1.6-2.0): Rapid squadron depletion, coverage gaps +• Sweet spot (1.0-1.4): Balanced response with good coverage duration + +ASYMMETRIC SCENARIOS: +• Set RED ratio 1.2, BLUE ratio 0.8 = RED advantage +• Set RED ratio 0.6, BLUE ratio 1.4 = BLUE advantage +• Different maxActiveCAP values create capacity imbalances +]] + + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + ADVANCED SETTINGS +═══════════════════════════════════════════════════════════════════════════════ + +These settings control more detailed behavior. Most users won't need to change these. +]] + +local ADVANCED_SETTINGS = { + -- Cargo aircraft detection patterns (aircraft with these names will replenish squadrons (Currently only fixed wing aircraft supported)) + cargoPatterns = {"CARGO", "TRANSPORT", "C130", "C-130", "AN26", "AN-26"}, + + -- 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, + + -- RTB settings + rtbAltitude = 6000, -- Return to base altitude (feet) + rtbSpeed = 430, -- Return to base speed (knots) + + -- Logging settings + 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, +} + +--[[ +═══════════════════════════════════════════════════════════════════════════════ + SYSTEM CODE + (DO NOT MODIFY BELOW THIS LINE) +═══════════════════════════════════════════════════════════════════════════════ +]] + + + +-- Internal tracking variables - separate for each coalition +local activeInterceptors = { + red = {}, + blue = {} +} +local lastLaunchTime = { + red = {}, + blue = {} +} +local assignedThreats = { + red = {}, + blue = {} +} +local squadronCooldowns = { + red = {}, + blue = {} +} +squadronAircraftCounts = { + red = {}, + 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 = {} +} + +local function coalitionKeyFromSide(side) + if side == coalition.side.RED then return "red" end + if side == coalition.side.BLUE then return "blue" end + return nil +end + +local function cleanupInterceptorEntry(interceptorName, coalitionKey) + if not interceptorName or not coalitionKey then return end + if activeInterceptors[coalitionKey] then + activeInterceptors[coalitionKey][interceptorName] = nil + end + if aircraftSpawnTracking[coalitionKey] then + aircraftSpawnTracking[coalitionKey][interceptorName] = nil + end +end + +local function destroyInterceptorGroup(interceptor, coalitionKey, delaySeconds) + if not interceptor then return end + + local name = nil + if interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + + local resolvedKey = coalitionKey + if not resolvedKey and interceptor.GetCoalition then + local ok, side = pcall(function() return interceptor:GetCoalition() end) + if ok then + resolvedKey = coalitionKeyFromSide(side) + end + end + + local function doDestroy() + if interceptor and interceptor.IsAlive and interceptor:IsAlive() then + pcall(function() interceptor:Destroy() end) + end + if name and resolvedKey then + cleanupInterceptorEntry(name, resolvedKey) + end + end + + if delaySeconds and delaySeconds > 0 then + timer.scheduleFunction(function() + doDestroy() + return + end, {}, timer.getTime() + delaySeconds) + else + doDestroy() + end +end + +local function finalizeCargoMission(cargoGroup, squadron, coalitionKey) + if not cargoMissions or not coalitionKey or not squadron or not squadron.airbaseName then + return + end + + local coalitionBucket = cargoMissions[coalitionKey] + if type(coalitionBucket) ~= "table" then + return + end + + local groupName = nil + if cargoGroup and cargoGroup.GetName then + local ok, value = pcall(function() return cargoGroup:GetName() end) + if ok then groupName = value end + end + + for idx = #coalitionBucket, 1, -1 do + local mission = coalitionBucket[idx] + if mission and mission.destination == squadron.airbaseName then + local missionGroupName = nil + if mission.group and mission.group.GetName then + local ok, value = pcall(function() return mission.group:GetName() end) + if ok then missionGroupName = value end + end + + if not groupName or missionGroupName == groupName then + mission.status = "completed" + mission.completedAt = timer.getTime() + + if mission.group and mission.group.Destroy then + local targetGroup = mission.group + timer.scheduleFunction(function() + pcall(function() + if targetGroup and targetGroup.IsAlive and targetGroup:IsAlive() then + targetGroup:Destroy() + end + end) + return + end, {}, timer.getTime() + 90) + end + + table.remove(coalitionBucket, idx) + end + end + end +end + +-- Logging function +local function log(message, detailed) + if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then + env.info(ADVANCED_SETTINGS.logPrefix .. " " .. message) + end +end + +local function safeCoordinate(object) + if not object or type(object) ~= "table" or not object.GetCoordinate then + return nil + end + local ok, coord = pcall(function() return object:GetCoordinate() end) + if ok and coord then + return coord + end + return nil +end + +-- Performance optimization: Cache SET_GROUP objects to avoid repeated creation +local cachedSets = { + redCargo = nil, + blueCargo = nil, + redAircraft = nil, + blueAircraft = nil +} + +if type(RED_SQUADRON_CONFIG) ~= "table" then + local msg = "CONFIG ERROR: RED_SQUADRON_CONFIG is missing or not loaded. Make sure Moose_TADC_SquadronConfigs_Load1st.lua is loaded before this script." + log(msg, true) + MESSAGE:New(msg, 30):ToAll() +end +if type(BLUE_SQUADRON_CONFIG) ~= "table" then + local msg = "CONFIG ERROR: BLUE_SQUADRON_CONFIG is missing or not loaded. Make sure Moose_TADC_SquadronConfigs_Load1st.lua is loaded before this script." + log(msg, true) + MESSAGE:New(msg, 30):ToAll() +end + +for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.red[squadron.templateName] = squadron.aircraft + end +end + +for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.aircraft and squadron.templateName then + squadronAircraftCounts.blue[squadron.templateName] = squadron.aircraft + end +end + +-- Squadron resource summary generator + +local function getSquadronResourceSummary(coalitionSide) + local function getStatus(remaining, max, state) + if state == "captured" then return "[CAPTURED]" end + if state == "destroyed" then return "[DESTROYED]" end + if state ~= "operational" then return "[OFFLINE]" end + + local percent = (remaining / max) * 100 + if percent <= 10 then return "[CRITICAL]" end + if percent <= 25 then return "[LOW]" end + return "OK" + end + + local lines = {} + table.insert(lines, "-=[ Tactical Air Defense Controller ]=-\n") + table.insert(lines, "Squadron Resource Summary:\n") + table.insert(lines, "| Squadron | Aircraft Remaining | Status |") + table.insert(lines, "|--------------|--------------------|-------------|") + + if coalitionSide == coalition.side.RED then + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local remaining = squadronAircraftCounts.red[squadron.templateName] or 0 + local max = squadron.aircraft or 0 + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) + table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) + end + elseif coalitionSide == coalition.side.BLUE then + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local remaining = squadronAircraftCounts.blue[squadron.templateName] or 0 + local max = squadron.aircraft or 0 + local state = squadron.state or "operational" + local status = getStatus(remaining, max, state) + table.insert(lines, string.format("| %-13s | %2d / %-15d | %-11s |", squadron.displayName or squadron.templateName, remaining, max, status)) + end + end + + table.insert(lines, "\n- [CAPTURED]: Airbase captured by enemy\n- [LOW]: Below 25%\n- [CRITICAL]: Below 10%\n- OK: Above 25%") + return table.concat(lines, "\n") +end + +-- Broadcast squadron summary to all players +local function broadcastSquadronSummary() + if TADC_SETTINGS.enableRed then + local summaryRed = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summaryRed, 20):ToCoalition(coalition.side.RED) + end + if TADC_SETTINGS.enableBlue then + local summaryBlue = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summaryBlue, 20):ToCoalition(coalition.side.BLUE) + end +end + +-- Coalition-specific settings helper +local function getCoalitionSettings(coalitionSide) + if coalitionSide == coalition.side.RED then + return TADC_SETTINGS.red, "RED" + elseif coalitionSide == coalition.side.BLUE then + return TADC_SETTINGS.blue, "BLUE" + else + return nil, "UNKNOWN" + end +end + +-- Get squadron config for coalition +local function getSquadronConfig(coalitionSide) + if coalitionSide == coalition.side.RED then + return RED_SQUADRON_CONFIG + elseif coalitionSide == coalition.side.BLUE then + return BLUE_SQUADRON_CONFIG + else + return {} + end +end + +-- Check if coordinate is within a zone +local function isInZone(coordinate, zoneName) + if not zoneName or zoneName == "" then + return false + end + + -- Try to find the zone + local zone = ZONE:FindByName(zoneName) + if zone then + return zone:IsCoordinateInZone(coordinate) + else + -- Try to create polygon zone from helicopter group waypoints if not found + local group = GROUP:FindByName(zoneName) + if group then + -- Create polygon zone using the group's waypoints as vertices + zone = ZONE_POLYGON:NewFromGroupName(zoneName, zoneName) + if zone then + log("Created polygon zone '" .. zoneName .. "' from helicopter waypoints") + return zone:IsCoordinateInZone(coordinate) + else + log("Warning: Could not create polygon zone from group '" .. zoneName .. "' - check waypoints") + end + else + log("Warning: No group named '" .. zoneName .. "' found for zone creation") + end + + log("Warning: Zone '" .. zoneName .. "' not found in mission and could not create from helicopter group", true) + return false + end +end + +-- Get default zone configuration +local function getDefaultZoneConfig() + return { + primaryResponse = 1.0, + secondaryResponse = 0.6, + tertiaryResponse = 1.4, + maxRange = 200, + enableFallback = false, + priorityThreshold = 4, + ignoreLowPriority = false, + } +end + +-- Check if squadron should respond to fallback conditions +local function checkFallbackConditions(squadron, coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if airbase is under attack (simplified - check if base has low aircraft) + local currentAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local aircraftRatio = currentAircraft / maxAircraft + + -- Trigger fallback if squadron is below 50% strength or base is threatened + if aircraftRatio < 0.5 then + return true + end + + -- Could add more complex conditions here (base under attack, etc.) + return false +end + +-- Get threat zone priority and response ratio for squadron +local function getThreatZonePriority(threatCoord, squadron, coalitionSide) + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + + -- Check distance from airbase first + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase then + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) / 1852 -- Convert meters to nautical miles + + if distance > zoneConfig.maxRange then + return "none", 0, "out of range (" .. math.floor(distance) .. "nm > " .. zoneConfig.maxRange .. "nm)" + end + end + + -- Check tertiary zone first (highest priority if fallback enabled) + if squadron.tertiaryZone and zoneConfig.enableFallback then + if checkFallbackConditions(squadron, coalitionSide) then + if isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "fallback zone (enhanced response)" + end + end + end + + -- Check primary zone + if squadron.primaryZone and isInZone(threatCoord, squadron.primaryZone) then + return "primary", zoneConfig.primaryResponse, "primary AOR" + end + + -- Check secondary zone + if squadron.secondaryZone and isInZone(threatCoord, squadron.secondaryZone) then + return "secondary", zoneConfig.secondaryResponse, "secondary AOR" + end + + -- Check tertiary zone (normal priority) + if squadron.tertiaryZone and isInZone(threatCoord, squadron.tertiaryZone) then + return "tertiary", zoneConfig.tertiaryResponse, "tertiary zone" + end + + -- If no zones are defined, use global response + if not squadron.primaryZone and not squadron.secondaryZone and not squadron.tertiaryZone then + return "global", 1.0, "global response (no zones defined)" + end + + -- Outside all defined zones + return "none", 0, "outside defined zones" +end + +-- Startup validation +local function validateConfiguration() + local errors = {} + + -- Check coalition enablement + if not TADC_SETTINGS.enableRed and not TADC_SETTINGS.enableBlue then + table.insert(errors, "Both coalitions disabled - enable at least one in TADC_SETTINGS") + end + + -- Validate RED squadrons if enabled + if TADC_SETTINGS.enableRed then + if #RED_SQUADRON_CONFIG == 0 then + table.insert(errors, "No RED squadrons configured but RED TADC is enabled") + else + for i, squadron in pairs(RED_SQUADRON_CONFIG) do + local prefix = "RED Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "RED_CAP_SQUADRON_1" or + squadron.templateName == "RED_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_RED_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + -- Check if there's a helicopter unit/group with this name for zone creation + local unit = UNIT:FindByName(zoneName) + local group = GROUP:FindByName(zoneName) + if not unit and not group then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") + end + end + end + end + end + end + end + + -- Validate BLUE squadrons if enabled + if TADC_SETTINGS.enableBlue then + if #BLUE_SQUADRON_CONFIG == 0 then + table.insert(errors, "No BLUE squadrons configured but BLUE TADC is enabled") + else + for i, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local prefix = "BLUE Squadron " .. i .. ": " + + if not squadron.templateName or squadron.templateName == "" or + squadron.templateName == "BLUE_CAP_SQUADRON_1" or + squadron.templateName == "BLUE_CAP_SQUADRON_2" then + table.insert(errors, prefix .. "templateName not configured or using default example") + end + + if not squadron.displayName or squadron.displayName == "" then + table.insert(errors, prefix .. "displayName not configured") + end + + if not squadron.airbaseName or squadron.airbaseName == "" or + squadron.airbaseName:find("YOUR_BLUE_AIRBASE") then + table.insert(errors, prefix .. "airbaseName not configured or using default example") + end + + if not squadron.aircraft or squadron.aircraft <= 0 then + table.insert(errors, prefix .. "aircraft count not configured or invalid") + end + + -- Validate zone configuration if zones are specified + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + if squadron.zoneConfig then + local zc = squadron.zoneConfig + if zc.primaryResponse and (zc.primaryResponse < 0 or zc.primaryResponse > 5) then + table.insert(errors, prefix .. "primaryResponse ratio out of range (0-5)") + end + if zc.secondaryResponse and (zc.secondaryResponse < 0 or zc.secondaryResponse > 5) then + table.insert(errors, prefix .. "secondaryResponse ratio out of range (0-5)") + end + if zc.tertiaryResponse and (zc.tertiaryResponse < 0 or zc.tertiaryResponse > 5) then + table.insert(errors, prefix .. "tertiaryResponse ratio out of range (0-5)") + end + if zc.maxRange and (zc.maxRange < 10 or zc.maxRange > 1000) then + table.insert(errors, prefix .. "maxRange out of range (10-1000 nm)") + end + end + + -- Check if specified zones exist in mission + local zones = {} + if squadron.primaryZone then table.insert(zones, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, squadron.tertiaryZone) end + + for _, zoneName in ipairs(zones) do + local zoneObj = ZONE:FindByName(zoneName) + if not zoneObj then + -- Check if there's a helicopter unit/group with this name for zone creation + local unit = UNIT:FindByName(zoneName) + local group = GROUP:FindByName(zoneName) + if not unit and not group then + table.insert(errors, prefix .. "zone '" .. zoneName .. "' not found in mission (no zone or helicopter unit named '" .. zoneName .. "')") + end + end + end + end + end + end + end + + -- Report errors + if #errors > 0 then + log("CONFIGURATION ERRORS DETECTED:") + MESSAGE:New("CONFIGURATION ERRORS DETECTED:", 30):ToAll() + for _, error in pairs(errors) do + log(" ✗ " .. error) + MESSAGE:New("CONFIG ERROR: " .. error, 30):ToAll() + end + log("Please fix configuration before using Universal TADC!") + MESSAGE:New("Please fix configuration before using Universal TADC!", 30):ToAll() + return false + else + log("Configuration validation passed ✓") + MESSAGE:New("Universal TADC configuration passed ✓", 10):ToAll() + return true + end +end + +-- Process cargo delivery for a squadron +local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coalitionKey) + -- Simple delivery processor: dedupe by group ID and credit supplies directly. + if not _G.processedDeliveries then + _G.processedDeliveries = {} + 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 "" + 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 "" + 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 + + finalizeCargoMission(cargoGroup, squadron, coalitionKey) +end + +-- Event handler for cargo aircraft landing (backup for actual landings) +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 + -- 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 + -- 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 + + 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 + +-- 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 + + if #alternativeAirbases > 0 then + -- Select random alternative airbase + local newAirbase = alternativeAirbases[math.random(1, #alternativeAirbases)] + + -- 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 + + -- Only check aircraft that have been spawned for at least the threshold time + if timeSinceSpawn >= stuckThreshold then + local currentPos = safeCoordinate(trackingData.group) + local spawnPos = trackingData.spawnPos + local distanceMoved = nil + + if currentPos and spawnPos and type(spawnPos) == "table" and spawnPos.Get2DDistance then + local okDist, dist = pcall(function() return spawnPos:Get2DDistance(currentPos) end) + if okDist and dist then + distanceMoved = dist + end + end + + if distanceMoved then + + -- 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") + + -- Mark airbase as having stuck aircraft + airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft" + + -- Remove the stuck aircraft and clear tracking + pcall(function() trackingData.group:Destroy() end) + cleanupInterceptorEntry(aircraftName, coalitionKey) + + -- 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 + else + log("Stuck monitor: no coordinate data for " .. aircraftName .. "; removing from tracking", true) + aircraftSpawnTracking[coalitionKey][aircraftName] = nil + end + end + else + -- Clean up dead aircraft from tracking + aircraftSpawnTracking[coalitionKey][aircraftName] = nil + end + end + end +end + +-- Send interceptor back to base +local function sendInterceptorHome(interceptor, coalitionSide) + if not interceptor or not interceptor:IsAlive() then + return + end + + -- Find nearest friendly airbase + local interceptorCoord = safeCoordinate(interceptor) + if not interceptorCoord then + log("ERROR: Could not get interceptor coordinates for RTB", true) + return + end + local nearestAirbase = nil + local nearestAirbaseCoord = nil + local shortestDistance = math.huge + local squadronConfig = getSquadronConfig(coalitionSide) + + -- Check all squadron airbases to find the nearest one that's still friendly + for _, squadron in pairs(squadronConfig) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:GetCoalition() == coalitionSide and airbase:IsAlive() then + local airbaseCoord = safeCoordinate(airbase) + if airbaseCoord then + local okDist, distance = pcall(function() return interceptorCoord:Get2DDistance(airbaseCoord) end) + if okDist and distance and distance < shortestDistance then + shortestDistance = distance + nearestAirbase = airbase + nearestAirbaseCoord = airbaseCoord + end + end + end + end + + if nearestAirbase and nearestAirbaseCoord then + local airbaseName = "airbase" + local okABName, fetchedABName = pcall(function() return nearestAirbase:GetName() end) + if okABName and fetchedABName then + airbaseName = fetchedABName + end + + local rtbAltitude = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters + local okRtb, rtbCoord = pcall(function() return nearestAirbaseCoord:SetAltitude(rtbAltitude) end) + if not okRtb or not rtbCoord then + log("ERROR: Failed to compute RTB coordinate for " .. airbaseName, true) + return + end + + -- Clear current tasks and route home + pcall(function() interceptor:ClearTasks() end) + local routeOk, routeErr = pcall(function() interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") end) + + local _, coalitionName = getCoalitionSettings(coalitionSide) + local interceptorName = "interceptor" + local okName, fetchedName = pcall(function() return interceptor:GetName() end) + if okName and fetchedName then + interceptorName = fetchedName + end + + if not routeOk and routeErr then + log("ERROR: Failed to assign RTB route for " .. interceptorName .. " -> " .. airbaseName .. ": " .. tostring(routeErr), true) + else + log("Sending " .. coalitionName .. " " .. interceptorName .. " back to " .. airbaseName, true) + end + + -- Schedule cleanup after they should have landed + local coalitionSettings = getCoalitionSettings(coalitionSide) + local flightTime = math.ceil(shortestDistance / (ADVANCED_SETTINGS.rtbSpeed * 0.5144)) + coalitionSettings.rtbFlightBuffer + + SCHEDULER:New(nil, function() + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + local name = nil + if interceptor and interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + if name and activeInterceptors[coalitionKey][name] then + destroyInterceptorGroup(interceptor, coalitionKey, 0) + log("Cleaned up " .. coalitionName .. " " .. name .. " after RTB", true) + end + end, {}, flightTime) + else + local _, coalitionName = getCoalitionSettings(coalitionSide) + log("No friendly airbase found for " .. coalitionName .. " " .. interceptor:GetName() .. ", will clean up normally") + end +end + +-- Check if airbase is still usable +local function isAirbaseUsable(airbaseName, expectedCoalition) + local airbase = AIRBASE:FindByName(airbaseName) + if not airbase then + return false, "not found" + elseif airbase:GetCoalition() ~= expectedCoalition then + local capturedBy = "Unknown" + if airbase:GetCoalition() == coalition.side.RED then + capturedBy = "Red" + elseif airbase:GetCoalition() == coalition.side.BLUE then + capturedBy = "Blue" + else + capturedBy = "Neutral" + end + return false, "captured by " .. capturedBy + elseif not airbase:IsAlive() then + return false, "destroyed" + else + return true, "operational" + end +end + +-- Count active fighters for coalition +local function countActiveFighters(coalitionSide) + local count = 0 + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + for _, interceptorData in pairs(activeInterceptors[coalitionKey]) 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 for coalition using zone-based priorities +local function findBestSquadron(threatCoord, threatSize, coalitionSide) + local bestSquadron = nil + local bestPriority = "none" + local bestResponseRatio = 0 + local shortestDistance = math.huge + local currentTime = timer.getTime() + local squadronConfig = getSquadronConfig(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + local zonePriorityOrder = {"tertiary", "primary", "secondary", "global"} + + -- First pass: find squadrons that can respond to this threat + local availableSquadrons = {} + + for _, squadron in pairs(squadronConfig) do + -- Check basic availability + local squadronAvailable = true + local unavailableReason = "" + + -- Check squadron state first + if squadron.state and squadron.state ~= "operational" then + squadronAvailable = false + if squadron.state == "captured" then + unavailableReason = "airbase captured by enemy" + elseif squadron.state == "destroyed" then + unavailableReason = "airbase destroyed" + else + unavailableReason = "squadron not operational (state: " .. tostring(squadron.state) .. ")" + end + end + + -- Check cooldown + if squadronAvailable and squadronCooldowns[coalitionKey][squadron.templateName] then + local cooldownEnd = squadronCooldowns[coalitionKey][squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + squadronAvailable = false + unavailableReason = "on cooldown for " .. timeLeft .. "m" + else + -- Cooldown expired, remove it + squadronCooldowns[coalitionKey][squadron.templateName] = nil + log(coalitionName .. " Squadron " .. squadron.displayName .. " cooldown expired, available for launch", true) + end + end + + -- Check aircraft availability + if squadronAvailable then + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + if availableAircraft <= 0 then + squadronAvailable = false + unavailableReason = "no aircraft available (" .. availableAircraft .. "/" .. squadron.aircraft .. ")" + end + end + + -- Check airbase status + if squadronAvailable then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if not airbase then + squadronAvailable = false + unavailableReason = "airbase not found" + elseif airbase:GetCoalition() ~= coalitionSide then + squadronAvailable = false + unavailableReason = "airbase no longer under " .. coalitionName .. " control" + elseif not airbase:IsAlive() then + squadronAvailable = false + unavailableReason = "airbase destroyed" + end + end + + -- Check template exists (Note: Templates are validated during SPAWN:New() call) + -- Template validation is handled by MOOSE SPAWN class during actual spawning + + if squadronAvailable then + -- Get zone priority and response ratio + local zonePriority, responseRatio, zoneDescription = getThreatZonePriority(threatCoord, squadron, coalitionSide) + + -- Check if threat meets priority threshold for secondary zones + local zoneConfig = squadron.zoneConfig or getDefaultZoneConfig() + if zonePriority == "secondary" and zoneConfig.ignoreLowPriority then + if threatSize < zoneConfig.priorityThreshold then + log(coalitionName .. " " .. squadron.displayName .. " ignoring low-priority threat in secondary zone (" .. + threatSize .. " < " .. zoneConfig.priorityThreshold .. ")", true) + responseRatio = 0 + zonePriority = "none" + end + end + + if responseRatio > 0 then + local airbase = AIRBASE:FindByName(squadron.airbaseName) + local airbaseCoord = airbase:GetCoordinate() + local distance = airbaseCoord:Get2DDistance(threatCoord) + + table.insert(availableSquadrons, { + squadron = squadron, + zonePriority = zonePriority, + responseRatio = responseRatio, + distance = distance, + zoneDescription = zoneDescription + }) + + log(coalitionName .. " " .. squadron.displayName .. " can respond: " .. zoneDescription .. + " (ratio: " .. responseRatio .. ", distance: " .. math.floor(distance/1852) .. "nm)", true) + else + log(coalitionName .. " " .. squadron.displayName .. " will not respond: " .. zoneDescription, true) + end + else + log(coalitionName .. " " .. squadron.displayName .. " unavailable: " .. unavailableReason, true) + end + end + + -- Second pass: select best squadron by priority and distance + if #availableSquadrons > 0 then + -- Sort by zone priority (higher priority first), then by distance (closer first) + table.sort(availableSquadrons, function(a, b) + -- Get priority indices + local aPriorityIndex = 5 + local bPriorityIndex = 5 + for i, priority in ipairs(zonePriorityOrder) do + if a.zonePriority == priority then aPriorityIndex = i end + if b.zonePriority == priority then bPriorityIndex = i end + end + + -- First sort by priority (lower index = higher priority) + if aPriorityIndex ~= bPriorityIndex then + return aPriorityIndex < bPriorityIndex + end + + -- Then sort by distance (closer is better) + return a.distance < b.distance + end) + + local selected = availableSquadrons[1] + log("Selected " .. coalitionName .. " " .. selected.squadron.displayName .. " for response: " .. + selected.zoneDescription .. " (distance: " .. math.floor(selected.distance/1852) .. "nm)") + + return selected.squadron, selected.responseRatio, selected.zoneDescription + end + + if ADVANCED_SETTINGS.enableDetailedLogging then + log("No " .. coalitionName .. " squadron available for threat at coordinates") + end + return nil, 0, "no available squadrons" +end + +-- Launch interceptor for coalition +local function launchInterceptor(threatGroup, coalitionSide) + if not threatGroup or not threatGroup:IsAlive() then + return + end + + local threatCoord = threatGroup:GetCoordinate() + local threatName = threatGroup:GetName() + local threatSize = threatGroup:GetSize() + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + -- Check if threat already has interceptors assigned + if assignedThreats[coalitionKey][threatName] then + local assignedInterceptors = assignedThreats[coalitionKey][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[coalitionKey][threatName] = nil + end + end + + -- Find best squadron using zone-based priority system first + local squadron, zoneResponseRatio, zoneDescription = findBestSquadron(threatCoord, threatSize, coalitionSide) + + if not squadron then + if ADVANCED_SETTINGS.enableDetailedLogging then + log("No " .. coalitionName .. " squadron available") + end + return + end + + -- Calculate how many interceptors to launch using zone-modified ratio + local finalInterceptRatio = coalitionSettings.interceptRatio * zoneResponseRatio + local interceptorsNeeded = math.max(1, math.ceil(threatSize * finalInterceptRatio)) + + -- Check if we have capacity + if countActiveFighters(coalitionSide) + interceptorsNeeded > coalitionSettings.maxActiveCAP then + interceptorsNeeded = coalitionSettings.maxActiveCAP - countActiveFighters(coalitionSide) + if interceptorsNeeded <= 0 then + log(coalitionName .. " max fighters airborne, skipping launch") + return + end + end + if not squadron then + if ADVANCED_SETTINGS.enableDetailedLogging then + log("No " .. coalitionName .. " squadron available") + end + return + end + + -- Limit interceptors to available aircraft + local availableAircraft = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + interceptorsNeeded = math.min(interceptorsNeeded, availableAircraft) + + if interceptorsNeeded <= 0 then + log(coalitionName .. " Squadron " .. squadron.displayName .. " has no aircraft to launch") + return + end + + -- Launch multiple interceptors to match threat + local spawn = SPAWN:New(squadron.templateName) + if not spawn then + log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName) + return + end + spawn:InitCleanUp(900) + + 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 = safeCoordinate(threatGroup) + if currentThreatCoord then + local okIntercept, interceptCoord = pcall(function() + return currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) + end) + if okIntercept and interceptCoord then + pcall(function() + interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") + end) + end + + -- 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 + local interceptorName = "interceptor" + local okName, fetchedName = pcall(function() return interceptor:GetName() end) + if okName and fetchedName then + interceptorName = fetchedName + end + + activeInterceptors[coalitionKey][interceptorName] = { + group = interceptor, + squadron = squadron.templateName, + displayName = squadron.displayName + } + + -- Track spawn position for stuck aircraft detection + local spawnPos = safeCoordinate(interceptor) + if spawnPos then + aircraftSpawnTracking[coalitionKey][interceptorName] = { + spawnPos = spawnPos, + spawnTime = timer.getTime(), + squadron = squadron, + airbase = squadron.airbaseName + } + log("Tracking spawn position for " .. interceptorName .. " at " .. squadron.airbaseName, true) + end + + -- Emergency cleanup (safety net) + SCHEDULER:New(nil, function() + local name = nil + if interceptor and interceptor.GetName then + local ok, value = pcall(function() return interceptor:GetName() end) + if ok then name = value end + end + if name and activeInterceptors[coalitionKey][name] then + log("Emergency cleanup of " .. coalitionName .. " " .. name .. " (should have RTB'd)") + destroyInterceptorGroup(interceptor, coalitionKey, 0) + end + end, {}, coalitionSettings.emergencyCleanupTime) + end + end + + -- Log the launch and track assignment + if #interceptors > 0 then + -- Decrement squadron aircraft count + local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0 + squadronAircraftCounts[coalitionKey][squadron.templateName] = math.max(0, currentCount - #interceptors) + local remainingCount = squadronAircraftCounts[coalitionKey][squadron.templateName] + + log("Launched " .. #interceptors .. " x " .. coalitionName .. " " .. squadron.displayName .. " to intercept " .. + threatSize .. " x " .. threatName .. " (" .. zoneDescription .. ", ratio: " .. string.format("%.1f", finalInterceptRatio) .. + ", remaining: " .. remainingCount .. "/" .. squadron.aircraft .. ")") + assignedThreats[coalitionKey][threatName] = interceptors + lastLaunchTime[coalitionKey][threatName] = timer.getTime() + + -- Apply cooldown immediately when squadron launches + local currentTime = timer.getTime() + squadronCooldowns[coalitionKey][squadron.templateName] = currentTime + coalitionSettings.squadronCooldown + local cooldownMinutes = coalitionSettings.squadronCooldown / 60 + log(coalitionName .. " Squadron " .. squadron.displayName .. " LAUNCHED! Applying " .. cooldownMinutes .. " minute cooldown") + end +end + +-- Main threat detection loop for coalition +local function detectThreatsForCoalition(coalitionSide) + local coalitionSettings, coalitionName = getCoalitionSettings(coalitionSide) + local enemyCoalition = (coalitionSide == coalition.side.RED) and "blue" or "red" + local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" + + log("Scanning for " .. coalitionName .. " threats...", true) + + -- Clean up dead threats from tracking + local currentThreats = {} + + -- Find all enemy aircraft using cached set for performance + local cacheKey = enemyCoalition .. "Aircraft" + if not cachedSets[cacheKey] then + cachedSets[cacheKey] = SET_GROUP:New():FilterCoalitions(enemyCoalition):FilterCategoryAirplane():FilterStart() + end + local enemyAircraft = cachedSets[cacheKey] + local threatCount = 0 + + enemyAircraft:ForEach(function(enemyGroup) + if enemyGroup and enemyGroup:IsAlive() then + threatCount = threatCount + 1 + currentThreats[enemyGroup:GetName()] = true + log("Found " .. coalitionName .. " threat: " .. enemyGroup:GetName() .. " (" .. enemyGroup:GetTypeName() .. ")", true) + + -- Launch interceptor for this threat + launchInterceptor(enemyGroup, coalitionSide) + end + end) + + -- Clean up assignments for threats that no longer exist and send interceptors home + for threatName, assignedInterceptors in pairs(assignedThreats[coalitionKey]) do + if not currentThreats[threatName] then + log("Threat " .. threatName .. " eliminated, sending " .. coalitionName .. " 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, coalitionSide) + end + end + else + -- Handle legacy single interceptor assignment + if assignedInterceptors and assignedInterceptors:IsAlive() then + sendInterceptorHome(assignedInterceptors, coalitionSide) + end + end + + assignedThreats[coalitionKey][threatName] = nil + end + end + + -- Count assigned threats + local assignedCount = 0 + for _ in pairs(assignedThreats[coalitionKey]) do assignedCount = assignedCount + 1 end + + log(coalitionName .. " scan complete: " .. threatCount .. " threats, " .. countActiveFighters(coalitionSide) .. " active fighters, " .. + assignedCount .. " assigned") +end + +-- Main threat detection loop - calls both coalitions +local function detectThreats() + if TADC_SETTINGS.enableRed then + detectThreatsForCoalition(coalition.side.RED) + end + + if TADC_SETTINGS.enableBlue then + detectThreatsForCoalition(coalition.side.BLUE) + end +end + +-- Monitor interceptor groups for cleanup when destroyed +local function monitorInterceptors() + -- Check RED interceptors + if TADC_SETTINGS.enableRed then + for interceptorName, interceptorData in pairs(activeInterceptors.red) 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("RED Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.red[interceptorName] = nil + end + end + end + end + + -- Check BLUE interceptors + if TADC_SETTINGS.enableBlue then + for interceptorName, interceptorData in pairs(activeInterceptors.blue) 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("BLUE Interceptor from " .. displayName .. " destroyed: " .. interceptorName, true) + + -- Remove from active tracking + activeInterceptors.blue[interceptorName] = nil + end + end + end + end +end + +-- Periodic airbase status check +local function checkAirbaseStatus() + log("=== AIRBASE STATUS REPORT ===") + + local redUsableCount = 0 + local blueUsableCount = 0 + local currentTime = timer.getTime() + + -- Check RED airbases + if TADC_SETTINGS.enableRed then + log("=== RED COALITION STATUS ===") + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + capturedBy = "Blue" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown (only show for operational squadrons) + local cooldownStatus = "" + if squadron.state == "operational" and squadronCooldowns.red[squadron.templateName] then + local cooldownEnd = squadronCooldowns.red[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = statusText .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + redUsableCount = redUsableCount + 1 + end + + log(statusPrefix .. " " .. squadron.displayName .. " (" .. squadron.airbaseName .. ") - " .. fullStatus) + end + log("RED Status: " .. redUsableCount .. "/" .. #RED_SQUADRON_CONFIG .. " airbases operational") + end + + -- Check BLUE airbases + if TADC_SETTINGS.enableBlue then + log("=== BLUE COALITION STATUS ===") + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + + -- Determine status based on squadron state + local statusPrefix = "✗" + local statusText = "" + local usable = false + + if squadron.state == "operational" then + statusPrefix = "✓" + statusText = "Operational: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + usable = true + elseif squadron.state == "captured" then + -- Determine who captured it + local capturedBy = "enemy" + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + capturedBy = "Red" + elseif airbaseCoalition == coalition.side.NEUTRAL then + capturedBy = "neutral forces" + end + end + statusText = "Captured by " .. capturedBy .. ": " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + elseif squadron.state == "destroyed" then + statusText = "Destroyed: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + else + statusText = "Unknown state: " .. aircraftCount .. "/" .. maxAircraft .. " aircraft" + end + + -- Add zone information if configured + local zoneStatus = "" + if squadron.primaryZone or squadron.secondaryZone or squadron.tertiaryZone then + local zones = {} + if squadron.primaryZone then table.insert(zones, "P:" .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "S:" .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "T:" .. squadron.tertiaryZone) end + zoneStatus = " Zones: " .. table.concat(zones, " ") + end + + -- Check if squadron is on cooldown (only show for operational squadrons) + local cooldownStatus = "" + if squadron.state == "operational" and squadronCooldowns.blue[squadron.templateName] then + local cooldownEnd = squadronCooldowns.blue[squadron.templateName] + if currentTime < cooldownEnd then + local timeLeft = math.ceil((cooldownEnd - currentTime) / 60) + cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" + end + end + + local fullStatus = statusText .. zoneStatus .. cooldownStatus + + if usable and cooldownStatus == "" and aircraftCount > 0 then + blueUsableCount = blueUsableCount + 1 + end + + log(statusPrefix .. " " .. squadron.displayName .. " (" .. squadron.airbaseName .. ") - " .. fullStatus) + end + log("BLUE Status: " .. blueUsableCount .. "/" .. #BLUE_SQUADRON_CONFIG .. " airbases operational") + end +end + +-- Cleanup old delivery records to prevent memory buildup +local function cleanupOldDeliveries() + if _G.processedDeliveries then + local currentTime = timer.getTime() + local cleanupAge = 3600 -- Remove delivery records older than 1 hour + local removedCount = 0 + + for deliveryKey, timestamp in pairs(_G.processedDeliveries) do + if currentTime - timestamp > cleanupAge then + _G.processedDeliveries[deliveryKey] = nil + removedCount = removedCount + 1 + end + end + + if removedCount > 0 then + log("Cleaned up " .. removedCount .. " old cargo delivery records", true) + end + end +end + +-- Update squadron states based on airbase coalition control +local function updateSquadronStates() + -- Update RED squadrons + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.RED then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("RED Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end + + -- Update BLUE squadrons + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local airbase = AIRBASE:FindByName(squadron.airbaseName) + if airbase and airbase:IsAlive() then + local airbaseCoalition = airbase:GetCoalition() + if airbaseCoalition == coalition.side.BLUE then + -- Only update to operational if not already operational (avoid spam) + if squadron.state ~= "operational" then + squadron.state = "operational" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " is now operational") + end + else + -- Airbase captured + if squadron.state ~= "captured" then + squadron.state = "captured" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " has been captured by enemy") + end + end + else + -- Airbase destroyed or not found + if squadron.state ~= "destroyed" then + squadron.state = "destroyed" + log("BLUE Squadron " .. squadron.displayName .. " at " .. squadron.airbaseName .. " airbase destroyed or not found") + end + end + end +end + +-- System initialization +local function initializeSystem() + log("Universal Dual-Coalition TADC starting...") + + -- Create zones from late-activated helicopter units (MOOSE method) + -- This allows using helicopters named "RED_BORDER", "BLUE_BORDER" etc. as zone markers + -- Uses the helicopter's waypoints as polygon vertices (standard MOOSE method) + local function createZoneFromUnit(unitName) + -- Try to find as a group first (this is the standard MOOSE way) + local group = GROUP:FindByName(unitName) + if group then + -- Create polygon zone using the group's waypoints as vertices + local zone = ZONE_POLYGON:NewFromGroupName(unitName, unitName) + if zone then + log("Created polygon zone '" .. unitName .. "' from helicopter waypoints") + return zone + else + log("Warning: Could not create polygon zone from group '" .. unitName .. "' - check waypoints") + end + else + log("Warning: No group named '" .. unitName .. "' found for zone creation") + end + return nil + end + + -- Try to create zones for all configured zone names + local zoneNames = {} + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end + end + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + if squadron.primaryZone then table.insert(zoneNames, squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zoneNames, squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zoneNames, squadron.tertiaryZone) end + end + + -- Create zones from helicopters + for _, zoneName in ipairs(zoneNames) do + if not ZONE:FindByName(zoneName) then + createZoneFromUnit(zoneName) + end + end + + -- Validate configuration + if not validateConfiguration() then + log("System startup aborted due to configuration errors!") + return false + end + + -- Initialize squadron states + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + squadron.state = "operational" + end + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + squadron.state = "operational" + end + log("Squadron states initialized") + + -- Log enabled coalitions + local enabledCoalitions = {} + if TADC_SETTINGS.enableRed then + table.insert(enabledCoalitions, "RED (" .. #RED_SQUADRON_CONFIG .. " squadrons)") + end + if TADC_SETTINGS.enableBlue then + table.insert(enabledCoalitions, "BLUE (" .. #BLUE_SQUADRON_CONFIG .. " squadrons)") + end + log("Enabled coalitions: " .. table.concat(enabledCoalitions, ", ")) + + -- Log initial squadron aircraft counts + if TADC_SETTINGS.enableRed then + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.red[squadron.templateName] + log("Initial RED: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + if TADC_SETTINGS.enableBlue then + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local count = squadronAircraftCounts.blue[squadron.templateName] + log("Initial BLUE: " .. squadron.displayName .. " has " .. count .. "/" .. squadron.aircraft .. " aircraft") + end + end + + -- Start schedulers + -- 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 "" 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 "" + 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, updateSquadronStates, {}, 60, 30) -- Update squadron states every 30 seconds (60 sec initial delay to allow DCS airbase coalition to stabilize) + SCHEDULER:New(nil, cleanupOldDeliveries, {}, 60, 3600) -- Cleanup old delivery records every hour + + -- Start periodic squadron summary broadcast + SCHEDULER:New(nil, broadcastSquadronSummary, {}, 10, TADC_SETTINGS.squadronSummaryInterval) + + log("Universal Dual-Coalition TADC operational!") + log("RED Replenishment: " .. TADC_SETTINGS.red.cargoReplenishmentAmount .. " aircraft per cargo delivery") + log("BLUE Replenishment: " .. TADC_SETTINGS.blue.cargoReplenishmentAmount .. " aircraft per cargo delivery") + + return true +end + + +initializeSystem() + +-- Add F10 menu command for squadron summary +-- Use MenuManager to create coalition-specific menus (not mission-wide) +local menuRootBlue, menuRootRed + +if MenuManager then + menuRootBlue = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "TADC Utilities") + menuRootRed = MenuManager.CreateCoalitionMenu(coalition.side.RED, "TADC Utilities") +else + menuRootBlue = MENU_COALITION:New(coalition.side.BLUE, "TADC Utilities") + menuRootRed = MENU_COALITION:New(coalition.side.RED, "TADC Utilities") +end + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Squadron Resource Summary", menuRootRed, function() + local summary = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Squadron Resource Summary", menuRootBlue, function() + local summary = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 1. Show Airbase Status Report +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", menuRootRed, function() + local report = "=== RED Airbase Status ===\n" + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) + local aircraftCount = squadronAircraftCounts.red[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local cooldown = squadronCooldowns.red[squadron.templateName] + local cooldownStatus = "" + if cooldown then + local timeLeft = math.ceil((cooldown - timer.getTime()) / 60) + if timeLeft > 0 then cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" end + end + report = report .. string.format("%s: %s | Aircraft: %d/%d%s\n", squadron.displayName, status, aircraftCount, maxAircraft, cooldownStatus) + end + MESSAGE:New(report, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRootBlue, function() + local report = "=== BLUE Airbase Status ===\n" + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) + local aircraftCount = squadronAircraftCounts.blue[squadron.templateName] or 0 + local maxAircraft = squadron.aircraft + local cooldown = squadronCooldowns.blue[squadron.templateName] + local cooldownStatus = "" + if cooldown then + local timeLeft = math.ceil((cooldown - timer.getTime()) / 60) + if timeLeft > 0 then cooldownStatus = " (COOLDOWN: " .. timeLeft .. "m)" end + end + report = report .. string.format("%s: %s | Aircraft: %d/%d%s\n", squadron.displayName, status, aircraftCount, maxAircraft, cooldownStatus) + end + MESSAGE:New(report, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 2. Show Active Interceptors +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRootRed, function() + local lines = {"Active RED Interceptors:"} + for name, data in pairs(activeInterceptors.red) do + if data and data.group and data.group:IsAlive() then + table.insert(lines, string.format("%s (Squadron: %s, Threat: %s)", name, data.displayName or data.squadron, assignedThreats.red[name] or "N/A")) + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRootBlue, function() + local lines = {"Active BLUE Interceptors:"} + for name, data in pairs(activeInterceptors.blue) do + if data and data.group and data.group:IsAlive() then + table.insert(lines, string.format("%s (Squadron: %s, Threat: %s)", name, data.displayName or data.squadron, assignedThreats.blue[name] or "N/A")) + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 3. Show Threat Summary +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRootRed, function() + local lines = {"Detected BLUE Threats:"} + if cachedSets.blueAircraft then + cachedSets.blueAircraft:ForEach(function(group) + if group and group:IsAlive() then + table.insert(lines, string.format("%s (Size: %d)", group:GetName(), group:GetSize())) + end + end) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRootBlue, function() + local lines = {"Detected RED Threats:"} + if cachedSets.redAircraft then + cachedSets.redAircraft:ForEach(function(group) + if group and group:IsAlive() then + table.insert(lines, string.format("%s (Size: %d)", group:GetName(), group:GetSize())) + end + end) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 4. Request Immediate Squadron Summary Broadcast +MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRootRed, function() + local summary = getSquadronResourceSummary(coalition.side.RED) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Broadcast Squadron Summary Now", menuRootBlue, function() + local summary = getSquadronResourceSummary(coalition.side.BLUE) + MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) +end) + +-- 5. Show Cargo Delivery Log +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRootRed, function() + local lines = {"Recent RED Cargo Deliveries:"} + if _G.processedDeliveries then + for key, timestamp in pairs(_G.processedDeliveries) do + if string.find(key, "RED") then + table.insert(lines, string.format("%s at %d", key, timestamp)) + end + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuRootBlue, function() + local lines = {"Recent BLUE Cargo Deliveries:"} + if _G.processedDeliveries then + for key, timestamp in pairs(_G.processedDeliveries) do + if string.find(key, "BLUE") then + table.insert(lines, string.format("%s at %d", key, timestamp)) + end + end + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 6. Show Zone Coverage Map +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRootRed, function() + local lines = {"RED Zone Coverage:"} + for _, squadron in pairs(RED_SQUADRON_CONFIG) do + local zones = {} + if squadron.primaryZone then table.insert(zones, "Primary: " .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "Secondary: " .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "Tertiary: " .. squadron.tertiaryZone) end + table.insert(lines, string.format("%s: %s", squadron.displayName, table.concat(zones, ", "))) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) +end) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRootBlue, function() + local lines = {"BLUE Zone Coverage:"} + for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do + local zones = {} + if squadron.primaryZone then table.insert(zones, "Primary: " .. squadron.primaryZone) end + if squadron.secondaryZone then table.insert(zones, "Secondary: " .. squadron.secondaryZone) end + if squadron.tertiaryZone then table.insert(zones, "Tertiary: " .. squadron.tertiaryZone) end + table.insert(lines, string.format("%s: %s", squadron.displayName, table.concat(zones, ", "))) + end + MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) +end) + +-- 7. Admin/Debug Commands - Create submenus under each coalition's TADC Utilities +local menuAdminBlue = MENU_COALITION:New(coalition.side.BLUE, "Admin / Debug", menuRootBlue) +local menuAdminRed = MENU_COALITION:New(coalition.side.RED, "Admin / Debug", menuRootRed) + +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Emergency Cleanup Interceptors", menuAdminBlue, function() + local cleaned = 0 + for name, interceptors in pairs(activeInterceptors.red) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "red") + cleaned = cleaned + 1 + end + end + for name, interceptors in pairs(activeInterceptors.blue) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "blue") + cleaned = cleaned + 1 + end + end + MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Emergency Cleanup Interceptors", menuAdminRed, function() + local cleaned = 0 + for name, interceptors in pairs(activeInterceptors.red) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "red") + cleaned = cleaned + 1 + end + end + for name, interceptors in pairs(activeInterceptors.blue) do + if interceptors and interceptors.group and not interceptors.group:IsAlive() then + cleanupInterceptorEntry(name, "blue") + cleaned = cleaned + 1 + end + end + MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToRed() +end) + +-- 9. Show System Uptime/Status +local systemStartTime = timer.getTime() +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show TADC System Status", menuAdminBlue, function() + local uptime = math.floor((timer.getTime() - systemStartTime) / 60) + local status = string.format("TADC System Uptime: %d minutes\nCheck Interval: %ds\nMonitor Interval: %ds\nStatus Report Interval: %ds\nSquadron Summary Interval: %ds\nCargo Check Interval: %ds", uptime, TADC_SETTINGS.checkInterval, TADC_SETTINGS.monitorInterval, TADC_SETTINGS.statusReportInterval, TADC_SETTINGS.squadronSummaryInterval, TADC_SETTINGS.cargoCheckInterval) + MESSAGE:New(status, 20):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show TADC System Status", menuAdminRed, function() + local uptime = math.floor((timer.getTime() - systemStartTime) / 60) + local status = string.format("TADC System Uptime: %d minutes\nCheck Interval: %ds\nMonitor Interval: %ds\nStatus Report Interval: %ds\nSquadron Summary Interval: %ds\nCargo Check Interval: %ds", uptime, TADC_SETTINGS.checkInterval, TADC_SETTINGS.monitorInterval, TADC_SETTINGS.statusReportInterval, TADC_SETTINGS.squadronSummaryInterval, TADC_SETTINGS.cargoCheckInterval) + MESSAGE:New(status, 20):ToRed() +end) + +-- 10. Check for Stuck Aircraft (manual trigger) +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Check for Stuck Aircraft", menuAdminBlue, function() + monitorStuckAircraft() + MESSAGE:New("Stuck aircraft check completed", 10):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Check for Stuck Aircraft", menuAdminRed, function() + monitorStuckAircraft() + MESSAGE:New("Stuck aircraft check completed", 10):ToRed() +end) + +-- 11. Show Airbase Health Status +MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Health Status", menuAdminBlue, 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):ToBlue() +end) + +MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Health Status", menuAdminRed, 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):ToRed() +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) + + + diff --git a/Moose_TADC_SquadronConfigs_Load1st.lua b/Moose_TADC_SquadronConfigs_Load1st.lua new file mode 100644 index 0000000..01f1335 --- /dev/null +++ b/Moose_TADC_SquadronConfigs_Load1st.lua @@ -0,0 +1,244 @@ + +--[[ THIS FILE MUST BE LOADED BEFORE THE MAIN Moose_TADC.lua SCRIPT +═══════════════════════════════════════════════════════════════════════════════ + SQUADRON CONFIGURATION +═══════════════════════════════════════════════════════════════════════════════ + +INSTRUCTIONS: +1. Create fighter aircraft templates for BOTH coalitions in the mission editor +2. Place them at or near the airbases you want them to operate from +3. Configure RED squadrons in RED_SQUADRON_CONFIG +4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG + +TEMPLATE NAMING SUGGESTIONS: +• RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29" +• BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22" +• Include coalition and airbase name for easy identification + +AIRBASE NAMES: +• Use exact names as they appear in DCS (case sensitive) +• RED examples: "Batumi", "Senaki", "Gudauta" +• BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range" +• Find airbase names in the mission editor + +AIRCRAFT NUMBERS: +• Set realistic numbers based on mission requirements +• Consider aircraft consumption and cargo replenishment +• Balance between realism and gameplay performance + +ZONE-BASED AREAS OF RESPONSIBILITY: +• Create zones in mission editor (MOOSE polygons, circles, etc.) +• primaryZone: Squadron's main area (full response) +• secondaryZone: Backup/support area (reduced response) +• tertiaryZone: Emergency fallback area (enhanced response) +• Leave zones as nil for global threat response +• Multiple squadrons can share overlapping zones +• Use zone names exactly as they appear in mission editor + +ZONE BEHAVIOR EXAMPLES: +• Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE" +• Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true +• Layered Defense: Different zones per squadron with overlap +• Emergency Response: High tertiaryResponse ratio for critical areas +]] + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RED COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +RED_SQUADRON_CONFIG = { + --[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor + displayName = "Batumi F-15C CAP", -- Human-readable name for logs + airbaseName = "Batumi", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.GOOD, -- AI skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER" + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + ]] + + -- ADD YOUR RED SQUADRONS HERE + { + templateName = "Sukhumi CAP", -- Change to your RED template name + displayName = "Sukhumi CAP", -- Change to your preferred name + airbaseName = "Sukhumi-Babushara", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-21", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-21", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 12, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Gudauta CAP-MiG-23", -- Change to your RED template name + displayName = "Gudauta CAP-MiG-23", -- Change to your preferred name + airbaseName = "Gudauta", -- Change to your RED airbase + aircraft = 14, -- Adjust aircraft count + skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 25, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "GUDAUTA_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "RED_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = false, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BLUE COALITION SQUADRONS +-- ═══════════════════════════════════════════════════════════════════════════ + +BLUE_SQUADRON_CONFIG = { + --[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION + { + templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor + displayName = "Nellis F-16C CAP", -- Human-readable name for logs + airbaseName = "Nellis AFB", -- Exact airbase name from DCS + aircraft = 14, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI skill level + altitude = 22000, -- Patrol altitude (feet) + speed = 380, -- Patrol speed (knots) + patrolTime = 28, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft type + }, + ]] + + -- ADD YOUR BLUE SQUADRONS HERE + + { + templateName = "Kutaisi CAP", -- Change to your BLUE template name + displayName = "Kutaisi CAP", -- Change to your preferred name + airbaseName = "Kutaisi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BLUE_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = nil, -- Secondary coverage area (zone name) + tertiaryZone = nil, -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, + + { + templateName = "Batumi CAP", -- Change to your BLUE template name + displayName = "Batumi CAP", -- Change to your preferred name + airbaseName = "Batumi", -- Change to your BLUE airbase + aircraft = 18, -- Adjust aircraft count + skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT + altitude = 18000, -- Patrol altitude (feet) + speed = 320, -- Patrol speed (knots) + patrolTime = 22, -- Time on station (minutes) + type = "FIGHTER", + + -- Zone-based Areas of Responsibility (optional - leave nil for global response) + primaryZone = "BATUMI_BORDER", -- Main responsibility area (zone name from mission editor) + secondaryZone = "BLUE_BORDER", -- Secondary coverage area (zone name) + tertiaryZone = "BATUMI_BORDER", -- Emergency/fallback zone (zone name) + + -- Zone behavior settings (optional - uses defaults if not specified) + zoneConfig = { + primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone + secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone + tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone + maxRange = 200, -- Maximum engagement range from airbase (nm) + enableFallback = true, -- Auto-switch to tertiary when base threatened + priorityThreshold = 4, -- Min aircraft count for "major threat" + ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones + } + }, +} + + diff --git a/TADC_Example.miz b/TADC_Example.miz new file mode 100644 index 0000000..bd22a9f Binary files /dev/null and b/TADC_Example.miz differ diff --git a/TADC_Menu.lua b/TADC_Menu.lua new file mode 100644 index 0000000..5e92cd4 --- /dev/null +++ b/TADC_Menu.lua @@ -0,0 +1,194 @@ + +-- ================================================================ +-- UNIVERSAL MOOSE SPAWNER UTILITY MENU +-- ================================================================ +-- Allows spawning any group template (fighter, cargo, etc.) at any airbase +-- for either coalition, with options for cold/hot/runway start. +-- Includes cleanup and status commands. +-- ================================================================ + +-- List of available airbases (Caucasus map, add/remove as needed) +local AIRBASES = { + "Kutaisi", "Senaki-Kolkhi", "Sukhumi-Babushara", "Gudauta", "Sochi-Adler", + "Krymsk", "Anapa-Vityazevo", "Krasnodar-Pashkovsky", "Mineralnye Vody", + "Nalchik", "Mozdok", "Beslan" +} + +-- List of example templates (add your own as needed) +local TEMPLATES = { + "CARGO", "CARGO_RU", "Kutaisi CAP", "Sukhumi CAP", "Batumi CAP", "Gudauta CAP" + -- Add more fighter/cargo templates here +} + +-- Coalition options +local COALITIONS = { + {name = "Blue", side = coalition.side.BLUE}, + {name = "Red", side = coalition.side.RED} +} + +-- Start types +local START_TYPES = { + {name = "Cold Start", value = SPAWN.Takeoff.Cold}, + {name = "Hot Start", value = SPAWN.Takeoff.Hot}, + {name = "Runway", value = SPAWN.Takeoff.Runway} +} + +-- Track spawned groups for cleanup +local spawnedGroups = {} + +-- Utility: Add group to cleanup tracking +local function TrackGroup(group) + if group and group:IsAlive() then + table.insert(spawnedGroups, group) + end +end + +-- Utility: Cleanup all spawned groups +local function CleanupAll() + local cleaned = 0 + for _, group in ipairs(spawnedGroups) do + if group and group:IsAlive() then + group:Destroy() + cleaned = cleaned + 1 + end + end + spawnedGroups = {} + MESSAGE:New("Cleaned up " .. cleaned .. " spawned groups", 10):ToAll() +end + +-- Utility: Show status of spawned groups +local function ShowStatus() + local alive = 0 + for _, group in ipairs(spawnedGroups) do + if group and group:IsAlive() then alive = alive + 1 end + end + MESSAGE:New("Spawner Status:\nAlive groups: " .. alive .. "\nTotal spawned: " .. #spawnedGroups, 15):ToAll() +end + +-- Main menu +local MenuRoot = MENU_MISSION:New("Universal Spawner") + +-- Submenus for coalition +local MenuBlue = MENU_MISSION:New("Spawn for BLUE", MenuRoot) +local MenuRed = MENU_MISSION:New("Spawn for RED", MenuRoot) + +-- For each coalition, create template/airbase/start type menus +for _, coalitionData in ipairs(COALITIONS) do + local menuCoalition = (coalitionData.side == coalition.side.BLUE) and MenuBlue or MenuRed + + for _, templateName in ipairs(TEMPLATES) do + local menuTemplate = MENU_MISSION:New("Template: " .. templateName, menuCoalition) + + for _, airbaseName in ipairs(AIRBASES) do + local menuAirbase = MENU_MISSION:New("Airbase: " .. airbaseName, menuTemplate) + + for _, startType in ipairs(START_TYPES) do + local menuStartType = MENU_MISSION:New(startType.name, menuAirbase) + for numToSpawn = 1, 5 do + MENU_MISSION_COMMAND:New( + "Spawn " .. numToSpawn, + menuStartType, + function() + local airbase = AIRBASE:FindByName(airbaseName) + if not airbase then + MESSAGE:New("Airbase not found: " .. airbaseName, 10):ToAll() + return + end + local spawnObj = SPAWN:New(templateName) + spawnObj:InitLimit(10, 20) + local spawned = 0 + for i = 1, numToSpawn do + local group = spawnObj:SpawnAtAirbase(airbase, startType.value) + if group then + TrackGroup(group) + spawned = spawned + 1 + end + end + if spawned > 0 then + MESSAGE:New("Spawned " .. spawned .. " '" .. templateName .. "' at " .. airbaseName .. " (" .. startType.name .. ")", 10):ToAll() + else + MESSAGE:New("Failed to spawn '" .. templateName .. "' at " .. airbaseName, 10):ToAll() + end + end + ) + end + end + end + end +end + +-- Quick spawn (first template, first airbase, cold start) +MENU_MISSION_COMMAND:New( + "Quick Spawn (" .. TEMPLATES[1] .. ")", + MenuRoot, + function() + local airbase = AIRBASE:FindByName(AIRBASES[1]) + local spawnObj = SPAWN:New(TEMPLATES[1]) + spawnObj:InitLimit(10, 20) + local spawned = 0 + for i = 1, 1 do + local group = spawnObj:SpawnAtAirbase(airbase, SPAWN.Takeoff.Cold) + if group then + TrackGroup(group) + spawned = spawned + 1 + end + end + if spawned > 0 then + MESSAGE:New("Quick spawned '" .. TEMPLATES[1] .. "' at " .. AIRBASES[1], 10):ToAll() + else + MESSAGE:New("Failed to quick spawn '" .. TEMPLATES[1] .. "' at " .. AIRBASES[1], 10):ToAll() + end + end +) + +-- Status and cleanup commands +MENU_MISSION_COMMAND:New("Show Spawner Status", MenuRoot, ShowStatus) +MENU_MISSION_COMMAND:New("Cleanup All Spawned Groups", MenuRoot, CleanupAll) + +-- ================================================================ +-- CONFIGURATION +-- ================================================================ + +-- Menu configuration +local MENU_CONFIG = { + rootMenuText = "CARGO OPERATIONS", + coalitionSide = coalition.side.BLUE, -- Change to RED if needed + debugMode = true +} + +-- Spawn configuration +local SPAWN_CONFIG = { + templateName = "CARGO", -- Template name in mission editor + maxActive = 3, -- Maximum active aircraft + maxSpawns = 10, -- Maximum total spawns + cleanupTime = 300, -- Cleanup time in seconds (5 minutes) + spawnAirbase = "Kutaisi", -- Default spawn airbase + takeoffType = SPAWN.Takeoff.Cold -- Cold start by default +} + +-- Available airbases for spawning (Caucasus map) +local AVAILABLE_AIRBASES = { + "Kutaisi", + "Senaki-Kolkhi", + "Sukhumi-Babushara", + "Gudauta", + "Sochi-Adler", + "Krymsk", + "Anapa-Vityazevo", + "Krasnodar-Pashkovsky", + "Mineralnye Vody", + "Nalchik", + "Mozdok", + "Beslan" +} + +-- ================================================================ +-- GLOBAL VARIABLES +-- ================================================================ + +-- Spawn object +local CargoSpawn = nil + +-- Menu objects +local MenuRoot = nil +local MenuSpawn = nil diff --git a/TADC_SYSTEM_GUIDE.html b/TADC_SYSTEM_GUIDE.html new file mode 100644 index 0000000..0fa7556 --- /dev/null +++ b/TADC_SYSTEM_GUIDE.html @@ -0,0 +1,1353 @@ + + + + + + Universal TADC System - Mission Maker's Guide + + + +
+

Universal TADC System - Mission Maker's Guide

+

Tactical Air Defense Controller with Automated Logistics

+ + + +

What is TADC?

+ +

TADC (Tactical Air Defense Controller) is an automated air defense system for DCS missions that creates realistic, dynamic fighter aircraft responses to airborne threats. Think of it as an AI commander that:

+ +
    +
  • Detects enemy aircraft automatically
  • +
  • Launches fighters to intercept threats
  • +
  • Manages squadron resources (aircraft availability, cooldowns)
  • +
  • Replenishes squadrons through cargo aircraft deliveries
  • +
  • Operates independently for both RED and BLUE coalitions
  • +
+ +

Why Use TADC?

+ +
    +
  • Realistic Air Defense - Squadrons respond intelligently to threats
  • +
  • Dynamic Gameplay - Air battles happen organically without manual triggers
  • +
  • Balanced Competition - Both sides operate with equal capabilities
  • +
  • Sustainable Operations - Cargo system allows long missions with resupply
  • +
  • Easy Configuration - Simple tables instead of complex scripting
  • +
+ +

System Overview

+ +

The TADC system consists of three main scripts that work together:

+ +

1. Squadron Configuration (Moose_TADC_SquadronConfigs_Load1st.lua)

+

Purpose: Define all fighter squadrons for RED and BLUE coalitions
+ Contains: Aircraft templates, airbases, patrol parameters, zone assignments
+ Load Order: FIRST (must load before main TADC script)

+ +

2. Main TADC System (Moose_TADC_Load2nd.lua)

+

Purpose: Core threat detection and interceptor management
+ Contains: Threat scanning, squadron selection, intercept logic, F10 menus
+ Load Order: SECOND (after squadron config)

+ +

3. Cargo Dispatcher (Moose_TADC_CargoDispatcher.lua)

+

Purpose: Automated squadron resupply through cargo aircraft
+ Contains: Squadron monitoring, cargo spawning, delivery tracking
+ Load Order: THIRD (optional, only if using resupply system)

+ +

Quick Start Guide

+ +

Prerequisites

+ +

Before setting up TADC, you need:

+ +
    +
  • MOOSE Framework loaded in your mission (download from MOOSE GitHub)
  • +
  • Fighter aircraft templates created in DCS mission editor (as GROUPS, not units)
  • +
  • Airbases under correct coalition control
  • +
  • (Optional) Cargo aircraft templates for resupply missions
  • +
+ +

5-Minute Setup

+ +

1 Create Fighter Templates

+ +
    +
  1. Open your mission in DCS Mission Editor
  2. +
  3. Place fighter aircraft as LATE ACTIVATION GROUPS (not individual units)
  4. +
  5. Name them clearly (example: RED_CAP_Kilpyavr_MiG29)
  6. +
  7. Position them at or near the airbases they'll operate from
  8. +
  9. Set them to the correct coalition (RED or BLUE)
  10. +
+ +
+ Important: Use GROUP templates, not UNIT templates! +
+ +

2 Load MOOSE Framework

+ +
    +
  1. In mission editor, go to Triggers
  2. +
  3. Create a new trigger: MISSION START
  4. +
  5. Add action: DO SCRIPT FILE
  6. +
  7. Select your MOOSE.lua file
  8. +
  9. This must be the FIRST script loaded
  10. +
+ +

3 Load Squadron Configuration

+ +
    +
  1. Create another DO SCRIPT FILE action (after MOOSE)
  2. +
  3. Select Moose_TADC_SquadronConfigs_Load1st.lua
  4. +
  5. Edit the file to configure your squadrons (see below)
  6. +
+ +

4 Load Main TADC System

+ +
    +
  1. Create another DO SCRIPT FILE action
  2. +
  3. Select Moose_TADC_Load2nd.lua
  4. +
  5. (Optional) Adjust settings in the file if needed
  6. +
+ +

5 (Optional) Load Cargo Dispatcher

+ +
    +
  1. If using resupply system, create another DO SCRIPT FILE action
  2. +
  3. Select Moose_TADC_CargoDispatcher.lua
  4. +
+ +
+ Load Order in Mission Editor: +
1. MOOSE.lua
+2. Moose_TADC_SquadronConfigs_Load1st.lua
+3. Moose_TADC_Load2nd.lua
+4. Moose_TADC_CargoDispatcher.lua (optional)
+
+ +

Detailed Configuration

+ +

Squadron Configuration Explained

+ +

Open Moose_TADC_SquadronConfigs_Load1st.lua and find the squadron configuration sections.

+ +

Basic Squadron Example

+ +
{
+    templateName = "RED_CAP_Kilpyavr_MiG29",     -- Must match mission editor template name
+    displayName = "Kilpyavr CAP MiG-29A",        -- Human-readable name for logs/messages
+    airbaseName = "Kilpyavr",                    -- Exact airbase name from DCS
+    aircraft = 12,                                -- Maximum aircraft in squadron
+    skill = AI.Skill.EXCELLENT,                   -- AI pilot skill level
+    altitude = 20000,                             -- Patrol altitude (feet)
+    speed = 350,                                  -- Patrol speed (knots)
+    patrolTime = 30,                              -- Time on station (minutes)
+    type = "FIGHTER"                              -- Aircraft role
+}
+ +

Parameter Guide

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescriptionExample Values
templateNameGroup name from mission editor (EXACT match)"RED_CAP_Base_F15"
displayNameFriendly name shown in messages"Kilpyavr CAP Squadron"
airbaseNameDCS airbase name (case sensitive)"Kilpyavr", "Nellis AFB"
aircraftMax squadron size8, 12, 16
skillAI difficultyAI.Skill.AVERAGE, GOOD, HIGH, EXCELLENT, ACE
altitudeCAP patrol altitude15000 (feet)
speedCAP patrol speed300 (knots)
patrolTimeMinutes on station before RTB20, 30, 40
typeAircraft role"FIGHTER"
+ +

Finding Airbase Names

+ +

Method 1: Mission Editor

+
    +
  1. Open mission editor
  2. +
  3. Click on any airbase
  4. +
  5. The exact name appears in the properties panel
  6. +
  7. Copy this name EXACTLY (case sensitive!)
  8. +
+ +

Method 2: Common Airbases

+ +

Kola Peninsula (Example Map):

+
    +
  • RED: "Kilpyavr", "Severomorsk-1", "Severomorsk-3", "Murmansk International"
  • +
  • BLUE: "Luostari Pechenga", "Ivalo", "Alakurtti"
  • +
+ +

Nevada:

+
    +
  • "Nellis AFB", "McCarran International", "Creech AFB", "Tonopah Test Range"
  • +
+ +

Caucasus:

+
    +
  • "Batumi", "Gudauta", "Senaki-Kolkhi", "Kobuleti", "Kutaisi"
  • +
+ +

Adding Multiple Squadrons

+ +

You can add as many squadrons as you want. Just copy the squadron block and modify the values:

+ +
RED_SQUADRON_CONFIG = {
+    -- First Squadron
+    {
+        templateName = "RED_CAP_Base1_MiG29",
+        displayName = "Base 1 CAP",
+        airbaseName = "Kilpyavr",
+        aircraft = 12,
+        skill = AI.Skill.EXCELLENT,
+        altitude = 20000,
+        speed = 350,
+        patrolTime = 30,
+        type = "FIGHTER"
+    },
+    
+    -- Second Squadron (different base)
+    {
+        templateName = "RED_CAP_Base2_SU27",
+        displayName = "Base 2 CAP",
+        airbaseName = "Severomorsk-1",
+        aircraft = 16,
+        skill = AI.Skill.ACE,
+        altitude = 25000,
+        speed = 380,
+        patrolTime = 25,
+        type = "FIGHTER"
+    },
+    
+    -- Add more squadrons here...
+}
+ +
+ Repeat the same process for BLUE squadrons in the BLUE_SQUADRON_CONFIG section. +
+ +

Zone-Based Defense Setup

+ +

Zones allow squadrons to have specific areas of responsibility, creating realistic layered defense.

+ +

Why Use Zones?

+ +
    +
  • Border Defense: Squadrons patrol specific sectors
  • +
  • Layered Defense: Multiple squadrons cover overlapping areas
  • +
  • Priority Response: Squadrons respond differently based on threat location
  • +
  • Realistic Behavior: Fighters don't fly across the entire map for minor threats
  • +
+ +

Zone Types

+ +

Each squadron can have up to 3 zone types:

+ +
    +
  1. Primary Zone - Main area of responsibility (full response)
  2. +
  3. Secondary Zone - Support area (reduced response, 60% by default)
  4. +
  5. Tertiary Zone - Emergency fallback (enhanced response when squadron weakened)
  6. +
+ +

Creating Zones in Mission Editor

+ +

Method: Helicopter Waypoint Method

+ +
    +
  1. Place a helicopter group (late activation, any type)
  2. +
  3. Name it clearly (example: "RED BORDER")
  4. +
  5. Add waypoints that outline your zone boundary
  6. +
  7. The script will automatically create a polygon zone from these waypoints
  8. +
  9. Repeat for each zone you want to create
  10. +
+ +
+ Example Zone Setup: +
Mission Editor:
+- Helicopter Group: "RED BORDER" with waypoints forming a polygon
+- Helicopter Group: "BLUE BORDER" with waypoints forming a polygon
+- Helicopter Group: "CONTESTED ZONE" with waypoints forming a polygon
+
+ +

Configuring Zone Response

+ +

Add zone configuration to your squadron:

+ +
{
+    templateName = "RED_CAP_Kilpyavr_MiG29",
+    displayName = "Kilpyavr CAP",
+    airbaseName = "Kilpyavr",
+    aircraft = 12,
+    skill = AI.Skill.EXCELLENT,
+    altitude = 20000,
+    speed = 350,
+    patrolTime = 30,
+    type = "FIGHTER",
+    
+    -- Zone Configuration
+    primaryZone = "RED BORDER",                    -- Main responsibility area
+    secondaryZone = "CONTESTED ZONE",              -- Backup coverage
+    tertiaryZone = nil,                            -- No tertiary zone
+    
+    -- Optional: Customize zone behavior
+    zoneConfig = {
+        primaryResponse = 1.0,                     -- Full response in primary zone
+        secondaryResponse = 0.6,                   -- 60% response in secondary
+        tertiaryResponse = 1.4,                    -- 140% response in tertiary
+        enableFallback = false,                    -- Don't auto-switch to tertiary
+        fallbackThreshold = 0.3,                   -- Switch when <30% aircraft remain
+        secondaryLowPriorityFilter = true,         -- Ignore small threats in secondary
+        secondaryLowPriorityThreshold = 2          -- "Small threat" = 2 or fewer aircraft
+    }
+}
+ +

Zone Behavior Examples

+ +

Example 1: Border Defense Squadron

+
primaryZone = "RED BORDER",          -- Patrols the border
+secondaryZone = "INTERIOR",          -- Helps with interior threats if needed
+tertiaryZone = nil                   -- No fallback
+ +

Example 2: Base Defense with Fallback

+
primaryZone = "NORTHERN SECTOR",     -- Main patrol area
+secondaryZone = nil,                 -- No secondary
+tertiaryZone = "BASE PERIMETER",     -- Falls back to defend base when weakened
+enableFallback = true,               -- Auto-switch to tertiary when low
+fallbackThreshold = 0.4              -- Switch at 40% strength
+ +

Example 3: Layered Defense

+
-- Squadron A: Outer layer
+primaryZone = "OUTER PERIMETER"
+
+-- Squadron B: Middle layer
+primaryZone = "MIDDLE PERIMETER"
+
+-- Squadron C: Inner/base defense
+primaryZone = "BASE DEFENSE"
+ +

Global Response (No Zones)

+ +

If you DON'T want zone restrictions, simply leave all zones as nil:

+ +
{
+    templateName = "RED_CAP_Base_MiG29",
+    displayName = "Global Response CAP",
+    airbaseName = "Kilpyavr",
+    aircraft = 12,
+    skill = AI.Skill.EXCELLENT,
+    altitude = 20000,
+    speed = 350,
+    patrolTime = 30,
+    type = "FIGHTER",
+    
+    -- No zones = responds to threats anywhere on the map
+    primaryZone = nil,
+    secondaryZone = nil,
+    tertiaryZone = nil
+}
+ +

Cargo Replenishment System

+ +

The cargo system automatically replenishes squadrons by spawning transport aircraft that fly supplies to airbases.

+ +

How Cargo Works

+ +
    +
  1. Monitoring: Script checks squadron aircraft counts every minute
  2. +
  3. Detection: When squadron drops below threshold (90% by default), cargo is dispatched
  4. +
  5. Spawning: Transport aircraft spawns at a supply airfield
  6. +
  7. Delivery: Flies to destination airbase and lands
  8. +
  9. Replenishment: Squadron aircraft count increases upon delivery
  10. +
  11. Cooldown: 5-minute cooldown before next delivery to same base
  12. +
+ +

Cargo Aircraft Detection

+ +

The system detects cargo by aircraft name patterns:

+
    +
  • CARGO
  • +
  • TRANSPORT
  • +
  • C130 or C-130
  • +
  • AN26 or AN-26
  • +
+ +

Delivery Methods:

+
    +
  • Landing: Aircraft lands at destination airbase
  • +
+ +

Configuring Cargo Templates

+ +

Edit Moose_TADC_CargoDispatcher.lua and find CARGO_SUPPLY_CONFIG:

+ +
local CARGO_SUPPLY_CONFIG = {
+    red = {
+        cargoTemplate = "CARGO_RED_AN26_TEMPLATE",         -- Template name from mission editor
+        supplyAirfields = {"Airbase1", "Airbase2"},        -- List of supply bases
+        replenishAmount = 4,                               -- Aircraft added per delivery
+        threshold = 0.90                                   -- Trigger at 90% capacity
+    },
+    blue = {
+        cargoTemplate = "CARGO_BLUE_C130_TEMPLATE",
+        supplyAirfields = {"Airbase3", "Airbase4"},
+        replenishAmount = 4,
+        threshold = 0.90
+    }
+}
+ +

Creating Cargo Templates

+ +

1. In Mission Editor:

+
    +
  • Place transport aircraft group (C-130, An-26, etc.)
  • +
  • Name it: CARGO_RED_AN26_TEMPLATE or CARGO_BLUE_C130_TEMPLATE
  • +
  • Set LATE ACTIVATION
  • +
  • Position at any friendly airbase (starting position doesn't matter)
  • +
+ +

2. In Configuration:

+
    +
  • Use the EXACT template name in cargoTemplate field
  • +
  • List supply airbases in supplyAirfields array
  • +
  • Set how many aircraft each delivery adds (replenishAmount)
  • +
+ +

Supply Airfield Strategy

+ +

Choose rear/safe airbases for supplies:

+ +
red = {
+    cargoTemplate = "CARGO_RED_AN26_TEMPLATE",
+    supplyAirfields = {
+        "Rear_Base_1",              -- Far from frontline, safe
+        "Rear_Base_2",              -- Alternate supply source
+        "Central_Logistics_Hub"     -- Main supply depot
+    },
+    replenishAmount = 4,
+    threshold = 0.90
+}
+ +

Tips:

+
    +
  • Use 3-5 supply airbases for redundancy
  • +
  • Choose bases far from combat zones
  • +
  • Ensure supply bases are well-defended
  • +
  • Balance geographic coverage
  • +
+ +

Disabling Cargo System

+ +

If you don't want automated resupply:

+
    +
  1. Don't load Moose_TADC_CargoDispatcher.lua
  2. +
  3. Squadrons will operate with their initial aircraft count only
  4. +
  5. System still works perfectly for shorter missions
  6. +
+ +

Testing & Troubleshooting

+ +

Validation Tools

+ +

The system includes built-in validation. Check the DCS log file after mission start for:

+ +
+
[Universal TADC] ═══════════════════════════════════════
+[Universal TADC] Configuration Validation Results:
+[Universal TADC] ✓ All templates exist
+[Universal TADC] ✓ All airbases valid
+[Universal TADC] ✓ All zones found
+[Universal TADC] Configuration is VALID
+[Universal TADC] ═══════════════════════════════════════
+
+ +

In-Game F10 Menu Commands

+ +

Press F10 in-game to access TADC utilities:

+ +

Available to Each Coalition:

+
    +
  • Show Squadron Resource Summary - Current aircraft counts
  • +
  • Show Airbase Status Report - Operational status of all bases
  • +
  • Show Active Interceptors - Currently airborne fighters
  • +
  • Show Threat Summary - Detected enemy aircraft
  • +
  • Broadcast Squadron Summary Now - Force immediate status update
  • +
  • Show Cargo Delivery Log - Recent supply missions
  • +
  • Show Zone Coverage Map - Squadron zone assignments
  • +
+ +

Available to All (Mission Commands):

+
    +
  • Emergency Cleanup Interceptors - Remove stuck/dead groups
  • +
  • Show TADC System Status - Uptime and system health
  • +
  • Check for Stuck Aircraft - Manual stuck aircraft check
  • +
  • Show Airbase Health Status - Parking/spawn issues
  • +
+ +

Common Issues & Solutions

+ +

Issue: "Template not found in mission"

+
+

Cause: Template name in config doesn't match mission editor

+

Solution:

+
    +
  1. Check exact spelling and case
  2. +
  3. Ensure template is in mission editor
  4. +
  5. Verify template is a GROUP (not a unit)
  6. +
  7. Check template name in mission editor properties
  8. +
+
+ +

Issue: "Airbase not found or wrong coalition"

+
+

Cause: Airbase name wrong or captured by enemy

+

Solution:

+
    +
  1. Check exact airbase spelling (case sensitive)
  2. +
  3. Verify airbase is owned by correct coalition in mission editor
  4. +
  5. Use _G.TDAC_CheckAirbase("AirbaseName") in DCS console
  6. +
+
+ +

Issue: "No interceptors launching"

+
+

Check:

+
    +
  1. Are enemy aircraft detected? (F10 → Show Threat Summary)
  2. +
  3. Are squadrons operational? (F10 → Show Squadron Resource Summary)
  4. +
  5. Is airbase captured/destroyed? (F10 → Show Airbase Status Report)
  6. +
  7. Are squadrons on cooldown? (F10 → Show Squadron Resource Summary)
  8. +
  9. Check intercept ratio settings (might be too low)
  10. +
+
+ +

Issue: "Cargo not delivering"

+
+

Check:

+
    +
  1. Is cargo template name correct?
  2. +
  3. Are supply airbases valid and friendly?
  4. +
  5. Is destination airbase captured/operational?
  6. +
  7. Check parking availability (F10 → Show Airbase Health Status)
  8. +
  9. Look for "Cargo delivery detected" messages in log
  10. +
+
+ +

Issue: "Aircraft spawning stuck at parking"

+
+

Cause: Parking spots occupied or insufficient space

+

Solution:

+
    +
  1. Use F10 → Check for Stuck Aircraft
  2. +
  3. Use F10 → Emergency Cleanup Interceptors
  4. +
  5. Check airbase parking capacity (larger aircraft need more space)
  6. +
  7. Reduce squadron sizes if parking is limited
  8. +
+
+ +

DCS Console Diagnostics

+ +

Open DCS Lua console (F12 or scripting console) and run:

+ +
-- Check all supply airbase ownership
+_G.TDAC_CheckAirbaseOwnership()
+
+-- Check specific airbase
+_G.TDAC_CheckAirbase("Kilpyavr")
+
+-- Validate dispatcher configuration
+_G.TDAC_RunConfigCheck()
+
+-- Check airbase parking availability
+_G.TDAC_LogAirbaseParking("Kilpyavr")
+
+-- Test cargo spawn (debugging)
+_G.TDAC_CargoDispatcher_TestSpawn("CARGO_RED_AN26_TEMPLATE", "SupplyBase", "DestinationBase")
+ +

Advanced Features

+ +

Intercept Ratio System

+ +

The interceptRatio setting controls how many fighters launch per enemy aircraft.

+ +

In Moose_TADC_Load2nd.lua:

+ +
local TADC_SETTINGS = {
+    red = {
+        interceptRatio = 0.8,          -- RED launches 0.8 fighters per threat
+        maxActiveCAP = 8,              -- Max 8 groups in air simultaneously
+        defaultCooldown = 300,         -- 5-minute cooldown after engagement
+    },
+    blue = {
+        interceptRatio = 1.2,          -- BLUE launches 1.2 fighters per threat
+        maxActiveCAP = 10,             -- Max 10 groups in air simultaneously
+        defaultCooldown = 300,
+    }
+}
+ +

Intercept Ratio Chart

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ratio1 Enemy4 Enemies8 EnemiesEffect
0.51 fighter2 fighters4 fightersConservative response
0.81 fighter4 fighters7 fightersBalanced (default)
1.01 fighter4 fighters8 fighters1:1 parity
1.42 fighters6 fighters12 fightersStrong response
2.02 fighters8 fighters16 fightersOverwhelming force
+ +

Tactical Effects:

+
    +
  • Low (0.5-0.8): Sustainable defense, squadrons last longer
  • +
  • Medium (0.8-1.2): Balanced dogfights, realistic attrition
  • +
  • High (1.4-2.0): Strong defense, rapid squadron depletion
  • +
+ +

Asymmetric Scenarios:

+
-- RED advantage
+red = { interceptRatio = 1.4 },
+blue = { interceptRatio = 0.8 }
+
+-- BLUE advantage
+red = { interceptRatio = 0.8 },
+blue = { interceptRatio = 1.4 }
+ +

Distance-Based Engagement

+ +

Control how far squadrons will chase threats:

+ +
{
+    templateName = "RED_CAP_Base_MiG29",
+    displayName = "Base Defense",
+    airbaseName = "Kilpyavr",
+    aircraft = 12,
+    -- ... other settings ...
+    
+    zoneConfig = {
+        maxEngagementRange = 50000,      -- Won't engage threats >50km from base
+        primaryResponse = 1.0,
+        secondaryResponse = 0.6
+    }
+}
+ +

Cooldown System

+ +

After launching interceptors, squadrons go on cooldown to prevent spam:

+ +
local TADC_SETTINGS = {
+    red = {
+        defaultCooldown = 300,           -- 5 minutes between launches
+        -- ... other settings ...
+    }
+}
+ +

Per-Squadron Cooldown (optional):

+
{
+    templateName = "RED_CAP_Base_MiG29",
+    cooldownOverride = 600,              -- This squadron: 10-minute cooldown
+    -- ... other settings ...
+}
+ +

Aircraft Skill Levels

+ +

Adjust AI difficulty per squadron:

+ +
skill = AI.Skill.AVERAGE      -- Easiest, good for training
+skill = AI.Skill.GOOD         -- Below average
+skill = AI.Skill.HIGH         -- Average pilots
+skill = AI.Skill.EXCELLENT    -- Above average (recommended)
+skill = AI.Skill.ACE          -- Hardest, expert pilots
+ +

Mixed Difficulty Example:

+
RED_SQUADRON_CONFIG = {
+    {
+        displayName = "Elite Squadron",
+        skill = AI.Skill.ACE,             -- Best pilots
+        aircraft = 8,
+        -- ...
+    },
+    {
+        displayName = "Regular Squadron",
+        skill = AI.Skill.GOOD,            -- Average pilots
+        aircraft = 12,
+        -- ...
+    }
+}
+ +

Common Scenarios

+ +

Scenario 1: Simple Border Defense

+ +

Goal: RED defends northern border, BLUE defends southern border

+ +
-- RED Configuration
+RED_SQUADRON_CONFIG = {
+    {
+        templateName = "RED_CAP_North_MiG29",
+        displayName = "Northern Border CAP",
+        airbaseName = "Northern_Base",
+        aircraft = 12,
+        skill = AI.Skill.EXCELLENT,
+        altitude = 20000,
+        speed = 350,
+        patrolTime = 30,
+        type = "FIGHTER",
+        primaryZone = "RED BORDER"
+    }
+}
+
+-- BLUE Configuration
+BLUE_SQUADRON_CONFIG = {
+    {
+        templateName = "BLUE_CAP_South_F16",
+        displayName = "Southern Border CAP",
+        airbaseName = "Southern_Base",
+        aircraft = 12,
+        skill = AI.Skill.EXCELLENT,
+        altitude = 20000,
+        speed = 350,
+        patrolTime = 30,
+        type = "FIGHTER",
+        primaryZone = "BLUE BORDER"
+    }
+}
+ +
+ In Mission Editor: +
    +
  • Create zone "RED BORDER" (helicopter waypoints on northern border)
  • +
  • Create zone "BLUE BORDER" (helicopter waypoints on southern border)
  • +
+
+ +

Scenario 2: Layered Defense Network

+ +

Goal: Multiple squadrons covering overlapping zones with different priorities

+ +
RED_SQUADRON_CONFIG = {
+    -- Outer Layer: Long-range interceptors
+    {
+        templateName = "RED_LONG_RANGE_MiG31",
+        displayName = "Long Range Interceptors",
+        airbaseName = "Forward_Base",
+        aircraft = 8,
+        skill = AI.Skill.EXCELLENT,
+        altitude = 35000,
+        speed = 450,
+        patrolTime = 20,
+        type = "FIGHTER",
+        primaryZone = "OUTER PERIMETER"
+    },
+    
+    -- Middle Layer: General defense
+    {
+        templateName = "RED_CAP_MiG29",
+        displayName = "Middle Defense CAP",
+        airbaseName = "Central_Base",
+        aircraft = 12,
+        skill = AI.Skill.EXCELLENT,
+        altitude = 25000,
+        speed = 350,
+        patrolTime = 30,
+        type = "FIGHTER",
+        primaryZone = "MIDDLE PERIMETER",
+        secondaryZone = "OUTER PERIMETER"
+    },
+    
+    -- Inner Layer: Point defense
+    {
+        templateName = "RED_BASE_DEFENSE_SU27",
+        displayName = "Base Defense",
+        airbaseName = "Main_Base",
+        aircraft = 16,
+        skill = AI.Skill.ACE,
+        altitude = 20000,
+        speed = 320,
+        patrolTime = 40,
+        type = "FIGHTER",
+        primaryZone = "BASE PERIMETER",
+        tertiaryZone = "BASE PERIMETER",
+        zoneConfig = {
+            enableFallback = true,
+            fallbackThreshold = 0.3
+        }
+    }
+}
+ +

Scenario 3: Sustained Operations with Resupply

+ +

Goal: Long mission with automated squadron replenishment

+ +

Squadron Config:

+
RED_SQUADRON_CONFIG = {
+    {
+        templateName = "RED_CAP_Frontline_MiG29",
+        displayName = "Frontline CAP",
+        airbaseName = "Frontline_Base",
+        aircraft = 12,                    -- Will be resupplied
+        skill = AI.Skill.EXCELLENT,
+        altitude = 20000,
+        speed = 350,
+        patrolTime = 30,
+        type = "FIGHTER",
+        primaryZone = "COMBAT ZONE"
+    }
+}
+ +

Cargo Config (in Moose_TADC_CargoDispatcher.lua):

+
local CARGO_SUPPLY_CONFIG = {
+    red = {
+        cargoTemplate = "CARGO_RED_AN26",
+        supplyAirfields = {
+            "Rear_Base_1",                -- Safe logistics hub
+            "Rear_Base_2",                -- Backup supply source
+            "Central_Depot"               -- Main supply depot
+        },
+        replenishAmount = 4,              -- +4 aircraft per delivery
+        threshold = 0.75                  -- Trigger at 75% (9/12 aircraft)
+    }
+}
+ +

Mission Flow:

+
    +
  1. Frontline squadron intercepts threats
  2. +
  3. Squadron drops to 9 aircraft (75%)
  4. +
  5. Cargo automatically dispatched from rear base
  6. +
  7. Transport flies to frontline base
  8. +
  9. Cargo delivers, squadron back to 12 aircraft
  10. +
  11. Cycle repeats throughout mission
  12. +
+ +

Scenario 4: Asymmetric Warfare

+ +

Goal: RED has numerical superiority, BLUE has quality advantage

+ +
-- RED: More squadrons, lower skill
+local TADC_SETTINGS = {
+    red = {
+        interceptRatio = 0.8,            -- Conservative response
+        maxActiveCAP = 12,               -- More groups allowed
+    }
+}
+
+RED_SQUADRON_CONFIG = {
+    {
+        templateName = "RED_CAP_1",
+        airbaseName = "Base_1",
+        aircraft = 16,                    -- Large squadron
+        skill = AI.Skill.GOOD,            -- Average skill
+        -- ...
+    },
+    {
+        templateName = "RED_CAP_2",
+        airbaseName = "Base_2",
+        aircraft = 16,
+        skill = AI.Skill.GOOD,
+        -- ...
+    },
+    {
+        templateName = "RED_CAP_3",
+        airbaseName = "Base_3",
+        aircraft = 16,
+        skill = AI.Skill.GOOD,
+        -- ...
+    }
+}
+
+-- BLUE: Fewer squadrons, higher skill
+local TADC_SETTINGS = {
+    blue = {
+        interceptRatio = 1.2,            -- Aggressive response
+        maxActiveCAP = 8,                -- Fewer groups
+    }
+}
+
+BLUE_SQUADRON_CONFIG = {
+    {
+        templateName = "BLUE_CAP_1",
+        airbaseName = "Base_1",
+        aircraft = 10,                    -- Smaller squadron
+        skill = AI.Skill.ACE,             -- Elite pilots
+        -- ...
+    },
+    {
+        templateName = "BLUE_CAP_2",
+        airbaseName = "Base_2",
+        aircraft = 10,
+        skill = AI.Skill.ACE,
+        -- ...
+    }
+}
+ +

Tips for New Mission Makers

+ +

Start Simple

+ +
    +
  1. First Mission: Use 1-2 squadrons per side with no zones
  2. +
  3. Second Mission: Add zone-based defense
  4. +
  5. Third Mission: Add cargo resupply system
  6. +
  7. Advanced: Multi-squadron layered defense with fallback
  8. +
+ +

Realistic Aircraft Numbers

+ +

Small Airbase: 6-8 aircraft per squadron
+ Medium Airbase: 10-12 aircraft per squadron
+ Large Airbase: 14-18 aircraft per squadron

+ +

Balance across map: If RED has 40 total aircraft, BLUE should have similar unless asymmetric

+ +

Performance Considerations

+ +
    +
  • Limit active groups: Use maxActiveCAP to prevent FPS drops
  • +
  • Zone sizes matter: Smaller zones = less scanning overhead
  • +
  • Cargo cooldowns: Prevent cargo spam with reasonable cooldowns
  • +
  • Squadron counts: 3-5 squadrons per side is a good starting point
  • +
+ +

Testing Workflow

+ +
    +
  1. Create minimal setup (1 squadron each side)
  2. +
  3. Test in mission editor using "Fly Now"
  4. +
  5. Check F10 menus for squadron status
  6. +
  7. Spawn enemy aircraft to test intercepts
  8. +
  9. Review DCS.log for validation messages
  10. +
  11. Expand gradually once basic system works
  12. +
+ +

Common Mistakes to Avoid

+ +
    +
  • Using UNIT templates instead of GROUP templates
  • +
  • Misspelling airbase names
  • +
  • Loading scripts in wrong order
  • +
  • Setting intercept ratio too high
  • +
  • Forgetting to load MOOSE first
  • +
+ +
    +
  • Use GROUP templates (late activation groups)
  • +
  • Copy exact names from mission editor
  • +
  • Squadron Config → Main TADC → Cargo Dispatcher
  • +
  • Start with 0.8-1.0, adjust after testing
  • +
  • MOOSE must be first script loaded
  • +
+ +

Conclusion

+ +

The Universal TADC system provides mission makers with powerful, automated air defense capabilities that create dynamic, realistic air combat scenarios. By following this guide, even new mission makers can create sophisticated missions with minimal scripting knowledge.

+ +

Key Takeaways

+ +
    +
  • Three scripts work together: Squadron Config → Main TADC → Cargo Dispatcher
  • +
  • Configuration is simple: Edit tables, not complex code
  • +
  • Both coalitions operate independently: Balanced or asymmetric scenarios
  • +
  • Zones enable tactical behavior: Realistic area-of-responsibility system
  • +
  • Cargo enables sustained operations: Long missions with automatic resupply
  • +
  • Built-in validation: Checks configuration before mission starts
  • +
  • F10 menus provide visibility: Monitor system status in real-time
  • +
+ +

Getting Help

+ +

If you encounter issues:

+ +
    +
  1. Check DCS.log for validation errors
  2. +
  3. Use F10 menu diagnostics
  4. +
  5. Run console commands for detailed info
  6. +
  7. Review this guide's troubleshooting section
  8. +
  9. Start simple and expand gradually
  10. +
+ +

Next Steps

+ +
    +
  1. Set up your first squadron (1 RED, 1 BLUE)
  2. +
  3. Test basic intercept behavior
  4. +
  5. Add zones for tactical depth
  6. +
  7. Implement cargo resupply for long missions
  8. +
  9. Experiment with advanced features
  10. +
+ +

Happy mission making! 🚁✈️

+ + +
+ + \ No newline at end of file diff --git a/TADC_SYSTEM_GUIDE.md b/TADC_SYSTEM_GUIDE.md new file mode 100644 index 0000000..70bf452 --- /dev/null +++ b/TADC_SYSTEM_GUIDE.md @@ -0,0 +1,965 @@ +# Universal TADC System - Mission Maker's Guide +## Tactical Air Defense Controller with Automated Logistics + +--- + +## 📋 Table of Contents + +1. [What is TADC?](#what-is-tadc) +2. [System Overview](#system-overview) +3. [Quick Start Guide](#quick-start-guide) +4. [Detailed Configuration](#detailed-configuration) +5. [Zone-Based Defense Setup](#zone-based-defense-setup) +6. [Cargo Replenishment System](#cargo-replenishment-system) +7. [Testing & Troubleshooting](#testing--troubleshooting) +8. [Advanced Features](#advanced-features) +9. [Common Scenarios](#common-scenarios) + +--- + +## What is TADC? + +**TADC (Tactical Air Defense Controller)** is an automated air defense system for DCS missions that creates realistic, dynamic fighter aircraft responses to airborne threats. Think of it as an AI commander that: + +- **Detects enemy aircraft** automatically +- **Launches fighters** to intercept threats +- **Manages squadron resources** (aircraft availability, cooldowns) +- **Replenishes squadrons** through cargo aircraft deliveries +- **Operates independently** for both RED and BLUE coalitions + +### Why Use TADC? + +✅ **Realistic Air Defense** - Squadrons respond intelligently to threats +✅ **Dynamic Gameplay** - Air battles happen organically without manual triggers +✅ **Balanced Competition** - Both sides operate with equal capabilities +✅ **Sustainable Operations** - Cargo system allows long missions with resupply +✅ **Easy Configuration** - Simple tables instead of complex scripting + +--- + +## System Overview + +The TADC system consists of **three main scripts** that work together: + +### 1. Squadron Configuration (`Moose_TADC_SquadronConfigs_Load1st.lua`) +**Purpose:** Define all fighter squadrons for RED and BLUE coalitions +**Contains:** Aircraft templates, airbases, patrol parameters, zone assignments +**Load Order:** **FIRST** (must load before main TADC script) + +### 2. Main TADC System (`Moose_TADC_Load2nd.lua`) +**Purpose:** Core threat detection and interceptor management +**Contains:** Threat scanning, squadron selection, intercept logic, F10 menus +**Load Order:** **SECOND** (after squadron config) + +### 3. Cargo Dispatcher (`Moose_TADC_CargoDispatcher.lua`) +**Purpose:** Automated squadron resupply through cargo aircraft +**Contains:** Squadron monitoring, cargo spawning, delivery tracking +**Load Order:** **THIRD** (optional, only if using resupply system) + +--- + +## Quick Start Guide + +### Prerequisites + +Before setting up TADC, you need: + +- ✅ **MOOSE Framework** loaded in your mission (download from [MOOSE GitHub](https://github.com/FlightControl-Master/MOOSE)) +- ✅ **Fighter aircraft templates** created in DCS mission editor (as GROUPS, not units) +- ✅ **Airbases** under correct coalition control +- ✅ (Optional) **Cargo aircraft templates** for resupply missions + +### 5-Minute Setup + +#### Step 1: Create Fighter Templates + +1. Open your mission in DCS Mission Editor +2. Place fighter aircraft as **LATE ACTIVATION GROUPS** (not individual units) +3. Name them clearly (example: `RED_CAP_Kilpyavr_MiG29`) +4. Position them at or near the airbases they'll operate from +5. Set them to the correct coalition (RED or BLUE) + +**Important:** Use GROUP templates, not UNIT templates! + +#### Step 2: Load MOOSE Framework + +1. In mission editor, go to **Triggers** +2. Create a new trigger: **MISSION START** +3. Add action: **DO SCRIPT FILE** +4. Select your MOOSE.lua file +5. This must be the FIRST script loaded + +#### Step 3: Load Squadron Configuration + +1. Create another **DO SCRIPT FILE** action (after MOOSE) +2. Select `Moose_TADC_SquadronConfigs_Load1st.lua` +3. Edit the file to configure your squadrons (see below) + +#### Step 4: Load Main TADC System + +1. Create another **DO SCRIPT FILE** action +2. Select `Moose_TADC_Load2nd.lua` +3. (Optional) Adjust settings in the file if needed + +#### Step 5: (Optional) Load Cargo Dispatcher + +1. If using resupply system, create another **DO SCRIPT FILE** action +2. Select `Moose_TADC_CargoDispatcher.lua` + +**Load Order in Mission Editor:** +``` +1. MOOSE.lua +2. Moose_TADC_SquadronConfigs_Load1st.lua +3. Moose_TADC_Load2nd.lua +4. Moose_TADC_CargoDispatcher.lua (optional) +``` + +--- + +## Detailed Configuration + +### Squadron Configuration Explained + +Open `Moose_TADC_SquadronConfigs_Load1st.lua` and find the squadron configuration sections. + +#### Basic Squadron Example + +```lua +{ + templateName = "RED_CAP_Kilpyavr_MiG29", -- Must match mission editor template name + displayName = "Kilpyavr CAP MiG-29A", -- Human-readable name for logs/messages + airbaseName = "Kilpyavr", -- Exact airbase name from DCS + aircraft = 12, -- Maximum aircraft in squadron + skill = AI.Skill.EXCELLENT, -- AI pilot skill level + altitude = 20000, -- Patrol altitude (feet) + speed = 350, -- Patrol speed (knots) + patrolTime = 30, -- Time on station (minutes) + type = "FIGHTER" -- Aircraft role +} +``` + +#### Parameter Guide + +| Parameter | Description | Example Values | +|-----------|-------------|----------------| +| **templateName** | Group name from mission editor (EXACT match) | `"RED_CAP_Base_F15"` | +| **displayName** | Friendly name shown in messages | `"Kilpyavr CAP Squadron"` | +| **airbaseName** | DCS airbase name (case sensitive) | `"Kilpyavr"`, `"Nellis AFB"` | +| **aircraft** | Max squadron size | `8`, `12`, `16` | +| **skill** | AI difficulty | `AI.Skill.AVERAGE`, `GOOD`, `HIGH`, `EXCELLENT`, `ACE` | +| **altitude** | CAP patrol altitude | `15000` (feet) | +| **speed** | CAP patrol speed | `300` (knots) | +| **patrolTime** | Minutes on station before RTB | `20`, `30`, `40` | +| **type** | Aircraft role | `"FIGHTER"` | + +### Finding Airbase Names + +**Method 1: Mission Editor** +1. Open mission editor +2. Click on any airbase +3. The exact name appears in the properties panel +4. Copy this name EXACTLY (case sensitive!) + +**Method 2: Common Airbases** + +**Kola Peninsula (Example Map):** +- RED: `"Kilpyavr"`, `"Severomorsk-1"`, `"Severomorsk-3"`, `"Murmansk International"` +- BLUE: `"Luostari Pechenga"`, `"Ivalo"`, `"Alakurtti"` + +**Nevada:** +- `"Nellis AFB"`, `"McCarran International"`, `"Creech AFB"`, `"Tonopah Test Range"` + +**Caucasus:** +- `"Batumi"`, `"Gudauta"`, `"Senaki-Kolkhi"`, `"Kobuleti"`, `"Kutaisi"` + +### Adding Multiple Squadrons + +You can add as many squadrons as you want. Just copy the squadron block and modify the values: + +```lua +RED_SQUADRON_CONFIG = { + -- First Squadron + { + templateName = "RED_CAP_Base1_MiG29", + displayName = "Base 1 CAP", + airbaseName = "Kilpyavr", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER" + }, + + -- Second Squadron (different base) + { + templateName = "RED_CAP_Base2_SU27", + displayName = "Base 2 CAP", + airbaseName = "Severomorsk-1", + aircraft = 16, + skill = AI.Skill.ACE, + altitude = 25000, + speed = 380, + patrolTime = 25, + type = "FIGHTER" + }, + + -- Add more squadrons here... +} +``` + +**Repeat the same process for BLUE squadrons** in the `BLUE_SQUADRON_CONFIG` section. + +--- + +## Zone-Based Defense Setup + +Zones allow squadrons to have specific areas of responsibility, creating realistic layered defense. + +### Why Use Zones? + +- **Border Defense:** Squadrons patrol specific sectors +- **Layered Defense:** Multiple squadrons cover overlapping areas +- **Priority Response:** Squadrons respond differently based on threat location +- **Realistic Behavior:** Fighters don't fly across the entire map for minor threats + +### Zone Types + +Each squadron can have up to 3 zone types: + +1. **Primary Zone** - Main area of responsibility (full response) +2. **Secondary Zone** - Support area (reduced response, 60% by default) +3. **Tertiary Zone** - Emergency fallback (enhanced response when squadron weakened) + +### Creating Zones in Mission Editor + +**Method: Helicopter Waypoint Method** + +1. Place a **helicopter group** (late activation, any type) +2. Name it clearly (example: `"RED BORDER"`) +3. Add waypoints that outline your zone boundary +4. The script will automatically create a polygon zone from these waypoints +5. Repeat for each zone you want to create + +**Example Zone Setup:** +``` +Mission Editor: +- Helicopter Group: "RED BORDER" with waypoints forming a polygon +- Helicopter Group: "BLUE BORDER" with waypoints forming a polygon +- Helicopter Group: "CONTESTED ZONE" with waypoints forming a polygon +``` + +### Configuring Zone Response + +Add zone configuration to your squadron: + +```lua +{ + templateName = "RED_CAP_Kilpyavr_MiG29", + displayName = "Kilpyavr CAP", + airbaseName = "Kilpyavr", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + + -- Zone Configuration + primaryZone = "RED BORDER", -- Main responsibility area + secondaryZone = "CONTESTED ZONE", -- Backup coverage + tertiaryZone = nil, -- No tertiary zone + + -- Optional: Customize zone behavior + zoneConfig = { + primaryResponse = 1.0, -- Full response in primary zone + secondaryResponse = 0.6, -- 60% response in secondary + tertiaryResponse = 1.4, -- 140% response in tertiary + enableFallback = false, -- Don't auto-switch to tertiary + fallbackThreshold = 0.3, -- Switch when <30% aircraft remain + secondaryLowPriorityFilter = true, -- Ignore small threats in secondary + secondaryLowPriorityThreshold = 2 -- "Small threat" = 2 or fewer aircraft + } +} +``` + +### Zone Behavior Examples + +**Example 1: Border Defense Squadron** +```lua +primaryZone = "RED BORDER", -- Patrols the border +secondaryZone = "INTERIOR", -- Helps with interior threats if needed +tertiaryZone = nil -- No fallback +``` + +**Example 2: Base Defense with Fallback** +```lua +primaryZone = "NORTHERN SECTOR", -- Main patrol area +secondaryZone = nil, -- No secondary +tertiaryZone = "BASE PERIMETER", -- Falls back to defend base when weakened +enableFallback = true, -- Auto-switch to tertiary when low +fallbackThreshold = 0.4 -- Switch at 40% strength +``` + +**Example 3: Layered Defense** +```lua +-- Squadron A: Outer layer +primaryZone = "OUTER PERIMETER" + +-- Squadron B: Middle layer +primaryZone = "MIDDLE PERIMETER" + +-- Squadron C: Inner/base defense +primaryZone = "BASE DEFENSE" +``` + +### Global Response (No Zones) + +If you **DON'T** want zone restrictions, simply leave all zones as `nil`: + +```lua +{ + templateName = "RED_CAP_Base_MiG29", + displayName = "Global Response CAP", + airbaseName = "Kilpyavr", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + + -- No zones = responds to threats anywhere on the map + primaryZone = nil, + secondaryZone = nil, + tertiaryZone = nil +} +``` + +--- + +## Cargo Replenishment System + +The cargo system automatically replenishes squadrons by spawning transport aircraft that fly supplies to airbases. + +### How Cargo Works + +1. **Monitoring:** Script checks squadron aircraft counts every minute +2. **Detection:** When squadron drops below threshold (90% by default), cargo is dispatched +3. **Spawning:** Transport aircraft spawns at a supply airfield +4. **Delivery:** Flies to destination airbase and lands +5. **Replenishment:** Squadron aircraft count increases upon delivery +6. **Cooldown:** 5-minute cooldown before next delivery to same base + +### Cargo Aircraft Detection + +The system detects cargo by aircraft name patterns: +- `CARGO` +- `TRANSPORT` +- `C130` or `C-130` +- `AN26` or `AN-26` + +**Delivery Methods:** +- **Landing:** Aircraft lands at destination airbase + +### Configuring Cargo Templates + +Edit `Moose_TADC_CargoDispatcher.lua` and find `CARGO_SUPPLY_CONFIG`: + +```lua +local CARGO_SUPPLY_CONFIG = { + red = { + cargoTemplate = "CARGO_RED_AN26_TEMPLATE", -- Template name from mission editor + supplyAirfields = {"Airbase1", "Airbase2"}, -- List of supply bases + replenishAmount = 4, -- Aircraft added per delivery + threshold = 0.90 -- Trigger at 90% capacity + }, + blue = { + cargoTemplate = "CARGO_BLUE_C130_TEMPLATE", + supplyAirfields = {"Airbase3", "Airbase4"}, + replenishAmount = 4, + threshold = 0.90 + } +} +``` + +### Creating Cargo Templates + +1. **In Mission Editor:** + - Place transport aircraft group (C-130, An-26, etc.) + - Name it: `CARGO_RED_AN26_TEMPLATE` or `CARGO_BLUE_C130_TEMPLATE` + - Set **LATE ACTIVATION** + - Position at any friendly airbase (starting position doesn't matter) + +2. **In Configuration:** + - Use the EXACT template name in `cargoTemplate` field + - List supply airbases in `supplyAirfields` array + - Set how many aircraft each delivery adds (`replenishAmount`) + +### Supply Airfield Strategy + +**Choose rear/safe airbases for supplies:** + +```lua +red = { + cargoTemplate = "CARGO_RED_AN26_TEMPLATE", + supplyAirfields = { + "Rear_Base_1", -- Far from frontline, safe + "Rear_Base_2", -- Alternate supply source + "Central_Logistics_Hub" -- Main supply depot + }, + replenishAmount = 4, + threshold = 0.90 +} +``` + +**Tips:** +- Use 3-5 supply airbases for redundancy +- Choose bases far from combat zones +- Ensure supply bases are well-defended +- Balance geographic coverage + +### Disabling Cargo System + +If you don't want automated resupply: +1. **Don't load** `Moose_TADC_CargoDispatcher.lua` +2. Squadrons will operate with their initial aircraft count only +3. System still works perfectly for shorter missions + +--- + +## Testing & Troubleshooting + +### Validation Tools + +The system includes built-in validation. Check the DCS log file after mission start for: + +``` +[Universal TADC] ═══════════════════════════════════════ +[Universal TADC] Configuration Validation Results: +[Universal TADC] ✓ All templates exist +[Universal TADC] ✓ All airbases valid +[Universal TADC] ✓ All zones found +[Universal TADC] Configuration is VALID +[Universal TADC] ═══════════════════════════════════════ +``` + +### In-Game F10 Menu Commands + +Press **F10** in-game to access TADC utilities: + +**Available to Each Coalition:** +- **Show Squadron Resource Summary** - Current aircraft counts +- **Show Airbase Status Report** - Operational status of all bases +- **Show Active Interceptors** - Currently airborne fighters +- **Show Threat Summary** - Detected enemy aircraft +- **Broadcast Squadron Summary Now** - Force immediate status update +- **Show Cargo Delivery Log** - Recent supply missions +- **Show Zone Coverage Map** - Squadron zone assignments + +**Available to All (Mission Commands):** +- **Emergency Cleanup Interceptors** - Remove stuck/dead groups +- **Show TADC System Status** - Uptime and system health +- **Check for Stuck Aircraft** - Manual stuck aircraft check +- **Show Airbase Health Status** - Parking/spawn issues + +### Common Issues & Solutions + +#### Issue: "Template not found in mission" + +**Cause:** Template name in config doesn't match mission editor +**Solution:** +1. Check exact spelling and case +2. Ensure template is in mission editor +3. Verify template is a GROUP (not a unit) +4. Check template name in mission editor properties + +#### Issue: "Airbase not found or wrong coalition" + +**Cause:** Airbase name wrong or captured by enemy +**Solution:** +1. Check exact airbase spelling (case sensitive) +2. Verify airbase is owned by correct coalition in mission editor +3. Use `_G.TDAC_CheckAirbase("AirbaseName")` in DCS console + +#### Issue: "No interceptors launching" + +**Check:** +1. Are enemy aircraft detected? (F10 → Show Threat Summary) +2. Are squadrons operational? (F10 → Show Squadron Resource Summary) +3. Is airbase captured/destroyed? (F10 → Show Airbase Status Report) +4. Are squadrons on cooldown? (F10 → Show Squadron Resource Summary) +5. Check intercept ratio settings (might be too low) + +#### Issue: "Cargo not delivering" + +**Check:** +1. Is cargo template name correct? +2. Are supply airbases valid and friendly? +3. Is destination airbase captured/operational? +4. Check parking availability (F10 → Show Airbase Health Status) +5. Look for "Cargo delivery detected" messages in log + +#### Issue: "Aircraft spawning stuck at parking" + +**Cause:** Parking spots occupied or insufficient space +**Solution:** +1. Use F10 → Check for Stuck Aircraft +2. Use F10 → Emergency Cleanup Interceptors +3. Check airbase parking capacity (larger aircraft need more space) +4. Reduce squadron sizes if parking is limited + +### DCS Console Diagnostics + +Open DCS Lua console (**F12** or scripting console) and run: + +```lua +-- Check all supply airbase ownership +_G.TDAC_CheckAirbaseOwnership() + +-- Check specific airbase +_G.TDAC_CheckAirbase("Kilpyavr") + +-- Validate dispatcher configuration +_G.TDAC_RunConfigCheck() + +-- Check airbase parking availability +_G.TDAC_LogAirbaseParking("Kilpyavr") + +-- Test cargo spawn (debugging) +_G.TDAC_CargoDispatcher_TestSpawn("CARGO_RED_AN26_TEMPLATE", "SupplyBase", "DestinationBase") +``` + +--- + +## Advanced Features + +### Intercept Ratio System + +The `interceptRatio` setting controls how many fighters launch per enemy aircraft. + +**In `Moose_TADC_Load2nd.lua`:** + +```lua +local TADC_SETTINGS = { + red = { + interceptRatio = 0.8, -- RED launches 0.8 fighters per threat + maxActiveCAP = 8, -- Max 8 groups in air simultaneously + defaultCooldown = 300, -- 5-minute cooldown after engagement + }, + blue = { + interceptRatio = 1.2, -- BLUE launches 1.2 fighters per threat + maxActiveCAP = 10, -- Max 10 groups in air simultaneously + defaultCooldown = 300, + } +} +``` + +**Intercept Ratio Chart:** + +| Ratio | 1 Enemy | 4 Enemies | 8 Enemies | Effect | +|-------|---------|-----------|-----------|--------| +| 0.5 | 1 fighter | 2 fighters | 4 fighters | Conservative response | +| 0.8 | 1 fighter | 4 fighters | 7 fighters | **Balanced (default)** | +| 1.0 | 1 fighter | 4 fighters | 8 fighters | 1:1 parity | +| 1.4 | 2 fighters | 6 fighters | 12 fighters | Strong response | +| 2.0 | 2 fighters | 8 fighters | 16 fighters | Overwhelming force | + +**Tactical Effects:** +- **Low (0.5-0.8):** Sustainable defense, squadrons last longer +- **Medium (0.8-1.2):** Balanced dogfights, realistic attrition +- **High (1.4-2.0):** Strong defense, rapid squadron depletion + +**Asymmetric Scenarios:** +```lua +-- RED advantage +red = { interceptRatio = 1.4 }, +blue = { interceptRatio = 0.8 } + +-- BLUE advantage +red = { interceptRatio = 0.8 }, +blue = { interceptRatio = 1.4 } +``` + +### Distance-Based Engagement + +Control how far squadrons will chase threats: + +```lua +{ + templateName = "RED_CAP_Base_MiG29", + displayName = "Base Defense", + airbaseName = "Kilpyavr", + aircraft = 12, + -- ... other settings ... + + zoneConfig = { + maxEngagementRange = 50000, -- Won't engage threats >50km from base + primaryResponse = 1.0, + secondaryResponse = 0.6 + } +} +``` + +### Cooldown System + +After launching interceptors, squadrons go on cooldown to prevent spam: + +```lua +local TADC_SETTINGS = { + red = { + defaultCooldown = 300, -- 5 minutes between launches + -- ... other settings ... + } +} +``` + +**Per-Squadron Cooldown (optional):** +```lua +{ + templateName = "RED_CAP_Base_MiG29", + cooldownOverride = 600, -- This squadron: 10-minute cooldown + -- ... other settings ... +} +``` + +### Aircraft Skill Levels + +Adjust AI difficulty per squadron: + +```lua +skill = AI.Skill.AVERAGE -- Easiest, good for training +skill = AI.Skill.GOOD -- Below average +skill = AI.Skill.HIGH -- Average pilots +skill = AI.Skill.EXCELLENT -- Above average (recommended) +skill = AI.Skill.ACE -- Hardest, expert pilots +``` + +**Mixed Difficulty Example:** +```lua +RED_SQUADRON_CONFIG = { + { + displayName = "Elite Squadron", + skill = AI.Skill.ACE, -- Best pilots + aircraft = 8, + -- ... + }, + { + displayName = "Regular Squadron", + skill = AI.Skill.GOOD, -- Average pilots + aircraft = 12, + -- ... + } +} +``` + +--- + +## Common Scenarios + +### Scenario 1: Simple Border Defense + +**Goal:** RED defends northern border, BLUE defends southern border + +```lua +-- RED Configuration +RED_SQUADRON_CONFIG = { + { + templateName = "RED_CAP_North_MiG29", + displayName = "Northern Border CAP", + airbaseName = "Northern_Base", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + primaryZone = "RED BORDER" + } +} + +-- BLUE Configuration +BLUE_SQUADRON_CONFIG = { + { + templateName = "BLUE_CAP_South_F16", + displayName = "Southern Border CAP", + airbaseName = "Southern_Base", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + primaryZone = "BLUE BORDER" + } +} +``` + +**In Mission Editor:** +- Create zone "RED BORDER" (helicopter waypoints on northern border) +- Create zone "BLUE BORDER" (helicopter waypoints on southern border) + +--- + +### Scenario 2: Layered Defense Network + +**Goal:** Multiple squadrons covering overlapping zones with different priorities + +```lua +RED_SQUADRON_CONFIG = { + -- Outer Layer: Long-range interceptors + { + templateName = "RED_LONG_RANGE_MiG31", + displayName = "Long Range Interceptors", + airbaseName = "Forward_Base", + aircraft = 8, + skill = AI.Skill.EXCELLENT, + altitude = 35000, + speed = 450, + patrolTime = 20, + type = "FIGHTER", + primaryZone = "OUTER PERIMETER" + }, + + -- Middle Layer: General defense + { + templateName = "RED_CAP_MiG29", + displayName = "Middle Defense CAP", + airbaseName = "Central_Base", + aircraft = 12, + skill = AI.Skill.EXCELLENT, + altitude = 25000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + primaryZone = "MIDDLE PERIMETER", + secondaryZone = "OUTER PERIMETER" + }, + + -- Inner Layer: Point defense + { + templateName = "RED_BASE_DEFENSE_SU27", + displayName = "Base Defense", + airbaseName = "Main_Base", + aircraft = 16, + skill = AI.Skill.ACE, + altitude = 20000, + speed = 320, + patrolTime = 40, + type = "FIGHTER", + primaryZone = "BASE PERIMETER", + tertiaryZone = "BASE PERIMETER", + zoneConfig = { + enableFallback = true, + fallbackThreshold = 0.3 + } + } +} +``` + +--- + +### Scenario 3: Sustained Operations with Resupply + +**Goal:** Long mission with automated squadron replenishment + +**Squadron Config:** +```lua +RED_SQUADRON_CONFIG = { + { + templateName = "RED_CAP_Frontline_MiG29", + displayName = "Frontline CAP", + airbaseName = "Frontline_Base", + aircraft = 12, -- Will be resupplied + skill = AI.Skill.EXCELLENT, + altitude = 20000, + speed = 350, + patrolTime = 30, + type = "FIGHTER", + primaryZone = "COMBAT ZONE" + } +} +``` + +**Cargo Config** (in `Moose_TADC_CargoDispatcher.lua`): +```lua +local CARGO_SUPPLY_CONFIG = { + red = { + cargoTemplate = "CARGO_RED_AN26", + supplyAirfields = { + "Rear_Base_1", -- Safe logistics hub + "Rear_Base_2", -- Backup supply source + "Central_Depot" -- Main supply depot + }, + replenishAmount = 4, -- +4 aircraft per delivery + threshold = 0.75 -- Trigger at 75% (9/12 aircraft) + } +} +``` + +**Mission Flow:** +1. Frontline squadron intercepts threats +2. Squadron drops to 9 aircraft (75%) +3. Cargo automatically dispatched from rear base +4. Transport flies to frontline base +5. Cargo delivers, squadron back to 12 aircraft +6. Cycle repeats throughout mission + +--- + +### Scenario 4: Asymmetric Warfare + +**Goal:** RED has numerical superiority, BLUE has quality advantage + +```lua +-- RED: More squadrons, lower skill +local TADC_SETTINGS = { + red = { + interceptRatio = 0.8, -- Conservative response + maxActiveCAP = 12, -- More groups allowed + } +} + +RED_SQUADRON_CONFIG = { + { + templateName = "RED_CAP_1", + airbaseName = "Base_1", + aircraft = 16, -- Large squadron + skill = AI.Skill.GOOD, -- Average skill + -- ... + }, + { + templateName = "RED_CAP_2", + airbaseName = "Base_2", + aircraft = 16, + skill = AI.Skill.GOOD, + -- ... + }, + { + templateName = "RED_CAP_3", + airbaseName = "Base_3", + aircraft = 16, + skill = AI.Skill.GOOD, + -- ... + } +} + +-- BLUE: Fewer squadrons, higher skill +local TADC_SETTINGS = { + blue = { + interceptRatio = 1.2, -- Aggressive response + maxActiveCAP = 8, -- Fewer groups + } +} + +BLUE_SQUADRON_CONFIG = { + { + templateName = "BLUE_CAP_1", + airbaseName = "Base_1", + aircraft = 10, -- Smaller squadron + skill = AI.Skill.ACE, -- Elite pilots + -- ... + }, + { + templateName = "BLUE_CAP_2", + airbaseName = "Base_2", + aircraft = 10, + skill = AI.Skill.ACE, + -- ... + } +} +``` + +--- + +## Tips for New Mission Makers + +### Start Simple + +1. **First Mission:** Use 1-2 squadrons per side with no zones +2. **Second Mission:** Add zone-based defense +3. **Third Mission:** Add cargo resupply system +4. **Advanced:** Multi-squadron layered defense with fallback + +### Realistic Aircraft Numbers + +**Small Airbase:** 6-8 aircraft per squadron +**Medium Airbase:** 10-12 aircraft per squadron +**Large Airbase:** 14-18 aircraft per squadron + +**Balance across map:** If RED has 40 total aircraft, BLUE should have similar unless asymmetric + +### Performance Considerations + +- **Limit active groups:** Use `maxActiveCAP` to prevent FPS drops +- **Zone sizes matter:** Smaller zones = less scanning overhead +- **Cargo cooldowns:** Prevent cargo spam with reasonable cooldowns +- **Squadron counts:** 3-5 squadrons per side is a good starting point + +### Testing Workflow + +1. **Create minimal setup** (1 squadron each side) +2. **Test in mission editor** using "Fly Now" +3. **Check F10 menus** for squadron status +4. **Spawn enemy aircraft** to test intercepts +5. **Review DCS.log** for validation messages +6. **Expand gradually** once basic system works + +### Common Mistakes to Avoid + +❌ **Using UNIT templates instead of GROUP templates** +✅ Use GROUP templates (late activation groups) + +❌ **Misspelling airbase names** +✅ Copy exact names from mission editor + +❌ **Loading scripts in wrong order** +✅ Squadron Config → Main TADC → Cargo Dispatcher + +❌ **Setting intercept ratio too high** +✅ Start with 0.8-1.0, adjust after testing + +❌ **Forgetting to load MOOSE first** +✅ MOOSE must be first script loaded + +--- + +## Conclusion + +The Universal TADC system provides mission makers with powerful, automated air defense capabilities that create dynamic, realistic air combat scenarios. By following this guide, even new mission makers can create sophisticated missions with minimal scripting knowledge. + +### Key Takeaways + +✅ **Three scripts work together:** Squadron Config → Main TADC → Cargo Dispatcher +✅ **Configuration is simple:** Edit tables, not complex code +✅ **Both coalitions operate independently:** Balanced or asymmetric scenarios +✅ **Zones enable tactical behavior:** Realistic area-of-responsibility system +✅ **Cargo enables sustained operations:** Long missions with automatic resupply +✅ **Built-in validation:** Checks configuration before mission starts +✅ **F10 menus provide visibility:** Monitor system status in real-time + +### Getting Help + +If you encounter issues: + +1. Check DCS.log for validation errors +2. Use F10 menu diagnostics +3. Run console commands for detailed info +4. Review this guide's troubleshooting section +5. Start simple and expand gradually + +### Next Steps + +1. Set up your first squadron (1 RED, 1 BLUE) +2. Test basic intercept behavior +3. Add zones for tactical depth +4. Implement cargo resupply for long missions +5. Experiment with advanced features + +Happy mission making! 🚁✈️ + +--- +*Author: F99th-TracerFacer +*Document Version: 1.0* +*Last Updated: October 2025* +*Compatible with: MOOSE Framework & DCS World*