All tracked data structures now have proper lifecycle management and will not accumulate indefinitely. The system is production-ready for long-running missions without memory concerns.

This commit is contained in:
iTracerFacer 2025-11-22 19:51:42 -06:00
parent 9ec7b7ac36
commit b1a28478c6

View File

@ -361,7 +361,7 @@ CTLD.Config = {
-- 2 = INFO - Important state changes, initialization, cleanup (default for production) -- 2 = INFO - Important state changes, initialization, cleanup (default for production)
-- 3 = VERBOSE - Detailed operational info (zone validation, menus, builds, MEDEVAC events) -- 3 = VERBOSE - Detailed operational info (zone validation, menus, builds, MEDEVAC events)
-- 4 = DEBUG - Everything including hover checks, crate pickups, detailed troop spawns -- 4 = DEBUG - Everything including hover checks, crate pickups, detailed troop spawns
LogLevel = 4, -- lowered from DEBUG (4) to INFO (2) for production performance LogLevel = 1, -- lowered from DEBUG (4) to INFO (2) for production performance
MessageDuration = 15, -- seconds for on-screen messages MessageDuration = 15, -- seconds for on-screen messages
-- Debug toggles for detailed crate proximity logging (useful when tuning hover coach / ground autoload) -- Debug toggles for detailed crate proximity logging (useful when tuning hover coach / ground autoload)
@ -3460,6 +3460,7 @@ local function _clearPerUnitCachesForGroup(group)
if CTLD._hoverState then CTLD._hoverState[uname] = nil end if CTLD._hoverState then CTLD._hoverState[uname] = nil end
if CTLD._unitLast then CTLD._unitLast[uname] = nil end if CTLD._unitLast then CTLD._unitLast[uname] = nil end
if CTLD._coachState then CTLD._coachState[uname] = nil end if CTLD._coachState then CTLD._coachState[uname] = nil end
if CTLD._groundLoadState then CTLD._groundLoadState[uname] = nil end
end end
end end
end end
@ -3508,6 +3509,11 @@ function CTLD:_cleanupTransportGroup(group, groupName)
if mooseGroup then _clearPerUnitCachesForGroup(mooseGroup) end if mooseGroup then _clearPerUnitCachesForGroup(mooseGroup) end
end end
-- Cleanup JTAC registry if this group had JTAC registered
if self._jtacRegistry and self._jtacRegistry[gname] then
self:_cleanupJTACEntry(gname)
end
_logDebug(string.format('[MenuCleanup] Cleared CTLD state for group %s', gname)) _logDebug(string.format('[MenuCleanup] Cleared CTLD state for group %s', gname))
end end
@ -5027,6 +5033,12 @@ function CTLD:New(cfg)
if not ok then _logError('CleanupDeployedTroops scheduler error: '..tostring(err)) end if not ok then _logError('CleanupDeployedTroops scheduler error: '..tostring(err)) end
end, {}, 30, 30) end, {}, 30, 30)
-- Periodic comprehensive state maintenance (prune orphaned entries)
o.StateMaintSched = SCHEDULER:New(nil, function()
local ok, err = pcall(function() o:PruneOrphanedState() end)
if not ok then _logError('PruneOrphanedState scheduler error: '..tostring(err)) end
end, {}, 120, 120) -- Run every 2 minutes
-- Optional: auto-build FOBs inside FOB zones when crates present -- Optional: auto-build FOBs inside FOB zones when crates present
if o.Config.AutoBuildFOBInZones then if o.Config.AutoBuildFOBInZones then
o.AutoFOBSched = SCHEDULER:New(nil, function() o.AutoFOBSched = SCHEDULER:New(nil, function()
@ -8504,12 +8516,106 @@ function CTLD:CleanupDeployedTroops()
if troopMeta.side == self.Side then if troopMeta.side == self.Side then
local troopGroup = GROUP:FindByName(troopGroupName) local troopGroup = GROUP:FindByName(troopGroupName)
if not troopGroup or not troopGroup:IsAlive() then if not troopGroup or not troopGroup:IsAlive() then
-- Remove from spatial grid if point is available
if troopMeta.point then
_removeFromSpatialGrid(troopGroupName, troopMeta.point, 'troops')
end
CTLD._deployedTroops[troopGroupName] = nil CTLD._deployedTroops[troopGroupName] = nil
_logDebug('Cleaned up deployed troop group: '..troopGroupName) _logDebug('Cleaned up deployed troop group: '..troopGroupName)
end end
end end
end end
end end
-- Comprehensive state pruning to prevent memory leaks
function CTLD:PruneOrphanedState()
local pruned = 0
-- 1. Prune spatial grid entries for non-existent crates/troops
for gridKey, cell in pairs(CTLD._spatialGrid) do
-- Check crates in this cell
for crateName, _ in pairs(cell.crates) do
if not CTLD._crates[crateName] then
cell.crates[crateName] = nil
pruned = pruned + 1
end
end
-- Check troops in this cell
for troopName, _ in pairs(cell.troops) do
if not CTLD._deployedTroops[troopName] then
cell.troops[troopName] = nil
pruned = pruned + 1
end
end
-- Remove empty cells
if not next(cell.crates) and not next(cell.troops) then
CTLD._spatialGrid[gridKey] = nil
end
end
-- 2. Prune JTAC registry for non-existent groups
if self._jtacRegistry then
for gname, _ in pairs(self._jtacRegistry) do
local g = Group.getByName(gname)
if not g or not g:isExist() then
self:_cleanupJTACEntry(gname)
pruned = pruned + 1
end
end
end
-- 3. Prune hover/coach state for non-existent units
local function pruneUnitState(stateTbl, label)
if not stateTbl then return end
for unitName, _ in pairs(stateTbl) do
local u = Unit.getByName(unitName)
if not u or not u:isExist() or u:getLife() <= 0 then
stateTbl[unitName] = nil
pruned = pruned + 1
end
end
end
pruneUnitState(CTLD._hoverState, 'hover')
pruneUnitState(CTLD._coachState, 'coach')
pruneUnitState(CTLD._groundLoadState, 'groundLoad')
pruneUnitState(CTLD._unitLast, 'unitLast')
-- 4. Prune group-level state for non-existent groups
local function pruneGroupState(stateTbl, label)
if not stateTbl then return end
for gname, _ in pairs(stateTbl) do
local g = GROUP:FindByName(gname)
if not g or not g:IsAlive() then
stateTbl[gname] = nil
pruned = pruned + 1
end
end
end
pruneGroupState(CTLD._troopsLoaded, 'troopsLoaded')
pruneGroupState(CTLD._loadedCrates, 'loadedCrates')
pruneGroupState(CTLD._loadedTroopTypes, 'loadedTroopTypes')
pruneGroupState(CTLD._buildConfirm, 'buildConfirm')
pruneGroupState(CTLD._medevacUnloadStates, 'medevacUnload')
pruneGroupState(CTLD._medevacLoadStates, 'medevacLoad')
pruneGroupState(CTLD._medevacEnrouteStates, 'medevacEnroute')
-- 5. Prune _inStockMenus for non-existent groups
if CTLD._inStockMenus then
for gname, _ in pairs(CTLD._inStockMenus) do
local g = GROUP:FindByName(gname)
if not g or not g:IsAlive() then
CTLD._inStockMenus[gname] = nil
pruned = pruned + 1
end
end
end
if pruned > 0 then
_logVerbose(string.format('[StateMaint] Pruned %d orphaned state entries', pruned))
end
end
-- #endregion Crates -- #endregion Crates
-- ========================= -- =========================