mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
Performance improvments. No new functionality.
This commit is contained in:
parent
bb59b12601
commit
21a9754b33
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -28,6 +28,7 @@
|
|||||||
-- 13) Inventory helpers
|
-- 13) Inventory helpers
|
||||||
-- 14) Public helpers (catalog registration/merge)
|
-- 14) Public helpers (catalog registration/merge)
|
||||||
-- 15) Export
|
-- 15) Export
|
||||||
|
-- #region Config
|
||||||
|
|
||||||
local CTLD = {}
|
local CTLD = {}
|
||||||
CTLD.__index = CTLD
|
CTLD.__index = CTLD
|
||||||
@ -168,7 +169,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,
|
LogLevel = 2, -- lowered from DEBUG (4) to INFO (2) for production performance
|
||||||
MessageDuration = 15, -- seconds for on-screen messages
|
MessageDuration = 15, -- seconds for on-screen messages
|
||||||
|
|
||||||
-- === Menu & Catalog ===
|
-- === Menu & Catalog ===
|
||||||
@ -1798,7 +1799,8 @@ end
|
|||||||
|
|
||||||
local function _logError(msg) _log(LOG_ERROR, msg) end
|
local function _logError(msg) _log(LOG_ERROR, msg) end
|
||||||
local function _logInfo(msg) _log(LOG_INFO, 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
|
local function _logDebug(msg) _log(LOG_DEBUG, msg) end
|
||||||
|
|
||||||
function CTLD:_collectEntryUnitTypes(entry)
|
function CTLD:_collectEntryUnitTypes(entry)
|
||||||
@ -2232,11 +2234,6 @@ local function _nextMarkupId()
|
|||||||
return CTLD._NextMarkupId
|
return CTLD._NextMarkupId
|
||||||
end
|
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)
|
local function _spawnCrateSmoke(position, color, config, crateId)
|
||||||
if not position or not color then return end
|
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
|
offsetVertical = tonumber(config.OffsetVertical) or 2
|
||||||
end
|
end
|
||||||
|
|
||||||
-- If smoke is disabled, skip entirely
|
|
||||||
if not enabled then return end
|
if not enabled then return end
|
||||||
|
|
||||||
-- Apply offset to smoke position so helicopters don't hover in the smoke
|
-- Compute ground-adjusted position with offsets
|
||||||
local smokePos = { x = position.x, y = position.y, z = position.z }
|
local sx, sz = position.x, position.z
|
||||||
if offsetMeters > 0 then
|
local sy = position.y or 0
|
||||||
local angle = 0 -- North by default
|
if sy == 0 and land and land.getHeight then
|
||||||
if offsetRandom then
|
local ok, h = pcall(land.getHeight, { x = sx, y = sz })
|
||||||
angle = math.random() * 2 * math.pi -- Random direction
|
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
|
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)
|
local coord = COORDINATE:New(smokePos.x, smokePos.y, smokePos.z)
|
||||||
if coord and coord.Smoke then
|
if coord and coord.Smoke then
|
||||||
-- MOOSE smoke method - produces better looking smoke similar to F6 cargo smoke
|
|
||||||
if color == trigger.smokeColor.Green then
|
if color == trigger.smokeColor.Green then
|
||||||
coord:SmokeGreen()
|
coord:SmokeGreen()
|
||||||
elseif color == trigger.smokeColor.Red then
|
elseif color == trigger.smokeColor.Red then
|
||||||
@ -2290,104 +2289,159 @@ local function _spawnCrateSmoke(position, color, config, crateId)
|
|||||||
elseif color == trigger.smokeColor.Blue then
|
elseif color == trigger.smokeColor.Blue then
|
||||||
coord:SmokeBlue()
|
coord:SmokeBlue()
|
||||||
else
|
else
|
||||||
coord:SmokeGreen() -- default
|
coord:SmokeGreen()
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- Fallback to trigger.action.smoke if MOOSE COORDINATE not available
|
|
||||||
trigger.action.smoke(smokePos, color)
|
trigger.action.smoke(smokePos, color)
|
||||||
end
|
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
|
if autoRefresh and crateId and refreshInterval > 0 and maxRefreshDuration > 0 then
|
||||||
|
CTLD._crates = CTLD._crates or {}
|
||||||
CTLD._smokeRefreshSchedules = CTLD._smokeRefreshSchedules or {}
|
local meta = CTLD._crates[crateId]
|
||||||
|
if meta then
|
||||||
-- Clear any existing schedule for this crate
|
meta._smoke = meta._smoke or {}
|
||||||
if CTLD._smokeRefreshSchedules[crateId] then
|
if not meta._smoke.enabled then
|
||||||
timer.removeFunction(CTLD._smokeRefreshSchedules[crateId].funcId)
|
meta._smoke.enabled = true
|
||||||
end
|
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
|
||||||
|
|
||||||
local startTime = timer.getTime()
|
-- Ensure background ticker(s) are running
|
||||||
local capturedColor = color -- Capture variables for the closure
|
if CTLD._ensureBackgroundTasks then
|
||||||
local capturedOffsetMeters = offsetMeters
|
CTLD:_ensureBackgroundTasks()
|
||||||
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)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return timer.getTime() + refreshInterval
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
local funcId = timer.scheduleFunction(refreshSmoke, nil, timer.getTime() + refreshInterval)
|
|
||||||
CTLD._smokeRefreshSchedules[crateId] = { funcId = funcId, startTime = startTime }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Clean up smoke refresh schedule for a crate
|
-- Clean up smoke refresh schedule for a crate
|
||||||
local function _cleanupCrateSmoke(crateId)
|
local function _cleanupCrateSmoke(crateId)
|
||||||
if not crateId then return end
|
if not crateId then return end
|
||||||
|
-- Clear legacy per-crate schedule if present
|
||||||
CTLD._smokeRefreshSchedules = CTLD._smokeRefreshSchedules or {}
|
CTLD._smokeRefreshSchedules = CTLD._smokeRefreshSchedules or {}
|
||||||
if CTLD._smokeRefreshSchedules[crateId] then
|
if CTLD._smokeRefreshSchedules[crateId] then
|
||||||
if CTLD._smokeRefreshSchedules[crateId].funcId then
|
if CTLD._smokeRefreshSchedules[crateId].funcId then
|
||||||
timer.removeFunction(CTLD._smokeRefreshSchedules[crateId].funcId)
|
pcall(timer.removeFunction, CTLD._smokeRefreshSchedules[crateId].funcId)
|
||||||
end
|
end
|
||||||
CTLD._smokeRefreshSchedules[crateId] = nil
|
CTLD._smokeRefreshSchedules[crateId] = nil
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- Spawn smoke for MEDEVAC crews with offset system
|
-- Spawn smoke for MEDEVAC crews with offset system
|
||||||
@ -2912,8 +2966,6 @@ local function _coachSend(self, group, unitName, key, data, isCoach)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function _eventSend(self, group, side, key, data)
|
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 now = timer.getTime()
|
||||||
local scopeKey
|
local scopeKey
|
||||||
if group then scopeKey = 'GRP:'..group:GetName() else scopeKey = 'COAL:'..tostring(side or self.Side) end
|
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)
|
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) < minGap then return end
|
||||||
if last > 0 and (now - last) < repeatGap 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)
|
local text = _fmtTemplate(tpl, data)
|
||||||
if not text or text == '' then return end
|
if not text or text == '' then return end
|
||||||
if group then _msgGroup(group, text) else _msgCoalition(side or self.Side, text) end
|
if group then _msgGroup(group, text) else _msgCoalition(side or self.Side, text) end
|
||||||
@ -3145,34 +3198,37 @@ function CTLD:New(cfg)
|
|||||||
o._ZoneFlagState = {}
|
o._ZoneFlagState = {}
|
||||||
o._ZoneFlagsPrimed = false
|
o._ZoneFlagsPrimed = false
|
||||||
o.ZoneFlagSched = SCHEDULER:New(nil, function()
|
o.ZoneFlagSched = SCHEDULER:New(nil, function()
|
||||||
if not o._ZoneFlagsPrimed then
|
local ok, err = pcall(function()
|
||||||
-- Prime states on first run without spamming messages
|
if not o._ZoneFlagsPrimed then
|
||||||
|
-- Prime states on first run without spamming messages
|
||||||
|
for _,b in ipairs(o._BindingsMerged) do
|
||||||
|
if b and b.flag and b.kind and b.name then
|
||||||
|
local val = (trigger and trigger.misc and trigger.misc.getUserFlag) and trigger.misc.getUserFlag(b.flag) or 0
|
||||||
|
local activeWhen = (b.activeWhen ~= nil) and b.activeWhen or 1
|
||||||
|
local shouldBeActive = (val == activeWhen)
|
||||||
|
local key = tostring(b.kind)..'|'..tostring(b.name)
|
||||||
|
o._ZoneFlagState[key] = shouldBeActive
|
||||||
|
o:SetZoneActive(b.kind, b.name, shouldBeActive, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
o._ZoneFlagsPrimed = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Subsequent runs: announce changes
|
||||||
for _,b in ipairs(o._BindingsMerged) do
|
for _,b in ipairs(o._BindingsMerged) do
|
||||||
if b and b.flag and b.kind and b.name then
|
if b and b.flag and b.kind and b.name then
|
||||||
local val = (trigger and trigger.misc and trigger.misc.getUserFlag) and trigger.misc.getUserFlag(b.flag) or 0
|
local val = (trigger and trigger.misc and trigger.misc.getUserFlag) and trigger.misc.getUserFlag(b.flag) or 0
|
||||||
local activeWhen = (b.activeWhen ~= nil) and b.activeWhen or 1
|
local activeWhen = (b.activeWhen ~= nil) and b.activeWhen or 1
|
||||||
local shouldBeActive = (val == activeWhen)
|
local shouldBeActive = (val == activeWhen)
|
||||||
local key = tostring(b.kind)..'|'..tostring(b.name)
|
local key = tostring(b.kind)..'|'..tostring(b.name)
|
||||||
o._ZoneFlagState[key] = shouldBeActive
|
if o._ZoneFlagState[key] ~= shouldBeActive then
|
||||||
o:SetZoneActive(b.kind, b.name, shouldBeActive, true)
|
o._ZoneFlagState[key] = shouldBeActive
|
||||||
|
o:SetZoneActive(b.kind, b.name, shouldBeActive, false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
o._ZoneFlagsPrimed = true
|
end)
|
||||||
return
|
if not ok then _logError('ZoneFlagSched error: '..tostring(err)) end
|
||||||
end
|
|
||||||
-- Subsequent runs: announce changes
|
|
||||||
for _,b in ipairs(o._BindingsMerged) do
|
|
||||||
if b and b.flag and b.kind and b.name then
|
|
||||||
local val = (trigger and trigger.misc and trigger.misc.getUserFlag) and trigger.misc.getUserFlag(b.flag) or 0
|
|
||||||
local activeWhen = (b.activeWhen ~= nil) and b.activeWhen or 1
|
|
||||||
local shouldBeActive = (val == activeWhen)
|
|
||||||
local key = tostring(b.kind)..'|'..tostring(b.name)
|
|
||||||
if o._ZoneFlagState[key] ~= shouldBeActive then
|
|
||||||
o._ZoneFlagState[key] = shouldBeActive
|
|
||||||
o:SetZoneActive(b.kind, b.name, shouldBeActive, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end, {}, 1, 1)
|
end, {}, 1, 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -3190,34 +3246,39 @@ function CTLD:New(cfg)
|
|||||||
|
|
||||||
-- Periodic cleanup for crates
|
-- Periodic cleanup for crates
|
||||||
o.Sched = SCHEDULER:New(nil, function()
|
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)
|
end, {}, 60, 60)
|
||||||
|
|
||||||
-- Periodic cleanup for deployed troops (remove dead/missing groups)
|
-- Periodic cleanup for deployed troops (remove dead/missing groups)
|
||||||
o.TroopCleanupSched = SCHEDULER:New(nil, function()
|
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)
|
end, {}, 30, 30)
|
||||||
|
|
||||||
-- 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()
|
||||||
o:AutoBuildFOBCheck()
|
local ok, err = pcall(function() o:AutoBuildFOBCheck() end)
|
||||||
end, {}, 10, 10) -- check every 10 seconds
|
if not ok then _logError('AutoBuildFOBCheck scheduler error: '..tostring(err)) end
|
||||||
|
end, {}, 10, 10) -- check every 10 seconds (tunable)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 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 = SCHEDULER:New(nil, function()
|
||||||
o:ScanHoverPickup()
|
local ok, err = pcall(function() o:ScanHoverPickup() end)
|
||||||
end, {}, 0.5, 0.5)
|
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
|
||||||
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
|
if CTLD.MEDEVAC and CTLD.MEDEVAC.Enabled then
|
||||||
local checkInterval = (CTLD.MEDEVAC.AutoPickup and CTLD.MEDEVAC.AutoPickup.CheckInterval) or 3
|
local checkInterval = (CTLD.MEDEVAC.AutoPickup and CTLD.MEDEVAC.AutoPickup.CheckInterval) or 3
|
||||||
o.MEDEVACSched = SCHEDULER:New(nil, function()
|
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, {}, checkInterval, checkInterval)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -3229,7 +3290,8 @@ function CTLD:New(cfg)
|
|||||||
jtacInterval = math.max(2, math.min(refresh, idle, 10))
|
jtacInterval = math.max(2, math.min(refresh, idle, 10))
|
||||||
end
|
end
|
||||||
o.JTACSched = SCHEDULER:New(nil, function()
|
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, {}, jtacInterval, jtacInterval)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -7604,14 +7666,16 @@ function CTLD:InitMEDEVAC()
|
|||||||
|
|
||||||
-- Start crew timeout checker (runs every 30 seconds)
|
-- Start crew timeout checker (runs every 30 seconds)
|
||||||
self.MEDEVACSched = SCHEDULER:New(nil, function()
|
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)
|
end, {}, 30, 30)
|
||||||
|
|
||||||
-- Start sling-load salvage crate checker (runs every 5 seconds by default)
|
-- Start sling-load salvage crate checker (runs every 5 seconds by default)
|
||||||
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
|
local interval = self.Config.SlingLoadSalvage.DetectionInterval or 5
|
||||||
self.SalvageSched = SCHEDULER:New(nil, function()
|
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)
|
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
|
||||||
@ -8356,7 +8420,11 @@ function CTLD:ScanMEDEVACAutoActions()
|
|||||||
end
|
end
|
||||||
|
|
||||||
if cfg.AutoUnload and cfg.AutoUnload.Enabled and hasGroundContact then
|
if cfg.AutoUnload and cfg.AutoUnload.Enabled and hasGroundContact then
|
||||||
self:AutoUnloadMEDEVACCrew(group)
|
-- 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
|
end
|
||||||
|
|
||||||
self:_TickMedevacEnrouteMessage(group, unit, isAirborne)
|
self:_TickMedevacEnrouteMessage(group, unit, isAirborne)
|
||||||
@ -8394,6 +8462,10 @@ function CTLD:AutoUnloadMEDEVACCrew(group)
|
|||||||
if not unit or not unit:IsAlive() then return end
|
if not unit or not unit:IsAlive() then return end
|
||||||
local gname = group:GetName() or 'UNKNOWN'
|
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 autoCfg = cfg.AutoUnload or {}
|
||||||
local aglLimit = autoCfg.GroundContactAGL or 2.0
|
local aglLimit = autoCfg.GroundContactAGL or 2.0
|
||||||
local gsLimit = autoCfg.MaxLandingSpeed or 2.0
|
local gsLimit = autoCfg.MaxLandingSpeed or 2.0
|
||||||
@ -8428,10 +8500,7 @@ function CTLD:AutoUnloadMEDEVACCrew(group)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local crews = self:_CollectRescuedCrewsForGroup(group:GetName())
|
local crews = self:_CollectRescuedCrewsForGroup(group:GetName())
|
||||||
if #crews == 0 then
|
if #crews == 0 then return end
|
||||||
_logDebug(string.format('[MEDEVAC][AutoUnload] %s skipped: no rescued crews onboard', gname))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check if inside MASH zone
|
-- Check if inside MASH zone
|
||||||
local pos = unit:GetPointVec3()
|
local pos = unit:GetPointVec3()
|
||||||
@ -10148,29 +10217,32 @@ function CTLD:_CreateMobileMASH(group, position, catalogDef)
|
|||||||
if cfg.MobileMASH.AnnouncementInterval and cfg.MobileMASH.AnnouncementInterval > 0 then
|
if cfg.MobileMASH.AnnouncementInterval and cfg.MobileMASH.AnnouncementInterval > 0 then
|
||||||
local ctldInstance = self
|
local ctldInstance = self
|
||||||
local scheduler = SCHEDULER:New(nil, function()
|
local scheduler = SCHEDULER:New(nil, function()
|
||||||
if not groupIsAlive() then
|
local ok, err = pcall(function()
|
||||||
ctldInstance:_RemoveMobileMASH(mashId)
|
if not groupIsAlive() then
|
||||||
return
|
ctldInstance:_RemoveMobileMASH(mashId)
|
||||||
end
|
return
|
||||||
|
|
||||||
local vec3 = groupVec3()
|
|
||||||
if vec3 then
|
|
||||||
mashData.position = { x = vec3.x, z = vec3.z }
|
|
||||||
if mashData.zone then
|
|
||||||
if mashData.zone.SetPointVec3 then
|
|
||||||
mashData.zone:SetPointVec3({ x = vec3.x, y = vec3.y or 0, z = vec3.z })
|
|
||||||
elseif mashData.zone.SetVec2 then
|
|
||||||
mashData.zone:SetVec2({ x = vec3.x, y = vec3.z })
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
local currentGrid = ctldInstance:_GetMGRSString({ x = vec3.x, z = vec3.z })
|
|
||||||
trigger.action.outTextForCoalition(side, _fmtTemplate(CTLD.Messages.medevac_mash_announcement, {
|
local vec3 = groupVec3()
|
||||||
mash_id = index,
|
if vec3 then
|
||||||
grid = currentGrid,
|
mashData.position = { x = vec3.x, z = vec3.z }
|
||||||
freq = beaconFreq,
|
if mashData.zone then
|
||||||
}), 20)
|
if mashData.zone.SetPointVec3 then
|
||||||
_logDebug(string.format('[MobileMASH] Announcement tick for %s at grid %s', displayName, tostring(currentGrid)))
|
mashData.zone:SetPointVec3({ x = vec3.x, y = vec3.y or 0, z = vec3.z })
|
||||||
end
|
elseif mashData.zone.SetVec2 then
|
||||||
|
mashData.zone:SetVec2({ x = vec3.x, y = vec3.z })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local currentGrid = ctldInstance:_GetMGRSString({ x = vec3.x, z = vec3.z })
|
||||||
|
trigger.action.outTextForCoalition(side, _fmtTemplate(CTLD.Messages.medevac_mash_announcement, {
|
||||||
|
mash_id = index,
|
||||||
|
grid = currentGrid,
|
||||||
|
freq = beaconFreq,
|
||||||
|
}), 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)
|
end, {}, cfg.MobileMASH.AnnouncementInterval, cfg.MobileMASH.AnnouncementInterval)
|
||||||
|
|
||||||
mashData.scheduler = scheduler
|
mashData.scheduler = scheduler
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user