diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index 64c9f0f..bb37236 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -2375,9 +2375,33 @@ function CTLD:_cancelSchedule(key) end local function _removeMenuHandle(menu) - if not menu then return end - if type(menu) ~= 'table' then return end - if menu.Remove then pcall(function() menu:Remove() end) end + if not menu or type(menu) ~= 'table' then return 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.Delete then pcall(function() menu:Delete() end) end end @@ -9165,7 +9189,7 @@ function CTLD:_SpawnMEDEVACCrew(eventData, catalogEntry) salvageValue = salvageValue, originalHeading = heading, requestTime = nil, -- Will be set after announcement delay - warningsSent = 0, + warningsSent = {}, invulnerable = false, invulnerableUntil = 0, greetingSent = false @@ -9302,6 +9326,9 @@ function CTLD:_CheckMEDEVACTimeouts() local requestTime = data.requestTime if requestTime then -- Only check after crew has requested pickup local elapsed = now - requestTime + if type(data.warningsSent) ~= 'table' then + data.warningsSent = {} + end local remaining = (cfg.CrewTimeout or 3600) - elapsed -- Check for approaching rescue helos (pop smoke and send greeting with cooldown) diff --git a/Moose_CTLD_Pure/Moose_CTLD_FAC.lua b/Moose_CTLD_Pure/Moose_CTLD_FAC.lua index cdf9954..61922ea 100644 --- a/Moose_CTLD_Pure/Moose_CTLD_FAC.lua +++ b/Moose_CTLD_Pure/Moose_CTLD_FAC.lua @@ -454,13 +454,44 @@ end -- #endregion Event wiring -- #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) local menuSet = self._menus[gname] if not menuSet then return end for _,menu in pairs(menuSet) do - if menu and menu.Remove then - pcall(function() menu:Remove() end) - end + self:_safeRemoveMenu(menu, gname) end self._menus[gname] = nil self._menuLastSeen[gname] = nil diff --git a/Moose_TADC/Moose_TADC_Load2nd.lua b/Moose_TADC/Moose_TADC_Load2nd.lua index 749b23c..7b06b50 100644 --- a/Moose_TADC/Moose_TADC_Load2nd.lua +++ b/Moose_TADC/Moose_TADC_Load2nd.lua @@ -382,6 +382,17 @@ local function log(message, detailed) 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 local cachedSets = { redCargo = nil, @@ -1360,9 +1371,18 @@ local function monitorStuckAircraft() -- Only check aircraft that have been spawned for at least the threshold time if timeSinceSpawn >= stuckThreshold then - local currentPos = trackingData.group:GetCoordinate() - if currentPos and trackingData.spawnPos then - local distanceMoved = trackingData.spawnPos:Get2DDistance(currentPos) + local currentPos = safeCoordinate(trackingData.group) + local spawnPos = trackingData.spawnPos + 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) 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) aircraftSpawnTracking[coalitionKey][aircraftName] = nil end + else + log("Stuck monitor: no coordinate data for " .. aircraftName .. "; removing from tracking", true) + aircraftSpawnTracking[coalitionKey][aircraftName] = nil end end else @@ -1402,12 +1425,13 @@ local function sendInterceptorHome(interceptor, coalitionSide) end -- Find nearest friendly airbase - local interceptorCoord = interceptor:GetCoordinate() + local interceptorCoord = safeCoordinate(interceptor) if not interceptorCoord then log("ERROR: Could not get interceptor coordinates for RTB", true) return end local nearestAirbase = nil + local nearestAirbaseCoord = nil local shortestDistance = math.huge local squadronConfig = getSquadronConfig(coalitionSide) @@ -1415,26 +1439,48 @@ local function sendInterceptorHome(interceptor, coalitionSide) for _, squadron in pairs(squadronConfig) do local airbase = AIRBASE:FindByName(squadron.airbaseName) if airbase and airbase:GetCoalition() == coalitionSide and airbase:IsAlive() then - local airbaseCoord = airbase:GetCoordinate() - local distance = interceptorCoord:Get2DDistance(airbaseCoord) - if distance < shortestDistance then - shortestDistance = distance - nearestAirbase = airbase + local airbaseCoord = safeCoordinate(airbase) + if airbaseCoord then + local okDist, distance = pcall(function() return interceptorCoord:Get2DDistance(airbaseCoord) end) + if okDist and distance and distance < shortestDistance then + shortestDistance = distance + nearestAirbase = airbase + nearestAirbaseCoord = airbaseCoord + end end end end - if nearestAirbase then - local airbaseCoord = nearestAirbase:GetCoordinate() + if nearestAirbase and nearestAirbaseCoord then + 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 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 - interceptor:ClearTasks() - interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") -- Convert knots to m/s + pcall(function() interceptor:ClearTasks() end) + local routeOk, routeErr = pcall(function() interceptor:RouteAirTo(rtbCoord, ADVANCED_SETTINGS.rtbSpeed * 0.5144, "BARO") end) 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 local coalitionSettings = getCoalitionSettings(coalitionSide) @@ -1739,10 +1785,16 @@ local function launchInterceptor(threatGroup, coalitionSide) interceptor:OptionROTVertical() -- Route to threat - local currentThreatCoord = threatGroup:GetCoordinate() + local currentThreatCoord = safeCoordinate(threatGroup) if currentThreatCoord then - local interceptCoord = currentThreatCoord:SetAltitude(squadron.altitude * 0.3048) -- Convert feet to meters - interceptor:RouteAirTo(interceptCoord, squadron.speed * 0.5144, "BARO") -- Convert knots to m/s + local okIntercept, interceptCoord = pcall(function() + 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 local attackTask = { @@ -1760,22 +1812,28 @@ local function launchInterceptor(threatGroup, coalitionSide) end, {}, 3) -- 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, squadron = squadron.templateName, displayName = squadron.displayName } -- Track spawn position for stuck aircraft detection - local spawnPos = interceptor:GetCoordinate() + local spawnPos = safeCoordinate(interceptor) if spawnPos then - aircraftSpawnTracking[coalitionKey][interceptor:GetName()] = { + aircraftSpawnTracking[coalitionKey][interceptorName] = { spawnPos = spawnPos, spawnTime = timer.getTime(), squadron = squadron, 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 -- Emergency cleanup (safety net)