mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
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:
parent
dddd39407a
commit
88c159d52a
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user