mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Performance enhancements.
This commit is contained in:
parent
c91f427ff3
commit
f169d825d9
Binary file not shown.
20
Moose_.lua
20
Moose_.lua
@ -1,4 +1,4 @@
|
|||||||
env.info('*** MOOSE GITHUB Commit Hash ID: 2025-11-09T22:02:44+01:00-16706cd4830e5c7855caca65db1400ef7b4aada0 ***')
|
env.info('*** MOOSE GITHUB Commit Hash ID: 2025-11-11T12:57:41+01:00-d7b0b3c898fb636dd8b728721e247763383a5bdb ***')
|
||||||
if not MOOSE_DEVELOPMENT_FOLDER then
|
if not MOOSE_DEVELOPMENT_FOLDER then
|
||||||
MOOSE_DEVELOPMENT_FOLDER='Scripts'
|
MOOSE_DEVELOPMENT_FOLDER='Scripts'
|
||||||
end
|
end
|
||||||
@ -35093,13 +35093,29 @@ end
|
|||||||
end
|
end
|
||||||
self.LastPosition=pos
|
self.LastPosition=pos
|
||||||
else
|
else
|
||||||
if self.timer and self.timer:IsRunning()then self.timer:Stop()end
|
if self.timer and self.timer:IsRunning()then
|
||||||
|
self.timer:Stop()
|
||||||
|
self.timer=nil
|
||||||
|
end
|
||||||
self:T(self.lid.." dead! "..self.CargoState.."-> REMOVED")
|
self:T(self.lid.." dead! "..self.CargoState.."-> REMOVED")
|
||||||
self.CargoState=DYNAMICCARGO.State.REMOVED
|
self.CargoState=DYNAMICCARGO.State.REMOVED
|
||||||
_DATABASE:CreateEventDynamicCargoRemoved(self)
|
_DATABASE:CreateEventDynamicCargoRemoved(self)
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
function DYNAMICCARGO:Destroy(GenerateEvent)
|
||||||
|
local DCSObject=self:GetDCSObject()
|
||||||
|
if DCSObject then
|
||||||
|
local GenerateEvent=(GenerateEvent~=nil and GenerateEvent==false)and false or true
|
||||||
|
if GenerateEvent and GenerateEvent==true then
|
||||||
|
self:CreateEventDead(timer.getTime(),DCSObject)
|
||||||
|
end
|
||||||
|
DCSObject:destroy()
|
||||||
|
self:_UpdatePosition()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
function DYNAMICCARGO._FilterHeloTypes(client)
|
function DYNAMICCARGO._FilterHeloTypes(client)
|
||||||
if not client then return false end
|
if not client then return false end
|
||||||
local typename=client:GetTypeName()
|
local typename=client:GetTypeName()
|
||||||
|
|||||||
@ -12,26 +12,11 @@
|
|||||||
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
|
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
|
||||||
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
|
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
|
||||||
|
|
||||||
-- Table of Contents (navigation)
|
|
||||||
-- 1) Config (version, messaging, main Config table)
|
|
||||||
-- 2) State
|
|
||||||
-- 3) Utilities
|
|
||||||
-- 4) Construction (zones, bindings, init)
|
|
||||||
-- 5) Menus (group/coalition, dynamic lists)
|
|
||||||
-- 6) Coalition Summary
|
|
||||||
-- 7) Crates (request/spawn, nearby, cleanup)
|
|
||||||
-- 8) Build logic
|
|
||||||
-- 9) Loaded crate management
|
|
||||||
-- 10) Hover pickup scanner
|
|
||||||
-- 11) Troops
|
|
||||||
-- 12) Auto-build FOB in zones
|
|
||||||
-- 13) Inventory helpers
|
|
||||||
-- 14) Public helpers (catalog registration/merge)
|
|
||||||
-- 15) Export
|
|
||||||
-- #region Config
|
-- #region Config
|
||||||
|
|
||||||
local CTLD = {}
|
local CTLD = {}
|
||||||
CTLD.__index = CTLD
|
CTLD.__index = CTLD
|
||||||
|
CTLD._lastSalvageInterval = CTLD._lastSalvageInterval or 0
|
||||||
|
|
||||||
-- General CTLD event messages (non-hover). Tweak freely.
|
-- General CTLD event messages (non-hover). Tweak freely.
|
||||||
CTLD.Messages = {
|
CTLD.Messages = {
|
||||||
@ -431,13 +416,15 @@ CTLD.Config = {
|
|||||||
HeavyDamage = 0.5, -- < 50% health
|
HeavyDamage = 0.5, -- < 50% health
|
||||||
},
|
},
|
||||||
|
|
||||||
CrateLifetime = 10800, -- 3 hours (seconds)
|
CrateLifetime = 3600, -- 1 hour (seconds)
|
||||||
WarningTimes = { 1800, 300 }, -- Warn at 30min and 5min remaining
|
WarningTimes = { 1800, 300 }, -- Warn at 30min and 5min remaining
|
||||||
|
|
||||||
-- Visual indicators
|
-- Visual indicators
|
||||||
SpawnSmoke = false,
|
SpawnSmoke = false,
|
||||||
SmokeDuration = 120, -- 2 minutes
|
SmokeDuration = 120, -- 2 minutes
|
||||||
SmokeColor = trigger.smokeColor.Orange,
|
SmokeColor = trigger.smokeColor.Orange,
|
||||||
|
MaxActiveCrates = 40, -- hard cap on simultaneously spawned salvage crates per coalition
|
||||||
|
AdaptiveIntervals = { idle = 10, low = 10, medium = 15, high = 20 },
|
||||||
|
|
||||||
-- Spawn restrictions
|
-- Spawn restrictions
|
||||||
MinSpawnDistance = 25, -- meters from death location
|
MinSpawnDistance = 25, -- meters from death location
|
||||||
@ -458,6 +445,8 @@ CTLD.Config = {
|
|||||||
|
|
||||||
-- Salvage Collection Zone defaults
|
-- Salvage Collection Zone defaults
|
||||||
DefaultZoneRadius = 300,
|
DefaultZoneRadius = 300,
|
||||||
|
DynamicZoneLifetime = 5400, -- seconds a player-created zone stays active (0 disables auto-expiry)
|
||||||
|
MaxDynamicZones = 6, -- cap player-created zones per coalition instance (oldest retire first)
|
||||||
ZoneColors = {
|
ZoneColors = {
|
||||||
border = {1, 0.5, 0, 0.85}, -- orange border
|
border = {1, 0.5, 0, 0.85}, -- orange border
|
||||||
fill = {1, 0.5, 0, 0.15}, -- light orange fill
|
fill = {1, 0.5, 0, 0.15}, -- light orange fill
|
||||||
@ -2384,6 +2373,205 @@ function CTLD:_cancelSchedule(key)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function _removeMenuHandle(menu)
|
||||||
|
if not menu then return end
|
||||||
|
if type(menu) ~= 'table' then return end
|
||||||
|
if menu.Remove then pcall(function() menu:Remove() end) end
|
||||||
|
if menu.Destroy then pcall(function() menu:Destroy() end) end
|
||||||
|
if menu.Delete then pcall(function() menu:Delete() end) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _countTableEntries(t)
|
||||||
|
if not t then return 0 end
|
||||||
|
local n = 0
|
||||||
|
for _ in pairs(t) do n = n + 1 end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _cleanupGroupMenus(ctld, groupName)
|
||||||
|
if not (ctld and groupName) then return end
|
||||||
|
if ctld.MenusByGroup and ctld.MenusByGroup[groupName] then
|
||||||
|
_removeMenuHandle(ctld.MenusByGroup[groupName])
|
||||||
|
ctld.MenusByGroup[groupName] = nil
|
||||||
|
end
|
||||||
|
if CTLD._inStockMenus and CTLD._inStockMenus[groupName] then
|
||||||
|
for _, menu in pairs(CTLD._inStockMenus[groupName]) do
|
||||||
|
_removeMenuHandle(menu)
|
||||||
|
end
|
||||||
|
CTLD._inStockMenus[groupName] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _clearPerGroupCaches(groupName)
|
||||||
|
if not groupName then return end
|
||||||
|
local caches = {
|
||||||
|
CTLD._troopsLoaded,
|
||||||
|
CTLD._loadedCrates,
|
||||||
|
CTLD._loadedTroopTypes,
|
||||||
|
CTLD._deployedTroops,
|
||||||
|
CTLD._buildConfirm,
|
||||||
|
CTLD._buildCooldown,
|
||||||
|
CTLD._medevacUnloadStates,
|
||||||
|
CTLD._medevacLoadStates,
|
||||||
|
CTLD._medevacEnrouteStates,
|
||||||
|
CTLD._coachOverride,
|
||||||
|
}
|
||||||
|
for _, tbl in ipairs(caches) do
|
||||||
|
if tbl then tbl[groupName] = nil end
|
||||||
|
end
|
||||||
|
if CTLD._msgState then
|
||||||
|
CTLD._msgState['GRP:'..groupName] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _clearPerUnitCachesForGroup(group)
|
||||||
|
if not group then return end
|
||||||
|
local units
|
||||||
|
local ok, res = pcall(function() return group:GetUnits() end)
|
||||||
|
if ok and type(res) == 'table' then
|
||||||
|
units = res
|
||||||
|
else
|
||||||
|
local okUnit, unit = pcall(function() return group:GetUnit(1) end)
|
||||||
|
if okUnit and unit then units = { unit } end
|
||||||
|
end
|
||||||
|
if not units then return end
|
||||||
|
for _, unit in ipairs(units) do
|
||||||
|
if unit then
|
||||||
|
local uname = unit.GetName and unit:GetName()
|
||||||
|
if uname then
|
||||||
|
if CTLD._hoverState then CTLD._hoverState[uname] = nil end
|
||||||
|
if CTLD._unitLast then CTLD._unitLast[uname] = nil end
|
||||||
|
if CTLD._coachState then CTLD._coachState[uname] = nil end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _groupHasAliveTransport(group, allowedTypes)
|
||||||
|
if not (group and allowedTypes) then return false end
|
||||||
|
local units
|
||||||
|
local ok, res = pcall(function() return group:GetUnits() end)
|
||||||
|
if ok and type(res) == 'table' then
|
||||||
|
units = res
|
||||||
|
else
|
||||||
|
local okUnit, unit = pcall(function() return group:GetUnit(1) end)
|
||||||
|
if okUnit and unit then units = { unit } end
|
||||||
|
end
|
||||||
|
if not units then return false end
|
||||||
|
for _, unit in ipairs(units) do
|
||||||
|
if unit and unit.IsAlive and unit:IsAlive() then
|
||||||
|
local typ = _getUnitType(unit)
|
||||||
|
if typ and _isIn(allowedTypes, typ) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:_cleanupTransportGroup(group, groupName)
|
||||||
|
local gname = groupName
|
||||||
|
if not gname and group and group.GetName then
|
||||||
|
gname = group:GetName()
|
||||||
|
end
|
||||||
|
if not gname then return end
|
||||||
|
|
||||||
|
_cleanupGroupMenus(self, gname)
|
||||||
|
_clearPerGroupCaches(gname)
|
||||||
|
|
||||||
|
if group then
|
||||||
|
_clearPerUnitCachesForGroup(group)
|
||||||
|
else
|
||||||
|
local mooseGroup = nil
|
||||||
|
if GROUP and GROUP.FindByName then
|
||||||
|
local ok, res = pcall(function() return GROUP:FindByName(gname) end)
|
||||||
|
if ok then mooseGroup = res end
|
||||||
|
end
|
||||||
|
if mooseGroup then _clearPerUnitCachesForGroup(mooseGroup) end
|
||||||
|
end
|
||||||
|
|
||||||
|
_logDebug(string.format('[MenuCleanup] Cleared CTLD state for group %s', gname))
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:_removeDynamicSalvageZone(zoneName, reason)
|
||||||
|
if not zoneName then return end
|
||||||
|
|
||||||
|
if self.SalvageDropZones then
|
||||||
|
for idx = #self.SalvageDropZones, 1, -1 do
|
||||||
|
local zone = self.SalvageDropZones[idx]
|
||||||
|
if zone and zone.GetName and zone:GetName() == zoneName then
|
||||||
|
table.remove(self.SalvageDropZones, idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._ZoneDefs and self._ZoneDefs.SalvageDropZones then
|
||||||
|
self._ZoneDefs.SalvageDropZones[zoneName] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._ZoneActive and self._ZoneActive.SalvageDrop then
|
||||||
|
self._ZoneActive.SalvageDrop[zoneName] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._DynamicSalvageZones then
|
||||||
|
self._DynamicSalvageZones[zoneName] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if self._DynamicSalvageQueue then
|
||||||
|
for idx = #self._DynamicSalvageQueue, 1, -1 do
|
||||||
|
if self._DynamicSalvageQueue[idx] == zoneName then
|
||||||
|
table.remove(self._DynamicSalvageQueue, idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:_removeZoneDrawing('SalvageDrop', zoneName)
|
||||||
|
_logInfo(string.format('[SlingLoadSalvage] Removed dynamic salvage zone %s (%s)', zoneName, reason or 'cleanup'))
|
||||||
|
local ok, err = pcall(function() self:DrawZonesOnMap() end)
|
||||||
|
if not ok then
|
||||||
|
_logError(string.format('[SlingLoadSalvage] DrawZonesOnMap failed after removing %s: %s', zoneName, tostring(err)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:_enforceDynamicSalvageZoneLimit()
|
||||||
|
local cfg = self.Config and self.Config.SlingLoadSalvage or nil
|
||||||
|
if not cfg or not cfg.Enabled then return end
|
||||||
|
|
||||||
|
local zones = self._DynamicSalvageZones
|
||||||
|
if not zones then return end
|
||||||
|
|
||||||
|
local lifetime = tonumber(cfg.DynamicZoneLifetime or 0) or 0
|
||||||
|
local maxZones = tonumber(cfg.MaxDynamicZones or 0) or 0
|
||||||
|
local now = timer and timer.getTime and timer.getTime() or 0
|
||||||
|
|
||||||
|
if lifetime > 0 then
|
||||||
|
local expired = {}
|
||||||
|
for name, meta in pairs(zones) do
|
||||||
|
if meta then
|
||||||
|
local expiresAt = meta.expiresAt
|
||||||
|
if not expiresAt and meta.createdAt then
|
||||||
|
expiresAt = meta.createdAt + lifetime
|
||||||
|
end
|
||||||
|
if expiresAt and now >= expiresAt then
|
||||||
|
table.insert(expired, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, zname in ipairs(expired) do
|
||||||
|
self:_removeDynamicSalvageZone(zname, 'expired')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if maxZones > 0 and self._DynamicSalvageQueue then
|
||||||
|
while #self._DynamicSalvageQueue > maxZones do
|
||||||
|
local oldest = table.remove(self._DynamicSalvageQueue, 1)
|
||||||
|
if oldest and zones[oldest] then
|
||||||
|
self:_removeDynamicSalvageZone(oldest, 'max-cap')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Global smoke refresh ticker (single loop for all crates)
|
-- Global smoke refresh ticker (single loop for all crates)
|
||||||
function CTLD:_ensureGlobalSmokeTicker()
|
function CTLD:_ensureGlobalSmokeTicker()
|
||||||
if self._schedules and self._schedules.smokeTicker then return end
|
if self._schedules and self._schedules.smokeTicker then return end
|
||||||
@ -2471,6 +2659,266 @@ function CTLD:_ensureBackgroundTasks()
|
|||||||
self._bgStarted = true
|
self._bgStarted = true
|
||||||
self:_ensureGlobalSmokeTicker()
|
self:_ensureGlobalSmokeTicker()
|
||||||
self:_ensurePeriodicGC()
|
self:_ensurePeriodicGC()
|
||||||
|
self:_ensureAdaptiveBackgroundLoop()
|
||||||
|
self:_startHourlyDiagnostics()
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:_startHoverScheduler()
|
||||||
|
local coachCfg = CTLD.HoverCoachConfig or {}
|
||||||
|
if not coachCfg.enabled or self.HoverSched then return end
|
||||||
|
local interval = coachCfg.interval or 0.75
|
||||||
|
local startDelay = coachCfg.startDelay or interval
|
||||||
|
self.HoverSched = SCHEDULER:New(nil, function()
|
||||||
|
local ok, err = pcall(function() self:ScanHoverPickup() end)
|
||||||
|
if not ok then _logError('HoverSched ScanHoverPickup error: '..tostring(err)) end
|
||||||
|
end, {}, startDelay, interval)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Adaptive background loop consolidating salvage checks and periodic pruning
|
||||||
|
function CTLD:_ensureAdaptiveBackgroundLoop()
|
||||||
|
if self._schedules and self._schedules.backgroundLoop then return end
|
||||||
|
|
||||||
|
local function backgroundTick()
|
||||||
|
local now = timer.getTime()
|
||||||
|
|
||||||
|
-- Salvage crate housekeeping
|
||||||
|
local cfg = self.Config.SlingLoadSalvage
|
||||||
|
local activeCrates = 0
|
||||||
|
if cfg and cfg.Enabled then
|
||||||
|
for _, meta in pairs(CTLD._salvageCrates) do
|
||||||
|
if meta and meta.side == self.Side then
|
||||||
|
activeCrates = activeCrates + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run the standard checker to handle delivery/expiration
|
||||||
|
local ok, err = pcall(function() self:_CheckSlingLoadSalvageCrates() end)
|
||||||
|
if not ok then
|
||||||
|
_logError('[SlingLoadSalvage] backgroundTick error: '..tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stale crate cleanup: destroy any crate that lost its static object reference
|
||||||
|
for cname, meta in pairs(CTLD._salvageCrates) do
|
||||||
|
if meta and (not meta.staticObject or (meta.staticObject.destroy and not meta.staticObject:isExist())) then
|
||||||
|
CTLD._salvageCrates[cname] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Dynamic salvage zone lifetime enforcement
|
||||||
|
self:_enforceDynamicSalvageZoneLimit()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hover coach cleanup: remove entries for missing units
|
||||||
|
if CTLD._coachState then
|
||||||
|
for uname, _ in pairs(CTLD._coachState) do
|
||||||
|
if not Unit.getByName(uname) then
|
||||||
|
CTLD._coachState[uname] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- If hover coach now empty, stop its scheduler
|
||||||
|
if next(CTLD._coachState) == nil then
|
||||||
|
if self.HoverSched and self.HoverSched.Stop then
|
||||||
|
pcall(function() self.HoverSched:Stop() end)
|
||||||
|
end
|
||||||
|
self.HoverSched = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Determine next wake interval based on active salvage crates
|
||||||
|
local baseInterval = (cfg and cfg.DetectionInterval) or 5
|
||||||
|
local adaptive = (cfg and cfg.AdaptiveIntervals) or {}
|
||||||
|
local nextInterval
|
||||||
|
if activeCrates == 0 then
|
||||||
|
nextInterval = adaptive.idle or math.max(baseInterval * 2, 10)
|
||||||
|
elseif activeCrates <= 20 then
|
||||||
|
nextInterval = adaptive.low or math.max(baseInterval * 2, 10)
|
||||||
|
elseif activeCrates <= 35 then
|
||||||
|
nextInterval = adaptive.medium or math.max(baseInterval * 3, 15)
|
||||||
|
else
|
||||||
|
nextInterval = adaptive.high or math.max(baseInterval * 4, 20)
|
||||||
|
end
|
||||||
|
nextInterval = math.min(nextInterval, 30)
|
||||||
|
|
||||||
|
CTLD._lastSalvageInterval = nextInterval
|
||||||
|
|
||||||
|
return now + nextInterval
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = timer.scheduleFunction(function()
|
||||||
|
local nextTime = backgroundTick()
|
||||||
|
return nextTime
|
||||||
|
end, nil, timer.getTime() + 2)
|
||||||
|
self:_registerSchedule('backgroundLoop', id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:_startHourlyDiagnostics()
|
||||||
|
if not (timer and timer.scheduleFunction) then return end
|
||||||
|
if self._schedules and self._schedules.hourlyDiagnostics then return end
|
||||||
|
|
||||||
|
local function diagTick()
|
||||||
|
local salvageCount = 0
|
||||||
|
for _, _ in ipairs(self.SalvageDropZones or {}) do salvageCount = salvageCount + 1 end
|
||||||
|
local menuCount = _countTableEntries(self.MenusByGroup)
|
||||||
|
local dynamicCount = _countTableEntries(self._DynamicSalvageZones)
|
||||||
|
local sideLabel = (self.Side == coalition.side.BLUE and 'BLUE')
|
||||||
|
or (self.Side == coalition.side.RED and 'RED')
|
||||||
|
or (self.Side == coalition.side.NEUTRAL and 'NEUTRAL')
|
||||||
|
or tostring(self.Side)
|
||||||
|
env.info(string.format('[CTLD][SoakTest][%s] salvageZones=%d dynamicZones=%d menus=%d',
|
||||||
|
sideLabel, salvageCount, dynamicCount, menuCount))
|
||||||
|
return timer.getTime() + 3600
|
||||||
|
end
|
||||||
|
|
||||||
|
local id = timer.scheduleFunction(function()
|
||||||
|
return diagTick()
|
||||||
|
end, nil, timer.getTime() + 3600)
|
||||||
|
self:_registerSchedule('hourlyDiagnostics', id)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _ctldHelperGet()
|
||||||
|
local ctldClass = _G._MOOSE_CTLD or CTLD
|
||||||
|
if not ctldClass then
|
||||||
|
env.info('[CTLD] Runtime helper unavailable: CTLD not loaded')
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return ctldClass
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _ctldSideName(side)
|
||||||
|
if side == coalition.side.BLUE then return 'BLUE' end
|
||||||
|
if side == coalition.side.RED then return 'RED' end
|
||||||
|
if side == coalition.side.NEUTRAL then return 'NEUTRAL' end
|
||||||
|
return tostring(side or 'nil')
|
||||||
|
end
|
||||||
|
|
||||||
|
local function _ctldFormatSeconds(secs)
|
||||||
|
if not secs or secs <= 0 then return '0s' end
|
||||||
|
local minutes = math.floor(secs / 60)
|
||||||
|
local seconds = math.floor(secs % 60)
|
||||||
|
if minutes > 0 then
|
||||||
|
return string.format('%dm%02ds', minutes, seconds)
|
||||||
|
end
|
||||||
|
return string.format('%ds', seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.CTLD_DumpRuntimeStats then
|
||||||
|
function _G.CTLD_DumpRuntimeStats()
|
||||||
|
local ctldClass = _ctldHelperGet()
|
||||||
|
if not ctldClass then return end
|
||||||
|
|
||||||
|
local now = timer and timer.getTime and timer.getTime() or 0
|
||||||
|
local salvageBySide = {}
|
||||||
|
local oldestBySide = {}
|
||||||
|
for name, meta in pairs(ctldClass._salvageCrates or {}) do
|
||||||
|
if meta and meta.side then
|
||||||
|
salvageBySide[meta.side] = (salvageBySide[meta.side] or 0) + 1
|
||||||
|
if meta.spawnTime then
|
||||||
|
local age = now - meta.spawnTime
|
||||||
|
if age > (oldestBySide[meta.side] or 0) then
|
||||||
|
oldestBySide[meta.side] = age
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local medevacCount = 0
|
||||||
|
for _ in pairs(ctldClass._medevacCrews or {}) do
|
||||||
|
medevacCount = medevacCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
env.info(string.format('[CTLD] Active salvage crates: BLUE=%d RED=%d',
|
||||||
|
salvageBySide[coalition.side.BLUE] or 0,
|
||||||
|
salvageBySide[coalition.side.RED] or 0))
|
||||||
|
env.info(string.format('[CTLD] Oldest salvage crate age: BLUE=%s RED=%s',
|
||||||
|
_ctldFormatSeconds(oldestBySide[coalition.side.BLUE] or 0),
|
||||||
|
_ctldFormatSeconds(oldestBySide[coalition.side.RED] or 0)))
|
||||||
|
env.info(string.format('[CTLD] Active MEDEVAC crews: %d', medevacCount))
|
||||||
|
|
||||||
|
local totalSchedules = 0
|
||||||
|
local instanceCount = 0
|
||||||
|
for _, inst in ipairs(ctldClass._instances or {}) do
|
||||||
|
instanceCount = instanceCount + 1
|
||||||
|
if inst._schedules then
|
||||||
|
for _ in pairs(inst._schedules) do
|
||||||
|
totalSchedules = totalSchedules + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for key, value in pairs(inst) do
|
||||||
|
if type(value) == 'table' and value.Stop and key:match('Sched') then
|
||||||
|
totalSchedules = totalSchedules + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.info(string.format('[CTLD] Instances: %d, scheduler objects (approx): %d', instanceCount, totalSchedules))
|
||||||
|
env.info(string.format('[CTLD] Last salvage loop interval: %s', _ctldFormatSeconds(ctldClass._lastSalvageInterval or 0)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.CTLD_DumpCrateAges then
|
||||||
|
function _G.CTLD_DumpCrateAges()
|
||||||
|
local ctldClass = _ctldHelperGet()
|
||||||
|
if not ctldClass then return end
|
||||||
|
|
||||||
|
local crates = {}
|
||||||
|
local now = timer and timer.getTime and timer.getTime() or 0
|
||||||
|
local lifetime = (ctldClass.Config and ctldClass.Config.SlingLoadSalvage and ctldClass.Config.SlingLoadSalvage.CrateLifetime) or 0
|
||||||
|
for name, meta in pairs(ctldClass._salvageCrates or {}) do
|
||||||
|
local age = meta.spawnTime and (now - meta.spawnTime) or 0
|
||||||
|
table.insert(crates, {
|
||||||
|
name = name,
|
||||||
|
side = meta.side,
|
||||||
|
age = age,
|
||||||
|
remaining = math.max(0, lifetime - age),
|
||||||
|
weight = meta.weight or 0,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(crates, function(a, b) return a.age > b.age end)
|
||||||
|
|
||||||
|
env.info(string.format('[CTLD] Listing %d salvage crates (lifetime=%ss)', #crates, tostring(lifetime)))
|
||||||
|
for i = 1, math.min(#crates, 25) do
|
||||||
|
local c = crates[i]
|
||||||
|
env.info(string.format('[CTLD] #%02d %s side=%s weight=%dkg age=%s remaining=%s',
|
||||||
|
i, c.name, _ctldSideName(c.side), c.weight,
|
||||||
|
_ctldFormatSeconds(c.age), _ctldFormatSeconds(c.remaining)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.CTLD_ListSchedules then
|
||||||
|
function _G.CTLD_ListSchedules()
|
||||||
|
local ctldClass = _ctldHelperGet()
|
||||||
|
if not ctldClass then return end
|
||||||
|
|
||||||
|
local instances = ctldClass._instances or {}
|
||||||
|
if #instances == 0 then
|
||||||
|
env.info('[CTLD] No CTLD instances registered')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for idx, inst in ipairs(instances) do
|
||||||
|
local sideLabel = _ctldSideName(inst.Side)
|
||||||
|
env.info(string.format('[CTLD] Instance #%d side=%s', idx, sideLabel))
|
||||||
|
|
||||||
|
if inst._schedules then
|
||||||
|
for key, funcId in pairs(inst._schedules) do
|
||||||
|
env.info(string.format(' [timer] key=%s funcId=%s', tostring(key), tostring(funcId)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for key, value in pairs(inst) do
|
||||||
|
if type(value) == 'table' and value.Stop and key:match('Sched') then
|
||||||
|
local running = 'unknown'
|
||||||
|
if type(value.IsRunning) == 'function' then
|
||||||
|
local ok, res = pcall(value.IsRunning, value)
|
||||||
|
if ok then running = tostring(res) end
|
||||||
|
end
|
||||||
|
env.info(string.format(' [sched] key=%s running=%s', tostring(key), running))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Spawn smoke for MEDEVAC crews with offset system
|
-- Spawn smoke for MEDEVAC crews with offset system
|
||||||
@ -2578,7 +3026,7 @@ function CTLD:_drawZoneCircleAndLabel(kind, mz, opts)
|
|||||||
local textPos = { x = nx, y = 0, z = nz }
|
local textPos = { x = nx, y = 0, z = nz }
|
||||||
trigger.action.textToAll(side, textId, textPos, {1,1,1,0.9}, {0,0,0,0}, fontSize, readOnly, label)
|
trigger.action.textToAll(side, textId, textPos, {1,1,1,0.9}, {0,0,0,0}, fontSize, readOnly, label)
|
||||||
-- Track ids so they can be cleared later
|
-- Track ids so they can be cleared later
|
||||||
self._MapMarkup = self._MapMarkup or { Pickup = {}, Drop = {}, FOB = {} }
|
self._MapMarkup = self._MapMarkup or { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
|
||||||
self._MapMarkup[kind] = self._MapMarkup[kind] or {}
|
self._MapMarkup[kind] = self._MapMarkup[kind] or {}
|
||||||
self._MapMarkup[kind][zname] = { circle = circleId, text = textId }
|
self._MapMarkup[kind][zname] = { circle = circleId, text = textId }
|
||||||
end
|
end
|
||||||
@ -2591,7 +3039,7 @@ function CTLD:ClearMapDrawings()
|
|||||||
if ids.text then pcall(trigger.action.removeMark, ids.text) end
|
if ids.text then pcall(trigger.action.removeMark, ids.text) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} }
|
self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
|
||||||
end
|
end
|
||||||
|
|
||||||
function CTLD:_removeZoneDrawing(kind, zname)
|
function CTLD:_removeZoneDrawing(kind, zname)
|
||||||
@ -2976,6 +3424,11 @@ end
|
|||||||
|
|
||||||
local function _coachSend(self, group, unitName, key, data, isCoach)
|
local function _coachSend(self, group, unitName, key, data, isCoach)
|
||||||
local cfg = CTLD.HoverCoachConfig or {}
|
local cfg = CTLD.HoverCoachConfig or {}
|
||||||
|
if cfg.enabled and (not self.HoverSched) then
|
||||||
|
if self._startHoverScheduler then
|
||||||
|
self:_startHoverScheduler()
|
||||||
|
end
|
||||||
|
end
|
||||||
local now = timer.getTime()
|
local now = timer.getTime()
|
||||||
CTLD._coachState[unitName] = CTLD._coachState[unitName] or { lastKeyTimes = {} }
|
CTLD._coachState[unitName] = CTLD._coachState[unitName] or { lastKeyTimes = {} }
|
||||||
local st = CTLD._coachState[unitName]
|
local st = CTLD._coachState[unitName]
|
||||||
@ -3148,6 +3601,8 @@ function CTLD:New(cfg)
|
|||||||
o.Config.CountryId = o.CountryId
|
o.Config.CountryId = o.CountryId
|
||||||
o.MenuRoots = {}
|
o.MenuRoots = {}
|
||||||
o.MenusByGroup = {}
|
o.MenusByGroup = {}
|
||||||
|
o._DynamicSalvageZones = {}
|
||||||
|
o._DynamicSalvageQueue = {}
|
||||||
o._jtacRegistry = {}
|
o._jtacRegistry = {}
|
||||||
|
|
||||||
-- If caller disabled builtin catalog, clear it before merging any globals
|
-- If caller disabled builtin catalog, clear it before merging any globals
|
||||||
@ -3296,10 +3751,8 @@ function CTLD:New(cfg)
|
|||||||
-- Optional: hover pickup scanner
|
-- Optional: hover pickup scanner
|
||||||
local coachCfg = CTLD.HoverCoachConfig or {}
|
local coachCfg = CTLD.HoverCoachConfig or {}
|
||||||
if coachCfg.enabled then
|
if coachCfg.enabled then
|
||||||
o.HoverSched = SCHEDULER:New(nil, function()
|
o.HoverSched = nil
|
||||||
local ok, err = pcall(function() o:ScanHoverPickup() end)
|
o:_startHoverScheduler()
|
||||||
if not ok then _logError('HoverSched ScanHoverPickup error: '..tostring(err)) end
|
|
||||||
end, {}, 0.75, 0.75) -- slowed from 0.5s to 0.75s for performance
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- MEDEVAC auto-pickup and auto-unload scheduler
|
-- MEDEVAC auto-pickup and auto-unload scheduler
|
||||||
@ -3333,6 +3786,7 @@ function CTLD:New(cfg)
|
|||||||
end
|
end
|
||||||
|
|
||||||
table.insert(CTLD._instances, o)
|
table.insert(CTLD._instances, o)
|
||||||
|
o:_ensureBackgroundTasks()
|
||||||
local versionLabel = CTLD.Version or 'unknown'
|
local versionLabel = CTLD.Version or 'unknown'
|
||||||
_msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', versionLabel))
|
_msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', versionLabel))
|
||||||
return o
|
return o
|
||||||
@ -3537,7 +3991,74 @@ end
|
|||||||
function CTLD:WireBirthHandler()
|
function CTLD:WireBirthHandler()
|
||||||
local handler = EVENTHANDLER:New()
|
local handler = EVENTHANDLER:New()
|
||||||
handler:HandleEvent(EVENTS.Birth)
|
handler:HandleEvent(EVENTS.Birth)
|
||||||
|
handler:HandleEvent(EVENTS.Dead)
|
||||||
|
handler:HandleEvent(EVENTS.Crash)
|
||||||
|
handler:HandleEvent(EVENTS.PilotDead)
|
||||||
|
handler:HandleEvent(EVENTS.Ejection)
|
||||||
|
handler:HandleEvent(EVENTS.PlayerLeaveUnit)
|
||||||
local selfref = self
|
local selfref = self
|
||||||
|
|
||||||
|
local function hasTrackedState(gname)
|
||||||
|
if not gname then return false end
|
||||||
|
if selfref.MenusByGroup and selfref.MenusByGroup[gname] then return true end
|
||||||
|
local tbls = {
|
||||||
|
CTLD._inStockMenus,
|
||||||
|
CTLD._loadedCrates,
|
||||||
|
CTLD._troopsLoaded,
|
||||||
|
CTLD._loadedTroopTypes,
|
||||||
|
CTLD._deployedTroops,
|
||||||
|
CTLD._buildConfirm,
|
||||||
|
CTLD._buildCooldown,
|
||||||
|
CTLD._coachOverride,
|
||||||
|
CTLD._medevacUnloadStates,
|
||||||
|
CTLD._medevacLoadStates,
|
||||||
|
CTLD._medevacEnrouteStates,
|
||||||
|
}
|
||||||
|
for _, tbl in ipairs(tbls) do
|
||||||
|
if tbl and tbl[gname] then return true end
|
||||||
|
end
|
||||||
|
if CTLD._msgState and CTLD._msgState['GRP:'..gname] then return true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function teardownIfGroupInactive(eventData, reason)
|
||||||
|
if not eventData then return end
|
||||||
|
local unit = eventData.IniUnit
|
||||||
|
local group = eventData.IniGroup or (unit and unit.GetGroup and unit:GetGroup()) or nil
|
||||||
|
if not group or not group.GetName then return end
|
||||||
|
if group.GetCoalition and group:GetCoalition() ~= selfref.Side then return end
|
||||||
|
local gname = group:GetName()
|
||||||
|
if not gname or gname == '' then return end
|
||||||
|
if not hasTrackedState(gname) then return end
|
||||||
|
local allowed = selfref.Config and selfref.Config.AllowedAircraft or {}
|
||||||
|
if _groupHasAliveTransport(group, allowed) then return end
|
||||||
|
selfref:_cleanupTransportGroup(group, gname)
|
||||||
|
if reason then
|
||||||
|
_logDebug(string.format('[MenuCleanup] Group %s removed due to %s', gname, reason))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scheduleDeferredCleanup(group)
|
||||||
|
if not (group and group.GetName) then return end
|
||||||
|
local gname = group:GetName()
|
||||||
|
if not gname or gname == '' then return end
|
||||||
|
if not hasTrackedState(gname) then return end
|
||||||
|
if not (timer and timer.scheduleFunction) then return end
|
||||||
|
timer.scheduleFunction(function(arg)
|
||||||
|
local name = arg and arg.groupName or nil
|
||||||
|
if not name then return nil end
|
||||||
|
if not selfref then return nil end
|
||||||
|
local grp = GROUP and GROUP:FindByName(name) or nil
|
||||||
|
local allowed = selfref.Config and selfref.Config.AllowedAircraft or {}
|
||||||
|
if grp and _groupHasAliveTransport(grp, allowed) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
selfref:_cleanupTransportGroup(grp, name)
|
||||||
|
_logDebug(string.format('[MenuCleanup] Group %s cleaned up after player left', name))
|
||||||
|
return nil
|
||||||
|
end, { groupName = gname }, timer.getTime() + 3)
|
||||||
|
end
|
||||||
|
|
||||||
function handler:OnEventBirth(eventData)
|
function handler:OnEventBirth(eventData)
|
||||||
local unit = eventData.IniUnit
|
local unit = eventData.IniUnit
|
||||||
if not unit or not unit:IsAlive() then return end
|
if not unit or not unit:IsAlive() then return end
|
||||||
@ -3551,6 +4072,32 @@ function CTLD:WireBirthHandler()
|
|||||||
selfref.MenusByGroup[gname] = selfref:BuildGroupMenus(grp)
|
selfref.MenusByGroup[gname] = selfref:BuildGroupMenus(grp)
|
||||||
_msgGroup(grp, 'CTLD menu available (F10)')
|
_msgGroup(grp, 'CTLD menu available (F10)')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function handler:OnEventDead(eventData)
|
||||||
|
teardownIfGroupInactive(eventData, 'Dead')
|
||||||
|
end
|
||||||
|
|
||||||
|
function handler:OnEventCrash(eventData)
|
||||||
|
teardownIfGroupInactive(eventData, 'Crash')
|
||||||
|
end
|
||||||
|
|
||||||
|
function handler:OnEventPilotDead(eventData)
|
||||||
|
teardownIfGroupInactive(eventData, 'PilotDead')
|
||||||
|
end
|
||||||
|
|
||||||
|
function handler:OnEventEjection(eventData)
|
||||||
|
teardownIfGroupInactive(eventData, 'Ejection')
|
||||||
|
end
|
||||||
|
|
||||||
|
function handler:OnEventPlayerLeaveUnit(eventData)
|
||||||
|
local unit = eventData and eventData.IniUnit
|
||||||
|
if not unit then return end
|
||||||
|
if unit.GetCoalition and unit:GetCoalition() ~= selfref.Side then return end
|
||||||
|
local group = unit.GetGroup and unit:GetGroup() or nil
|
||||||
|
if not group then return end
|
||||||
|
scheduleDeferredCleanup(group)
|
||||||
|
end
|
||||||
|
|
||||||
self.BirthHandler = handler
|
self.BirthHandler = handler
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -4058,6 +4605,7 @@ function CTLD:BuildGroupMenus(group)
|
|||||||
local salvageZoneRoot = MENU_GROUP:New(group, 'Salvage Collection Zones', toolsRoot)
|
local salvageZoneRoot = MENU_GROUP:New(group, 'Salvage Collection Zones', toolsRoot)
|
||||||
CMD('Create Salvage Zone Here', salvageZoneRoot, function() self:CreateSalvageZoneAtGroup(group) end)
|
CMD('Create Salvage Zone Here', salvageZoneRoot, function() self:CreateSalvageZoneAtGroup(group) end)
|
||||||
CMD('Show Active Salvage Zones', salvageZoneRoot, function() self:ShowActiveSalvageZones(group) end)
|
CMD('Show Active Salvage Zones', salvageZoneRoot, function() self:ShowActiveSalvageZones(group) end)
|
||||||
|
CMD('Retire Oldest Salvage Zone', salvageZoneRoot, function() self:RetireOldestDynamicSalvageZone(group) end)
|
||||||
-- Dynamic per-zone management will be added by _rebuildSalvageZoneMenus
|
-- Dynamic per-zone management will be added by _rebuildSalvageZoneMenus
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -4933,6 +5481,9 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
|
|||||||
local obj = StaticObject.getByName(c.name)
|
local obj = StaticObject.getByName(c.name)
|
||||||
if obj then obj:destroy() end
|
if obj then obj:destroy() end
|
||||||
_cleanupCrateSmoke(c.name) -- Clean up smoke refresh schedule
|
_cleanupCrateSmoke(c.name) -- Clean up smoke refresh schedule
|
||||||
|
if c.meta and c.meta.point then
|
||||||
|
_removeFromSpatialGrid(c.name, c.meta.point, 'crate') -- prune hover pickup spatial cache
|
||||||
|
end
|
||||||
CTLD._crates[c.name] = nil
|
CTLD._crates[c.name] = nil
|
||||||
removed = removed + 1
|
removed = removed + 1
|
||||||
end
|
end
|
||||||
@ -8014,13 +8565,8 @@ function CTLD:InitMEDEVAC()
|
|||||||
if not ok then _logError('MEDEVAC timeout scheduler error: '..tostring(err)) end
|
if not ok then _logError('MEDEVAC timeout scheduler error: '..tostring(err)) end
|
||||||
end, {}, 30, 30)
|
end, {}, 30, 30)
|
||||||
|
|
||||||
-- Start sling-load salvage crate checker (runs every 5 seconds by default)
|
-- Sling-load salvage is handled by adaptive background loop
|
||||||
if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
|
if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
|
||||||
local interval = self.Config.SlingLoadSalvage.DetectionInterval or 5
|
|
||||||
self.SalvageSched = SCHEDULER:New(nil, function()
|
|
||||||
local ok, err = pcall(function() selfref:_CheckSlingLoadSalvageCrates() end)
|
|
||||||
if not ok then _logError('Sling-Load Salvage scheduler error: '..tostring(err)) end
|
|
||||||
end, {}, interval, interval)
|
|
||||||
_logInfo('Sling-Load Salvage system initialized for coalition '..tostring(self.Side))
|
_logInfo('Sling-Load Salvage system initialized for coalition '..tostring(self.Side))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -10954,6 +11500,20 @@ function CTLD:_SpawnSlingLoadSalvageCrate(unitPos, unitTypeName, enemySide, even
|
|||||||
local sidePrefix = (enemySide == coalition.side.BLUE) and 'R' or 'B'
|
local sidePrefix = (enemySide == coalition.side.BLUE) and 'R' or 'B'
|
||||||
local crateName = string.format('SALVAGE-%s-%06d', sidePrefix, math.random(100000, 999999))
|
local crateName = string.format('SALVAGE-%s-%06d', sidePrefix, math.random(100000, 999999))
|
||||||
|
|
||||||
|
-- Enforce active salvage crate cap before spawning
|
||||||
|
if cfg.MaxActiveCrates then
|
||||||
|
local activeCount = 0
|
||||||
|
for cname, meta in pairs(CTLD._salvageCrates or {}) do
|
||||||
|
if meta and meta.side == enemySide then
|
||||||
|
activeCount = activeCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if activeCount >= cfg.MaxActiveCrates then
|
||||||
|
_logVerbose(string.format('[SlingLoadSalvage] Max active crates (%d) reached for side %d; skipping spawn', cfg.MaxActiveCrates, enemySide))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Spawn the static cargo
|
-- Spawn the static cargo
|
||||||
local countryId = self.CountryId
|
local countryId = self.CountryId
|
||||||
if eventData and eventData.initiator and eventData.initiator.getCountry then
|
if eventData and eventData.initiator and eventData.initiator.getCountry then
|
||||||
@ -11271,6 +11831,12 @@ function CTLD:CreateSalvageZoneAtGroup(group)
|
|||||||
local coord = COORDINATE:NewFromVec3(pos)
|
local coord = COORDINATE:NewFromVec3(pos)
|
||||||
local radius = cfg.DefaultZoneRadius or 300
|
local radius = cfg.DefaultZoneRadius or 300
|
||||||
|
|
||||||
|
self._DynamicSalvageZones = self._DynamicSalvageZones or {}
|
||||||
|
self._DynamicSalvageQueue = self._DynamicSalvageQueue or {}
|
||||||
|
|
||||||
|
-- Pre-clean existing dynamics so limit checks are up to date
|
||||||
|
self:_enforceDynamicSalvageZoneLimit()
|
||||||
|
|
||||||
-- Generate unique zone name
|
-- Generate unique zone name
|
||||||
local zoneName = string.format('SalvageZone-%s-%d', (self.Side == coalition.side.BLUE and 'BLUE' or 'RED'),
|
local zoneName = string.format('SalvageZone-%s-%d', (self.Side == coalition.side.BLUE and 'BLUE' or 'RED'),
|
||||||
math.random(1000, 9999))
|
math.random(1000, 9999))
|
||||||
@ -11280,17 +11846,86 @@ function CTLD:CreateSalvageZoneAtGroup(group)
|
|||||||
|
|
||||||
-- Add to instance zones
|
-- Add to instance zones
|
||||||
table.insert(self.SalvageDropZones, zone)
|
table.insert(self.SalvageDropZones, zone)
|
||||||
self._ZoneDefs.SalvageDropZones[zoneName] = { name = zoneName, side = self.Side, active = true }
|
local now = timer and timer.getTime and timer.getTime() or 0
|
||||||
|
local lifetime = tonumber(cfg.DynamicZoneLifetime or 0) or 0
|
||||||
|
local expiresAt = (lifetime > 0) and (now + lifetime) or nil
|
||||||
|
|
||||||
|
self._ZoneDefs.SalvageDropZones[zoneName] = {
|
||||||
|
name = zoneName,
|
||||||
|
side = self.Side,
|
||||||
|
active = true,
|
||||||
|
radius = radius,
|
||||||
|
dynamic = true,
|
||||||
|
createdAt = now,
|
||||||
|
expiresAt = expiresAt,
|
||||||
|
}
|
||||||
self._ZoneActive.SalvageDrop[zoneName] = true
|
self._ZoneActive.SalvageDrop[zoneName] = true
|
||||||
|
|
||||||
|
-- Track dynamic zone metadata for cleanup enforcement
|
||||||
|
self._DynamicSalvageZones[zoneName] = {
|
||||||
|
zone = zone,
|
||||||
|
createdAt = now,
|
||||||
|
expiresAt = expiresAt,
|
||||||
|
radius = radius,
|
||||||
|
}
|
||||||
|
table.insert(self._DynamicSalvageQueue, zoneName)
|
||||||
|
|
||||||
|
-- Enforce limits after registering the new zone
|
||||||
|
self:_enforceDynamicSalvageZoneLimit()
|
||||||
|
if not self._DynamicSalvageZones[zoneName] then
|
||||||
|
_msgGroup(group, 'Unable to create salvage zone (limit reached). Older zones were not cleared in time.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Announce
|
-- Announce
|
||||||
local msg = _fmtTemplate(self.Messages.slingload_salvage_zone_created, {
|
local msg = _fmtTemplate(self.Messages.slingload_salvage_zone_created, {
|
||||||
zone = zoneName,
|
zone = zoneName,
|
||||||
radius = radius,
|
radius = radius,
|
||||||
})
|
})
|
||||||
|
if lifetime > 0 then
|
||||||
|
msg = msg .. string.format(' Expires in %s.', _ctldFormatSeconds(lifetime))
|
||||||
|
end
|
||||||
|
local maxZones = tonumber(cfg.MaxDynamicZones or 0) or 0
|
||||||
|
if maxZones > 0 then
|
||||||
|
msg = msg .. string.format(' Active zone cap: %d.', maxZones)
|
||||||
|
end
|
||||||
_msgGroup(group, msg)
|
_msgGroup(group, msg)
|
||||||
|
|
||||||
_logInfo(string.format('[SlingLoadSalvage] Created zone %s at %s', zoneName, coord:ToStringLLDMS()))
|
_logInfo(string.format('[SlingLoadSalvage] Created zone %s at %s', zoneName, coord:ToStringLLDMS()))
|
||||||
|
|
||||||
|
local ok, err = pcall(function() self:DrawZonesOnMap() end)
|
||||||
|
if not ok then
|
||||||
|
_logError(string.format('[SlingLoadSalvage] DrawZonesOnMap failed after creating %s: %s', zoneName, tostring(err)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function CTLD:RetireOldestDynamicSalvageZone(group)
|
||||||
|
local cfg = self.Config.SlingLoadSalvage
|
||||||
|
if not cfg or not cfg.Enabled then return end
|
||||||
|
|
||||||
|
self._DynamicSalvageQueue = self._DynamicSalvageQueue or {}
|
||||||
|
if #self._DynamicSalvageQueue == 0 then
|
||||||
|
if group then _msgGroup(group, 'No dynamic salvage zones to retire.') end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local oldest = self._DynamicSalvageQueue[1]
|
||||||
|
if not oldest then
|
||||||
|
if group then _msgGroup(group, 'No dynamic salvage zones to retire.') end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local exists = self._DynamicSalvageZones and self._DynamicSalvageZones[oldest]
|
||||||
|
if exists then
|
||||||
|
self:_removeDynamicSalvageZone(oldest, 'manual-retire')
|
||||||
|
if group then
|
||||||
|
_msgGroup(group, string.format('Retired salvage zone: %s', oldest))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Remove stale entry from queue and notify user
|
||||||
|
table.remove(self._DynamicSalvageQueue, 1)
|
||||||
|
if group then _msgGroup(group, 'Oldest salvage zone already retired.') end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Menu: Show active salvage zones
|
-- Menu: Show active salvage zones
|
||||||
|
|||||||
145
Moose_Intel.lua
Normal file
145
Moose_Intel.lua
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
--Ops - Office of Military Intelligence.
|
||||||
|
--
|
||||||
|
--Main Features:
|
||||||
|
---Detect and track contacts consistently
|
||||||
|
---Detect and track clusters of contacts consistently
|
||||||
|
---Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours
|
||||||
|
-- Docs: https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.Intel.html
|
||||||
|
|
||||||
|
-- Setup Detection Group
|
||||||
|
local msgTime = 15
|
||||||
|
Blue_Intel_Message_Setting = false
|
||||||
|
Blue_Intel_Sound_Setting = true
|
||||||
|
|
||||||
|
Blue_Intel_DetectionGroup = SET_GROUP:New()
|
||||||
|
Blue_Intel_DetectionGroup:FilterCoalitions("blue"):FilterActive(true):FilterStart()
|
||||||
|
|
||||||
|
-- Setup the INTEL
|
||||||
|
Blue_Intel = INTEL:New(Blue_Intel_DetectionGroup, "blue", "CIA")
|
||||||
|
Blue_Intel:SetClusterAnalysis(true, true)
|
||||||
|
Blue_Intel:SetClusterRadius(5)
|
||||||
|
Blue_Intel:SetVerbosity(2)
|
||||||
|
Blue_Intel:__Start(10)
|
||||||
|
|
||||||
|
-- On After New Contact
|
||||||
|
function Blue_Intel:OnAfterNewContact(From, Event, To, Contact)
|
||||||
|
local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")
|
||||||
|
|
||||||
|
if (Blue_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New(text, msgTime, "CIA"):ToBlue()
|
||||||
|
if (Blue_Intel_Sound_Setting == true) then
|
||||||
|
USERSOUND:New("morsecode.ogg"):ToCoalition(coalition.side.BLUE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- On After New Cluster
|
||||||
|
function Blue_Intel:OnAfterNewCluster(From, Event, To, Cluster)
|
||||||
|
local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size)
|
||||||
|
|
||||||
|
if (Blue_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New(text, msgTime,"CIA"):ToBlue()
|
||||||
|
if (Blue_Intel_Sound_Setting == true) then
|
||||||
|
USERSOUND:New("morsecode.ogg"):ToCoalition(coalition.side.BLUE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Blue_IntelMessageSettingOn()
|
||||||
|
if (Blue_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New("Setting INTEL messages to ON", msgTime,"CIA"):ToBlue()
|
||||||
|
Blue_Intel_Message_Setting = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Blue_IntelMessageSettingOff()
|
||||||
|
if (Blue_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New("Setting INTEL messages to OFF", msgTime,"CIA"):ToBlue()
|
||||||
|
Blue_Intel_Message_Setting = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Blue_IntelSoundSettingOff()
|
||||||
|
MESSAGE:New("Disabling morse code sound", msgTime, "CIA"):ToBlue()
|
||||||
|
Blue_Intel_Sound_Setting = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Blue_IntelSoundSettingOn()
|
||||||
|
MESSAGE:New("Enabling morse code sound", msgTime, "CIA"):ToBlue()
|
||||||
|
Blue_Intel_Sound_Setting = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local INTELMenu = MENU_COALITION:New(coalition.side.BLUE,"INTEL HQ", missionMenu)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Display Messages (ON)", INTELMenu, Blue_IntelMessageSettingOn)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Display Messages (OFF)", INTELMenu, Blue_IntelMessageSettingOff)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Disable Morse Code Sound", INTELMenu, Blue_IntelSoundSettingOff)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.BLUE, "Enable Morse Code Sound", INTELMenu, Blue_IntelSoundSettingOn)
|
||||||
|
|
||||||
|
|
||||||
|
Red_Intel_Message_Setting = false
|
||||||
|
Red_Intel_Sound_Setting = true
|
||||||
|
|
||||||
|
Red_Intel_DetectionGroup = SET_GROUP:New()
|
||||||
|
--Red_Intel_DetectionGroup:FilterPrefixes( { "RED EWR", "RED RECON" } )
|
||||||
|
Red_Intel_DetectionGroup:FilterCoalitions("red"):FilterActive(true):FilterStart()
|
||||||
|
|
||||||
|
|
||||||
|
-- Setup the INTEL
|
||||||
|
Red_Intel = INTEL:New(Red_Intel_DetectionGroup, "red", "KGB")
|
||||||
|
Red_Intel:SetClusterAnalysis(true, true)
|
||||||
|
Red_Intel:SetClusterRadius(5)
|
||||||
|
Red_Intel:SetVerbosity(2)
|
||||||
|
Red_Intel:__Start(10)
|
||||||
|
|
||||||
|
-- On After New Contact
|
||||||
|
function Red_Intel:OnAfterNewContact(From, Event, To, Contact)
|
||||||
|
local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")
|
||||||
|
|
||||||
|
if (Red_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New(text, msgTime, "KGB"):ToRed()
|
||||||
|
USERSOUND:New("morsecode.ogg"):ToCoalition(coalition.side.RED)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- On After New Cluster
|
||||||
|
function Red_Intel:OnAfterNewCluster(From, Event, To, Cluster)
|
||||||
|
local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size)
|
||||||
|
|
||||||
|
if (Red_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New(text, msgTime,"KGB"):ToRed()
|
||||||
|
USERSOUND:New("morsecode.ogg"):ToCoalition(coalition.side.RED)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Red_IntelMessageSettingOn()
|
||||||
|
if (Red_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New("Setting INTEL messages to ON", msgTime,"KGB"):ToRed()
|
||||||
|
USERSOUND:New("morsecode.ogg"):ToCoalition(coalition.side.RED)
|
||||||
|
Red_Intel_Message_Setting = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Red_IntelMessageSettingOff()
|
||||||
|
if (Red_Intel_Message_Setting == true) then
|
||||||
|
MESSAGE:New("Setting INTEL messages to OFF", msgTime,"KGB"):ToRed()
|
||||||
|
Red_Intel_Message_Setting = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Red_IntelSoundSettingOff()
|
||||||
|
MESSAGE:New("Disabling morse code sound", msgTime, "KGB"):ToRed()
|
||||||
|
Red_Intel_Sound_Setting = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Red_IntelSoundSettingOn()
|
||||||
|
MESSAGE:New("Enabling morse code sound", msgTime, "KGB"):ToRed()
|
||||||
|
Red_Intel_Sound_Setting = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create the "INTEL HQ" submenu under "Tanker & Other Settings"
|
||||||
|
local RedINTELMenu = MENU_COALITION:New(coalition.side.RED, "INTEL HQ", missionMenu)
|
||||||
|
|
||||||
|
-- Add menu items to the "INTEL HQ" submenu
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.RED, "Display Messages (ON)", RedINTELMenu, Red_IntelMessageSettingOn)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.RED, "Display Messages (OFF)", RedINTELMenu, Red_IntelMessageSettingOff)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.RED, "Disable Morse Code Sound", RedINTELMenu, Red_IntelSoundSettingOff)
|
||||||
|
MENU_COALITION_COMMAND:New(coalition.side.RED, "Enable Morse Code Sound", RedINTELMenu, Red_IntelSoundSettingOn)
|
||||||
78
docs/performance_tuning.md
Normal file
78
docs/performance_tuning.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Mission Performance & Memory Diagnostics
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide covers runtime checks and configuration adjustments you can use to monitor and tune the script systems used in Operation Polar Shield. It focuses on the pure-MOOSE CTLD integration, but several concepts apply to other Moose modules you employ.
|
||||||
|
|
||||||
|
## Console Helpers
|
||||||
|
|
||||||
|
### `CTLD_DumpRuntimeStats()`
|
||||||
|
|
||||||
|
**Purpose**
|
||||||
|
- Dump current counts of salvage crates, MEDEVAC crews, and active CTLD timers.
|
||||||
|
- Print the statuses of the adaptive salvage scheduler loop.
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
1. Open the DCS server console (LShift+LWin+L to focus, F12 to open the Lua console).
|
||||||
|
2. Execute:
|
||||||
|
```lua
|
||||||
|
CTLD_DumpRuntimeStats()
|
||||||
|
```
|
||||||
|
3. Inspect server log (`Saved Games\DCS\Logs\dcs.log`) for output lines that start with `[CTLD]`.
|
||||||
|
|
||||||
|
**What it reports**
|
||||||
|
- Number of active salvage crates per coalition.
|
||||||
|
- Age distribution of salvage crates, including longest-living crate in seconds.
|
||||||
|
- Total scheduled timers registered via `_registerSchedule` (adaptive loops).
|
||||||
|
- Adaptive salvage interval currently in use.
|
||||||
|
|
||||||
|
### `CTLD_DumpCrateAges()`
|
||||||
|
|
||||||
|
**Purpose**
|
||||||
|
- Print a sorted list of active salvage crates with the spawn timestamp and remaining time until expiration.
|
||||||
|
- Useful to confirm that the 1-hour lifetime setting is applied and crates are pruned appropriately.
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
```lua
|
||||||
|
CTLD_DumpCrateAges()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `CTLD_ListSchedules()`
|
||||||
|
|
||||||
|
**Purpose**
|
||||||
|
- Enumerate the CTLD scheduler registry entries (`smokeTicker`, `backgroundLoop`, `periodicGC`, etc.).
|
||||||
|
- Helps confirm that no duplicate or stale timer functions are registered.
|
||||||
|
|
||||||
|
**How to use**
|
||||||
|
```lua
|
||||||
|
CTLD_ListSchedules()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tracking Active Schedules
|
||||||
|
|
||||||
|
The adaptive background loop maintained by `_ensureAdaptiveBackgroundLoop()` returns the next wake-up time to `timer.scheduleFunction`. The runtime stats show the current loop period. When salvage crate count is low, the interval is short (twice the detection interval); it expands automatically with more crates.
|
||||||
|
|
||||||
|
### Typical values to expect
|
||||||
|
- Idle (no crates): background loop runs every 10 seconds.
|
||||||
|
- Moderate activity (up to 20 crates): loop every ~10 seconds.
|
||||||
|
- Heavy load (>20 crates): loop extends to 15–20 seconds automatically.
|
||||||
|
|
||||||
|
If you observe constant short intervals despite high crate counts, re-run `CTLD_ListSchedules()` to ensure no duplicate loops are reinstated.
|
||||||
|
|
||||||
|
## Salvage System Lifecycle
|
||||||
|
|
||||||
|
### Spawn limits
|
||||||
|
- Hard cap: 40 active salvage crates per coalition. Additional spawn opportunities are skipped once the cap is reached.
|
||||||
|
- Inline log entries record when spawn suppression occurs.
|
||||||
|
|
||||||
|
### Lifetime
|
||||||
|
- Each crate expires 3600 seconds (1 hour) after spawning.
|
||||||
|
- Adaptive cleanup sweeps run every minute to destroy expired crates and remove references.
|
||||||
|
|
||||||
|
## Tips for Long-Running Missions
|
||||||
|
|
||||||
|
- Use the console helpers every few hours to verify that salvage and MEDEVAC tables are not growing unexpectedly.
|
||||||
|
- Monitor the `dcs.log` for `[CTLD]` log statements; they will indicate when salvaged crates are skipped due to hitting the cap or when the adaptive interval increases.
|
||||||
|
- Consider tying the new helper functions to your existing remote admin tool/Discord bot for quick telemetry.
|
||||||
|
|
||||||
|
For additional tuning (e.g., adjusting detection frequency or spawn caps), modify the `SlingLoadSalvage` configuration block in `Moose_CTLD.lua`.
|
||||||
Loading…
x
Reference in New Issue
Block a user