Added a guarded _safeRemoveMenu helper in Moose_CTLD_FAC.lua, so menu cleanup now verifies registration and logs stale handles instead of triggering BASE menu errors.

Introduced a safeCoordinate utility and threaded it through the stuck-aircraft monitor, RTB routing, and interceptor launch flow in Moose_TADC_Load2nd.lua, wrapping all coordinate math/tasking calls with pcall and emitting clear diagnostics when data vanishes mid-mission.
Normalized interceptor name/coordinate tracking to reuse the same safely-fetched identifiers, preventing future nil dereferences when RTB or cleanup runs after units despawn.
This commit is contained in:
iTracerFacer 2025-11-15 02:04:01 -06:00
parent dddd39407a
commit 88c159d52a
3 changed files with 145 additions and 29 deletions

View File

@ -2375,9 +2375,33 @@ function CTLD:_cancelSchedule(key)
end end
local function _removeMenuHandle(menu) local function _removeMenuHandle(menu)
if not menu then return end if not menu or type(menu) ~= 'table' then return end
if type(menu) ~= 'table' then return end
if menu.Remove then pcall(function() menu:Remove() end) end local function _menuIsRegistered(m)
if not MENU_INDEX then return true end
if not m.Group or not m.MenuText then return true end
local okPath, path = pcall(function()
return MENU_INDEX:ParentPath(m.ParentMenu, m.MenuText)
end)
if not okPath or not path then
return false
end
local okHas, registered = pcall(function()
return MENU_INDEX:HasGroupMenu(m.Group, path)
end)
if not okHas then
return false
end
return registered == m
end
if menu.Remove and _menuIsRegistered(menu) then
local ok, err = pcall(function() menu:Remove() end)
if not ok then
_logVerbose(string.format('[MenuCleanup] Failed to remove menu %s: %s', tostring(menu.MenuText), tostring(err)))
end
end
if menu.Destroy then pcall(function() menu:Destroy() end) end if menu.Destroy then pcall(function() menu:Destroy() end) end
if menu.Delete then pcall(function() menu:Delete() end) end if menu.Delete then pcall(function() menu:Delete() end) end
end end
@ -9165,7 +9189,7 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry)
salvageValue = salvageValue, salvageValue = salvageValue,
originalHeading = heading, originalHeading = heading,
requestTime = nil, -- Will be set after announcement delay requestTime = nil, -- Will be set after announcement delay
warningsSent = 0, warningsSent = {},
invulnerable = false, invulnerable = false,
invulnerableUntil = 0, invulnerableUntil = 0,
greetingSent = false greetingSent = false
@ -9302,6 +9326,9 @@ function CTLD:_CheckMEDEVACTimeouts()
local requestTime = data.requestTime local requestTime = data.requestTime
if requestTime then -- Only check after crew has requested pickup if requestTime then -- Only check after crew has requested pickup
local elapsed = now - requestTime local elapsed = now - requestTime
if type(data.warningsSent) ~= 'table' then
data.warningsSent = {}
end
local remaining = (cfg.CrewTimeout or 3600) - elapsed local remaining = (cfg.CrewTimeout or 3600) - elapsed
-- Check for approaching rescue helos (pop smoke and send greeting with cooldown) -- Check for approaching rescue helos (pop smoke and send greeting with cooldown)

View File

@ -454,13 +454,44 @@ end
-- #endregion Event wiring -- #endregion Event wiring
-- #region Housekeeping -- #region Housekeeping
function FAC:_safeRemoveMenu(menu, reason)
if not menu or type(menu) ~= 'table' then return end
local shouldRemove = true
if MENU_INDEX and menu.Group and menu.MenuText then
local okPath, path = pcall(function()
return MENU_INDEX:ParentPath(menu.ParentMenu, menu.MenuText)
end)
if okPath and path then
local okHas, registered = pcall(function()
return MENU_INDEX:HasGroupMenu(menu.Group, path)
end)
if not okHas or registered ~= menu then
shouldRemove = false
end
else
shouldRemove = false
end
end
if shouldRemove and menu.Remove then
local ok, err = pcall(function() menu:Remove() end)
if not ok and err then
_log(self, LOG_VERBOSE, string.format('Failed removing menu (%s): %s', tostring(reason or menu.MenuText or 'unknown'), tostring(err)))
end
elseif not shouldRemove then
_log(self, LOG_DEBUG, string.format('Skip stale menu removal (%s)', tostring(reason or menu.MenuText or 'unknown')))
end
if menu.Destroy then pcall(function() menu:Destroy() end) end
if menu.Delete then pcall(function() menu:Delete() end) end
end
function FAC:_cleanupMenuForGroup(gname) function FAC:_cleanupMenuForGroup(gname)
local menuSet = self._menus[gname] local menuSet = self._menus[gname]
if not menuSet then return end if not menuSet then return end
for _,menu in pairs(menuSet) do for _,menu in pairs(menuSet) do
if menu and menu.Remove then self:_safeRemoveMenu(menu, gname)
pcall(function() menu:Remove() end)
end
end end
self._menus[gname] = nil self._menus[gname] = nil
self._menuLastSeen[gname] = nil self._menuLastSeen[gname] = nil

