Moose_TADC/Moose_TADC_Load2nd.lua
2025-12-01 10:30:19 -06:00

2630 lines
129 KiB
Lua

---@diagnostic disable: undefined-field
--[[
═══════════════════════════════════════════════════════════════════════════════
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
═══════════════════════════════════════════════════════════════════════════════
]]
---@diagnostic disable: undefined-global, lowercase-global
-- MOOSE framework globals are defined at runtime by DCS World
--[[
═══════════════════════════════════════════════════════════════════════════════
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 + coalition as dedupe key to avoid double crediting when the same group
-- triggers multiple events or moves between airbases rapidly.
local okId, grpId = pcall(function() return cargoGroup and cargoGroup.GetID and cargoGroup:GetID() end)
local groupIdStr = (okId and grpId) and tostring(grpId) or "<no-id>"
local deliveryKey = coalitionKey:upper() .. "_" .. groupIdStr .. "_" .. tostring(squadron.airbaseName)
-- Diagnostic log: show group name, id, and delivery key when processor invoked
local okName, grpName = pcall(function() return cargoGroup and cargoGroup.GetName and cargoGroup:GetName() end)
local groupNameStr = (okName and grpName) and tostring(grpName) or "<no-name>"
log("PROCESS CARGO: invoked for group=" .. groupNameStr .. " id=" .. groupIdStr .. " targetAirbase=" .. tostring(squadron.airbaseName) .. " deliveryKey=" .. deliveryKey, true)
if _G.processedDeliveries[deliveryKey] then
-- Already processed recently, ignore
log("PROCESS CARGO: deliveryKey " .. deliveryKey .. " already processed at " .. tostring(_G.processedDeliveries[deliveryKey]), true)
return
end
-- Mark processed immediately
_G.processedDeliveries[deliveryKey] = timer.getTime()
-- Credit the squadron
local currentCount = squadronAircraftCounts[coalitionKey][squadron.templateName] or 0
local maxCount = squadron.aircraft or 0
local addAmount = TADC_SETTINGS[coalitionKey].cargoReplenishmentAmount or 0
local newCount = math.min(currentCount + addAmount, maxCount)
local actualAdded = newCount - currentCount
if actualAdded > 0 then
squadronAircraftCounts[coalitionKey][squadron.templateName] = newCount
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. cargoGroup:GetName() .. " delivered " .. actualAdded ..
" aircraft to " .. (squadron.displayName or squadron.templateName) ..
" (" .. newCount .. "/" .. maxCount .. ")"
log(msg)
MESSAGE:New(msg, 20):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
else
local msg = coalitionKey:upper() .. " CARGO DELIVERY: " .. (squadron.displayName or squadron.templateName) .. " already at max capacity"
log(msg, true)
MESSAGE:New(msg, 10):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end
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 and closestSquadron 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 and closestSquadron 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 closestSquadron and closestDistance 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 closestSquadron and closestDistance 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
local distanceStr = closestDistance and math.floor(closestDistance) .. "m" or "unknown"
log("LANDING DELIVERY (raw-proxy): " .. tostring(rawName) .. " landed and delivered at " .. closestSquadron.airbaseName .. " (distance: " .. distanceStr .. ") - 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 rtbBuffer = (coalitionSettings and coalitionSettings.rtbFlightBuffer) or 300
local flightTime = math.ceil(shortestDistance / (ADVANCED_SETTINGS.rtbSpeed * 0.5144)) + rtbBuffer
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 baseInterceptRatio = (coalitionSettings and coalitionSettings.interceptRatio) or 1.0
local finalInterceptRatio = baseInterceptRatio * zoneResponseRatio
local interceptorsNeeded = math.max(1, math.ceil(threatSize * finalInterceptRatio))
-- Check if we have capacity
if coalitionSettings and 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)
local cleanupTime = (coalitionSettings and coalitionSettings.emergencyCleanupTime) or 7200
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, {}, cleanupTime)
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()
if coalitionSettings and coalitionSettings.squadronCooldown then
squadronCooldowns[coalitionKey][squadron.templateName] = currentTime + coalitionSettings.squadronCooldown
local cooldownMinutes = coalitionSettings.squadronCooldown / 60
cooldownMinutes = coalitionSettings.squadronCooldown / 60
end
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
if enemyAircraft then
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
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 "<nil>" end
local ok, n = pcall(function()
if obj.GetName then return obj:GetName() end
if obj.getName then return obj:getName() end
return nil
end)
return (ok and n) and tostring(n) or "<unavailable>"
end
local iniUnitPresent = EventData.IniUnit ~= nil
local iniGroupPresent = EventData.IniGroup ~= nil
local placePresent = EventData.Place ~= nil
local iniUnitName = safeName(EventData.IniUnit)
local iniGroupName = safeName(EventData.IniGroup)
local placeName = safeName(EventData.Place)
log("MOOSE LAND EVENT: IniUnitPresent=" .. tostring(iniUnitPresent) .. ", IniUnitName=" .. tostring(iniUnitName) .. ", IniGroupPresent=" .. tostring(iniGroupPresent) .. ", IniGroupName=" .. tostring(iniGroupName) .. ", PlacePresent=" .. tostring(placePresent) .. ", PlaceName=" .. tostring(placeName), true)
end
local rawEvent = {
id = world.event.S_EVENT_LAND,
initiator = EventData.IniUnit or EventData.IniGroup or nil,
place = EventData.Place or nil,
-- Provide the original EventData for potential fallback use
_moose_original = EventData
}
-- Log and return; the world event handler `cargoEventHandler` will handle the actual processing.
return
end
-- Register the MOOSE handler
TADC_CARGO_LANDING_HANDLER:HandleEvent(EVENTS.Land)
end
SCHEDULER:New(nil, detectThreats, {}, 5, TADC_SETTINGS.checkInterval)
SCHEDULER:New(nil, monitorInterceptors, {}, 10, TADC_SETTINGS.monitorInterval)
SCHEDULER:New(nil, checkAirbaseStatus, {}, 30, TADC_SETTINGS.statusReportInterval)
SCHEDULER:New(nil, 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)