Fixed a memory leak (scchulders built but not properly torn down.

This commit is contained in:
iTracerFacer 2025-11-17 04:56:13 -06:00
parent a674c7a2fd
commit 924757919f
4 changed files with 782 additions and 41 deletions

View File

@ -34,7 +34,10 @@ local blueCfg = {
Zones = {
PickupZones = { { name = 'S1', flag = 9001, activeWhen = 0 },
{ name = "S2", flag = 9004, activeWhen = 0 },
{ name = "S3", flag = 9005, activeWhen = 0 } },
{ name = "S3", flag = 9005, activeWhen = 0 },
{ name = "S4", flag = 9006, activeWhen = 0 },
{ name = "S5", flag = 9007, activeWhen = 0 },
{ name = "S6", flag = 9008, 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 } },
@ -67,7 +70,8 @@ local redCfg = {
{ name = "RedLoadZone2", flag = 9104, activeWhen = 0 },
{ name = "RedLoadZone3", flag = 9105, activeWhen = 0 },
{ name = "RedLoadZone4", flag = 9106, activeWhen = 0 },
{ name = "RedLoadZone5", flag = 9107, activeWhen = 0 } },
{ name = "RedLoadZone5", flag = 9107, activeWhen = 0 },
{ name = "RedLoadZone6", flag = 9108, 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 } },

View File

@ -0,0 +1,334 @@
-- CrateCatalog_CTLD_Extract.lua
-- Auto-generated from CTLD.lua (Operation_Polar_Shield) spawnableCrates config
-- Returns a table of crate definitions suitable for CTLD:MergeCatalog()
-- Notes:
-- - Each entry has keys: description/menu, dcsCargoType, required or requires (composite), side, category, build(point, headingDeg)
-- - Single-unit entries spawn one unit by DCS type. Composite "SITE" entries spawn a multi-unit group approximating system components.
local function singleUnit(unitType)
return function(point, headingDeg)
local name = string.format('%s-%d', unitType, math.random(100000,999999))
local hdg = math.rad(headingDeg or 0)
return {
visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={},
units={ { type=unitType, name=name, x=point.x, y=point.z, heading=hdg } },
name = 'CTLD_'..name
}
end
end
-- Build a single AIR unit that spawns in the air at a configured altitude/speed.
-- Falls back gracefully to singleUnit behavior if config is unavailable/disabled.
local function singleAirUnit(unitType)
return function(point, headingDeg)
local cfg = (rawget(_G, 'CTLD') and CTLD.Config and CTLD.Config.DroneAirSpawn) or nil
if not cfg or cfg.Enabled == false then
return singleUnit(unitType)(point, headingDeg)
end
local name = string.format('%s-%d', unitType, math.random(100000,999999))
local hdgDeg = headingDeg or 0
local hdg = math.rad(hdgDeg)
local alt = tonumber(cfg.AltitudeMeters) or 1200
local spd = tonumber(cfg.SpeedMps) or 120
-- Create a tiny 2-point route to ensure forward flight at the chosen altitude.
local function fwdOffset(px, pz, meters, headingRadians)
return px + math.sin(headingRadians) * meters, pz + math.cos(headingRadians) * meters
end
local p1x, p1z = point.x, point.z
local p2x, p2z = fwdOffset(point.x, point.z, 1000, hdg) -- 1 km ahead
local group = {
visible=false,
lateActivation=false,
tasks={},
task='CAS',
route={
points={
{
alt = alt, alt_type = 'BARO',
type = 'Turning Point', action = 'Turning Point',
x = p1x, y = p1z,
speed = spd, ETA = 0, ETA_locked = false,
task = {}
},
{
alt = alt, alt_type = 'BARO',
type = 'Turning Point', action = 'Turning Point',
x = p2x, y = p2z,
speed = spd, ETA = 0, ETA_locked = false,
task = {}
}
}
},
units={
{
type=unitType, name=name,
x=p1x, y=p1z,
heading=hdg,
speed = spd,
alt = alt, alt_type = 'BARO'
}
},
name = 'CTLD_'..name
}
return group
end
end
local function multiUnits(units)
-- units: array of { type, dx, dz }
return function(point, headingDeg)
local hdg = math.rad(headingDeg or 0)
local function off(dx, dz) return { x = point.x + dx, z = point.z + dz } end
local list = {}
for i,u in ipairs(units) do
local p = off(u.dx or 0, u.dz or 3*i)
table.insert(list, {
type = u.type, name = string.format('CTLD-%s-%d', u.type, math.random(100000,999999)),
x = p.x, y = p.z, heading = hdg
})
end
return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, units=list, name=string.format('CTLD_SITE_%d', math.random(100000,999999)) }
end
end
local BLUE = coalition.side.BLUE
local RED = coalition.side.RED
local cat = {}
cat['BLUE_M1128_STRYKER_MGS_CRATE'] = { hidden=true, description='M1128 Stryker MGS crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M1128_STRYKER_MGS'] = { menuCategory='Combat Vehicles', menu='M1128 Stryker MGS', description='M1128 Stryker MGS', dcsCargoType='container_cargo', requires={ BLUE_M1128_STRYKER_MGS_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1128 Stryker MGS'), unitType='M1128 Stryker MGS', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['BLUE_M60A3_PATTON_CRATE'] = { hidden=true, description='M-60A3 Patton crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M60A3_PATTON'] = { menuCategory='Combat Vehicles', menu='M-60A3 Patton', description='M-60A3 Patton', dcsCargoType='container_cargo', requires={ BLUE_M60A3_PATTON_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-60'), unitType='M-60', MEDEVAC=true, salvageValue=3, crewSize=4 }
cat['BLUE_HMMWV_TOW_CRATE'] = { hidden=true, description='Humvee - TOW crate', dcsCargoType='container_cargo', required=1, initialStock=36, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_HMMWV_TOW'] = { menuCategory='Combat Vehicles', menu='Humvee - TOW', description='Humvee - TOW', dcsCargoType='container_cargo', requires={ BLUE_HMMWV_TOW_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1045 HMMWV TOW'), unitType='M1045 HMMWV TOW', MEDEVAC=true, salvageValue=3, crewSize=2 }
cat['BLUE_M1134_STRYKER_ATGM_CRATE']= { hidden=true, description='M1134 Stryker ATGM crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M1134_STRYKER_ATGM'] = { menuCategory='Combat Vehicles', menu='M1134 Stryker ATGM', description='M1134 Stryker ATGM', dcsCargoType='container_cargo', requires={ BLUE_M1134_STRYKER_ATGM_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1134 Stryker ATGM'), unitType='M1134 Stryker ATGM', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['BLUE_LAV25_CRATE'] = { hidden=true, description='LAV-25 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_LAV25'] = { menuCategory='Combat Vehicles', menu='LAV-25', description='LAV-25', dcsCargoType='container_cargo', requires={ BLUE_LAV25_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('LAV-25'), unitType='LAV-25', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['BLUE_M2A2_BRADLEY_CRATE'] = { hidden=true, description='M2A2 Bradley crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M2A2_BRADLEY'] = { menuCategory='Combat Vehicles', menu='M2A2 Bradley', description='M2A2 Bradley', dcsCargoType='container_cargo', requires={ BLUE_M2A2_BRADLEY_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-2 Bradley'), unitType='M-2 Bradley', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['BLUE_VAB_MEPHISTO_CRATE'] = { hidden=true, description='ATGM VAB Mephisto crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_VAB_MEPHISTO'] = { menuCategory='Combat Vehicles', menu='ATGM VAB Mephisto', description='ATGM VAB Mephisto', dcsCargoType='container_cargo', requires={ BLUE_VAB_MEPHISTO_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('VAB_Mephisto'), unitType='VAB_Mephisto', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['BLUE_M1A2C_ABRAMS_CRATE'] = { hidden=true, description='M1A2C Abrams crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M1A2C_ABRAMS'] = { menuCategory='Combat Vehicles', menu='M1A2C Abrams', description='M1A2C Abrams', dcsCargoType='container_cargo', requires={ BLUE_M1A2C_ABRAMS_CRATE=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1A2C_SEP_V3'), unitType='M1A2C_SEP_V3', MEDEVAC=true, salvageValue=3, crewSize=4 }
-- Combat Vehicles (RED)
cat['RED_BTR82A_CRATE'] = { hidden=true, description='BTR-82A crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND }
cat['RED_BTR82A'] = { menuCategory='Combat Vehicles', menu='BTR-82A', description='BTR-82A', dcsCargoType='container_cargo', requires={ RED_BTR82A_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR-82A'), unitType='BTR-82A', MEDEVAC=true, salvageValue=2, crewSize=3 }
cat['RED_BRDM2_CRATE'] = { hidden=true, description='BRDM-2 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND }
cat['RED_BRDM2'] = { menuCategory='Combat Vehicles', menu='BRDM-2', description='BRDM-2', dcsCargoType='container_cargo', requires={ RED_BRDM2_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BRDM-2'), unitType='BRDM-2', MEDEVAC=true, salvageValue=2, crewSize=2 }
cat['RED_BMP3_CRATE'] = { hidden=true, description='BMP-3 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND }
cat['RED_BMP3'] = { menuCategory='Combat Vehicles', menu='BMP-3', description='BMP-3', dcsCargoType='container_cargo', requires={ RED_BMP3_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BMP-3'), unitType='BMP-3', MEDEVAC=true, salvageValue=2, crewSize=3 }
cat['RED_BMP2_CRATE'] = { hidden=true, description='BMP-2 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND }
cat['RED_BMP2'] = { menuCategory='Combat Vehicles', menu='BMP-2', description='BMP-2', dcsCargoType='container_cargo', requires={ RED_BMP2_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BMP-2'), unitType='BMP-2', MEDEVAC=true, salvageValue=2, crewSize=3 }
cat['RED_BTR80_CRATE'] = { hidden=true, description='BTR-80 crate', dcsCargoType='container_cargo', required=1, initialStock=30, side=RED, category=Group.Category.GROUND }
cat['RED_BTR80'] = { menuCategory='Combat Vehicles', menu='BTR-80', description='BTR-80', dcsCargoType='container_cargo', requires={ RED_BTR80_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR-80'), unitType='BTR-80', MEDEVAC=true, salvageValue=2, crewSize=3 }
cat['RED_T72B3_CRATE'] = { hidden=true, description='T-72B3 crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=RED, category=Group.Category.GROUND }
cat['RED_T72B3'] = { menuCategory='Combat Vehicles', menu='T-72B3', description='T-72B3', dcsCargoType='container_cargo', requires={ RED_T72B3_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('T-72B3'), unitType='T-72B3', MEDEVAC=true, salvageValue=3, crewSize=3 }
cat['RED_T90M_CRATE'] = { hidden=true, description='T-90M crate', dcsCargoType='container_cargo', required=1, initialStock=24, side=RED, category=Group.Category.GROUND }
cat['RED_T90M'] = { menuCategory='Combat Vehicles', menu='T-90M', description='T-90M', dcsCargoType='container_cargo', requires={ RED_T90M_CRATE=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('CHAP_T90M'), unitType='CHAP_T90M', MEDEVAC=true, salvageValue=3, crewSize=3 }
-- Support (BLUE)
cat['BLUE_MRAP_JTAC'] = { menuCategory='Support', menu='MRAP - JTAC', description='JTAC MRAP', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MaxxPro_MRAP'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } }
cat['BLUE_M818_AMMO'] = { menuCategory='Support', menu='M-818 Ammo Truck', description='M-818 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M 818'), salvageValue=1, crewSize=2 }
cat['BLUE_M978_TANKER'] = { menuCategory='Support', menu='M-978 Tanker', description='M-978 Tanker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M978 HEMTT Tanker'), salvageValue=1, crewSize=2 }
cat['BLUE_EWR_FPS117'] = { menuCategory='Support', menu='EWR Radar FPS-117', description='EWR Radar FPS-117', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('FPS-117'), salvageValue=1, crewSize=3 }
-- Support (RED)
cat['RED_TIGR_JTAC'] = { menuCategory='Support', menu='Tigr - JTAC', description='JTAC Tigr', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Tigr_233036'), MEDEVAC=true, salvageValue=1, crewSize=4, roles={'JTAC'}, jtac={ platform='ground' } }
cat['RED_URAL4320_AMMO'] = { menuCategory='Support', menu='Ural-4320-31 Ammo Truck', description='Ural-4320-31 Ammo Truck', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-4320-31'), salvageValue=1, crewSize=2 }
cat['RED_ATZ10_TANKER'] = { menuCategory='Support', menu='ATZ-10 Refueler', description='ATZ-10 Refueler', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ATZ-10'), salvageValue=1, crewSize=2 }
cat['RED_EWR_1L13'] = { menuCategory='Support', menu='EWR Radar 1L13', description='EWR Radar 1L13', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('1L13 EWR'), salvageValue=1, crewSize=3 }
-- Artillery (BLUE)
cat['BLUE_MLRS_CRATE'] = { hidden=true, description='MLRS crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_MLRS'] = { menuCategory='Artillery', menu='MLRS', description='MLRS', dcsCargoType='container_cargo', requires={ BLUE_MLRS_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('MLRS'), salvageValue=2, crewSize=3 }
cat['BLUE_SMERCH_CM_CRATE'] = { hidden=true, description='Smerch (CM) crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_SMERCH_CM'] = { menuCategory='Artillery', menu='Smerch_CM', description='Smerch (CM)', dcsCargoType='container_cargo', requires={ BLUE_SMERCH_CM_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch'), salvageValue=2, crewSize=3 }
cat['BLUE_L118_105MM'] = { menuCategory='Artillery', menu='L118 Light Artillery 105mm', description='L118 105mm', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('L118_Unit'), salvageValue=1, crewSize=5 }
cat['BLUE_SMERCH_HE_CRATE'] = { hidden=true, description='Smerch (HE) crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_SMERCH_HE'] = { menuCategory='Artillery', menu='Smerch_HE', description='Smerch (HE)', dcsCargoType='container_cargo', requires={ BLUE_SMERCH_HE_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Smerch_HE'), salvageValue=2, crewSize=3 }
cat['BLUE_M109_CRATE'] = { hidden=true, description='M-109 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M109'] = { menuCategory='Artillery', menu='M-109', description='M-109', dcsCargoType='container_cargo', requires={ BLUE_M109_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-109'), salvageValue=2, crewSize=4 }
-- Artillery (RED)
cat['RED_GVOZDIKA_CRATE'] = { hidden=true, description='SAU Gvozdika crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_GVOZDika'] = { menuCategory='Artillery', menu='SAU Gvozdika', description='SAU Gvozdika', dcsCargoType='container_cargo', requires={ RED_GVOZDIKA_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Gvozdika'), salvageValue=2, crewSize=3 }
cat['RED_2S19_MSTA_CRATE'] = { hidden=true, description='SPH 2S19 Msta crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_2S19_MSTA'] = { menuCategory='Artillery', menu='SPH 2S19 Msta', description='SPH 2S19 Msta', dcsCargoType='container_cargo', requires={ RED_2S19_MSTA_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('SAU Msta'), salvageValue=2, crewSize=4 }
cat['RED_URAGAN_BM27_CRATE'] = { hidden=true, description='Uragan BM-27 crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND }
cat['RED_URAGAN_BM27'] = { menuCategory='Artillery', menu='Uragan_BM-27', description='Uragan BM-27', dcsCargoType='container_cargo', requires={ RED_URAGAN_BM27_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Uragan_BM-27'), salvageValue=2, crewSize=3 }
cat['RED_BM21_GRAD_CRATE'] = { hidden=true, description='BM-21 Grad crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_BM21_GRAD'] = { menuCategory='Artillery', menu='BM-21 Grad Ural', description='BM-21 Grad Ural', dcsCargoType='container_cargo', requires={ RED_BM21_GRAD_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Grad-URAL'), salvageValue=2, crewSize=3 }
cat['RED_PLZ05_CRATE'] = { hidden=true, description='PLZ-05 crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND }
cat['RED_PLZ05'] = { menuCategory='Artillery', menu='PLZ-05 Mobile Artillery', description='PLZ-05', dcsCargoType='container_cargo', requires={ RED_PLZ05_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('PLZ05'), salvageValue=2, crewSize=4 }
-- AAA (BLUE)
cat['BLUE_GEPARD'] = { menuCategory='AAA', menu='Gepard AAA', description='Gepard AAA', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Gepard'), salvageValue=1, crewSize=3 }
cat['BLUE_CRAM'] = { menuCategory='AAA', menu='LPWS C-RAM', description='LPWS C-RAM', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('HEMTT_C-RAM_Phalanx'), salvageValue=1, crewSize=2 }
cat['BLUE_VULCAN_M163'] = { menuCategory='AAA', menu='SPAAA Vulcan M163', description='Vulcan M163', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Vulcan'), salvageValue=1, crewSize=2 }
cat['BLUE_BOFORS40'] = { menuCategory='AAA', menu='Bofors 40mm', description='Bofors 40mm', dcsCargoType='container_cargo', required=1, initialStock=12, side=BLUE, category=Group.Category.GROUND, build=singleUnit('bofors40'), salvageValue=1, crewSize=4 }
-- AAA (RED)
cat['RED_URAL_ZU23'] = { menuCategory='AAA', menu='Ural-375 ZU-23', description='Ural-375 ZU-23', dcsCargoType='container_cargo', required=1, initialStock=12, side=RED, category=Group.Category.GROUND, build=singleUnit('Ural-375 ZU-23'), salvageValue=1, crewSize=3 }
cat['RED_SHILKA'] = { menuCategory='AAA', menu='ZSU-23-4 Shilka', description='ZSU-23-4 Shilka', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU-23-4 Shilka'), salvageValue=1, crewSize=3 }
cat['RED_ZSU57_2'] = { menuCategory='AAA', menu='ZSU_57_2', description='ZSU_57_2', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('ZSU_57_2'), salvageValue=1, crewSize=3 }
cat['BLUE_M1097_AVENGER_CRATE'] = { hidden=true, description='M1097 Avenger crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M1097_AVENGER'] = { menuCategory='SAM short range', menu='M1097 Avenger', description='M1097 Avenger', dcsCargoType='container_cargo', requires={ BLUE_M1097_AVENGER_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M1097 Avenger') }
cat['BLUE_M48_CHAPARRAL_CRATE'] = { hidden=true, description='M48 Chaparral crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_M48_CHAPARRAL'] = { menuCategory='SAM short range', menu='M48 Chaparral', description='M48 Chaparral', dcsCargoType='container_cargo', requires={ BLUE_M48_CHAPARRAL_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M48 Chaparral') }
cat['BLUE_ROLAND_ADS_CRATE'] = { hidden=true, description='Roland ADS crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=BLUE, category=Group.Category.GROUND }
cat['BLUE_ROLAND_ADS'] = { menuCategory='SAM short range', menu='Roland ADS', description='Roland ADS', dcsCargoType='container_cargo', requires={ BLUE_ROLAND_ADS_CRATE=2 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Roland ADS') }
cat['BLUE_M6_LINEBACKER'] = { menuCategory='SAM short range', menu='M6 Linebacker', description='M6 Linebacker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M6 Linebacker') }
cat['BLUE_RAPIER_LN'] = { menuCategory='SAM short range', menu='Rapier Launcher', description='Rapier Launcher', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_launcher') }
cat['BLUE_RAPIER_SR'] = { menuCategory='SAM short range', menu='Rapier SR', description='Rapier SR', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_blindfire_radar') }
cat['BLUE_RAPIER_TR'] = { menuCategory='SAM short range', menu='Rapier Tracker', description='Rapier Tracker', dcsCargoType='container_cargo', required=1, initialStock=10, side=BLUE, category=Group.Category.GROUND, build=singleUnit('rapier_fsa_optical_tracker_unit') }
cat['BLUE_RAPIER_SITE'] = { menuCategory='SAM short range', menu='Rapier - All crates', description='Rapier Site', dcsCargoType='container_cargo', requires={ BLUE_RAPIER_LN=1, BLUE_RAPIER_SR=1, BLUE_RAPIER_TR=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='rapier_fsa_launcher'}, {type='rapier_fsa_blindfire_radar', dx=12, dz=6}, {type='rapier_fsa_optical_tracker_unit', dx=-12, dz=6} }) }
-- SAM short range (RED)
cat['RED_OSA_9K33_CRATE'] = { hidden=true, description='9K33 Osa crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_OSA_9K33'] = { menuCategory='SAM short range', menu='9K33 Osa', description='9K33 Osa', dcsCargoType='container_cargo', requires={ RED_OSA_9K33_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Osa 9A33 ln') }
cat['RED_STRELA1_9P31_CRATE'] = { hidden=true, description='9P31 Strela-1 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_STRELA1_9P31'] = { menuCategory='SAM short range', menu='9P31 Strela-1', description='9P31 Strela-1', dcsCargoType='container_cargo', requires={ RED_STRELA1_9P31_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-1 9P31') }
cat['RED_TUNGUSKA_2S6_CRATE'] = { hidden=true, description='2K22 Tunguska crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_TUNGUSKA_2S6'] = { menuCategory='SAM short range', menu='2K22 Tunguska', description='2K22 Tunguska', dcsCargoType='container_cargo', requires={ RED_TUNGUSKA_2S6_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('2S6 Tunguska') }
cat['RED_STRELA10M3_CRATE'] = { hidden=true, description='SA-13 Strela-10M3 crate', dcsCargoType='container_cargo', required=1, initialStock=16, side=RED, category=Group.Category.GROUND }
cat['RED_STRELA10M3'] = { menuCategory='SAM short range', menu='SA-13 Strela-10M3', description='SA-13 Strela-10M3', dcsCargoType='container_cargo', requires={ RED_STRELA10M3_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('Strela-10M3') }
-- HQ-7 components and site
cat['RED_HQ7_LN_CRATE'] = { hidden=true, description='HQ-7 Launcher crate', dcsCargoType='container_cargo', required=1, initialStock=20, side=RED, category=Group.Category.GROUND }
cat['RED_HQ7_LN'] = { menuCategory='SAM short range', menu='HQ-7_Launcher', description='HQ-7 Launcher', dcsCargoType='container_cargo', requires={ RED_HQ7_LN_CRATE=2 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_LN_SP') }
cat['RED_HQ7_STR'] = { menuCategory='SAM short range', menu='HQ-7_STR_SP', description='HQ-7 STR', dcsCargoType='container_cargo', required=1, initialStock=10, side=RED, category=Group.Category.GROUND, build=singleUnit('HQ-7_STR_SP') }
cat['RED_HQ7_SITE'] = { menuCategory='SAM short range', menu='HQ-7 - All crates', description='HQ-7 Site', dcsCargoType='container_cargo', requires={ RED_HQ7_LN=1, RED_HQ7_STR=1 }, initialStock=0, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='HQ-7_LN_SP'}, {type='HQ-7_STR_SP', dx=10, dz=8} }) }
-- SAM mid range (BLUE) HAWK + NASAMS
cat['BLUE_HAWK_LN'] = { menuCategory='SAM mid range', menu='HAWK Launcher', description='HAWK Launcher', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk ln') }
cat['BLUE_HAWK_SR'] = { menuCategory='SAM mid range', menu='HAWK Search Radar', description='HAWK SR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk sr') }
cat['BLUE_HAWK_TR'] = { menuCategory='SAM mid range', menu='HAWK Track Radar', description='HAWK TR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk tr') }
cat['BLUE_HAWK_PCP'] = { menuCategory='SAM mid range', menu='HAWK PCP', description='HAWK PCP', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk pcp') }
cat['BLUE_HAWK_CWAR'] = { menuCategory='SAM mid range', menu='HAWK CWAR', description='HAWK CWAR', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Hawk cwar') }
cat['BLUE_HAWK_SITE'] = { menuCategory='SAM mid range', menu='HAWK - All crates', description='HAWK Site', dcsCargoType='container_cargo', requires={ BLUE_HAWK_LN=1, BLUE_HAWK_SR=1, BLUE_HAWK_TR=1, BLUE_HAWK_PCP=1, BLUE_HAWK_CWAR=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='Hawk ln'}, {type='Hawk sr', dx=12, dz=8}, {type='Hawk tr', dx=-12, dz=8}, {type='Hawk pcp', dx=18, dz=12}, {type='Hawk cwar', dx=-18, dz=12} }) }
-- HAWK site repair/augment (adds +1 launcher, repairs site by respawn)
cat['BLUE_HAWK_REPAIR'] = { menuCategory='SAM mid range', menu='HAWK Repair/Launcher +1', description='HAWK Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg)
-- Build is handled specially in CTLD:BuildSpecificAtGroup for isRepair entries
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['BLUE_NASAMS_LN'] = { menuCategory='SAM mid range', menu='NASAMS Launcher 120C', description='NASAMS LN 120C', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_LN_C') }
cat['BLUE_NASAMS_RADAR'] = { menuCategory='SAM mid range', menu='NASAMS Search/Track Radar', description='NASAMS Radar', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Radar_MPQ64F1') }
cat['BLUE_NASAMS_CP'] = { menuCategory='SAM mid range', menu='NASAMS Command Post', description='NASAMS CP', dcsCargoType='container_cargo', required=1, initialStock=8, side=BLUE, category=Group.Category.GROUND, build=singleUnit('NASAMS_Command_Post') }
cat['BLUE_NASAMS_SITE'] = { menuCategory='SAM mid range', menu='NASAMS - All crates', description='NASAMS Site', dcsCargoType='container_cargo', requires={ BLUE_NASAMS_LN=1, BLUE_NASAMS_RADAR=1, BLUE_NASAMS_CP=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='NASAMS_LN_C'}, {type='NASAMS_Radar_MPQ64F1', dx=12, dz=8}, {type='NASAMS_Command_Post', dx=-12, dz=8} }) }
-- SAM mid range (RED) KUB
cat['RED_KUB_LN'] = { menuCategory='SAM mid range', menu='KUB Launcher', description='KUB Launcher', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 2P25 ln') }
cat['RED_KUB_RADAR'] = { menuCategory='SAM mid range', menu='KUB Radar', description='KUB Radar', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, build=singleUnit('Kub 1S91 str') }
cat['RED_KUB_SITE'] = { menuCategory='SAM mid range', menu='KUB - All crates', description='KUB Site', dcsCargoType='container_cargo', requires={ RED_KUB_LN=1, RED_KUB_RADAR=1 }, initialStock=0, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='Kub 2P25 ln'}, {type='Kub 1S91 str', dx=12, dz=8} }) }
-- KUB site repair/augment (adds +1 launcher, repairs site by respawn)
cat['RED_KUB_REPAIR'] = { menuCategory='SAM mid range', menu='KUB Repair/Launcher +1', description='KUB Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=8, side=RED, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg)
return singleUnit('Ural-375')(point, headingDeg)
end }
-- SAM long range (BLUE) Patriot
cat['BLUE_PATRIOT_LN'] = { menuCategory='SAM long range', menu='Patriot Launcher', description='Patriot Launcher', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ln') }
cat['BLUE_PATRIOT_RADAR'] = { menuCategory='SAM long range', menu='Patriot Radar', description='Patriot Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot str') }
cat['BLUE_PATRIOT_ECS'] = { menuCategory='SAM long range', menu='Patriot ECS', description='Patriot ECS', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, build=singleUnit('Patriot ECS') }
cat['BLUE_PATRIOT_SITE'] = { menuCategory='SAM long range', menu='Patriot - All crates', description='Patriot Site', dcsCargoType='container_cargo', requires={ BLUE_PATRIOT_LN=1, BLUE_PATRIOT_RADAR=1, BLUE_PATRIOT_ECS=1 }, initialStock=0, side=BLUE, category=Group.Category.GROUND,
build=multiUnits({ {type='Patriot ln'}, {type='Patriot str', dx=14, dz=10}, {type='Patriot ECS', dx=-14, dz=10} }) }
-- Patriot site repair/augment (adds +1 launcher, repairs site by respawn)
cat['BLUE_PATRIOT_REPAIR'] = { menuCategory='SAM long range', menu='Patriot Repair/Launcher +1', description='Patriot Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=6, side=BLUE, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg)
return singleUnit('Ural-375')(point, headingDeg)
end }
-- SAM long range (RED) BUK
cat['RED_BUK_LN'] = { menuCategory='SAM long range', menu='BUK Launcher', description='BUK Launcher', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk LN 9A310M1') }
cat['RED_BUK_SR'] = { menuCategory='SAM long range', menu='BUK Search Radar', description='BUK Search Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk SR 9S18M1') }
cat['RED_BUK_CC'] = { menuCategory='SAM long range', menu='BUK CC Radar', description='BUK CC Radar', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, build=singleUnit('SA-11 Buk CC 9S470M1') }
cat['RED_BUK_SITE'] = { menuCategory='SAM long range', menu='BUK - All crates', description='BUK Site', dcsCargoType='container_cargo', requires={ RED_BUK_LN=1, RED_BUK_SR=1, RED_BUK_CC=1 }, initialStock=0, side=RED, category=Group.Category.GROUND,
build=multiUnits({ {type='SA-11 Buk LN 9A310M1'}, {type='SA-11 Buk SR 9S18M1', dx=12, dz=8}, {type='SA-11 Buk CC 9S470M1', dx=-12, dz=8} }) }
-- BUK site repair/augment (adds +1 launcher, repairs site by respawn)
cat['RED_BUK_REPAIR'] = { menuCategory='SAM long range', menu='BUK Repair/Launcher +1', description='BUK Repair (adds launcher)', dcsCargoType='container_cargo', required=1, initialStock=6, side=RED, category=Group.Category.GROUND, isRepair=true, build=function(point, headingDeg)
return singleUnit('Ural-375')(point, headingDeg)
end }
-- Drones (JTAC)
cat['BLUE_MQ9'] = { menuCategory='Drones', menu='MQ-9 Reaper - JTAC', description='MQ-9 JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=BLUE, category=Group.Category.AIRPLANE, build=singleAirUnit('MQ-9 Reaper'), roles={'JTAC'}, jtac={ platform='air' } }
cat['RED_WINGLOONG'] = { menuCategory='Drones', menu='WingLoong-I - JTAC', description='WingLoong-I JTAC', dcsCargoType='container_cargo', required=1, initialStock=3, side=RED, category=Group.Category.AIRPLANE, build=singleAirUnit('WingLoong-I'), roles={'JTAC'}, jtac={ platform='air' } }
-- FOB crates (Support) — three small crates build a FOB site
cat['FOB_SMALL'] = { hidden=true, description='FOB small crate', dcsCargoType='container_cargo', required=1, initialStock=12, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg)
-- spawns a harmless placeholder truck for visibility; consumed by FOB_SITE build
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['FOB_SITE'] = { menuCategory='Support', menu='FOB Crates - All', description='FOB Site', isFOB=true, dcsCargoType='container_cargo', requires={ FOB_SMALL=3 }, initialStock=0, side=nil, category=Group.Category.GROUND,
build=multiUnits({ {type='HEMTT TFFT'}, {type='Ural-375 PBU', dx=10, dz=8}, {type='Ural-375', dx=-10, dz=8} }) }
-- Mobile MASH (Support) — three crates build a Mobile MASH unit
cat['MOBILE_MASH_SMALL'] = { hidden=true, description='Mobile MASH crate', dcsCargoType='container_cargo', required=1, initialStock=6, side=nil, category=Group.Category.GROUND, build=function(point, headingDeg)
-- spawns placeholder truck for visibility; consumed by MOBILE_MASH build
return singleUnit('Ural-375')(point, headingDeg)
end }
cat['BLUE_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Blue Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=BLUE, category=Group.Category.GROUND, build=singleUnit('M-113') }
cat['RED_MOBILE_MASH'] = { menuCategory='Support', menu='Mobile MASH - All', description='Red Mobile MASH Unit', isMobileMASH=true, dcsCargoType='container_cargo', requires={ MOBILE_MASH_SMALL=3 }, initialStock=0, side=RED, category=Group.Category.GROUND, build=singleUnit('BTR_D') }
-- =========================
-- Troop Type Definitions
-- =========================
-- These define the composition of troop squads for Load/Unload Troops (NOT crates)
-- Structure: { label, size, unitsBlue, unitsRed, units (fallback) }
local troops = {}
-- Assault Squad: general-purpose rifles/MG
troops['AS'] = {
label = 'Assault Squad',
size = 8,
unitsBlue = { 'Soldier M4', 'Soldier M249' },
unitsRed = { 'Infantry AK', 'Infantry AK ver3' },
units = { 'Infantry AK' },
}
-- MANPADS Team: Anti-air element
troops['AA'] = {
label = 'MANPADS Team',
size = 4,
unitsBlue = { 'Soldier stinger', 'Stinger comm' },
unitsRed = { 'SA-18 Igla-S manpad', 'SA-18 Igla comm' },
units = { 'Infantry AK' },
}
-- AT Team: Anti-tank element
troops['AT'] = {
label = 'AT Team',
size = 4,
unitsBlue = { 'Soldier RPG', 'Soldier RPG' },
unitsRed = { 'Soldier RPG', 'Soldier RPG' },
units = { 'Infantry AK' },
}
-- Mortar Team: Indirect fire element
troops['AR'] = {
label = 'Mortar Team',
size = 4,
unitsBlue = { '2B11 mortar' },
unitsRed = { '2B11 mortar' },
units = { '2B11 mortar' },
}
-- Export troop types
_CTLD_TROOP_TYPES = troops
-- Also export as a global for mission setups that load via DO SCRIPT FILE (no return capture)
_CTLD_EXTRACTED_CATALOG = cat
return cat

View File

@ -9,6 +9,7 @@
- Warehouse-based reinforcement system
- Dynamic spawn frequency based on warehouse survival
- Automated AI tasking to patrol nearest enemy zones
- Zone garrison system (defenders stay in captured zones)
- Optional infantry patrol control
- Warehouse status intel markers
- CTLD troop integration
@ -41,8 +42,12 @@
4. Updated every UPDATE_MARK_POINTS_SCHED seconds
AI Task Assignment:
- Groups spawn in friendly zones, then patrol toward nearest enemy zone
- Reassignment occurs every ASSIGN_TASKS_SCHED seconds
- Groups spawn in friendly zones
- Each zone maintains a minimum garrison (defenders) that patrol only their zone
- Non-defender groups patrol toward nearest enemy zone
- Election system assigns defenders automatically based on zone needs
- Defenders are never reassigned and stay permanently in their zone
- Reassignment occurs every ASSIGN_TASKS_SCHED seconds for non-defenders only
- Only stationary units get new orders (moving units are left alone)
- CTLD-dropped troops automatically integrate
@ -72,6 +77,10 @@
-- USER CONFIGURATION SECTION
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Zone Garrison (Defender) Settings
local DEFENDERS_PER_ZONE = 2 -- Minimum number of groups that will garrison each friendly zone (recommended: 2)
local ALLOW_DEFENDER_ROTATION = true -- If true, fresh units can replace existing defenders when zone is over-garrisoned
-- Infantry Patrol Settings
local MOVING_INFANTRY_PATROLS = true -- Set to false to disable infantry movement (they spawn and hold position)
@ -80,24 +89,28 @@ local ENABLE_WAREHOUSE_MARKERS = true -- Enable/disable warehouse map markers (
local UPDATE_MARK_POINTS_SCHED = 300 -- Update warehouse markers every 300 seconds (5 minutes)
local MAX_WAREHOUSE_UNIT_LIST_DISTANCE = 5000 -- Max distance to search for units near warehouses for markers
-- Warehouse Status Message Settings
local ENABLE_WAREHOUSE_STATUS_MESSAGES = true -- Enable/disable periodic warehouse status announcements
local WAREHOUSE_STATUS_MESSAGE_FREQUENCY = 1800 -- How often to announce warehouse status (seconds, default: 1800 = 30 minutes)
-- Spawn Frequency and Limits
-- Red Side Settings
local INIT_RED_INFANTRY = 25 -- Initial number of Red Infantry groups
local MAX_RED_INFANTRY = 100 -- Maximum number of Red Infantry groups
local SPAWN_SCHED_RED_INFANTRY = 1800 -- Base spawn frequency for Red Infantry (seconds)
local SPAWN_SCHED_RED_INFANTRY = 1200 -- Base spawn frequency for Red Infantry (seconds)
local INIT_RED_ARMOR = 25 -- Initial number of Red Armor groups
local MAX_RED_ARMOR = 500 -- Maximum number of Red Armor groups
local SPAWN_SCHED_RED_ARMOR = 300 -- Base spawn frequency for Red Armor (seconds)
local SPAWN_SCHED_RED_ARMOR = 200 -- Base spawn frequency for Red Armor (seconds)
-- Blue Side Settings
local INIT_BLUE_INFANTRY = 25 -- Initial number of Blue Infantry groups
local MAX_BLUE_INFANTRY = 100 -- Maximum number of Blue Infantry groups
local SPAWN_SCHED_BLUE_INFANTRY = 1800 -- Base spawn frequency for Blue Infantry (seconds)
local SPAWN_SCHED_BLUE_INFANTRY = 1200 -- Base spawn frequency for Blue Infantry (seconds)
local INIT_BLUE_ARMOR = 25 -- Initial number of Blue Armor groups
local MAX_BLUE_ARMOR = 500 -- Maximum number of Blue Armor groups
local SPAWN_SCHED_BLUE_ARMOR = 300 -- Base spawn frequency for Blue Armor (seconds)
local SPAWN_SCHED_BLUE_ARMOR = 200 -- Base spawn frequency for Blue Armor (seconds)
local ASSIGN_TASKS_SCHED = 600 -- How often to reassign tasks to idle groups (seconds)
@ -264,6 +277,14 @@ env.info("[DGB PLUGIN] Found " .. #zoneCaptureObjects .. " zones from DualCoalit
-- Track active markers to prevent memory leaks
local activeMarkers = {}
-- Zone Garrison Tracking System
-- Structure: zoneGarrisons[zoneName] = { defenders = {groupName1, groupName2, ...}, lastUpdate = timestamp }
local zoneGarrisons = {}
-- Group garrison assignments
-- Structure: groupGarrisonAssignments[groupName] = zoneName (or nil if not a defender)
local groupGarrisonAssignments = {}
-- Reusable SET_GROUP to prevent memory leaks from repeated creation
local cachedAllGroups = nil
local function getAllGroups()
@ -395,11 +416,172 @@ local function IsInfantryGroup(group)
return false
end
-- Function to check if a group is assigned as a zone defender
local function IsDefender(group)
if not group then return false end
local groupName = group:GetName()
return groupGarrisonAssignments[groupName] ~= nil
end
-- Function to get garrison info for a zone
local function GetZoneGarrison(zoneName)
if not zoneGarrisons[zoneName] then
zoneGarrisons[zoneName] = {
defenders = {},
lastUpdate = timer.getTime()
}
end
return zoneGarrisons[zoneName]
end
-- Function to count alive defenders in a zone
local function CountAliveDefenders(zoneName)
local garrison = GetZoneGarrison(zoneName)
local aliveCount = 0
local deadDefenders = {}
for _, groupName in ipairs(garrison.defenders) do
local group = GROUP:FindByName(groupName)
if group and group:IsAlive() then
aliveCount = aliveCount + 1
else
-- Mark for cleanup
table.insert(deadDefenders, groupName)
end
end
-- Clean up dead defenders
for _, deadGroupName in ipairs(deadDefenders) do
for i, groupName in ipairs(garrison.defenders) do
if groupName == deadGroupName then
table.remove(garrison.defenders, i)
groupGarrisonAssignments[deadGroupName] = nil
env.info(string.format("[DGB PLUGIN] Removed destroyed defender %s from zone %s", deadGroupName, zoneName))
break
end
end
end
return aliveCount
end
-- Function to elect a group as a zone defender
local function ElectDefender(group, zone, reason)
if not group or not zone then return false end
local groupName = group:GetName()
local zoneName = zone:GetName()
-- Check if already a defender
if IsDefender(group) then
return false
end
local garrison = GetZoneGarrison(zoneName)
-- Add to garrison
table.insert(garrison.defenders, groupName)
groupGarrisonAssignments[groupName] = zoneName
garrison.lastUpdate = timer.getTime()
-- Assign patrol task for the zone
group:PatrolZones({zone}, 20, "Cone", 30, 60)
env.info(string.format("[DGB PLUGIN] Elected %s as defender of zone %s (%s)", groupName, zoneName, reason))
return true
end
-- Function to check if a zone needs more defenders
local function ZoneNeedsDefenders(zoneName)
local aliveDefenders = CountAliveDefenders(zoneName)
return aliveDefenders < DEFENDERS_PER_ZONE
end
-- Function to handle defender rotation (replace old defender with fresh unit)
local function TryDefenderRotation(group, zone)
if not ALLOW_DEFENDER_ROTATION then return false end
local zoneName = zone:GetName()
local garrison = GetZoneGarrison(zoneName)
-- Count idle groups in zone (including current group)
local idleGroups = {}
local allGroups = getAllGroups()
allGroups:ForEachGroup(function(g)
if g and g:IsAlive() and g:GetCoalition() == group:GetCoalition() then
if g:IsCompletelyInZone(zone) then
local velocity = g:GetVelocityVec3()
local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
if speed <= 0.5 then
table.insert(idleGroups, g)
end
end
end
end)
-- Only rotate if we have more than DEFENDERS_PER_ZONE idle units
if #idleGroups > DEFENDERS_PER_ZONE then
-- Find oldest defender to replace
local oldestDefender = nil
local oldestDefenderGroup = nil
for _, defenderName in ipairs(garrison.defenders) do
local defenderGroup = GROUP:FindByName(defenderName)
if defenderGroup and defenderGroup:IsAlive() then
if not oldestDefender then
oldestDefender = defenderName
oldestDefenderGroup = defenderGroup
end
break -- Just take the first one for rotation
end
end
if oldestDefender and oldestDefenderGroup:GetName() ~= group:GetName() then
-- Remove old defender
for i, defenderName in ipairs(garrison.defenders) do
if defenderName == oldestDefender then
table.remove(garrison.defenders, i)
groupGarrisonAssignments[oldestDefender] = nil
env.info(string.format("[DGB PLUGIN] Rotated out defender %s from zone %s", oldestDefender, zoneName))
break
end
end
-- Elect new defender
ElectDefender(group, zone, "rotation")
-- Old defender becomes mobile force
return true
end
end
return false
end
local function AssignTasks(group, currentZoneCapture)
if not group or not group.GetCoalition or not group.GetCoordinate or not group.GetVelocityVec3 then
return
end
-- GARRISON SYSTEM: Defenders never leave their zone
if IsDefender(group) then
local assignedZoneName = groupGarrisonAssignments[group:GetName()]
if assignedZoneName then
-- Find the zone object
for idx, zoneCapture in ipairs(zoneCaptureObjects) do
local zone = zoneCapture:GetZone()
if zone and zone:GetName() == assignedZoneName then
-- Keep patrolling home zone
group:PatrolZones({zone}, 20, "Cone", 30, 60)
return
end
end
end
-- If we get here, the defender's zone was lost or not found, but they still stay put
return
end
-- Don't reassign if already moving
local velocity = group:GetVelocityVec3()
local speed = math.sqrt(velocity.x^2 + velocity.y^2 + velocity.z^2)
@ -411,9 +593,23 @@ local function AssignTasks(group, currentZoneCapture)
local groupCoordinate = group:GetCoordinate()
local currentZone = currentZoneCapture and currentZoneCapture:GetZone() or nil
-- If the group is sitting inside a friendly zone that is currently under attack,
-- keep them local so they fight for the objective instead of leaving it exposed.
if currentZoneCapture and currentZone and currentZoneCapture.GetCoalition and currentZoneCapture:GetCoalition() == groupCoalition then
-- GARRISON SYSTEM: Check if current zone needs defenders
if currentZoneCapture and currentZone and currentZoneCapture:GetCoalition() == groupCoalition then
local zoneName = currentZone:GetName()
-- Try to elect as defender if zone needs one
if ZoneNeedsDefenders(zoneName) then
if ElectDefender(group, currentZone, "zone under-garrisoned") then
return
end
else
-- Try rotation if enabled
if TryDefenderRotation(group, currentZone) then
return
end
end
-- If the zone is under attack, all units help defend (even non-defenders)
local zoneState = currentZoneCapture.GetCurrentState and currentZoneCapture:GetCurrentState() or nil
if zoneState == "Attacked" then
env.info(string.format("[DGB PLUGIN] %s defending contested zone %s", group:GetName(), currentZone:GetName()))
@ -454,6 +650,7 @@ local function AssignTasksToGroups()
env.info("[DGB PLUGIN] Starting task assignment cycle...")
local allGroups = getAllGroups()
local tasksAssigned = 0
local defendersActive = 0
allGroups:ForEachGroup(function(group)
if group and group:IsAlive() then
@ -474,18 +671,23 @@ local function AssignTasksToGroups()
end
if inFriendlyZone then
-- Skip infantry if movement is disabled
if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS then
-- Skip infantry if movement is disabled (unless they're defenders)
if IsInfantryGroup(group) and not MOVING_INFANTRY_PATROLS and not IsDefender(group) then
return
end
-- Count defenders
if IsDefender(group) then
defendersActive = defendersActive + 1
end
AssignTasks(group, currentZoneCapture)
tasksAssigned = tasksAssigned + 1
end
end
end)
env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked.", tasksAssigned))
env.info(string.format("[DGB PLUGIN] Task assignment complete. %d groups tasked (%d defenders).", tasksAssigned, defendersActive))
end
-- Function to monitor and announce warehouse status
@ -496,16 +698,184 @@ local function MonitorWarehouses()
local redSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(redWarehouses)
local blueSpawnFrequencyPercentage = CalculateSpawnFrequencyPercentage(blueWarehouses)
local msg = "[Warehouse Status]\n"
msg = msg .. "Red warehouses alive: " .. redWarehousesAlive .. " Reinforcements: " .. redSpawnFrequencyPercentage .. "%\n"
msg = msg .. "Blue warehouses alive: " .. blueWarehousesAlive .. " Reinforcements: " .. blueSpawnFrequencyPercentage .. "%\n"
MESSAGE:New(msg, 30):ToAll()
if ENABLE_WAREHOUSE_STATUS_MESSAGES then
local msg = "[Warehouse Status]\n"
msg = msg .. "Red warehouses alive: " .. redWarehousesAlive .. " Reinforcements: " .. redSpawnFrequencyPercentage .. "%\n"
msg = msg .. "Blue warehouses alive: " .. blueWarehousesAlive .. " Reinforcements: " .. blueSpawnFrequencyPercentage .. "%\n"
MESSAGE:New(msg, 30):ToAll()
end
env.info(string.format("[DGB PLUGIN] Warehouse status - Red: %d/%d (%d%%), Blue: %d/%d (%d%%)",
redWarehousesAlive, redWarehouseTotal, redSpawnFrequencyPercentage,
blueWarehousesAlive, blueWarehouseTotal, blueSpawnFrequencyPercentage))
end
-- Function to count active units by coalition and type
local function CountActiveUnits(targetCoalition)
local infantry = 0
local armor = 0
local total = 0
local defenders = 0
local mobile = 0
local allGroups = getAllGroups()
allGroups:ForEachGroup(function(group)
if group and group:IsAlive() and group:GetCoalition() == targetCoalition then
total = total + 1
if IsDefender(group) then
defenders = defenders + 1
else
mobile = mobile + 1
end
if IsInfantryGroup(group) then
infantry = infantry + 1
else
armor = armor + 1
end
end
end)
return {
total = total,
infantry = infantry,
armor = armor,
defenders = defenders,
mobile = mobile
}
end
-- Function to get garrison status across all zones
local function GetGarrisonStatus(targetCoalition)
local garrisonedZones = 0
local underGarrisonedZones = 0
local totalFriendlyZones = 0
for idx, zoneCapture in ipairs(zoneCaptureObjects) do
if zoneCapture:GetCoalition() == targetCoalition then
totalFriendlyZones = totalFriendlyZones + 1
local zone = zoneCapture:GetZone()
if zone then
local zoneName = zone:GetName()
local defenderCount = CountAliveDefenders(zoneName)
if defenderCount >= DEFENDERS_PER_ZONE then
garrisonedZones = garrisonedZones + 1
else
underGarrisonedZones = underGarrisonedZones + 1
end
end
end
end
return {
totalZones = totalFriendlyZones,
garrisoned = garrisonedZones,
underGarrisoned = underGarrisonedZones
}
end
-- Function to display comprehensive system statistics
local function ShowSystemStatistics(playerCoalition)
-- Get warehouse stats
local redWarehousesAlive, redWarehouseTotal = GetWarehouseStats(redWarehouses)
local blueWarehousesAlive, blueWarehouseTotal = GetWarehouseStats(blueWarehouses)
-- Get unit counts
local redUnits = CountActiveUnits(coalition.side.RED)
local blueUnits = CountActiveUnits(coalition.side.BLUE)
-- Get garrison info
local redGarrison = GetGarrisonStatus(coalition.side.RED)
local blueGarrison = GetGarrisonStatus(coalition.side.BLUE)
-- Get spawn frequencies
local redSpawnFreqPct = CalculateSpawnFrequencyPercentage(redWarehouses)
local blueSpawnFreqPct = CalculateSpawnFrequencyPercentage(blueWarehouses)
-- Calculate actual spawn intervals
local redInfantryInterval = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_INFANTRY, RED_INFANTRY_CADENCE_SCALAR)
local redArmorInterval = CalculateSpawnFrequency(redWarehouses, SPAWN_SCHED_RED_ARMOR, RED_ARMOR_CADENCE_SCALAR)
local blueInfantryInterval = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_INFANTRY, BLUE_INFANTRY_CADENCE_SCALAR)
local blueArmorInterval = CalculateSpawnFrequency(blueWarehouses, SPAWN_SCHED_BLUE_ARMOR, BLUE_ARMOR_CADENCE_SCALAR)
-- Build comprehensive report
local msg = "═══════════════════════════════════════\n"
msg = msg .. "DYNAMIC GROUND BATTLE - SYSTEM STATUS\n"
msg = msg .. "═══════════════════════════════════════\n\n"
-- Configuration Section
msg = msg .. "【CONFIGURATION】\n"
msg = msg .. " Defenders per Zone: " .. DEFENDERS_PER_ZONE .. "\n"
msg = msg .. " Defender Rotation: " .. (ALLOW_DEFENDER_ROTATION and "ENABLED" or "DISABLED") .. "\n"
msg = msg .. " Infantry Movement: " .. (MOVING_INFANTRY_PATROLS and "ENABLED" or "DISABLED") .. "\n"
msg = msg .. " Task Reassignment: Every " .. ASSIGN_TASKS_SCHED .. "s\n"
msg = msg .. " Warehouse Markers: " .. (ENABLE_WAREHOUSE_MARKERS and "ENABLED" or "DISABLED") .. "\n\n"
-- Spawn Limits Section
msg = msg .. "【SPAWN LIMITS】\n"
msg = msg .. " Red Infantry: " .. INIT_RED_INFANTRY .. "/" .. MAX_RED_INFANTRY .. "\n"
msg = msg .. " Red Armor: " .. INIT_RED_ARMOR .. "/" .. MAX_RED_ARMOR .. "\n"
msg = msg .. " Blue Infantry: " .. INIT_BLUE_INFANTRY .. "/" .. MAX_BLUE_INFANTRY .. "\n"
msg = msg .. " Blue Armor: " .. INIT_BLUE_ARMOR .. "/" .. MAX_BLUE_ARMOR .. "\n\n"
-- Red Coalition Section
msg = msg .. "【RED COALITION】\n"
msg = msg .. " Warehouses: " .. redWarehousesAlive .. "/" .. redWarehouseTotal .. " (" .. redSpawnFreqPct .. "%)\n"
msg = msg .. " Active Units: " .. redUnits.total .. " (" .. redUnits.infantry .. " inf, " .. redUnits.armor .. " armor)\n"
msg = msg .. " Defenders: " .. redUnits.defenders .. " | Mobile: " .. redUnits.mobile .. "\n"
msg = msg .. " Controlled Zones: " .. redGarrison.totalZones .. "\n"
msg = msg .. " - Garrisoned: " .. redGarrison.garrisoned .. "\n"
msg = msg .. " - Under-Garrisoned: " .. redGarrison.underGarrisoned .. "\n"
if redInfantryInterval then
msg = msg .. " Infantry Spawn: " .. math.floor(redInfantryInterval) .. "s\n"
else
msg = msg .. " Infantry Spawn: PAUSED (no warehouses)\n"
end
if redArmorInterval then
msg = msg .. " Armor Spawn: " .. math.floor(redArmorInterval) .. "s\n\n"
else
msg = msg .. " Armor Spawn: PAUSED (no warehouses)\n\n"
end
-- Blue Coalition Section
msg = msg .. "【BLUE COALITION】\n"
msg = msg .. " Warehouses: " .. blueWarehousesAlive .. "/" .. blueWarehouseTotal .. " (" .. blueSpawnFreqPct .. "%)\n"
msg = msg .. " Active Units: " .. blueUnits.total .. " (" .. blueUnits.infantry .. " inf, " .. blueUnits.armor .. " armor)\n"
msg = msg .. " Defenders: " .. blueUnits.defenders .. " | Mobile: " .. blueUnits.mobile .. "\n"
msg = msg .. " Controlled Zones: " .. blueGarrison.totalZones .. "\n"
msg = msg .. " - Garrisoned: " .. blueGarrison.garrisoned .. "\n"
msg = msg .. " - Under-Garrisoned: " .. blueGarrison.underGarrisoned .. "\n"
if blueInfantryInterval then
msg = msg .. " Infantry Spawn: " .. math.floor(blueInfantryInterval) .. "s\n"
else
msg = msg .. " Infantry Spawn: PAUSED (no warehouses)\n"
end
if blueArmorInterval then
msg = msg .. " Armor Spawn: " .. math.floor(blueArmorInterval) .. "s\n\n"
else
msg = msg .. " Armor Spawn: PAUSED (no warehouses)\n\n"
end
-- System Info
msg = msg .. "【SYSTEM INFO】\n"
msg = msg .. " Total Zones: " .. #zoneCaptureObjects .. "\n"
msg = msg .. " Active Garrisons: " .. (redGarrison.garrisoned + blueGarrison.garrisoned) .. "\n"
msg = msg .. " Total Active Units: " .. (redUnits.total + blueUnits.total) .. "\n\n"
msg = msg .. "═══════════════════════════════════════"
MESSAGE:New(msg, 45):ToCoalition(playerCoalition)
env.info("[DGB PLUGIN] System statistics displayed to coalition " .. playerCoalition)
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- INITIALIZATION
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -573,38 +943,55 @@ blueArmorSpawn = SPAWN:New(BLUE_ARMOR_SPAWN_GROUP)
-- Helper to schedule spawns per category so each uses its intended cadence.
local function ScheduleSpawner(spawnObject, getZonesFn, warehouses, baseFrequency, label, cadenceScalar)
local scheduler
local lastSpawnTime = 0
local checkInterval = 10 -- Check every 10 seconds if it's time to spawn
local function spawnCycle()
local nextInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar)
local function spawnCheck()
local currentTime = timer.getTime()
local spawnInterval = CalculateSpawnFrequency(warehouses, baseFrequency, cadenceScalar)
if not nextInterval then
env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive)", label))
if scheduler then
scheduler:Stop()
scheduler:Start(NO_WAREHOUSE_RECHECK_DELAY, NO_WAREHOUSE_RECHECK_DELAY)
if not spawnInterval then
-- No warehouses alive - use recheck delay
spawnInterval = NO_WAREHOUSE_RECHECK_DELAY
if currentTime - lastSpawnTime >= spawnInterval then
env.info(string.format("[DGB PLUGIN] %s spawn paused (no warehouses alive, will recheck)", label))
lastSpawnTime = currentTime
end
return
end
local friendlyZones = getZonesFn()
local zonesAvailable = #friendlyZones
-- Check if enough time has passed to spawn
if currentTime - lastSpawnTime >= spawnInterval then
local friendlyZones = getZonesFn()
local zonesAvailable = #friendlyZones
if zonesAvailable > 0 then
local chosenZone = friendlyZones[math.random(zonesAvailable)]
spawnObject:SpawnInZone(chosenZone, false)
else
env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label))
end
if scheduler then
scheduler:Stop()
scheduler:Start(nextInterval, nextInterval)
if zonesAvailable > 0 then
local chosenZone = friendlyZones[math.random(zonesAvailable)]
local spawnedGroup = spawnObject:SpawnInZone(chosenZone, false)
-- Check if the spawn zone needs defenders and auto-elect if so
if spawnedGroup then
local zoneName = chosenZone:GetName()
if ZoneNeedsDefenders(zoneName) then
SCHEDULER:New(nil, function()
local grp = GROUP:FindByName(spawnedGroup:GetName())
if grp and grp:IsAlive() then
ElectDefender(grp, chosenZone, "spawn in under-garrisoned zone")
end
end, {}, 2) -- Delay 2 seconds to ensure group is fully initialized
end
end
lastSpawnTime = currentTime
else
env.info(string.format("[DGB PLUGIN] %s spawn skipped (no friendly zones)", label))
lastSpawnTime = currentTime -- Reset timer even if no zones available
end
end
end
local initialFrequency = baseFrequency * (cadenceScalar or 1)
scheduler = SCHEDULER:New(nil, spawnCycle, {}, math.random(5, 15), initialFrequency)
-- Single scheduler that runs continuously at fixed check interval
local scheduler = SCHEDULER:New(nil, spawnCheck, {}, math.random(5, 15), checkInterval)
return scheduler
end
@ -620,7 +1007,9 @@ if ENABLE_WAREHOUSE_MARKERS then
end
-- Schedule warehouse monitoring
SCHEDULER:New(nil, MonitorWarehouses, {}, 30, 120)
if ENABLE_WAREHOUSE_STATUS_MESSAGES then
SCHEDULER:New(nil, MonitorWarehouses, {}, 30, WAREHOUSE_STATUS_MESSAGE_FREQUENCY)
end
-- Schedule task assignments
SCHEDULER:New(nil, AssignTasksToGroups, {}, 120, ASSIGN_TASKS_SCHED)
@ -630,15 +1019,29 @@ if MenuManager then
-- Create coalition-specific menus under Mission Options
local blueMenu = MenuManager.CreateCoalitionMenu(coalition.side.BLUE, "Ground Battle")
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Check Warehouse Status", blueMenu, MonitorWarehouses)
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Show System Statistics", blueMenu, function()
ShowSystemStatistics(coalition.side.BLUE)
end)
local redMenu = MenuManager.CreateCoalitionMenu(coalition.side.RED, "Ground Battle")
MENU_COALITION_COMMAND:New(coalition.side.RED, "Check Warehouse Status", redMenu, MonitorWarehouses)
MENU_COALITION_COMMAND:New(coalition.side.RED, "Show System Statistics", redMenu, function()
ShowSystemStatistics(coalition.side.RED)
end)
else
-- Fallback to root-level mission menu
local missionMenu = MENU_MISSION:New("Ground Battle")
MENU_MISSION_COMMAND:New("Check Warehouse Status", missionMenu, MonitorWarehouses)
MENU_MISSION_COMMAND:New("Show Blue Statistics", missionMenu, function()
ShowSystemStatistics(coalition.side.BLUE)
end)
MENU_MISSION_COMMAND:New("Show Red Statistics", missionMenu, function()
ShowSystemStatistics(coalition.side.RED)
end)
end
env.info("[DGB PLUGIN] Dynamic Ground Battle Plugin initialized successfully!")
env.info(string.format("[DGB PLUGIN] Zone garrison system: %d defenders per zone", DEFENDERS_PER_ZONE))
env.info(string.format("[DGB PLUGIN] Defender rotation: %s", ALLOW_DEFENDER_ROTATION and "ENABLED" or "DISABLED"))
env.info(string.format("[DGB PLUGIN] Infantry movement: %s", MOVING_INFANTRY_PATROLS and "ENABLED" or "DISABLED"))
env.info(string.format("[DGB PLUGIN] Warehouse markers: %s", ENABLE_WAREHOUSE_MARKERS and "ENABLED" or "DISABLED"))