mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Updating Insurgent Sandstorm.
This commit is contained in:
parent
b79a8f9b0a
commit
7dfe72afc6
Binary file not shown.
Binary file not shown.
@ -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
@ -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)
|
||||
|
||||
|
||||
|
||||
135
DCS_Afgainistan/Insurgent_Sandstorm/Moose_MenuManager.lua
Normal file
135
DCS_Afgainistan/Insurgent_Sandstorm/Moose_MenuManager.lua
Normal 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))
|
||||
@ -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
|
||||
|
||||
2617
DCS_Afgainistan/Insurgent_Sandstorm/Moose_TADC_Load2nd.lua
Normal file
2617
DCS_Afgainistan/Insurgent_Sandstorm/Moose_TADC_Load2nd.lua
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user