Performance improvments. No new functionality.

This commit is contained in:
iTracerFacer 2025-11-11 10:37:28 -06:00
parent bb59b12601
commit 21a9754b33
4 changed files with 238 additions and 166 deletions

View File

@ -28,6 +28,7 @@
-- 13) Inventory helpers
-- 14) Public helpers (catalog registration/merge)
-- 15) Export
-- #region Config
local CTLD = {}
CTLD.__index = CTLD
@ -168,7 +169,7 @@ CTLD.Config = {
-- 2 = INFO - Important state changes, initialization, cleanup (default for production)
-- 3 = VERBOSE - Detailed operational info (zone validation, menus, builds, MEDEVAC events)
-- 4 = DEBUG - Everything including hover checks, crate pickups, detailed troop spawns
LogLevel = 4,
LogLevel = 2, -- lowered from DEBUG (4) to INFO (2) for production performance
MessageDuration = 15, -- seconds for on-screen messages
-- === Menu & Catalog ===
@ -1798,7 +1799,8 @@ end
local function _logError(msg) _log(LOG_ERROR, msg) end
local function _logInfo(msg) _log(LOG_INFO, msg) end
local function _logVerbose(msg) _log(LOG_VERBOSE, msg) end
-- Treat VERBOSE as DEBUG-only to reduce noise unless LogLevel is 4
local function _logVerbose(msg) _log(LOG_DEBUG, msg) end
local function _logDebug(msg) _log(LOG_DEBUG, msg) end
function CTLD:_collectEntryUnitTypes(entry)
@ -2232,11 +2234,6 @@ local function _nextMarkupId()
return CTLD._NextMarkupId
end
-- Spawn smoke at a position using MOOSE COORDINATE smoke (better appearance) or trigger smoke (old thick ground smoke)
-- position: {x, y, z} table (Vec3)
-- color: trigger.smokeColor enum value
-- config: reference to a CrateSmoke config table (or nil to use defaults)
-- crateId: optional crate identifier for tracking smoke refresh schedules
local function _spawnCrateSmoke(position, color, config, crateId)
if not position or not color then return end
@ -2259,26 +2256,28 @@ local function _spawnCrateSmoke(position, color, config, crateId)
offsetVertical = tonumber(config.OffsetVertical) or 2
end
-- If smoke is disabled, skip entirely
if not enabled then return end
-- Apply offset to smoke position so helicopters don't hover in the smoke
local smokePos = { x = position.x, y = position.y, z = position.z }
if offsetMeters > 0 then
local angle = 0 -- North by default
if offsetRandom then
angle = math.random() * 2 * math.pi -- Random direction
-- Compute ground-adjusted position with offsets
local sx, sz = position.x, position.z
local sy = position.y or 0
if sy == 0 and land and land.getHeight then
local ok, h = pcall(land.getHeight, { x = sx, y = sz })
if ok and type(h) == 'number' then sy = h end
end
smokePos.x = smokePos.x + offsetMeters * math.cos(angle)
smokePos.z = smokePos.z + offsetMeters * math.sin(angle)
end
-- Apply vertical offset (above ground level)
smokePos.y = smokePos.y + offsetVertical
-- Spawn the smoke using MOOSE COORDINATE (better appearance than trigger.action.smoke)
-- Apply lateral and vertical offsets
local ox, oz = 0, 0
if offsetMeters > 0 then
local angle = offsetRandom and (math.random() * 2 * math.pi) or 0
ox = offsetMeters * math.cos(angle)
oz = offsetMeters * math.sin(angle)
end
local smokePos = { x = sx + ox, y = sy + offsetVertical, z = sz + oz }
-- Emit smoke now
local coord = COORDINATE:New(smokePos.x, smokePos.y, smokePos.z)
if coord and coord.Smoke then
-- MOOSE smoke method - produces better looking smoke similar to F6 cargo smoke
if color == trigger.smokeColor.Green then
coord:SmokeGreen()
elseif color == trigger.smokeColor.Red then
@ -2290,104 +2289,159 @@ local function _spawnCrateSmoke(position, color, config, crateId)
elseif color == trigger.smokeColor.Blue then
coord:SmokeBlue()
else
coord:SmokeGreen() -- default
coord:SmokeGreen()
end
else
-- Fallback to trigger.action.smoke if MOOSE COORDINATE not available
trigger.action.smoke(smokePos, color)
end
-- Schedule smoke refresh if enabled
-- Record smoke meta for global refresh loop instead of per-crate timer
if autoRefresh and crateId and refreshInterval > 0 and maxRefreshDuration > 0 then
CTLD._crates = CTLD._crates or {}
local meta = CTLD._crates[crateId]
if meta then
meta._smoke = meta._smoke or {}
if not meta._smoke.enabled then
meta._smoke.enabled = true
end
meta._smoke.auto = true
meta._smoke.startTime = timer.getTime()
meta._smoke.nextTime = timer.getTime() + refreshInterval
meta._smoke.interval = refreshInterval
meta._smoke.maxDuration = maxRefreshDuration
meta._smoke.color = color
meta._smoke.offsetMeters = offsetMeters
meta._smoke.offsetRandom = offsetRandom
meta._smoke.offsetVertical = offsetVertical
CTLD._smokeRefreshSchedules = CTLD._smokeRefreshSchedules or {}
-- Clear any existing schedule for this crate
if CTLD._smokeRefreshSchedules[crateId] then
timer.removeFunction(CTLD._smokeRefreshSchedules[crateId].funcId)
end
local startTime = timer.getTime()
local capturedColor = color -- Capture variables for the closure
local capturedOffsetMeters = offsetMeters
local capturedOffsetRandom = offsetRandom
local capturedOffsetVertical = offsetVertical
local function refreshSmoke()
local elapsed = timer.getTime() - startTime
if elapsed >= maxRefreshDuration then
-- Max refresh duration exceeded, stop refreshing (safety limit)
CTLD._smokeRefreshSchedules[crateId] = nil
return nil
end
-- Check if crate still exists
if not CTLD._crates or not CTLD._crates[crateId] then
-- Crate was picked up, built, or cleaned up - stop refreshing
CTLD._smokeRefreshSchedules[crateId] = nil
return nil
end
-- Refresh smoke at crate position
local crateMeta = CTLD._crates[crateId]
if crateMeta and crateMeta.point then
local sy = 0
if land and land.getHeight then
local ok, h = pcall(land.getHeight, { x = crateMeta.point.x, y = crateMeta.point.z })
if ok and type(h) == 'number' then sy = h end
end
-- Apply offset to smoke position
local refreshSmokePos = { x = crateMeta.point.x, y = sy, z = crateMeta.point.z }
if capturedOffsetMeters > 0 then
local angle = 0 -- North by default
if capturedOffsetRandom then
angle = math.random() * 2 * math.pi -- Random direction
end
refreshSmokePos.x = refreshSmokePos.x + capturedOffsetMeters * math.cos(angle)
refreshSmokePos.z = refreshSmokePos.z + capturedOffsetMeters * math.sin(angle)
end
-- Apply vertical offset
refreshSmokePos.y = refreshSmokePos.y + capturedOffsetVertical
local refreshCoord = COORDINATE:New(refreshSmokePos.x, refreshSmokePos.y, refreshSmokePos.z)
if refreshCoord and refreshCoord.Smoke then
if capturedColor == trigger.smokeColor.Green then
refreshCoord:SmokeGreen()
elseif capturedColor == trigger.smokeColor.Red then
refreshCoord:SmokeRed()
elseif capturedColor == trigger.smokeColor.White then
refreshCoord:SmokeWhite()
elseif capturedColor == trigger.smokeColor.Orange then
refreshCoord:SmokeOrange()
elseif capturedColor == trigger.smokeColor.Blue then
refreshCoord:SmokeBlue()
else
refreshCoord:SmokeGreen()
end
else
trigger.action.smoke(refreshSmokePos, capturedColor)
-- Ensure background ticker(s) are running
if CTLD._ensureBackgroundTasks then
CTLD:_ensureBackgroundTasks()
end
end
return timer.getTime() + refreshInterval
end
local funcId = timer.scheduleFunction(refreshSmoke, nil, timer.getTime() + refreshInterval)
CTLD._smokeRefreshSchedules[crateId] = { funcId = funcId, startTime = startTime }
end
end
-- Clean up smoke refresh schedule for a crate
local function _cleanupCrateSmoke(crateId)
if not crateId then return end
-- Clear legacy per-crate schedule if present
CTLD._smokeRefreshSchedules = CTLD._smokeRefreshSchedules or {}
if CTLD._smokeRefreshSchedules[crateId] then
if CTLD._smokeRefreshSchedules[crateId].funcId then
timer.removeFunction(CTLD._smokeRefreshSchedules[crateId].funcId)
pcall(timer.removeFunction, CTLD._smokeRefreshSchedules[crateId].funcId)
end
CTLD._smokeRefreshSchedules[crateId] = nil
end
-- Clear new smoke meta so the global loop stops refreshing
if CTLD._crates and CTLD._crates[crateId] then
CTLD._crates[crateId]._smoke = nil
end
end
-- Central schedule registry helpers
function CTLD:_registerSchedule(key, funcId)
self._schedules = self._schedules or {}
if self._schedules[key] then
pcall(timer.removeFunction, self._schedules[key])
end
self._schedules[key] = funcId
end
function CTLD:_cancelSchedule(key)
if self._schedules and self._schedules[key] then
pcall(timer.removeFunction, self._schedules[key])
self._schedules[key] = nil
end
end
-- Global smoke refresh ticker (single loop for all crates)
function CTLD:_ensureGlobalSmokeTicker()
if self._schedules and self._schedules.smokeTicker then return end
local function tick()
local now = timer.getTime()
if CTLD and CTLD._crates then
for name, meta in pairs(CTLD._crates) do
if meta and meta._smoke and meta._smoke.auto and meta.point then
local s = meta._smoke
if (now - (s.startTime or now)) > (s.maxDuration or 0) then
meta._smoke = nil
elseif now >= (s.nextTime or 0) then
-- Spawn another puff
local pos = { x = meta.point.x, y = 0, z = meta.point.z }
if land and land.getHeight then
local ok, h = pcall(land.getHeight, { x = pos.x, y = pos.z })
if ok and type(h) == 'number' then pos.y = h end
end
_spawnCrateSmoke(pos, s.color or trigger.smokeColor.Green, {
Enabled = true,
AutoRefresh = false, -- avoid recursion; we manage nextTime here
OffsetMeters = s.offsetMeters or 0,
OffsetRandom = s.offsetRandom ~= false,
OffsetVertical = s.offsetVertical or 0,
}, name)
s.nextTime = now + (s.interval or 240)
end
end
end
end
return timer.getTime() + 10 -- tick every 10s
end
local id = timer.scheduleFunction(tick, nil, timer.getTime() + 10)
self:_registerSchedule('smokeTicker', id)
end
-- Periodic GC to prune stale messaging/coach entries and smoke meta
function CTLD:_ensurePeriodicGC()
if self._schedules and self._schedules.periodicGC then return end
local function gcTick()
-- Coach state: remove units that no longer exist
if CTLD and CTLD._coachState then
for uname, _ in pairs(CTLD._coachState) do
local u = Unit.getByName(uname)
if not u then CTLD._coachState[uname] = nil end
end
end
-- Message throttle state: remove dead/missing groups
if CTLD and CTLD._msgState then
for scope, _ in pairs(CTLD._msgState) do
local gname = string.match(scope, '^GRP:(.+)$')
if gname then
local g = Group.getByName(gname)
if not g then CTLD._msgState[scope] = nil end
end
end
end
-- Smoke meta: prune crates without points or exceeded duration
if CTLD and CTLD._crates then
local now = timer.getTime()
for name, meta in pairs(CTLD._crates) do
if meta and meta._smoke then
local s = meta._smoke
if (not meta.point) or ((now - (s.startTime or now)) > (s.maxDuration or 0)) then
meta._smoke = nil
end
end
end
end
return timer.getTime() + 300 -- every 5 minutes
end
local id = timer.scheduleFunction(gcTick, nil, timer.getTime() + 300)
self:_registerSchedule('periodicGC', id)
end
function CTLD:_ensureBackgroundTasks()
if self._bgStarted then return end
self._bgStarted = true
self:_ensureGlobalSmokeTicker()
self:_ensurePeriodicGC()
end
-- Spawn smoke for MEDEVAC crews with offset system
@ -2912,8 +2966,6 @@ local function _coachSend(self, group, unitName, key, data, isCoach)
end
local function _eventSend(self, group, side, key, data)
local tpl = CTLD.Messages and CTLD.Messages[key]
if not tpl then return end
local now = timer.getTime()
local scopeKey
if group then scopeKey = 'GRP:'..group:GetName() else scopeKey = 'COAL:'..tostring(side or self.Side) end
@ -2925,7 +2977,8 @@ local function _eventSend(self, group, side, key, data)
local repeatGap = (cfg and cfg.throttle and cfg.throttle.repeatSame) or (minGap * 2)
if last > 0 and (now - last) < minGap then return end
if last > 0 and (now - last) < repeatGap then return end
local tpl = CTLD.Messages and CTLD.Messages[key]
if not tpl then return end
local text = _fmtTemplate(tpl, data)
if not text or text == '' then return end
if group then _msgGroup(group, text) else _msgCoalition(side or self.Side, text) end
@ -3145,6 +3198,7 @@ function CTLD:New(cfg)
o._ZoneFlagState = {}
o._ZoneFlagsPrimed = false
o.ZoneFlagSched = SCHEDULER:New(nil, function()
local ok, err = pcall(function()
if not o._ZoneFlagsPrimed then
-- Prime states on first run without spamming messages
for _,b in ipairs(o._BindingsMerged) do
@ -3173,6 +3227,8 @@ function CTLD:New(cfg)
end
end
end
end)
if not ok then _logError('ZoneFlagSched error: '..tostring(err)) end
end, {}, 1, 1)
end
end
@ -3190,34 +3246,39 @@ function CTLD:New(cfg)
-- Periodic cleanup for crates
o.Sched = SCHEDULER:New(nil, function()
o:CleanupCrates()
local ok, err = pcall(function() o:CleanupCrates() end)
if not ok then _logError('CleanupCrates scheduler error: '..tostring(err)) end
end, {}, 60, 60)
-- Periodic cleanup for deployed troops (remove dead/missing groups)
o.TroopCleanupSched = SCHEDULER:New(nil, function()
o:CleanupDeployedTroops()
local ok, err = pcall(function() o:CleanupDeployedTroops() end)
if not ok then _logError('CleanupDeployedTroops scheduler error: '..tostring(err)) end
end, {}, 30, 30)
-- Optional: auto-build FOBs inside FOB zones when crates present
if o.Config.AutoBuildFOBInZones then
o.AutoFOBSched = SCHEDULER:New(nil, function()
o:AutoBuildFOBCheck()
end, {}, 10, 10) -- check every 10 seconds
local ok, err = pcall(function() o:AutoBuildFOBCheck() end)
if not ok then _logError('AutoBuildFOBCheck scheduler error: '..tostring(err)) end
end, {}, 10, 10) -- check every 10 seconds (tunable)
end
-- Optional: hover pickup scanner
local coachCfg = CTLD.HoverCoachConfig or {}
if coachCfg.enabled then
o.HoverSched = SCHEDULER:New(nil, function()
o:ScanHoverPickup()
end, {}, 0.5, 0.5)
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
end
-- MEDEVAC auto-pickup and auto-unload scheduler
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
local checkInterval = (CTLD.MEDEVAC.AutoPickup and CTLD.MEDEVAC.AutoPickup.CheckInterval) or 3
o.MEDEVACSched = SCHEDULER:New(nil, function()
o:ScanMEDEVACAutoActions()
local ok, err = pcall(function() o:ScanMEDEVACAutoActions() end)
if not ok then _logError('MEDEVAC auto-actions scheduler error: '..tostring(err)) end
end, {}, checkInterval, checkInterval)
end
@ -3229,7 +3290,8 @@ function CTLD:New(cfg)
jtacInterval = math.max(2, math.min(refresh, idle, 10))
end
o.JTACSched = SCHEDULER:New(nil, function()
o:_tickJTACs()
local ok, err = pcall(function() o:_tickJTACs() end)
if not ok then _logError('JTAC tick scheduler error: '..tostring(err)) end
end, {}, jtacInterval, jtacInterval)
end
@ -7604,14 +7666,16 @@ function CTLD:InitMEDEVAC()
-- Start crew timeout checker (runs every 30 seconds)
self.MEDEVACSched = SCHEDULER:New(nil, function()
selfref:_CheckMEDEVACTimeouts()
local ok, err = pcall(function() selfref:_CheckMEDEVACTimeouts() end)
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)
if self.Config.SlingLoadSalvage and self.Config.SlingLoadSalvage.Enabled then
local interval = self.Config.SlingLoadSalvage.DetectionInterval or 5
self.SalvageSched = SCHEDULER:New(nil, function()
selfref:_CheckSlingLoadSalvageCrates()
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
@ -8356,8 +8420,12 @@ function CTLD:ScanMEDEVACAutoActions()
end
if cfg.AutoUnload and cfg.AutoUnload.Enabled and hasGroundContact then
-- Reduce log spam: only attempt auto-unload when there are rescued crews onboard
local crews = self:_CollectRescuedCrewsForGroup(group:GetName())
if crews and #crews > 0 then
self:AutoUnloadMEDEVACCrew(group)
end
end
self:_TickMedevacEnrouteMessage(group, unit, isAirborne)
else
@ -8394,6 +8462,10 @@ function CTLD:AutoUnloadMEDEVACCrew(group)
if not unit or not unit:IsAlive() then return end
local gname = group:GetName() or 'UNKNOWN'
-- Early silent exit (reduces log spam): only proceed if there are rescued crews onboard
local earlyCrews = self:_CollectRescuedCrewsForGroup(gname)
if not earlyCrews or #earlyCrews == 0 then return end
local autoCfg = cfg.AutoUnload or {}
local aglLimit = autoCfg.GroundContactAGL or 2.0
local gsLimit = autoCfg.MaxLandingSpeed or 2.0
@ -8428,10 +8500,7 @@ function CTLD:AutoUnloadMEDEVACCrew(group)
end
local crews = self:_CollectRescuedCrewsForGroup(group:GetName())
if #crews == 0 then
_logDebug(string.format('[MEDEVAC][AutoUnload] %s skipped: no rescued crews onboard', gname))
return
end
if #crews == 0 then return end
-- Check if inside MASH zone
local pos = unit:GetPointVec3()
@ -10148,6 +10217,7 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
if cfg.MobileMASH.AnnouncementInterval and cfg.MobileMASH.AnnouncementInterval > 0 then
local ctldInstance = self
local scheduler = SCHEDULER:New(nil, function()
local ok, err = pcall(function()
if not groupIsAlive() then
ctldInstance:_RemoveMobileMASH(mashId)
return
@ -10171,6 +10241,8 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
}), 20)
_logDebug(string.format('[MobileMASH] Announcement tick for %s at grid %s', displayName, tostring(currentGrid)))
end
end)
if not ok then _logError('Mobile MASH announcement scheduler error: '..tostring(err)) end
end, {}, cfg.MobileMASH.AnnouncementInterval, cfg.MobileMASH.AnnouncementInterval)
mashData.scheduler = scheduler