View File

@ -382,6 +382,17 @@ local function log(message, detailed)
end end
end end
local function safeCoordinate(object)
if not object or type(object) ~= "table" or not object.GetCoordinate then
return nil
end
local ok, coord = pcall(function() return object:GetCoordinate() end)
if ok and coord then
return coord
end
return nil
end
-- Performance optimization: Cache SET_GROUP objects to avoid repeated creation -- Performance optimization: Cache SET_GROUP objects to avoid repeated creation
local cachedSets = { local cachedSets = {
redCargo = nil, redCargo = nil,
@ -1360,9 +1371,18 @@ local function monitorStuckAircraft()
-- Only check aircraft that have been spawned for at least the threshold time -- Only check aircraft that have been spawned for at least the threshold time
if timeSinceSpawn >= stuckThreshold then if timeSinceSpawn >= stuckThreshold then
local currentPos = trackingData.group:GetCoordinate() local currentPos = safeCoordinate(trackingData.group)
if currentPos and trackingData.spawnPos then local spawnPos = trackingData.spawnPos
local distanceMoved = trackingData.spawnPos:Get2DDistance(currentPos) local distanceMoved = nil
if currentPos and spawnPos and type(spawnPos) == "table" and spawnPos.Get2DDistance then
local okDist, dist = pcall(function() return spawnPos:Get2DDistance(currentPos) end)
if okDist and dist then
distanceMoved = dist
end
end
if distanceMoved then
-- Check if aircraft has moved less than threshold (stuck) -- Check if aircraft has moved less than threshold (stuck)
if distanceMoved < movementThreshold then if distanceMoved < movementThreshold then
@ -1385,6 +1405,9 @@ local function monitorStuckAircraft()
log("Aircraft " .. aircraftName .. " has moved " .. math.floor(distanceMoved) .. "m - removing from stuck monitoring", true) log("Aircraft " .. aircraftName .. " has moved " .. math.floor(distanceMoved) .. "m - removing from stuck monitoring", true)
aircraftSpawnTracking[coalitionKey][aircraftName] = nil aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end end
else
log("Stuck monitor: no coordinate data for " .. aircraftName .. "; removing from tracking", true)
aircraftSpawnTracking[coalitionKey][aircraftName] = nil
end end
end end
else else
@ -1402,12 +1425,13 @@ local function sendInterceptorHome(interceptor, coalitionSide)
end end
-- Find nearest friendly airbase -- Find nearest friendly airbase
local interceptorCoord = interceptor:GetCoordinate() local interceptorCoord = safeCoordinate(interceptor)
if not interceptorCoord then if not interceptorCoord then
log("ERROR: Could not get interceptor coordinates for RTB", true) log("ERROR: Could not get interceptor coordinates for RTB", true)
return return
end end
local nearestAirbase = nil local nearestAirbase = nil
local nearestAirbaseCoord = nil
local shortestDistance = math.huge local shortestDistance = math.huge
local squadronConfig = getSquadronConfig(coalitionSide) local squadronConfig = getSquadronConfig(coalitionSide)
@ -1415,26 +1439,48 @@ local function sendInterceptorHome(interceptor, coalitionSide)
for _, squadron in pairs(squadronConfig) do for _, squadron in pairs(squadronConfig) do
local airbase = AIRBASE:FindByName(squadron.airbaseName) local airbase = AIRBASE:FindByName(squadron.airbaseName)
if airbase and airbase:GetCoalition() == coalitionSide and airbase:IsAlive() then if airbase and airbase:GetCoalition() == coalitionSide and airbase:IsAlive() then
local airbaseCoord = airbase:GetCoordinate() local airbaseCoord = safeCoordinate(airbase)
local distance = interceptorCoord:Get2DDistance(airbaseCoord) if airbaseCoord then
if distance < shortestDistance then local okDist, distance = pcall(function() return interceptorCoord:Get2DDistance(airbaseCoord) end)
shortestDistance = distance if okDist and distance and distance < shortestDistance then
nearestAirbase = airbase shortestDistance = distance
nearestAirbase = airbase
nearestAirbaseCoord = airbaseCoord
end
end end
end end
end end
if nearestAirbase then if nearestAirbase and nearestAirbaseCoord then
local airbaseCoord = nearestAirbase:GetCoordinate() local airbaseName = "airbase"
local okABName, fetchedABName = pcall(function() return nearestAirbase:GetName() end)
if okABName and fetchedABName then
airbaseName = fetchedABName
end
local rtbAltitude = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters local rtbAltitude = ADVANCED_SETTINGS.rtbAltitude * 0.3048 -- Convert feet to meters
local rtbCoord = airbaseCoord:SetAltitude(rtbAltitude) local okRtb, rtbCoord = pcall(function() return nearestAirbaseCoord:SetAltitude(rtbAltitude) end)
if not okRtb or not rtbCoord then
log("ERROR: Failed to compute RTB coordinate for " .. airbaseName, true)
return
end
-- Clear current tasks and route home -- Clear current tasks and route home
interceptor:ClearTasks() pcall(function() interceptor:ClearTasks() end)
interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") -- Convert knots to m/s local routeOk, routeErr = pcall(function() interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") end)
local _, coalitionName = getCoalitionSettings(coalitionSide) local _, coalitionName = getCoalitionSettings(coalitionSide)
log("Sending " .. coalitionName .. " " .. interceptor:GetName() .. " back to " .. nearestAirbase:GetName(), true) local interceptorName = "interceptor"
local okName, fetchedName = pcall(function() return interceptor:GetName() end)
if okName and fetchedName then
interceptorName = fetchedName
end
if not routeOk and routeErr then
log("ERROR: Failed to assign RTB route for " .. interceptorName .. " -> " .. airbaseName .. ": " .. tostring(routeErr), true)
else
log("Sending " .. coalitionName .. " " .. interceptorName .. " back to " .. airbaseName, true)
end
-- Schedule cleanup after they should have landed -- Schedule cleanup after they should have landed
local coalitionSettings = getCoalitionSettings(coalitionSide) local coalitionSettings = getCoalitionSettings(coalitionSide)
@ -1739,10 +1785,16 @@ local function launchInterceptor(threatGroup, coalitionSide)
interceptor:OptionROTVertical() interceptor:OptionROTVertical()
-- Route to threat -- Route to threat
local currentThreatCoord = threatGroup:GetCoordinate() local currentThreatCoord = safeCoordinate(threatGroup)
if currentThreatCoord then if currentThreatCoord then
local interceptCoord = currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) -- Convert feet to meters local okIntercept, interceptCoord = pcall(function()
interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") -- Convert knots to m/s return currentThreatCoord:SetAltitude(squadron.altitude * 0.3048)
end)
if okIntercept and interceptCoord then
pcall(function()
interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO")
end)
end
-- Attack the threat -- Attack the threat
local attackTask = { local attackTask = {
@ -1760,22 +1812,28 @@ local function launchInterceptor(threatGroup, coalitionSide)
end, {}, 3) end, {}, 3)
-- Track the interceptor with squadron info -- Track the interceptor with squadron info
activeInterceptors[coalitionKey][interceptor:GetName()] = { local interceptorName = "interceptor"
local okName, fetchedName = pcall(function() return interceptor:GetName() end)
if okName and fetchedName then
interceptorName = fetchedName
end
activeInterceptors[coalitionKey][interceptorName] = {
group = interceptor, group = interceptor,
squadron = squadron.templateName, squadron = squadron.templateName,
displayName = squadron.displayName displayName = squadron.displayName
} }
-- Track spawn position for stuck aircraft detection -- Track spawn position for stuck aircraft detection
local spawnPos = interceptor:GetCoordinate() local spawnPos = safeCoordinate(interceptor)
if spawnPos then if spawnPos then
aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = { aircraftSpawnTracking[coalitionKey][interceptorName] = {
spawnPos = spawnPos, spawnPos = spawnPos,
spawnTime = timer.getTime(), spawnTime = timer.getTime(),
squadron = squadron, squadron = squadron,
airbase = squadron.airbaseName airbase = squadron.airbaseName
} }
log("Tracking spawn position for " .. interceptor:GetName() .. " at " .. squadron.airbaseName, true) log("Tracking spawn position for " .. interceptorName .. " at " .. squadron.airbaseName, true)
end end
-- Emergency cleanup (safety net) -- Emergency cleanup (safety net)