Updating Insurgent Sandstorm.

This commit is contained in:
iTracerFacer 2025-11-16 10:54:20 -06:00
parent b79a8f9b0a
commit 7dfe72afc6
30 changed files with 5467 additions and 0 deletions

View File

@ -0,0 +1,122 @@
-- init_mission_dual_coalition.lua
-- Use in Mission Editor with DO SCRIPT FILE load order:
-- 1) Moose.lua
-- 2) Moose_CTLD_Pure/Moose_CTLD.lua
-- 3) Moose_CTLD_Pure/catalogs/CrateCatalog_CTLD_Extract.lua -- optional but recommended catalog with BLUE+RED items (_CTLD_EXTRACTED_CATALOG)
-- 4) Moose_CTLD_Pure/Moose_CTLD_FAC.lua -- optional FAC/RECCE support
-- 5) DO SCRIPT: dofile on this file OR paste the block below directly
--
-- IMPORTANT: F10 menu ordering depends on script execution order!
-- Load this initialization script BEFORE other mission scripts (TADC, CVN, Intel, etc.)
-- to ensure CTLD and FAC appear earlier in the F10 menu.
--
-- Zones you should create in the Mission Editor (as trigger zones):
-- BLUE: PICKUP_BLUE_MAIN, DROP_BLUE_1, FOB_BLUE_A
-- RED : PICKUP_RED_MAIN, DROP_RED_1, FOB_RED_A
-- Adjust names below if you use different zone names.
-- Create CTLD instances only if Moose and CTLD are available
if _MOOSE_CTLD and _G.BASE then
local blueCfg = {
CoalitionSide = coalition.side.BLUE,
PickupZoneSmokeColor = trigger.smokeColor.Green,
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
},
-- Optional: drive zone activation from mission flags (preferred: set per-zone below via flag/activeWhen)
MapDraw = {
Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing
},
Zones = {
PickupZones = { { name = 'S1', flag = 9001, activeWhen = 0 },
{ name = "S2", flag = 9004, activeWhen = 0 },
{ name = "S3", flag = 9005, activeWhen = 0 } },
--DropZones = { { name = 'BRAVO', flag = 9002, activeWhen = 0 } },
--FOBZones = { { name = 'CHARLIE', flag = 9003, activeWhen = 0 } },
--MASHZones = { { name = 'MASH Alpha', freq = '251.0 AM', radius = 500, flag = 9010, activeWhen = 0 } },
--SalvageDropZones = { { name = 'S1', flag = 9020, radius = 500, activeWhen = 0 } },
},
BuildRequiresGroundCrates = true,
}
env.info('[DEBUG] blueCfg.Zones.MASHZones count: ' .. tostring(blueCfg.Zones and blueCfg.Zones.MASHZones and #blueCfg.Zones.MASHZones or 'NIL'))
if blueCfg.Zones and blueCfg.Zones.MASHZones and blueCfg.Zones.MASHZones[1] then
env.info('[DEBUG] blueCfg.Zones.MASHZones[1].name: ' .. tostring(blueCfg.Zones.MASHZones[1].name))
end
ctldBlue = _MOOSE_CTLD:New(blueCfg)
local redCfg = {
CoalitionSide = coalition.side.RED,
PickupZoneSmokeColor = trigger.smokeColor.Green,
AllowedAircraft = { -- transport-capable unit type names (case-sensitive as in DCS DB)
'UH-1H','Mi-8MTV2','Mi-24P','SA342M','SA342L','SA342Minigun','UH-60L','CH-47Fbl1','CH-47F','Mi-17','GazelleAI'
},
-- Optional: drive zone activation for RED via per-zone flag/activeWhen
MapDraw = {
Enabled = true,
DrawMASHZones = true, -- Enable MASH zone drawing
},
Zones = {
PickupZones = { { name = 'ReadLoadZone1', flag = 9101, activeWhen = 0 },
{ name = "ReadLoadZone2", flag = 9104, activeWhen = 0 },
{ name = "ReadLoadZone3", flag = 9105, activeWhen = 0 } },
--DropZones = { { name = 'ECHO', flag = 9102, activeWhen = 0 } },
--FOBZones = { { name = 'FOXTROT', flag = 9103, activeWhen = 0 } },
--MASHZones = { { name = 'MASH Bravo', freq = '252.0 AM', radius = 500, flag = 9111, activeWhen = 0 } },
--SalvageDropZones = { { name = 'S2', flag = 9020, radius = 500, activeWhen = 0 } },
},
BuildRequiresGroundCrates = true,
}
env.info('[DEBUG] redCfg.Zones.MASHZones count: ' .. tostring(redCfg.Zones and redCfg.Zones.MASHZones and #redCfg.Zones.MASHZones or 'NIL'))
if redCfg.Zones and redCfg.Zones.MASHZones and redCfg.Zones.MASHZones[1] then
env.info('[DEBUG] redCfg.Zones.MASHZones[1].name: ' .. tostring(redCfg.Zones.MASHZones[1].name))
end
ctldRed = _MOOSE_CTLD:New(redCfg)
-- Merge catalog into both CTLD instances if catalog was loaded
env.info('[init_mission_dual_coalition] Checking for catalog: '..((_CTLD_EXTRACTED_CATALOG and 'FOUND') or 'NOT FOUND'))
if _CTLD_EXTRACTED_CATALOG then
local count = 0
for k,v in pairs(_CTLD_EXTRACTED_CATALOG) do count = count + 1 end
env.info('[init_mission_dual_coalition] Catalog has '..tostring(count)..' entries')
env.info('[init_mission_dual_coalition] Merging catalog into CTLD instances')
ctldBlue:MergeCatalog(_CTLD_EXTRACTED_CATALOG)
ctldRed:MergeCatalog(_CTLD_EXTRACTED_CATALOG)
env.info('[init_mission_dual_coalition] Catalog merged successfully')
-- Verify merge
local blueCount = 0
for k,v in pairs(ctldBlue.Config.CrateCatalog) do blueCount = blueCount + 1 end
env.info('[init_mission_dual_coalition] BLUE catalog now has '..tostring(blueCount)..' entries')
else
env.info('[init_mission_dual_coalition] WARNING: _CTLD_EXTRACTED_CATALOG not found - catalog not loaded!')
env.info('[init_mission_dual_coalition] Available globals: '..((_G._CTLD_EXTRACTED_CATALOG and 'in _G') or 'not in _G'))
end
else
env.info('[init_mission_dual_coalition] Moose or CTLD missing; skipping CTLD init')
end
-- Optional: FAC/RECCE for both sides (requires Moose_CTLD_FAC.lua)
if _MOOSE_CTLD_FAC and _G.BASE and ctldBlue and ctldRed then
facBlue = _MOOSE_CTLD_FAC:New(ctldBlue, {
CoalitionSide = coalition.side.BLUE,
Arty = { Enabled = false },
})
-- facBlue:AddRecceZone({ name = 'RECCE_BLUE_1' })
facBlue:Run()
facRed = _MOOSE_CTLD_FAC:New(ctldRed, {
CoalitionSide = coalition.side.RED,
Arty = { Enabled = false },
})
-- facRed:AddRecceZone({ name = 'RECCE_RED_1' })
facRed:Run()
else
env.info('[init_mission_dual_coalition] FAC not initialized (missing Moose/CTLD/FAC or CTLD not created)')
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,199 @@
local ENABLE_SAMS = true -- used for testing purposes. Set to true to enable SAMs, false to disable.
local TAC_DISPLAY = false -- Set to false to disable Tacview display for AI flights (default = false)
-- How many red/blue aircraft are in the air by default.
local RedA2ADefaultOverhead = 1.5
local RedDefaultCAP = 1
local BlueA2ADefaultOverhead = 1.5
local BlueDefaultCAP = 1
-- Create the main mission menu.
missionMenu = MENU_MISSION:New("Mission Menu")
--Build Command Center and Mission for Blue
US_CC = COMMANDCENTER:New( GROUP:FindByName( "BLUEHQ" ), "USA HQ" )
US_Mission = MISSION:New( US_CC, "Insurgent Sandstorm", "Primary", "Clear the front lines of enemy activity.", coalition.side.BLUE)
US_Score = SCORING:New( "Insurgent Sandstorm - Blue" )
US_Mission:AddScoring( US_Score )
US_Mission:Start()
US_Score:SetMessagesHit(false)
US_Score:SetMessagesDestroy(false)
US_Score:SetMessagesScore(false)
--Build Command Center and Mission Red
RU_CC = COMMANDCENTER:New( GROUP:FindByName( "REDHQ" ), "Russia HQ" )
RU_Mission = MISSION:New (RU_CC, "Insurgent Sandstorm", "Primary", "Destroy U.S. and NATO forces.", coalition.side.RED)
RU_Score = SCORING:New("Insurgent Sandstorm - Red")
RU_Mission:AddScoring( RU_Score)
RU_Mission:Start()
RU_Score:SetMessagesHit(false)
RU_Score:SetMessagesDestroy(false)
RU_Score:SetMessagesScore(false)
------------------------------------------------------------------------------------------------------------------------------------------------------
-- Setup SAM Systems
------------------------------------------------------------------------------------------------------------------------------------------------------
local RED_AA_ZONES = {
ZONE:New("RED-AA-1"),
ZONE:New("RED-AA-2"),
ZONE:New("RED-AA-3"),
ZONE:New("RED-AA-4"),
ZONE:New("RED-AA-5"),
ZONE:New("RED-AA-6"),
ZONE:New("RED-AA-7"),
ZONE:New("RED-AA-8"),
ZONE:New("RED-AA-9"),
ZONE:New("RED-AA-10"),
ZONE:New("RED-AA-11"),
ZONE:New("RED-AA-12"),
ZONE:New("RED-AA-13"),
ZONE:New("RED-AA-14"),
ZONE:New("RED-AA-15"),
ZONE:New("RED-AA-16"),
ZONE:New("RED-AA-17"),
ZONE:New("RED-AA-18"),
ZONE:New("RED-AA-19"),
ZONE:New("RED-AA-20"),
ZONE:New("RED-AA-21"),
ZONE:New("RED-AA-22"),
ZONE:New("RED-AA-23"),
ZONE:New("RED-AA-24"),
ZONE:New("RED-AA-25"),
ZONE:New("RED-AA-26"),
ZONE:New("RED-AA-27"),
ZONE:New("RED-AA-28"),
ZONE:New("RED-AA-29"),
ZONE:New("RED-AA-30"),
ZONE:New("RED-AA-31"),
ZONE:New("RED-AA-32"),
ZONE:New("RED-AA-33"),
ZONE:New("RED-AA-34"),
ZONE:New("RED-AA-35"),
ZONE:New("RED-AA-36"),
ZONE:New("RED-AA-37"),
ZONE:New("RED-AA-38"),
ZONE:New("RED-AA-39"),
ZONE:New("RED-AA-40")
}
-- Schedule RED AA spawns using the calculated frequencies
-- Must allow enough room for an entire group to spawn. If the group only has 1 unit and you put 5, 5 will spawn,
-- but if the group has 5 units, and you put 5, only 1 will spawn..if you only put 4, it will never spawn.
-- If you put 10, 2 of them will spawn, etc etc.
if ENABLE_SAMS then
RED_SA08 = SPAWN:New("RED EWR SA08")
:InitRandomizeZones(RED_AA_ZONES)
:InitLimit(8, 8)
:SpawnScheduled(1800, 0.5)
-- There are 18 units in this group. Need space for each one in the numbers. So if I want 3 SA10s i'm just rounding up to 60.
RED_SA10 = SPAWN:New("RED EWR AA-SA10-1")
:InitRandomizeZones(RED_AA_ZONES)
:InitLimit(60, 60)
:SpawnScheduled(1800, 0.5)
-- There are 12 units in this group. Need space for each one in the numbers. So if I want 4 SA11s i'm just rounding up to 48
RED_SA11 = SPAWN:New("RED EWR AA SA112-1")
:InitRandomizeZones(RED_AA_ZONES)
:InitLimit(36, 36)
:SpawnScheduled(1800, 0.5)
-- There are 11 units in this group. Need space for each one in the numbers. So if I want 4 SA11s i'm just rounding up to 44
RED_SA06 = SPAWN:New("RED EWR SA6")
:InitRandomizeZones(RED_AA_ZONES)
:InitLimit(33, 33)
:SpawnScheduled(1800, 0.5)
RED_SA02 = SPAWN:New("RED EWR SA2")
:InitRandomizeZones(RED_AA_ZONES)
:InitLimit(60, 60)
:SpawnScheduled(1800, 0.5)
end
------------------------------------------------------------------------------------------------------------------------------------------------------
-- Setup Air Dispatchers for RED and BLUE
------------------------------------------------------------------------------------------------------------------------------------------------------
BLUEBorderZone = ZONE_POLYGON:New( "BLUE BORDER", GROUP:FindByName( "BLUE BORDER" ) )
BLUEA2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "BLUE EWR" }, { "FIGHTER SWEEP BLUE" }, 'BLUE BORDER', 'BLUE BORDER', BlueDefaultCAP, 10000, 50000, 75000, 100)
BLUEA2ADispatcher:SetDefaultLandingAtRunway()
BLUEA2ADispatcher:SetDefaultTakeoffInAir()
BLUEA2ADispatcher:SetTacticalDisplay(TAC_DISPLAY)
BLUEA2ADispatcher:SetDefaultFuelThreshold( 0.20 )
BLUEA2ADispatcher:SetRefreshTimeInterval( 300 )
BLUEA2ADispatcher:SetDefaultOverhead(BlueA2ADefaultOverhead)
BLUEA2ADispatcher:SetDisengageRadius( 100000 )
BLUEA2ADispatcher:SetEngageRadius( 50000 )
BLUEA2ADispatcher:SetGciRadius( 75000 )
CCCPBorderZone = ZONE_POLYGON:New( "RED BORDER", GROUP:FindByName( "RED BORDER" ) )
RedA2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "RED EWR" }, { "FIGHTER SWEEP RED" }, "RED BORDER", "RED BORDER", RedDefaultCAP, 10000, 50000, 75000, 100)
RedA2ADispatcher:SetDefaultLandingAtRunway()
RedA2ADispatcher:SetDefaultTakeoffInAir()
RedA2ADispatcher:SetTacticalDisplay(TAC_DISPLAY)
RedA2ADispatcher:SetDefaultFuelThreshold( 0.20 )
RedA2ADispatcher:SetRefreshTimeInterval( 300 )
RedA2ADispatcher:SetDefaultOverhead(RedA2ADefaultOverhead)
RedA2ADispatcher:SetDisengageRadius( 100000 )
RedA2ADispatcher:SetEngageRadius( 50000 )
RedA2ADispatcher:SetGciRadius( 75000 )
DwyerBorderZone = ZONE_POLYGON:New( "DwyerBorderZone", GROUP:FindByName( "DwyerBorderZone" ) )
DwyerDispatcher = AI_A2A_GCICAP:NewWithBorder( { "RED EWR" }, { "DwyerBorderCAP" }, "DwyerBorderZone", "DwyerBorderZone", RedDefaultCAP, 10000, 50000, 75000, 100)
DwyerDispatcher:SetDefaultLandingAtRunway()
DwyerDispatcher:SetDefaultTakeoffInAir()
DwyerDispatcher:SetBorderZone( DwyerBorderZone )
DwyerDispatcher:SetTacticalDisplay(TAC_DISPLAY)
DwyerDispatcher:SetDefaultFuelThreshold( 0.20 )
DwyerDispatcher:SetRefreshTimeInterval( 300 )
DwyerDispatcher:SetDefaultOverhead(RedA2ADefaultOverhead)
DwyerDispatcher:SetDisengageRadius( 100000 )
DwyerDispatcher:SetEngageRadius( 50000 )
DwyerDispatcher:SetGciRadius( 75000 )
BostZone = ZONE_POLYGON:New( "BostBorderZone", GROUP:FindByName( "BostBorderZone" ) )
BostDispatcher = AI_A2A_GCICAP:NewWithBorder( { "RED EWR" }, { "BostBorderCAP" }, "BostBorderZone", "BostBorderZone", RedDefaultCAP, 10000, 50000, 75000, 100)
BostDispatcher:SetDefaultLandingAtRunway()
BostDispatcher:SetDefaultTakeoffInAir()
BostDispatcher:SetBorderZone(BostZone)
BostDispatcher:SetTacticalDisplay(TAC_DISPLAY)
BostDispatcher:SetDefaultFuelThreshold( 0.20 )
BostDispatcher:SetRefreshTimeInterval( 300 )
BostDispatcher:SetDefaultOverhead(RedA2ADefaultOverhead)
BostDispatcher:SetDisengageRadius( 100000 )
BostDispatcher:SetEngageRadius( 50000 )
BostDispatcher:SetGciRadius( 75000 )
------------------------------------------------------------------------------------------------------------------------------------------------------
-- Clean up the airbases of debris and stuck aircraft.
------------------------------------------------------------------------------------------------------------------------------------------------------
CleanUpAirports = CLEANUP_AIRBASE:New( {
AIRBASE.Afghanistan.Kandahar,
AIRBASE.Afghanistan.Camp_Bastion
} )
------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Spawns
------------------------------------------------------------------------------------------------------------------------------------------------------
-- Spawn the RED Bunker Buster
Red_Bunker_Buster = SPAWN:New("BUNKER BUSTER")
:InitLimit(1, 99)
:SpawnScheduled(900, 0.5)
Red_Bunker_Buster2 = SPAWN:New("BUNKER BUSTER-1")
:InitLimit(1, 99)
:SpawnScheduled(1800, 0.5)

