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
|
||||
MOOSE_DEVELOPMENT_FOLDER='Scripts'
|
||||
end
|
||||
@ -35093,13 +35093,29 @@ end
|
||||
end
|
||||
self.LastPosition=pos
|
||||
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.CargoState=DYNAMICCARGO.State.REMOVED
|
||||
_DATABASE:CreateEventDynamicCargoRemoved(self)
|
||||
end
|
||||
return self
|
||||
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)
|
||||
if not client then return false end
|
||||
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);
|
||||
-- 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
|
||||
|
||||
local CTLD = {}
|
||||
CTLD.__index = CTLD
|
||||
CTLD._lastSalvageInterval = CTLD._lastSalvageInterval or 0
|
||||
|
||||
-- General CTLD event messages (non-hover). Tweak freely.
|
||||
CTLD.Messages = {
|
||||
@ -431,13 +416,15 @@ CTLD.Config = {
|
||||
HeavyDamage = 0.5, -- < 50% health
|
||||
},
|
||||
|
||||
CrateLifetime = 10800, -- 3 hours (seconds)
|
||||
CrateLifetime = 3600, -- 1 hour (seconds)
|
||||
WarningTimes = { 1800, 300 }, -- Warn at 30min and 5min remaining
|
||||
|
||||
-- Visual indicators
|
||||
SpawnSmoke = false,
|
||||
SmokeDuration = 120, -- 2 minutes
|
||||
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
|
||||
MinSpawnDistance = 25, -- meters from death location
|
||||
@ -458,6 +445,8 @@ CTLD.Config = {
|
||||
|
||||
-- Salvage Collection Zone defaults
|
||||
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 = {
|
||||
border = {1, 0.5, 0, 0.85}, -- orange border
|
||||
fill = {1, 0.5, 0, 0.15}, -- light orange fill
|
||||
@ -2384,6 +2373,205 @@ function CTLD:_cancelSchedule(key)
|
||||
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)
|
||||
function CTLD:_ensureGlobalSmokeTicker()
|
||||
if self._schedules and self._schedules.smokeTicker then return end
|
||||
@ -2471,6 +2659,266 @@ function CTLD:_ensureBackgroundTasks()
|
||||
self._bgStarted = true
|
||||
self:_ensureGlobalSmokeTicker()
|
||||
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
|
||||
|
||||
-- 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 }
|
||||
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
|
||||
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][zname] = { circle = circleId, text = textId }
|
||||
end
|
||||
@ -2591,7 +3039,7 @@ function CTLD:ClearMapDrawings()
|
||||
if ids.text then pcall(trigger.action.removeMark, ids.text) end
|
||||
end
|
||||
end
|
||||
self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {} }
|
||||
self._MapMarkup = { Pickup = {}, Drop = {}, FOB = {}, MASH = {}, SalvageDrop = {} }
|
||||
end
|
||||
|
||||
function CTLD:_removeZoneDrawing(kind, zname)
|
||||
@ -2976,6 +3424,11 @@ end
|
||||
|
||||
local function _coachSend(self, group, unitName, key, data, isCoach)
|
||||
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()
|
||||
CTLD._coachState[unitName] = CTLD._coachState[unitName] or { lastKeyTimes = {} }
|
||||
local st = CTLD._coachState[unitName]
|
||||
@ -3148,6 +3601,8 @@ function CTLD:New(cfg)
|
||||
o.Config.CountryId = o.CountryId
|
||||
o.MenuRoots = {}
|
||||
o.MenusByGroup = {}
|
||||
o._DynamicSalvageZones = {}
|
||||
o._DynamicSalvageQueue = {}
|
||||
o._jtacRegistry = {}
|
||||
|
||||
-- If caller disabled builtin catalog, clear it before merging any globals
|
||||
@ -3296,10 +3751,8 @@ function CTLD:New(cfg)
|
||||
-- Optional: hover pickup scanner
|
||||
local coachCfg = CTLD.HoverCoachConfig or {}
|
||||
if coachCfg.enabled then
|
||||
o.HoverSched = SCHEDULER:New(nil, function()
|
||||
local ok, err = pcall(function() o:ScanHoverPickup() end)
|
||||
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
|
||||
o.HoverSched = nil
|
||||
o:_startHoverScheduler()
|
||||
end
|
||||
|
||||
-- MEDEVAC auto-pickup and auto-unload scheduler
|
||||
@ -3333,6 +3786,7 @@ function CTLD:New(cfg)
|
||||
end
|
||||
|
||||
table.insert(CTLD._instances, o)
|
||||
o:_ensureBackgroundTasks()
|
||||
local versionLabel = CTLD.Version or 'unknown'
|
||||
_msgCoalition(o.Side, string.format('CTLD %s initialized for coalition', versionLabel))
|
||||
return o
|
||||
@ -3537,7 +3991,74 @@ end
|
||||
function CTLD:WireBirthHandler()
|
||||
local handler = EVENTHANDLER:New()
|
||||
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 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)
|
||||
local unit = eventData.IniUnit
|
||||
if not unit or not unit:IsAlive() then return end
|
||||
@ -3551,6 +4072,32 @@ function CTLD:WireBirthHandler()
|
||||
selfref.MenusByGroup[gname] = selfref:BuildGroupMenus(grp)
|
||||
_msgGroup(grp, 'CTLD menu available (F10)')
|
||||
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
|
||||
end
|
||||
|
||||
@ -4058,6 +4605,7 @@ function CTLD:BuildGroupMenus(group)
|
||||
local salvageZoneRoot = MENU_GROUP:New(group, 'Salvage Collection Zones', toolsRoot)
|
||||
CMD('Create Salvage Zone Here', salvageZoneRoot, function() self:CreateSalvageZoneAtGroup(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
|
||||
end
|
||||
|
||||
@ -4933,6 +5481,9 @@ function CTLD:BuildSpecificAtGroup(group, recipeKey, opts)
|
||||
local obj = StaticObject.getByName(c.name)
|
||||
if obj then obj:destroy() end
|
||||
_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
|
||||
removed = removed + 1
|
||||
end
|
||||
@ -8014,13 +8565,8 @@ function CTLD:InitMEDEVAC()
|
||||
if not ok then _logError('MEDEVAC timeout scheduler error: '..tostring(err)) end
|
||||
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
|
||||
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))
|
||||
end
|
||||
|
||||
@ -10954,6 +11500,20 @@ function CTLD:_SpawnSlingLoadSalvageCrate(unitPos, unitTypeName, enemySide, even
|
||||
local sidePrefix = (enemySide == coalition.side.BLUE) and 'R' or 'B'
|
||||
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
|
||||
local countryId = self.CountryId
|
||||
if eventData and eventData.initiator and eventData.initiator.getCountry then
|
||||
@ -11271,6 +11831,12 @@ function CTLD:CreateSalvageZoneAtGroup(group)
|
||||
local coord = COORDINATE:NewFromVec3(pos)
|
||||
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
|
||||
local zoneName = string.format('SalvageZone-%s-%d', (self.Side == coalition.side.BLUE and 'BLUE' or 'RED'),
|
||||
math.random(1000, 9999))
|
||||
@ -11280,17 +11846,86 @@ function CTLD:CreateSalvageZoneAtGroup(group)
|
||||
|
||||
-- Add to instance zones
|
||||
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
|
||||
|
||||
-- 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
|
||||
local msg = _fmtTemplate(self.Messages.slingload_salvage_zone_created, {
|
||||
zone = zoneName,
|
||||
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)
|
||||
|
||||
_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
|
||||
|
||||
-- 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