Fixed major crash bug (call to undefined function). and major performance tweaks around the hover coach.

This commit is contained in:
iTracerFacer 2025-11-09 11:13:30 -06:00
parent fb806bd926
commit 77b0f9b5e8
2 changed files with 161 additions and 10 deletions

View File

@ -745,6 +745,9 @@ CTLD._msgState = { } -- messaging throttle state: [scopeKey] = { lastKeyT
CTLD._buildConfirm = {} -- [groupName] = time of first build request (awaiting confirmation) CTLD._buildConfirm = {} -- [groupName] = time of first build request (awaiting confirmation)
CTLD._buildCooldown = {} -- [groupName] = time of last successful build CTLD._buildCooldown = {} -- [groupName] = time of last successful build
CTLD._NextMarkupId = 10000 -- global-ish id generator shared by instances for map drawings CTLD._NextMarkupId = 10000 -- global-ish id generator shared by instances for map drawings
-- Spatial indexing for hover pickup performance
CTLD._spatialGrid = CTLD._spatialGrid or {} -- [gridKey] = { crates = {name->meta}, troops = {name->meta} }
CTLD._spatialGridSize = 500 -- meters per grid cell (tunable based on hover pickup distance)
-- Inventory state -- Inventory state
CTLD._stockByZone = CTLD._stockByZone or {} -- [zoneName] = { [crateKey] = count } CTLD._stockByZone = CTLD._stockByZone or {} -- [zoneName] = { [crateKey] = count }
CTLD._inStockMenus = CTLD._inStockMenus or {} -- per-group filtered menu handles CTLD._inStockMenus = CTLD._inStockMenus or {} -- per-group filtered menu handles
@ -764,6 +767,67 @@ CTLD._medevacStats = CTLD._medevacStats or { -- [coalition.side] = { spawne
-- Utilities -- Utilities
-- ========================= -- =========================
-- #region Utilities -- #region Utilities
-- Spatial indexing helpers for performance optimization
local function _getSpatialGridKey(x, z)
local gridSize = CTLD._spatialGridSize or 500
local gx = math.floor(x / gridSize)
local gz = math.floor(z / gridSize)
return string.format("%d_%d", gx, gz)
end
local function _addToSpatialGrid(name, meta, itemType)
if not meta or not meta.point then return end
local key = _getSpatialGridKey(meta.point.x, meta.point.z)
CTLD._spatialGrid[key] = CTLD._spatialGrid[key] or { crates = {}, troops = {} }
if itemType == 'crate' then
CTLD._spatialGrid[key].crates[name] = meta
elseif itemType == 'troops' then
CTLD._spatialGrid[key].troops[name] = meta
end
end
local function _removeFromSpatialGrid(name, point, itemType)
if not point then return end
local key = _getSpatialGridKey(point.x, point.z)
local cell = CTLD._spatialGrid[key]
if cell then
if itemType == 'crate' then
cell.crates[name] = nil
elseif itemType == 'troops' then
cell.troops[name] = nil
end
-- Clean up empty cells
if not next(cell.crates) and not next(cell.troops) then
CTLD._spatialGrid[key] = nil
end
end
end
local function _getNearbyFromSpatialGrid(x, z, maxDistance)
local gridSize = CTLD._spatialGridSize or 500
local cellRadius = math.ceil(maxDistance / gridSize) + 1
local centerGX = math.floor(x / gridSize)
local centerGZ = math.floor(z / gridSize)
local nearby = { crates = {}, troops = {} }
for dx = -cellRadius, cellRadius do
for dz = -cellRadius, cellRadius do
local key = string.format("%d_%d", centerGX + dx, centerGZ + dz)
local cell = CTLD._spatialGrid[key]
if cell then
for name, meta in pairs(cell.crates) do
nearby.crates[name] = meta
end
for name, meta in pairs(cell.troops) do
nearby.troops[name] = meta
end
end
end
end
return nearby
end
local function _isIn(list, value) local function _isIn(list, value)
for _,v in ipairs(list or {}) do if v == value then return true end end for _,v in ipairs(list or {}) do if v == value then return true end end
return false return false
@ -4094,6 +4158,9 @@ function CTLD:RequestCrateForGroup(group, crateKey)
requester = group:GetName(), requester = group:GetName(),
} }
-- Add to spatial index for efficient hover pickup scanning
_addToSpatialGrid(cname, CTLD._crates[cname], 'crate')
-- Now that crate is created, spawn smoke with refresh scheduling if enabled -- Now that crate is created, spawn smoke with refresh scheduling if enabled
if zone then if zone then
local zdef = self._ZoneDefs.PickupZones[zone:GetName()] local zdef = self._ZoneDefs.PickupZones[zone:GetName()]
@ -4205,6 +4272,7 @@ function CTLD:CleanupCrates()
local obj = StaticObject.getByName(name) local obj = StaticObject.getByName(name)
if obj then obj:destroy() end if obj then obj:destroy() end
_cleanupCrateSmoke(name) -- Clean up smoke refresh schedule _cleanupCrateSmoke(name) -- Clean up smoke refresh schedule
_removeFromSpatialGrid(name, meta.point, 'crate') -- Remove from spatial index
CTLD._crates[name] = nil CTLD._crates[name] = nil
if self.Config.Debug then env.info('[CTLD] Cleaned up crate '..name) end if self.Config.Debug then env.info('[CTLD] Cleaned up crate '..name) end
-- Notify requester group if still around; else coalition -- Notify requester group if still around; else coalition
@ -4563,6 +4631,8 @@ function CTLD:DropLoadedCrates(group, howMany)
local cname = string.format('CTLD_CRATE_%s_%d', k, math.random(100000,999999)) local cname = string.format('CTLD_CRATE_%s_%d', k, math.random(100000,999999))
_spawnStaticCargo(self.Side, dropPt, (cat and cat.dcsCargoType) or 'uh1h_cargo', cname) _spawnStaticCargo(self.Side, dropPt, (cat and cat.dcsCargoType) or 'uh1h_cargo', cname)
CTLD._crates[cname] = { key = k, side = self.Side, spawnTime = timer.getTime(), point = { x = dropPt.x, z = dropPt.z } } CTLD._crates[cname] = { key = k, side = self.Side, spawnTime = timer.getTime(), point = { x = dropPt.x, z = dropPt.z } }
-- Add to spatial index
_addToSpatialGrid(cname, CTLD._crates[cname], 'crate')
lc.byKey[k] = lc.byKey[k] - 1 lc.byKey[k] = lc.byKey[k] - 1
if lc.byKey[k] <= 0 then lc.byKey[k] = nil end if lc.byKey[k] <= 0 then lc.byKey[k] = nil end
lc.total = lc.total - 1 lc.total = lc.total - 1
@ -4725,11 +4795,15 @@ function CTLD:ScanHoverPickup()
end end
CTLD._unitLast[uname] = { x = p3.x, z = p3.z, t = now, agl = agl } CTLD._unitLast[uname] = { x = p3.x, z = p3.z, t = now, agl = agl }
-- find nearest crate within search distance -- Use spatial indexing to find nearby crates/troops efficiently
local bestName, bestMeta, bestd
local bestType = 'crate' -- Track whether we found a crate or troops
local maxd = coachCfg.autoPickupDistance or 25 local maxd = coachCfg.autoPickupDistance or 25
for name,meta in pairs(CTLD._crates) do local nearby = _getNearbyFromSpatialGrid(p3.x, p3.z, maxd)
local bestName, bestMeta, bestd
local bestType = 'crate'
-- Search nearby crates from spatial grid
for name, meta in pairs(nearby.crates) do
if meta.side == self.Side then if meta.side == self.Side then
local dx = (meta.point.x - p3.x) local dx = (meta.point.x - p3.x)
local dz = (meta.point.z - p3.z) local dz = (meta.point.z - p3.z)
@ -4741,11 +4815,10 @@ function CTLD:ScanHoverPickup()
end end
end end
-- Also scan for deployed troop groups to pick up -- Search nearby deployed troops from spatial grid
for troopGroupName, troopMeta in pairs(CTLD._deployedTroops) do for troopGroupName, troopMeta in pairs(nearby.troops) do
if troopMeta.side == self.Side then if troopMeta.side == self.Side then
local troopGroup = GROUP:FindByName(troopGroupName) local troopGroup = GROUP:FindByName(troopGroupName)
-- Only allow pickup if group exists and is alive
if troopGroup and troopGroup:IsAlive() then if troopGroup and troopGroup:IsAlive() then
local troopPos = troopGroup:GetCoordinate() local troopPos = troopGroup:GetCoordinate()
if troopPos then if troopPos then
@ -4760,6 +4833,7 @@ function CTLD:ScanHoverPickup()
end end
else else
-- Group doesn't exist or is dead, remove from tracking -- Group doesn't exist or is dead, remove from tracking
_removeFromSpatialGrid(troopGroupName, troopMeta.point, 'troops')
CTLD._deployedTroops[troopGroupName] = nil CTLD._deployedTroops[troopGroupName] = nil
end end
end end
@ -4915,6 +4989,7 @@ function CTLD:ScanHoverPickup()
local obj = StaticObject.getByName(bestName) local obj = StaticObject.getByName(bestName)
if obj then obj:destroy() end if obj then obj:destroy() end
_cleanupCrateSmoke(bestName) -- Clean up smoke refresh schedule _cleanupCrateSmoke(bestName) -- Clean up smoke refresh schedule
_removeFromSpatialGrid(bestName, bestMeta.point, 'crate') -- Remove from spatial index
CTLD._crates[bestName] = nil CTLD._crates[bestName] = nil
self:_addLoadedCrate(group, bestMeta.key) self:_addLoadedCrate(group, bestMeta.key)
if coachEnabled then if coachEnabled then
@ -4928,6 +5003,7 @@ function CTLD:ScanHoverPickup()
if troopGroup then if troopGroup then
troopGroup:Destroy() troopGroup:Destroy()
end end
_removeFromSpatialGrid(bestName, bestMeta.point, 'troops') -- Remove from spatial index
CTLD._deployedTroops[bestName] = nil CTLD._deployedTroops[bestName] = nil
-- ADD to existing troops if any, don't overwrite -- ADD to existing troops if any, don't overwrite
@ -5320,6 +5396,8 @@ function CTLD:UnloadTroops(group, opts)
weightKg = load.weightKg or 0, weightKg = load.weightKg or 0,
behavior = opts and opts.behavior or 'defend' behavior = opts and opts.behavior or 'defend'
} }
-- Add to spatial index for efficient hover pickup
_addToSpatialGrid(troopGroupName, CTLD._deployedTroops[troopGroupName], 'troops')
CTLD._troopsLoaded[gname] = nil CTLD._troopsLoaded[gname] = nil
@ -7038,7 +7116,7 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
-- Send initial deployment message -- Send initial deployment message
local gridStr = self:_GetMGRSString(position) local gridStr = self:_GetMGRSString(position)
local msg = _fmtMsg(CTLD.Messages.medevac_mash_deployed, { local msg = _fmtTemplate(CTLD.Messages.medevac_mash_deployed, {
mash_id = CTLD._mobileMASHCounter[side], mash_id = CTLD._mobileMASHCounter[side],
grid = gridStr, grid = gridStr,
freq = cfg.MobileMASH.BeaconFrequency or '30.0 FM' freq = cfg.MobileMASH.BeaconFrequency or '30.0 FM'
@ -7060,7 +7138,7 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
local currentPos = group:GetCoordinate() local currentPos = group:GetCoordinate()
if currentPos then if currentPos then
local currentGrid = ctldInstance:_GetMGRSString({x = currentPos.x, z = currentPos.z}) local currentGrid = ctldInstance:_GetMGRSString({x = currentPos.x, z = currentPos.z})
local announceMsg = _fmtMsg(CTLD.Messages.medevac_mash_announcement, { local announceMsg = _fmtTemplate(CTLD.Messages.medevac_mash_announcement, {
mash_id = CTLD._mobileMASHCounter[side], mash_id = CTLD._mobileMASHCounter[side],
grid = currentGrid, grid = currentGrid,
freq = cfg.MobileMASH.BeaconFrequency or '30.0 FM' freq = cfg.MobileMASH.BeaconFrequency or '30.0 FM'
@ -7106,7 +7184,7 @@ function CTLD:_RemoveMobileMASH(mashId)
if mash.textId then trigger.action.removeMark(mash.textId) end if mash.textId then trigger.action.removeMark(mash.textId) end
-- Send destruction message -- Send destruction message
local msg = _fmtMsg(CTLD.Messages.medevac_mash_destroyed, { local msg = _fmtTemplate(CTLD.Messages.medevac_mash_destroyed, {
mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?' mash_id = string.match(mashId, 'MOBILE_MASH_%d+_(%d+)') or '?'
}) })
trigger.action.outTextForCoalition(mash.side, msg, 20) trigger.action.outTextForCoalition(mash.side, msg, 20)
@ -7214,6 +7292,79 @@ end
function CTLD:SetAllowedAircraft(list) function CTLD:SetAllowedAircraft(list)
self.Config.AllowedAircraft = DeepCopy(list) self.Config.AllowedAircraft = DeepCopy(list)
end end
-- Explicit cleanup handler for mission end
-- Call this to properly shut down all CTLD schedulers and clear state
function CTLD:Cleanup()
env.info('[Moose_CTLD] Cleanup initiated - stopping all schedulers and clearing state')
-- Stop all smoke refresh schedulers
if CTLD._smokeRefreshSchedules then
for crateId, schedule in pairs(CTLD._smokeRefreshSchedules) do
if schedule.funcId then
pcall(function() timer.removeFunction(schedule.funcId) end)
end
end
CTLD._smokeRefreshSchedules = {}
end
-- Stop all Mobile MASH schedulers
if CTLD._mashZones then
for mashId, mash in pairs(CTLD._mashZones) do
if mash.scheduler then
pcall(function() mash.scheduler:Stop() end)
end
if mash.eventHandler then
-- Event handlers clean themselves up, but we can nil the reference
mash.eventHandler = nil
end
end
end
-- Stop any MEDEVAC timeout checkers or other schedulers
-- (If you add schedulers in the future, stop them here)
-- Clear spatial grid
CTLD._spatialGrid = {}
-- Clear state tables (optional - helps with memory in long-running missions)
CTLD._crates = {}
CTLD._troopsLoaded = {}
CTLD._loadedCrates = {}
CTLD._deployedTroops = {}
CTLD._hoverState = {}
CTLD._unitLast = {}
CTLD._coachState = {}
CTLD._msgState = {}
CTLD._buildConfirm = {}
CTLD._buildCooldown = {}
env.info('[Moose_CTLD] Cleanup complete')
end
-- Register mission end event to auto-cleanup
-- This ensures resources are properly released
if not CTLD._cleanupHandlerRegistered then
CTLD._cleanupHandlerRegistered = true
local cleanupHandler = EVENTHANDLER:New()
cleanupHandler:HandleEvent(EVENTS.MissionEnd)
function cleanupHandler:OnEventMissionEnd(EventData)
env.info('[Moose_CTLD] Mission end detected - initiating cleanup')
-- Cleanup all instances
for _, instance in pairs(CTLD._instances or {}) do
if instance and instance.Cleanup then
pcall(function() instance:Cleanup() end)
end
end
-- Also call static cleanup
if CTLD.Cleanup then
pcall(function() CTLD:Cleanup() end)
end
end
end
-- #endregion Public helpers -- #endregion Public helpers
-- ========================= -- =========================

Binary file not shown.