Updated with performance tweaks.

This commit is contained in:
iTracerFacer 2025-11-12 09:54:58 -06:00
parent 830f08441d
commit a0accf2adc
4 changed files with 425 additions and 170 deletions

View File

@ -1,6 +1,21 @@
-- Setup Capture Missions & Zones
-- Refactored version with configurable zone ownership -- Refactored version with configurable zone ownership
-- ==========================================
-- MESSAGE AND TIMING CONFIGURATION
-- ==========================================
local MESSAGE_CONFIG = {
STATUS_BROADCAST_FREQUENCY = 3602, -- Zone status broadcast cadence (seconds)
STATUS_BROADCAST_START_DELAY = 10, -- Delay before first broadcast (seconds)
COLOR_VERIFICATION_FREQUENCY = 240, -- Zone color verification cadence (seconds)
COLOR_VERIFICATION_START_DELAY = 60, -- Delay before first color check (seconds)
TACTICAL_UPDATE_FREQUENCY = 180, -- Tactical marker update cadence (seconds)
TACTICAL_UPDATE_START_DELAY = 30, -- Delay before first tactical update (seconds)
STATUS_MESSAGE_DURATION = 15, -- How long general status messages stay onscreen
VICTORY_MESSAGE_DURATION = 300, -- How long victory/defeat alerts stay onscreen
CAPTURE_MESSAGE_DURATION = 15, -- Duration for capture/guard/empty notices
ATTACK_MESSAGE_DURATION = 15 -- Duration for attack alerts
}
-- ========================================== -- ==========================================
-- ZONE COLOR CONFIGURATION (Centralized) -- ZONE COLOR CONFIGURATION (Centralized)
-- ========================================== -- ==========================================
@ -164,7 +179,7 @@ end
-- Logging configuration: toggle logging behavior for this module -- Logging configuration: toggle logging behavior for this module
-- Set `CAPTURE_ZONE_LOGGING.enabled = false` to silence module logs -- Set `CAPTURE_ZONE_LOGGING.enabled = false` to silence module logs
if not CAPTURE_ZONE_LOGGING then if not CAPTURE_ZONE_LOGGING then
CAPTURE_ZONE_LOGGING = { enabled = true, prefix = "[CAPTURE Module]" } CAPTURE_ZONE_LOGGING = { enabled = false, prefix = "[CAPTURE Module]" }
end end
local function log(message, detailed) local function log(message, detailed)
@ -254,43 +269,63 @@ end
local totalZones = InitializeZones() local totalZones = InitializeZones()
-- Helper functions for tactical information
-- Global cached unit set - created once and maintained automatically by MOOSE -- Global cached unit set - created once and maintained automatically by MOOSE
local CachedUnitSet = nil local CachedUnitSet = nil
-- Utility guard to safely test whether a unit is inside a zone without throwing
local function IsUnitInZone(unit, zone)
if not unit or not zone then
return false
end
local ok, point = pcall(function()
return unit:GetPointVec3()
end)
if not ok or not point then
return false
end
local inZone = false
pcall(function()
inZone = zone:IsPointVec3InZone(point)
end)
return inZone
end
-- Initialize the cached unit set once -- Initialize the cached unit set once
local function InitializeCachedUnitSet() local function InitializeCachedUnitSet()
if not CachedUnitSet then if not CachedUnitSet then
CachedUnitSet = SET_UNIT:New() CachedUnitSet = SET_UNIT:New()
:FilterCategories({"ground", "plane", "helicopter"}) -- Only scan relevant unit types :FilterCategories({"ground", "plane", "helicopter"}) -- Only scan relevant unit types
:FilterOnce() -- Don't filter continuously, we'll use the live set :FilterStart() -- Keep the set updated by MOOSE without recreating it
log("[PERFORMANCE] Initialized cached unit set for zone scanning") log("[PERFORMANCE] Initialized cached unit set for zone scanning")
end end
end end
local function GetZoneForceStrengths(ZoneCapture) local function GetZoneForceStrengths(ZoneCapture)
if not ZoneCapture then if not ZoneCapture then
return {red = 0, blue = 0, neutral = 0} return { red = 0, blue = 0, neutral = 0 }
end end
local success, zone = pcall(function() return ZoneCapture:GetZone() end) local success, zone = pcall(function()
if not success or not zone then return ZoneCapture:GetZone()
return {red = 0, blue = 0, neutral = 0} end)
if not success or not zone then
return { red = 0, blue = 0, neutral = 0 }
end end
local redCount = 0 local redCount = 0
local blueCount = 0 local blueCount = 0
local neutralCount = 0 local neutralCount = 0
-- Get all units in the zone using MOOSE's zone scanning InitializeCachedUnitSet()
local unitsInZone = SET_UNIT:New()
:FilterZones({zone}) if CachedUnitSet then
:FilterOnce() CachedUnitSet:ForEachUnit(function(unit)
if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then
if unitsInZone then
unitsInZone:ForEachUnit(function(unit)
if unit and unit:IsAlive() then
local unitCoalition = unit:GetCoalition() local unitCoalition = unit:GetCoalition()
if unitCoalition == coalition.side.RED then if unitCoalition == coalition.side.RED then
redCount = redCount + 1 redCount = redCount + 1
@ -302,10 +337,10 @@ local function GetZoneForceStrengths(ZoneCapture)
end end
end) end)
end end
log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d", log(string.format("[TACTICAL] Zone %s scan result: R:%d B:%d N:%d",
ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount)) ZoneCapture:GetZoneName(), redCount, blueCount, neutralCount))
return { return {
red = redCount, red = redCount,
blue = blueCount, blue = blueCount,
@ -314,63 +349,62 @@ local function GetZoneForceStrengths(ZoneCapture)
end end
local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition) local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition)
local zone = ZoneCapture:GetZone() if not ZoneCapture or not enemyCoalition then
if not zone then return {} end return {}
end
local success, zone = pcall(function()
return ZoneCapture:GetZone()
end)
if not success or not zone then
return {}
end
InitializeCachedUnitSet()
local coords = {} local coords = {}
-- Get all units in the zone using MOOSE's zone scanning
local unitsInZone = SET_UNIT:New()
:FilterZones({zone})
:FilterOnce()
local totalUnits = 0 local totalUnits = 0
local enemyUnits = 0 local enemyUnits = 0
local unitsWithCoords = 0 local unitsWithCoords = 0
if unitsInZone then if CachedUnitSet then
unitsInZone:ForEachUnit(function(unit) CachedUnitSet:ForEachUnit(function(unit)
totalUnits = totalUnits + 1 if unit and unit:IsAlive() and IsUnitInZone(unit, zone) then
if unit and unit:IsAlive() then totalUnits = totalUnits + 1
local unitCoalition = unit:GetCoalition() local unitCoalition = unit:GetCoalition()
-- Process units of the specified enemy coalition
if unitCoalition == enemyCoalition then if unitCoalition == enemyCoalition then
enemyUnits = enemyUnits + 1 enemyUnits = enemyUnits + 1
local coord = unit:GetCoordinate() local coord = unit:GetCoordinate()
if coord then if coord then
-- Try multiple methods to get coordinates
local mgrs = nil local mgrs = nil
local success_mgrs = false local success_mgrs = false
-- Method 1: Try ToStringMGRS
success_mgrs, mgrs = pcall(function() success_mgrs, mgrs = pcall(function()
return coord:ToStringMGRS(5) return coord:ToStringMGRS(5)
end) end)
-- Method 2: Try ToStringMGRS without precision parameter
if not success_mgrs or not mgrs then if not success_mgrs or not mgrs then
success_mgrs, mgrs = pcall(function() success_mgrs, mgrs = pcall(function()
return coord:ToStringMGRS() return coord:ToStringMGRS()
end) end)
end end
-- Method 3: Try ToMGRS
if not success_mgrs or not mgrs then if not success_mgrs or not mgrs then
success_mgrs, mgrs = pcall(function() success_mgrs, mgrs = pcall(function()
return coord:ToMGRS() return coord:ToMGRS()
end) end)
end end
-- Method 4: Fallback to Lat/Long
if not success_mgrs or not mgrs then if not success_mgrs or not mgrs then
success_mgrs, mgrs = pcall(function() success_mgrs, mgrs = pcall(function()
local lat, lon = coord:GetLLDDM() local lat, lon = coord:GetLLDDM()
return string.format("N%s E%s", lat, lon) return string.format("N%s E%s", lat, lon)
end) end)
end end
if success_mgrs and mgrs then if success_mgrs and mgrs then
unitsWithCoords = unitsWithCoords + 1 unitsWithCoords = unitsWithCoords + 1
local unitType = unit:GetTypeName() or "Unknown" local unitType = unit:GetTypeName() or "Unknown"
@ -389,13 +423,13 @@ local function GetEnemyUnitMGRSCoords(ZoneCapture, enemyCoalition)
end end
end) end)
end end
log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, Enemy units: %d, units with MGRS: %d", log(string.format("[TACTICAL DEBUG] %s - Total units scanned: %d, Enemy units: %d, units with MGRS: %d",
ZoneCapture:GetZoneName(), totalUnits, enemyUnits, unitsWithCoords)) ZoneCapture:GetZoneName(), totalUnits, enemyUnits, unitsWithCoords))
log(string.format("[TACTICAL] Found %d enemy units with coordinates in %s", log(string.format("[TACTICAL] Found %d enemy units with coordinates in %s",
#coords, ZoneCapture:GetZoneName())) #coords, ZoneCapture:GetZoneName()))
return coords return coords
end end
@ -463,19 +497,39 @@ local function CreateTacticalInfoMarker(ZoneCapture)
if coord then if coord then
local offsetCoord = coord:Translate(200, 45) -- 200m NE local offsetCoord = coord:Translate(200, 45) -- 200m NE
local function removeMarker(markerID)
if not markerID then
return
end
local removed = pcall(function()
offsetCoord:RemoveMark(markerID)
end)
if not removed then
removed = pcall(function()
trigger.action.removeMark(markerID)
end)
end
if not removed then
pcall(function()
coord:RemoveMark(markerID)
end)
end
end
-- Remove legacy single marker if present -- Remove legacy single marker if present
if ZoneCapture.TacticalMarkerID then if ZoneCapture.TacticalMarkerID then
log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName)) log(string.format("[TACTICAL] Removing old marker ID %d for %s", ZoneCapture.TacticalMarkerID, zoneName))
pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID) end) removeMarker(ZoneCapture.TacticalMarkerID)
pcall(function() trigger.action.removeMark(ZoneCapture.TacticalMarkerID) end)
pcall(function() coord:RemoveMark(ZoneCapture.TacticalMarkerID) end)
ZoneCapture.TacticalMarkerID = nil ZoneCapture.TacticalMarkerID = nil
end end
-- BLUE Coalition Marker -- BLUE Coalition Marker
if ZoneCapture.TacticalMarkerID_BLUE then if ZoneCapture.TacticalMarkerID_BLUE then
log(string.format("[TACTICAL] Removing old BLUE marker ID %d for %s", ZoneCapture.TacticalMarkerID_BLUE, zoneName)) log(string.format("[TACTICAL] Removing old BLUE marker ID %d for %s", ZoneCapture.TacticalMarkerID_BLUE, zoneName))
pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID_BLUE) end) removeMarker(ZoneCapture.TacticalMarkerID_BLUE)
ZoneCapture.TacticalMarkerID_BLUE = nil ZoneCapture.TacticalMarkerID_BLUE = nil
end end
local successBlue, markerIDBlue = pcall(function() local successBlue, markerIDBlue = pcall(function()
@ -492,7 +546,7 @@ local function CreateTacticalInfoMarker(ZoneCapture)
-- RED Coalition Marker -- RED Coalition Marker
if ZoneCapture.TacticalMarkerID_RED then if ZoneCapture.TacticalMarkerID_RED then
log(string.format("[TACTICAL] Removing old RED marker ID %d for %s", ZoneCapture.TacticalMarkerID_RED, zoneName)) log(string.format("[TACTICAL] Removing old RED marker ID %d for %s", ZoneCapture.TacticalMarkerID_RED, zoneName))
pcall(function() offsetCoord:RemoveMark(ZoneCapture.TacticalMarkerID_RED) end) removeMarker(ZoneCapture.TacticalMarkerID_RED)
ZoneCapture.TacticalMarkerID_RED = nil ZoneCapture.TacticalMarkerID_RED = nil
end end
local successRed, markerIDRed = pcall(function() local successRed, markerIDRed = pcall(function()
@ -516,16 +570,18 @@ local function OnEnterGuarded(ZoneCapture, From, Event, To)
ZoneCapture:Smoke( SMOKECOLOR.Blue ) ZoneCapture:Smoke( SMOKECOLOR.Blue )
-- Update zone visual markers to BLUE -- Update zone visual markers to BLUE
ZoneCapture:UndrawZone() ZoneCapture:UndrawZone()
ZoneCapture:DrawZone(-1, ZONE_COLORS.BLUE_CAPTURED, 0.5, ZONE_COLORS.BLUE_CAPTURED, 0.2, 2, true) local color = ZONE_COLORS.BLUE_CAPTURED
US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
else else
ZoneCapture:Smoke( SMOKECOLOR.Red ) ZoneCapture:Smoke( SMOKECOLOR.Red )
-- Update zone visual markers to RED -- Update zone visual markers to RED
ZoneCapture:UndrawZone() ZoneCapture:UndrawZone()
ZoneCapture:DrawZone(-1, ZONE_COLORS.RED_CAPTURED, 0.5, ZONE_COLORS.RED_CAPTURED, 0.2, 2, true) local color = ZONE_COLORS.RED_CAPTURED
RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
end end
-- Create/update tactical information marker -- Create/update tactical information marker
CreateTacticalInfoMarker(ZoneCapture) CreateTacticalInfoMarker(ZoneCapture)
@ -536,9 +592,10 @@ local function OnEnterEmpty(ZoneCapture)
ZoneCapture:Smoke( SMOKECOLOR.Green ) ZoneCapture:Smoke( SMOKECOLOR.Green )
-- Update zone visual markers to GREEN (neutral) -- Update zone visual markers to GREEN (neutral)
ZoneCapture:UndrawZone() ZoneCapture:UndrawZone()
ZoneCapture:DrawZone(-1, ZONE_COLORS.EMPTY, 0.5, ZONE_COLORS.EMPTY, 0.2, 2, true) local color = ZONE_COLORS.EMPTY
US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
-- Create/update tactical information marker -- Create/update tactical information marker
CreateTacticalInfoMarker(ZoneCapture) CreateTacticalInfoMarker(ZoneCapture)
end end
@ -551,14 +608,14 @@ local function OnEnterAttacked(ZoneCapture)
local color local color
if Coalition == coalition.side.BLUE then if Coalition == coalition.side.BLUE then
color = ZONE_COLORS.BLUE_ATTACKED color = ZONE_COLORS.BLUE_ATTACKED
US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION )
RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION )
else else
color = ZONE_COLORS.RED_ATTACKED color = ZONE_COLORS.RED_ATTACKED
RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION )
US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.ATTACK_MESSAGE_DURATION )
end end
ZoneCapture:DrawZone(-1, color, 0.5, color, 0.2, 2, true) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
-- Create/update tactical information marker -- Create/update tactical information marker
CreateTacticalInfoMarker(ZoneCapture) CreateTacticalInfoMarker(ZoneCapture)
end end
@ -591,13 +648,13 @@ local function CheckVictoryCondition()
"VICTORY! All capture zones have been secured by coalition forces!\n\n" .. "VICTORY! All capture zones have been secured by coalition forces!\n\n" ..
"Operation Polar Shield is complete. Outstanding work!\n" .. "Operation Polar Shield is complete. Outstanding work!\n" ..
"Mission will end in 60 seconds.", "Mission will end in 60 seconds.",
MESSAGE.Type.Information, 30 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
RU_CC:MessageTypeToCoalition( RU_CC:MessageTypeToCoalition(
"DEFEAT! All strategic positions have been lost to coalition forces.\n\n" .. "DEFEAT! All strategic positions have been lost to coalition forces.\n\n" ..
"Operation Polar Shield has failed. Mission ending in 60 seconds.", "Operation Polar Shield has failed. Mission ending in 60 seconds.",
MESSAGE.Type.Information, 30 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
-- Add victory celebration effects -- Add victory celebration effects
@ -632,13 +689,13 @@ local function CheckVictoryCondition()
"VICTORY! All strategic positions secured for the Motherland!\n\n" .. "VICTORY! All strategic positions secured for the Motherland!\n\n" ..
"NATO forces have been repelled. Outstanding work!\n" .. "NATO forces have been repelled. Outstanding work!\n" ..
"Mission will end in 60 seconds.", "Mission will end in 60 seconds.",
MESSAGE.Type.Information, 30 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
US_CC:MessageTypeToCoalition( US_CC:MessageTypeToCoalition(
"DEFEAT! All capture zones have been lost to Russian forces.\n\n" .. "DEFEAT! All capture zones have been lost to Russian forces.\n\n" ..
"Operation Polar Shield has failed. Mission ending in 60 seconds.", "Operation Polar Shield has failed. Mission ending in 60 seconds.",
MESSAGE.Type.Information, 30 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
-- Add victory celebration effects -- Add victory celebration effects
@ -673,15 +730,17 @@ local function OnEnterCaptured(ZoneCapture)
if Coalition == coalition.side.BLUE then if Coalition == coalition.side.BLUE then
-- Update zone visual markers to BLUE for captured -- Update zone visual markers to BLUE for captured
ZoneCapture:UndrawZone() ZoneCapture:UndrawZone()
ZoneCapture:DrawZone(-1, ZONE_COLORS.BLUE_CAPTURED, 0.5, ZONE_COLORS.BLUE_CAPTURED, 0.2, 2, true) local color = ZONE_COLORS.BLUE_CAPTURED
RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
else else
-- Update zone visual markers to RED for captured -- Update zone visual markers to RED for captured
ZoneCapture:UndrawZone() ZoneCapture:UndrawZone()
ZoneCapture:DrawZone(-1, ZONE_COLORS.RED_CAPTURED, 0.5, ZONE_COLORS.RED_CAPTURED, 0.2, 2, true) local color = ZONE_COLORS.RED_CAPTURED
US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) ZoneCapture:DrawZone(-1, {0, 0, 0}, 1, color, 0.2, 2, true)
RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information ) US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCapture:GetZoneName() ), MESSAGE.Type.Information, MESSAGE_CONFIG.CAPTURE_MESSAGE_DURATION )
end end
ZoneCapture:AddScore( "Captured", "Zone captured: Extra points granted.", ZONE_SETTINGS.captureScore ) ZoneCapture:AddScore( "Captured", "Zone captured: Extra points granted.", ZONE_SETTINGS.captureScore )
@ -712,17 +771,17 @@ for i, zoneCapture in ipairs(zoneCaptureObjects) do
-- Get initial coalition color for this zone -- Get initial coalition color for this zone
local initialCoalition = zoneCapture:GetCoalition() local initialCoalition = zoneCapture:GetCoalition()
local colorRGB = {0, 1, 0} -- Default green for neutral local colorRGB = ZONE_COLORS.EMPTY
if initialCoalition == coalition.side.RED then if initialCoalition == coalition.side.RED then
colorRGB = {1, 0, 0} -- Red colorRGB = ZONE_COLORS.RED_CAPTURED
elseif initialCoalition == coalition.side.BLUE then elseif initialCoalition == coalition.side.BLUE then
colorRGB = {0, 0, 1} -- Blue colorRGB = ZONE_COLORS.BLUE_CAPTURED
end end
-- Initialize zone borders with appropriate initial color -- Initialize zone borders with appropriate initial color
local drawSuccess, drawError = pcall(function() local drawSuccess, drawError = pcall(function()
zone:DrawZone(-1, colorRGB, 0.5, colorRGB, 0.2, 2, true) zone:DrawZone(-1, {0, 0, 0}, 1, colorRGB, 0.2, 2, true)
end) end)
if not drawSuccess then if not drawSuccess then
@ -834,13 +893,14 @@ local function BroadcastZoneStatus()
local fullMessage = reportMessage .. detailMessage local fullMessage = reportMessage .. detailMessage
-- Broadcast to BOTH coalitions with their specific victory progress -- Broadcast to BOTH coalitions with their specific victory progress
local blueProgressPercent = math.floor((status.blue / status.total) * 100) local totalZones = math.max(status.total, 1)
local blueProgressPercent = math.floor((status.blue / totalZones) * 100)
local blueFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", blueProgressPercent) local blueFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", blueProgressPercent)
US_CC:MessageTypeToCoalition( blueFullMessage, MESSAGE.Type.Information, 15 ) US_CC:MessageTypeToCoalition( blueFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION )
local redProgressPercent = math.floor((status.red / status.total) * 100) local redProgressPercent = math.floor((status.red / totalZones) * 100)
local redFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", redProgressPercent) local redFullMessage = fullMessage .. string.format("\n\nYour Progress to Victory: %d%%", redProgressPercent)
RU_CC:MessageTypeToCoalition( redFullMessage, MESSAGE.Type.Information, 15 ) RU_CC:MessageTypeToCoalition( redFullMessage, MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION )
log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | ")) log("[ZONE STATUS] " .. reportMessage:gsub("\n", " | "))
@ -856,13 +916,13 @@ local ZoneMonitorScheduler = SCHEDULER:New( nil, function()
US_CC:MessageTypeToCoalition( US_CC:MessageTypeToCoalition(
string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!",
status.total - status.blue), status.total - status.blue),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
RU_CC:MessageTypeToCoalition( RU_CC:MessageTypeToCoalition(
string.format("CRITICAL SITUATION! Coalition forces control %d/%d zones! We must recapture territory!", string.format("CRITICAL SITUATION! Coalition forces control %d/%d zones! We must recapture territory!",
status.blue, status.total), status.blue, status.total),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
end end
@ -871,17 +931,17 @@ local ZoneMonitorScheduler = SCHEDULER:New( nil, function()
RU_CC:MessageTypeToCoalition( RU_CC:MessageTypeToCoalition(
string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!", string.format("APPROACHING VICTORY! %d more zone(s) needed for complete success!",
status.total - status.red), status.total - status.red),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
US_CC:MessageTypeToCoalition( US_CC:MessageTypeToCoalition(
string.format("CRITICAL SITUATION! Russian forces control %d/%d zones! We must recapture territory!", string.format("CRITICAL SITUATION! Russian forces control %d/%d zones! We must recapture territory!",
status.red, status.total), status.red, status.total),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.VICTORY_MESSAGE_DURATION
) )
end end
end, {}, 10, 300 ) -- Start after 10 seconds, repeat every 300 seconds (5 minutes) end, {}, MESSAGE_CONFIG.STATUS_BROADCAST_START_DELAY, MESSAGE_CONFIG.STATUS_BROADCAST_FREQUENCY )
-- Periodic zone color verification system (every 2 minutes) -- Periodic zone color verification system (every 2 minutes)
local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function() local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
@ -897,8 +957,8 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
local zoneColor = GetZoneColor(zoneCapture) local zoneColor = GetZoneColor(zoneCapture)
-- Force redraw the zone with correct color based on CURRENT STATE -- Force redraw the zone with correct color based on CURRENT STATE
zoneCapture:UndrawZone() zoneCapture:UndrawZone()
zoneCapture:DrawZone(-1, zoneColor, 0.5, zoneColor, 0.2, 2, true) zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true)
-- Log the color assignment for debugging -- Log the color assignment for debugging
local colorName = "UNKNOWN" local colorName = "UNKNOWN"
@ -917,20 +977,35 @@ local ZoneColorVerificationScheduler = SCHEDULER:New( nil, function()
end end
end end
end, {}, 60, 120 ) -- Start after 60 seconds, repeat every 120 seconds (2 minutes) end, {}, MESSAGE_CONFIG.COLOR_VERIFICATION_START_DELAY, MESSAGE_CONFIG.COLOR_VERIFICATION_FREQUENCY )
-- Periodic tactical marker update system (every 1 minute) -- Periodic tactical marker update system with change detection
local __lastForceCountsByZone = {}
local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function() local TacticalMarkerUpdateScheduler = SCHEDULER:New( nil, function()
log("[TACTICAL] Running periodic tactical marker update...") log("[TACTICAL] Running periodic tactical marker update (change-detected)...")
-- Update tactical markers for all zones
for i, zoneCapture in ipairs(zoneCaptureObjects) do for i, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture then if zoneCapture then
CreateTacticalInfoMarker(zoneCapture) local zoneName = zoneCapture.GetZoneName and zoneCapture:GetZoneName() or (zoneNames[i] or ("Zone " .. i))
local counts = GetZoneForceStrengths(zoneCapture)
local last = __lastForceCountsByZone[zoneName]
local changed = (not last)
or (last.red ~= counts.red)
or (last.blue ~= counts.blue)
or (last.neutral ~= counts.neutral)
if changed then
__lastForceCountsByZone[zoneName] = {
red = counts.red,
blue = counts.blue,
neutral = counts.neutral
}
CreateTacticalInfoMarker(zoneCapture)
end
end end
end end
end, {}, 30, 60 ) -- Start after 30 seconds, repeat every 60 seconds (1 minute) end, {}, MESSAGE_CONFIG.TACTICAL_UPDATE_START_DELAY, MESSAGE_CONFIG.TACTICAL_UPDATE_FREQUENCY )
-- Function to refresh all zone colors based on current ownership -- Function to refresh all zone colors based on current ownership
local function RefreshAllZoneColors() local function RefreshAllZoneColors()
@ -945,11 +1020,11 @@ local function RefreshAllZoneColors()
-- Get color for current state/ownership -- Get color for current state/ownership
local zoneColor = GetZoneColor(zoneCapture) local zoneColor = GetZoneColor(zoneCapture)
-- Clear existing drawings -- Clear existing drawings
zoneCapture:UndrawZone() zoneCapture:UndrawZone()
-- Redraw with correct color -- Redraw with correct color
zoneCapture:DrawZone(-1, zoneColor, 0.5, zoneColor, 0.2, 2, true) zoneCapture:DrawZone(-1, {0, 0, 0}, 1, zoneColor, 0.2, 2, true)
-- Log the color assignment for debugging -- Log the color assignment for debugging
local colorName = "UNKNOWN" local colorName = "UNKNOWN"
@ -969,8 +1044,8 @@ local function RefreshAllZoneColors()
end end
-- Notify BOTH coalitions -- Notify BOTH coalitions
US_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, 5) US_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION)
RU_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, 5) RU_CC:MessageTypeToCoalition("Zone visual markers have been refreshed!", MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION)
end end
-- Manual zone status commands for players (F10 radio menu) - BOTH COALITIONS -- Manual zone status commands for players (F10 radio menu) - BOTH COALITIONS
@ -982,7 +1057,8 @@ local function SetupZoneStatusCommands()
MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Check Victory Progress", USMenu, function() MENU_COALITION_COMMAND:New( coalition.side.BLUE, "Check Victory Progress", USMenu, function()
local status = GetZoneOwnershipStatus() local status = GetZoneOwnershipStatus()
local progressPercent = math.floor((status.blue / status.total) * 100) local totalZones = math.max(status.total, 1)
local progressPercent = math.floor((status.blue / totalZones) * 100)
US_CC:MessageTypeToCoalition( US_CC:MessageTypeToCoalition(
string.format( string.format(
@ -998,7 +1074,7 @@ local function SetupZoneStatusCommands()
progressPercent >= 50 and "GOOD PROGRESS!" or progressPercent >= 50 and "GOOD PROGRESS!" or
"KEEP FIGHTING!" "KEEP FIGHTING!"
), ),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION
) )
end ) end )
@ -1013,7 +1089,8 @@ local function SetupZoneStatusCommands()
MENU_COALITION_COMMAND:New( coalition.side.RED, "Check Victory Progress", RUMenu, function() MENU_COALITION_COMMAND:New( coalition.side.RED, "Check Victory Progress", RUMenu, function()
local status = GetZoneOwnershipStatus() local status = GetZoneOwnershipStatus()
local progressPercent = math.floor((status.red / status.total) * 100) local totalZones = math.max(status.total, 1)
local progressPercent = math.floor((status.red / totalZones) * 100)
RU_CC:MessageTypeToCoalition( RU_CC:MessageTypeToCoalition(
string.format( string.format(
@ -1029,7 +1106,7 @@ local function SetupZoneStatusCommands()
progressPercent >= 50 and "GOOD PROGRESS!" or progressPercent >= 50 and "GOOD PROGRESS!" or
"KEEP FIGHTING!" "KEEP FIGHTING!"
), ),
MESSAGE.Type.Information, 10 MESSAGE.Type.Information, MESSAGE_CONFIG.STATUS_MESSAGE_DURATION
) )
end ) end )