View File

@ -0,0 +1,135 @@
--[[
Unified F10 Menu Manager
Purpose: Provides a centralized menu system to organize all mission scripts
into a consistent F10 menu structure.
Menu Organization:
F10 -> F1: Mission Options (all other scripts go here)
F10 -> F2: CTLD (reserved position)
F10 -> F3: AFAC Control (reserved position)
Usage:
1. Load this script FIRST before any other menu-creating scripts
2. Other scripts should use MenuManager to register their menus
Example:
-- In your script, instead of:
-- local MyMenu = MENU_COALITION:New(coalition.side.BLUE, "My Script")
-- Use:
-- local MyMenu = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "My Script")
]]--
MenuManager = {}
MenuManager.Version = "1.1"
-- Configuration
MenuManager.Config = {
EnableMissionOptionsMenu = true, -- Set to false to disable the parent menu system
MissionOptionsMenuName = "Mission Options", -- Name of the parent menu
Debug = false -- Set to true for debug messages
}
-- Storage for menu references
MenuManager.Menus = {
Blue = {},
Red = {},
Mission = {}
}
-- Parent menu references (created on first use)
MenuManager.ParentMenus = {
BlueCoalition = nil,
RedCoalition = nil,
Mission = nil
}
-- Initialize the parent menus
function MenuManager.Initialize()
if MenuManager.Config.EnableMissionOptionsMenu then
-- Create the parent "Mission Options" menu for each coalition
MenuManager.ParentMenus.BlueCoalition = MENU_COALITION:New(
coalition.side.BLUE,
MenuManager.Config.MissionOptionsMenuName
)
MenuManager.ParentMenus.RedCoalition = MENU_COALITION:New(
coalition.side.RED,
MenuManager.Config.MissionOptionsMenuName
)
-- Note: MENU_MISSION not created to avoid duplicate empty menu
-- Scripts that need mission-wide menus should use MENU_MISSION directly
if MenuManager.Config.Debug then
env.info("MenuManager: Initialized parent coalition menus")
end
end
end
-- Create a coalition menu under "Mission Options"
-- @param coalitionSide: coalition.side.BLUE or coalition.side.RED
-- @param menuName: Name of the menu
-- @param parentMenu: (Optional) If provided, creates as submenu of this parent instead of Mission Options
-- @return: MENU_COALITION object
function MenuManager.CreateCoalitionMenu(coalitionSide, menuName, parentMenu)
if MenuManager.Config.EnableMissionOptionsMenu and not parentMenu then
-- Create under Mission Options
local parent = (coalitionSide == coalition.side.BLUE)
and MenuManager.ParentMenus.BlueCoalition
or MenuManager.ParentMenus.RedCoalition
local menu = MENU_COALITION:New(coalitionSide, menuName, parent)
if MenuManager.Config.Debug then
local coalitionName = (coalitionSide == coalition.side.BLUE) and "BLUE" or "RED"
env.info(string.format("MenuManager: Created coalition menu '%s' for %s", menuName, coalitionName))
end
return menu
else
-- Create as root menu or under provided parent
local menu = MENU_COALITION:New(coalitionSide, menuName, parentMenu)
return menu
end
end
-- Create a mission menu (not nested under Mission Options, as that causes duplicates)
-- @param menuName: Name of the menu
-- @param parentMenu: (Optional) Parent menu
-- @return: MENU_MISSION object
-- Note: Mission menus are visible to all players and cannot be nested under coalition menus
function MenuManager.CreateMissionMenu(menuName, parentMenu)
-- Always create as root menu or under provided parent
-- Mission menus can't be nested under coalition-specific "Mission Options"
local menu = MENU_MISSION:New(menuName, parentMenu)
if MenuManager.Config.Debug then
env.info(string.format("MenuManager: Created mission menu '%s'", menuName))
end
return menu
end
-- Helper to disable the parent menu system at runtime
function MenuManager.DisableParentMenus()
MenuManager.Config.EnableMissionOptionsMenu = false
env.info("MenuManager: Parent menu system disabled")
end
-- Helper to enable the parent menu system at runtime
function MenuManager.EnableParentMenus()
MenuManager.Config.EnableMissionOptionsMenu = true
if not MenuManager.ParentMenus.BlueCoalition then
MenuManager.Initialize()
end
env.info("MenuManager: Parent menu system enabled")
end
-- Initialize on load
MenuManager.Initialize()
-- Announcement
env.info(string.format("MenuManager v%s loaded - Mission Options menu system ready", MenuManager.Version))

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,374 @@
--[[ THIS FILE MUST BE LOADED BEFORE THE MAIN Moose_TADC.lua SCRIPT
SQUADRON CONFIGURATION
INSTRUCTIONS:
1. Create fighter aircraft templates for BOTH coalitions in the mission editor
2. Place them at or near the airbases you want them to operate from
3. Configure RED squadrons in RED_SQUADRON_CONFIG
4. Configure BLUE squadrons in BLUE_SQUADRON_CONFIG
TEMPLATE NAMING SUGGESTIONS:
RED: "RED_CAP_Batumi_F15", "RED_INTERCEPT_Senaki_MiG29"
BLUE: "BLUE_CAP_Nellis_F16", "BLUE_INTERCEPT_Creech_F22"
Include coalition and airbase name for easy identification
AIRBASE NAMES:
Use exact names as they appear in DCS (case sensitive)
RED examples: "Batumi", "Senaki", "Gudauta"
BLUE examples: "Nellis AFB", "McCarran International", "Tonopah Test Range"
Find airbase names in the mission editor
AIRCRAFT NUMBERS:
Set realistic numbers based on mission requirements
Consider aircraft consumption and cargo replenishment
Balance between realism and gameplay performance
ZONE-BASED AREAS OF RESPONSIBILITY:
Create zones in mission editor (MOOSE polygons, circles, etc.)
primaryZone: Squadron's main area (full response)
secondaryZone: Backup/support area (reduced response)
tertiaryZone: Emergency fallback area (enhanced response)
Leave zones as nil for global threat response
Multiple squadrons can share overlapping zones
Use zone names exactly as they appear in mission editor
ZONE BEHAVIOR EXAMPLES:
Border Defense: primaryZone = "SECTOR_ALPHA", secondaryZone = "BUFFER_ZONE"
Base Defense: tertiaryZone = "BASE_PERIMETER", enableFallback = true
Layered Defense: Different zones per squadron with overlap
Emergency Response: High tertiaryResponse ratio for critical areas
]]
-- ═══════════════════════════════════════════════════════════════════════════
-- RED COALITION SQUADRONS
-- ═══════════════════════════════════════════════════════════════════════════
RED_SQUADRON_CONFIG = {
--[[ EXAMPLE RED SQUADRON - CUSTOMIZE FOR YOUR MISSION
{
templateName = "RED_CAP_Batumi_F15", -- Template name from mission editor
displayName = "Batumi F-15C CAP", -- Human-readable name for logs
airbaseName = "Batumi", -- Exact airbase name from DCS
aircraft = 12, -- Maximum aircraft in squadron
skill = AI.Skill.GOOD, -- AI skill level
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER"
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
]]
-- ADD YOUR RED SQUADRONS HERE
{
templateName = "Shindand MiG-31", -- Change to your RED template name
displayName = "Shindand MiG-31", -- Change to your preferred name
airbaseName = "Shindand", -- Change to your RED airbase
aircraft = 12, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT, ACE
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{
templateName = "Shindand F-14B", -- Change to your RED template name
displayName = "Shindand F-14B", -- Change to your preferred name
airbaseName = "Shindand", -- Change to your RED airbase
aircraft = 12, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{
templateName = "Shindand MiG-21", -- Change to your RED template name
displayName = "Shindand MiG-21", -- Change to your preferred name
airbaseName = "Shindand", -- Change to your RED airbase
aircraft = 14, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{ templateName = "Farah MiG-21", -- Change to your RED template name
displayName = "Farah MiG-21", -- Change to your preferred name
airbaseName = "Farah", -- Change to your RED airbase
aircraft = 14, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{ templateName = "Camp Bastion F-14B", -- Change to your RED template name
displayName = "Camp Bastion F-14B", -- Change to your preferred name
airbaseName = "Camp Bastion", -- Change to your RED airbase
aircraft = 12, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{ templateName = "Camp Bastion Su-27", -- Change to your RED template name
displayName = "Camp Bastion Su-27", -- Change to your preferred name
airbaseName = "Camp Bastion", -- Change to your RED airbase
aircraft = 12, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{ templateName = "Camp Bastion Mig-31", -- Change to your RED template name
displayName = "Camp Bastion Mig-31", -- Change to your preferred name
airbaseName = "Camp Bastion", -- Change to your RED airbase
aircraft = 12, -- Adjust aircraft count
skill = AI.Skill.ACE, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 20000, -- Patrol altitude (feet)
speed = 350, -- Patrol speed (knots)
patrolTime = 25, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "RED_BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = false, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
}
-- ═══════════════════════════════════════════════════════════════════════════
-- BLUE COALITION SQUADRONS
-- ═══════════════════════════════════════════════════════════════════════════
BLUE_SQUADRON_CONFIG = {
--[[ EXAMPLE BLUE SQUADRON - CUSTOMIZE FOR YOUR MISSION
{
templateName = "BLUE_CAP_Nellis_F16", -- Template name from mission editor
displayName = "Nellis F-16C CAP", -- Human-readable name for logs
airbaseName = "Nellis AFB", -- Exact airbase name from DCS
aircraft = 14, -- Maximum aircraft in squadron
skill = AI.Skill.EXCELLENT, -- AI skill level
altitude = 22000, -- Patrol altitude (feet)
speed = 380, -- Patrol speed (knots)
patrolTime = 28, -- Time on station (minutes)
type = "FIGHTER" -- Aircraft type
},
]]
-- ADD YOUR BLUE SQUADRONS HERE
{
templateName = "Kandahar F-4E", -- Change to your BLUE template name
displayName = "Kandahar F-4E", -- Change to your preferred name
airbaseName = "Kandahar", -- Change to your BLUE airbase
aircraft = 18, -- Adjust aircraft count
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 18000, -- Patrol altitude (feet)
speed = 320, -- Patrol speed (knots)
patrolTime = 22, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = true, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{
templateName = "Kandahar F-5E", -- Change to your BLUE template name
displayName = "Kandahar F-5E", -- Change to your preferred name
airbaseName = "Kandahar", -- Change to your BLUE airbase
aircraft = 18, -- Adjust aircraft count
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 18000, -- Patrol altitude (feet)
speed = 320, -- Patrol speed (knots)
patrolTime = 22, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = true, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
},
{ templateName = "Kandahar F-16", -- Change to your BLUE template name
displayName = "Kandahar F-16", -- Change to your preferred name
airbaseName = "Kandahar", -- Change to your BLUE airbase
aircraft = 18, -- Adjust aircraft count
skill = AI.Skill.EXCELLENT, -- AVERAGE, GOOD, HIGH, EXCELLENT
altitude = 18000, -- Patrol altitude (feet)
speed = 320, -- Patrol speed (knots)
patrolTime = 22, -- Time on station (minutes)
type = "FIGHTER",
-- Zone-based Areas of Responsibility (optional - leave nil for global response)
primaryZone = "BLUE BORDER", -- Main responsibility area (zone name from mission editor)
secondaryZone = nil, -- Secondary coverage area (zone name)
tertiaryZone = nil, -- Emergency/fallback zone (zone name)
-- Zone behavior settings (optional - uses defaults if not specified)
zoneConfig = {
primaryResponse = 1.0, -- Intercept ratio multiplier in primary zone
secondaryResponse = 0.6, -- Intercept ratio multiplier in secondary zone
tertiaryResponse = 1.4, -- Intercept ratio multiplier in tertiary zone
maxRange = 200, -- Maximum engagement range from airbase (nm)
enableFallback = true, -- Auto-switch to tertiary when base threatened
priorityThreshold = 4, -- Min aircraft count for "major threat"
ignoreLowPriority = false, -- Ignore threats below threshold in secondary zones
}
}