View File

@ -5,7 +5,8 @@
DESCRIPTION: DESCRIPTION:
This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them. It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system. This script monitors RED and BLUE squadrons for low aircraft counts and automatically dispatches CARGO aircraft from a list of supply airfields to replenish them.
It spawns cargo aircraft and routes them to destination airbases. Delivery detection and replenishment is handled by the main TADC system.
CONFIGURATION: CONFIGURATION:
- Update static templates and airfield lists as needed for your mission. - Update static templates and airfield lists as needed for your mission.
@ -19,6 +20,13 @@ REQUIRES:
]] ]]
-- Single-run guard to prevent duplicate dispatcher loops if script is reloaded
if _G.__TDAC_DISPATCHER_RUNNING then
env.info("[TDAC] CargoDispatcher already running; aborting duplicate load")
return
end
_G.__TDAC_DISPATCHER_RUNNING = true
--[[ --[[
GLOBAL STATE AND CONFIGURATION GLOBAL STATE AND CONFIGURATION
-------------------------------------------------------------------------- --------------------------------------------------------------------------
@ -264,9 +272,9 @@ local function cleanupCargoMissions()
for _, coalitionKey in ipairs({"red", "blue"}) do for _, coalitionKey in ipairs({"red", "blue"}) do
for i = #cargoMissions[coalitionKey], 1, -1 do for i = #cargoMissions[coalitionKey], 1, -1 do
local m = cargoMissions[coalitionKey][i] local m = cargoMissions[coalitionKey][i]
if m.status == "failed" then if m.status == "failed" or m.status == "completed" then
if not (m.group and m.group:IsAlive()) then if not (m.group and m.group:IsAlive()) then
log("Cleaning up failed cargo mission: " .. (m.group and m.group:GetName() or "nil group") .. " status: failed") log("Cleaning up " .. m.status .. " cargo mission: " .. (m.group and m.group:GetName() or "nil group"))
table.remove(cargoMissions[coalitionKey], i) table.remove(cargoMissions[coalitionKey], i)
end end
end end
@ -444,12 +452,21 @@ local function dispatchCargo(squadron, coalitionKey)
rat:SetDeparture(origin) rat:SetDeparture(origin)
rat:SetDestination(destination) rat:SetDestination(destination)
rat:NoRespawn() rat:NoRespawn()
rat:InitUnControlled(false) -- ensure template-level 'Uncontrolled' flag does not leave transports parked rat:InitUnControlled(false) -- force departing transports to spawn in a controllable state
rat:InitLateActivated(false) rat:InitLateActivated(false)
rat:SetSpawnLimit(1) rat:SetSpawnLimit(1)
rat:SetSpawnDelay(1) rat:SetSpawnDelay(1)
-- Ensure RAT takes off immediately from the runway (hot start) instead of staying parked
if rat.SetTakeoffHot then rat:SetTakeoffHot() end -- CRITICAL: Force takeoff from runway to prevent aircraft getting stuck at parking
-- SetTakeoffRunway() ensures aircraft spawn directly on runway and take off immediately
if rat.SetTakeoffRunway then
rat:SetTakeoffRunway()
log("DEBUG: Configured cargo to take off from runway at " .. origin, true)
else
log("WARNING: SetTakeoffRunway() not available - falling back to SetTakeoffHot()", true)
if rat.SetTakeoffHot then rat:SetTakeoffHot() end
end
-- Ensure RAT will look for parking and not despawn the group immediately on landing. -- Ensure RAT will look for parking and not despawn the group immediately on landing.
-- This makes the group taxi to parking and come to a stop so other scripts (e.g. Load2nd) -- This makes the group taxi to parking and come to a stop so other scripts (e.g. Load2nd)
-- that detect parked/stopped cargo aircraft can register the delivery. -- that detect parked/stopped cargo aircraft can register the delivery.

View File

@ -140,15 +140,15 @@ local TADC_SETTINGS = {
-- Timing settings (applies to both coalitions) -- Timing settings (applies to both coalitions)
checkInterval = 30, -- How often to scan for threats (seconds) checkInterval = 30, -- How often to scan for threats (seconds)
monitorInterval = 30, -- How often to check interceptor status (seconds) monitorInterval = 30, -- How often to check interceptor status (seconds)
statusReportInterval = 120, -- How often to report airbase status (seconds) statusReportInterval = 1805, -- How often to report airbase status (seconds)
squadronSummaryInterval = 600, -- How often to broadcast squadron summary (seconds) squadronSummaryInterval = 1800, -- How often to broadcast squadron summary (seconds)
cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds) cargoCheckInterval = 15, -- How often to check for cargo deliveries (seconds)
-- RED Coalition Settings -- RED Coalition Settings
red = { red = {
maxActiveCAP = 24, -- Maximum RED fighters airborne at once maxActiveCAP = 24, -- Maximum RED fighters airborne at once
squadronCooldown = 600, -- RED cooldown after squadron launch (seconds) squadronCooldown = 600, -- RED cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- RED interceptors per threat aircraft interceptRatio = 1.2, -- RED interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- RED aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- RED force cleanup time (seconds) emergencyCleanupTime = 7200, -- RED force cleanup time (seconds)
rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds) rtbFlightBuffer = 300, -- RED extra landing time before cleanup (seconds)
@ -158,7 +158,7 @@ local TADC_SETTINGS = {
blue = { blue = {
maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once maxActiveCAP = 24, -- Maximum BLUE fighters airborne at once
squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds) squadronCooldown = 600, -- BLUE cooldown after squadron launch (seconds)
interceptRatio = 0.8, -- BLUE interceptors per threat aircraft interceptRatio = 1.2, -- BLUE interceptors per threat aircraft
cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery cargoReplenishmentAmount = 4, -- BLUE aircraft added per cargo delivery
emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds) emergencyCleanupTime = 7200, -- BLUE force cleanup time (seconds)
rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds) rtbFlightBuffer = 300, -- BLUE extra landing time before cleanup (seconds)
@ -276,6 +276,105 @@ local airbaseHealthStatus = {
blue = {} 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 -- Logging function
local function log(message, detailed) local function log(message, detailed)
if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then if not detailed or ADVANCED_SETTINGS.enableDetailedLogging then
@ -709,6 +808,8 @@ local function processCargoDelivery(cargoGroup, squadron, coalitionSide, coaliti
MESSAGE:New(msg, 10):ToCoalition(coalitionSide) MESSAGE:New(msg, 10):ToCoalition(coalitionSide)
USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide) USERSOUND:New("Cargo_Delivered.ogg"):ToCoalition(coalitionSide)
end end
finalizeCargoMission(cargoGroup, squadron, coalitionKey)
end end
-- Event handler for cargo aircraft landing (backup for actual landings) -- Event handler for cargo aircraft landing (backup for actual landings)
@ -1271,10 +1372,9 @@ local function monitorStuckAircraft()
-- Mark airbase as having stuck aircraft -- Mark airbase as having stuck aircraft
airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft" airbaseHealthStatus[coalitionKey][trackingData.airbase] = "stuck-aircraft"
-- Remove the stuck aircraft -- Remove the stuck aircraft and clear tracking
trackingData.group:Destroy() pcall(function() trackingData.group:Destroy() end)
activeInterceptors[coalitionKey][aircraftName] = nil cleanupInterceptorEntry(aircraftName, coalitionKey)
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
-- Reassign squadron to alternative airbase -- Reassign squadron to alternative airbase
reassignSquadronToAlternativeAirbase(trackingData.squadron, coalitionKey) reassignSquadronToAlternativeAirbase(trackingData.squadron, coalitionKey)
@ -1342,9 +1442,14 @@ local function sendInterceptorHome(interceptor, coalitionSide)
SCHEDULER:New(nil, function() SCHEDULER:New(nil, function()
local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue" local coalitionKey = (coalitionSide == coalition.side.RED) and "red" or "blue"
if activeInterceptors[coalitionKey][interceptor:GetName()] then local name = nil
activeInterceptors[coalitionKey][interceptor:GetName()] = nil if interceptor and interceptor.GetName then
log("Cleaned up " .. coalitionName .. " " .. interceptor:GetName() .. " after RTB", true) 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
end, {}, flightTime) end, {}, flightTime)
else else
@ -1616,6 +1721,7 @@ local function launchInterceptor(threatGroup, coalitionSide)
log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName) log("ERROR: Failed to create SPAWN object for " .. coalitionName .. " " .. squadron.templateName)
return return
end end
spawn:InitCleanUp(900)
local interceptors = {} local interceptors = {}
@ -1674,11 +1780,14 @@ local function launchInterceptor(threatGroup, coalitionSide)
-- Emergency cleanup (safety net) -- Emergency cleanup (safety net)
SCHEDULER:New(nil, function() SCHEDULER:New(nil, function()
if activeInterceptors[coalitionKey][interceptor:GetName()] then local name = nil
log("Emergency cleanup of " .. coalitionName .. " " .. interceptor:GetName() .. " (should have RTB'd)") if interceptor and interceptor.GetName then
activeInterceptors[coalitionKey][interceptor:GetName()] = nil local ok, value = pcall(function() return interceptor:GetName() end)
-- Also clean up spawn tracking if ok then name = value end
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = nil end
if name and activeInterceptors[coalitionKey][name] then
log("Emergency cleanup of " .. coalitionName .. " " .. name .. " (should have RTB'd)")
destroyInterceptorGroup(interceptor, coalitionKey, 0)
end end
end, {}, coalitionSettings.emergencyCleanupTime) end, {}, coalitionSettings.emergencyCleanupTime)
end end
@ -2182,20 +2291,29 @@ end
initializeSystem() initializeSystem()
-- Add F10 menu command for squadron summary -- Add F10 menu command for squadron summary
local menuRoot = MENU_MISSION:New("TADC Utilities") -- Use MenuManager to create coalition-specific menus (not mission-wide)
local menuRootBlue, menuRootRed
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Squadron Resource Summary", menuRoot, function() 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) local summary = getSquadronResourceSummary(coalition.side.RED)
MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Squadron Resource Summary", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Squadron Resource Summary", menuRootBlue, function()
local summary = getSquadronResourceSummary(coalition.side.BLUE) local summary = getSquadronResourceSummary(coalition.side.BLUE)
MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE)
end) end)
-- 1. Show Airbase Status Report -- 1. Show Airbase Status Report
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", menuRootRed, function()
local report = "=== RED Airbase Status ===\n" local report = "=== RED Airbase Status ===\n"
for _, squadron in pairs(RED_SQUADRON_CONFIG) do for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED) local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.RED)
@ -2212,7 +2330,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Airbase Status Report", men
MESSAGE:New(report, 20):ToCoalition(coalition.side.RED) MESSAGE:New(report, 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", menuRootBlue, function()
local report = "=== BLUE Airbase Status ===\n" local report = "=== BLUE Airbase Status ===\n"
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE) local usable, status = isAirbaseUsable(squadron.airbaseName, coalition.side.BLUE)
@ -2230,7 +2348,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Status Report", me
end) end)
-- 2. Show Active Interceptors -- 2. Show Active Interceptors
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuRootRed, function()
local lines = {"Active RED Interceptors:"} local lines = {"Active RED Interceptors:"}
for name, data in pairs(activeInterceptors.red) do for name, data in pairs(activeInterceptors.red) do
if data and data.group and data.group:IsAlive() then if data and data.group and data.group:IsAlive() then
@ -2240,7 +2358,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Active Interceptors", menuR
MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menuRootBlue, function()
local lines = {"Active BLUE Interceptors:"} local lines = {"Active BLUE Interceptors:"}
for name, data in pairs(activeInterceptors.blue) do for name, data in pairs(activeInterceptors.blue) do
if data and data.group and data.group:IsAlive() then if data and data.group and data.group:IsAlive() then
@ -2251,7 +2369,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Active Interceptors", menu
end) end)
-- 3. Show Threat Summary -- 3. Show Threat Summary
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRootRed, function()
local lines = {"Detected BLUE Threats:"} local lines = {"Detected BLUE Threats:"}
if cachedSets.blueAircraft then if cachedSets.blueAircraft then
cachedSets.blueAircraft:ForEach(function(group) cachedSets.blueAircraft:ForEach(function(group)
@ -2263,7 +2381,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Threat Summary", menuRoot,
MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRootBlue, function()
local lines = {"Detected RED Threats:"} local lines = {"Detected RED Threats:"}
if cachedSets.redAircraft then if cachedSets.redAircraft then
cachedSets.redAircraft:ForEach(function(group) cachedSets.redAircraft:ForEach(function(group)
@ -2276,18 +2394,18 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Threat Summary", menuRoot,
end) end)
-- 4. Request Immediate Squadron Summary Broadcast -- 4. Request Immediate Squadron Summary Broadcast
MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Broadcast Squadron Summary Now", menuRootRed, function()
local summary = getSquadronResourceSummary(coalition.side.RED) local summary = getSquadronResourceSummary(coalition.side.RED)
MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED) MESSAGE:New(summary, 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Broadcast Squadron Summary Now", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Broadcast Squadron Summary Now", menuRootBlue, function()
local summary = getSquadronResourceSummary(coalition.side.BLUE) local summary = getSquadronResourceSummary(coalition.side.BLUE)
MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE) MESSAGE:New(summary, 20):ToCoalition(coalition.side.BLUE)
end) end)
-- 5. Show Cargo Delivery Log -- 5. Show Cargo Delivery Log
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRootRed, function()
local lines = {"Recent RED Cargo Deliveries:"} local lines = {"Recent RED Cargo Deliveries:"}
if _G.processedDeliveries then if _G.processedDeliveries then
for key, timestamp in pairs(_G.processedDeliveries) do for key, timestamp in pairs(_G.processedDeliveries) do
@ -2299,7 +2417,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Cargo Delivery Log", menuRo
MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuRootBlue, function()
local lines = {"Recent BLUE Cargo Deliveries:"} local lines = {"Recent BLUE Cargo Deliveries:"}
if _G.processedDeliveries then if _G.processedDeliveries then
for key, timestamp in pairs(_G.processedDeliveries) do for key, timestamp in pairs(_G.processedDeliveries) do
@ -2312,7 +2430,7 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Cargo Delivery Log", menuR
end) end)
-- 6. Show Zone Coverage Map -- 6. Show Zone Coverage Map
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRootRed, function()
local lines = {"RED Zone Coverage:"} local lines = {"RED Zone Coverage:"}
for _, squadron in pairs(RED_SQUADRON_CONFIG) do for _, squadron in pairs(RED_SQUADRON_CONFIG) do
local zones = {} local zones = {}
@ -2324,7 +2442,7 @@ MENU_COALITION_COMMAND:New(coalition.side.RED, "Show Zone Coverage Map", menuRoo
MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED) MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.RED)
end) end)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRootBlue, function()
local lines = {"BLUE Zone Coverage:"} local lines = {"BLUE Zone Coverage:"}
for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do for _, squadron in pairs(BLUE_SQUADRON_CONFIG) do
local zones = {} local zones = {}
@ -2336,40 +2454,71 @@ MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Zone Coverage Map", menuRo
MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE) MESSAGE:New(table.concat(lines, "\n"), 20):ToCoalition(coalition.side.BLUE)
end) end)
-- 7. Request Emergency Cleanup (admin/global) -- 7. Admin/Debug Commands - Create submenus under each coalition's TADC Utilities
MENU_MISSION_COMMAND:New("Emergency Cleanup Interceptors", menuRoot, function() 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 local cleaned = 0
for _, interceptors in pairs(activeInterceptors.red) do for name, interceptors in pairs(activeInterceptors.red) do
if interceptors and interceptors.group and not interceptors.group:IsAlive() then if interceptors and interceptors.group and not interceptors.group:IsAlive() then
interceptors.group = nil cleanupInterceptorEntry(name, "red")
cleaned = cleaned + 1 cleaned = cleaned + 1
end end
end end
for _, interceptors in pairs(activeInterceptors.blue) do for name, interceptors in pairs(activeInterceptors.blue) do
if interceptors and interceptors.group and not interceptors.group:IsAlive() then if interceptors and interceptors.group and not interceptors.group:IsAlive() then
interceptors.group = nil cleanupInterceptorEntry(name, "blue")
cleaned = cleaned + 1 cleaned = cleaned + 1
end end
end end
MESSAGE:New("Cleaned up " .. cleaned .. " dead interceptor groups.", 20):ToAll() 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) end)
-- 9. Show System Uptime/Status -- 9. Show System Uptime/Status
local systemStartTime = timer.getTime() local systemStartTime = timer.getTime()
MENU_MISSION_COMMAND:New("Show TADC System Status", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show TADC System Status", menuAdminBlue, function()
local uptime = math.floor((timer.getTime() - systemStartTime) / 60) 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) 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):ToAll() 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) end)
-- 10. Check for Stuck Aircraft (manual trigger) -- 10. Check for Stuck Aircraft (manual trigger)
MENU_MISSION_COMMAND:New("Check for Stuck Aircraft", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Check for Stuck Aircraft", menuAdminBlue, function()
monitorStuckAircraft() monitorStuckAircraft()
MESSAGE:New("Stuck aircraft check completed", 10):ToAll() 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) end)
-- 11. Show Airbase Health Status -- 11. Show Airbase Health Status
MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function() MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show Airbase Health Status", menuAdminBlue, function()
local lines = {"Airbase Health Status:"} local lines = {"Airbase Health Status:"}
for _, coalitionKey in ipairs({"red", "blue"}) do for _, coalitionKey in ipairs({"red", "blue"}) do
local coalitionName = (coalitionKey == "red") and "RED" or "BLUE" local coalitionName = (coalitionKey == "red") and "RED" or "BLUE"
@ -2378,7 +2527,19 @@ MENU_MISSION_COMMAND:New("Show Airbase Health Status", menuRoot, function()
table.insert(lines, " " .. airbaseName .. ": " .. status) table.insert(lines, " " .. airbaseName .. ": " .. status)
end end
end end
MESSAGE:New(table.concat(lines, "\n"), 20):ToAll() 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) end)
-- Initialize airbase health status for all configured airbases -- Initialize airbase health status for all configured airbases

Binary file not shown.