From bcdc5e3c057b556e6c1bb0cf244dd794954c0e96 Mon Sep 17 00:00:00 2001 From: wh1ter0se <62149665+wh1ter0se69@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:23:15 +0300 Subject: [PATCH 1/3] bugfix(ai): Do not rebuild the locomotor set of a dead aircraft A slow death module disables flight by mutating the current Locomotor instance - JetSlowDeathBehavior applies a negative maxLift and a zero maxTurnRate. A dead aircraft's AI keeps running, and when it changes locomotor set, chooseLocomotorSetExplicit() clears the set (deleting every Locomotor) and rebuilds from template, whose constructor resets both back to BIGNUM. The wreck regains full lift and turn rate and circles forever, since DestructionDelay is effectively infinite. This lives in AIUpdateInterface because every aircraft reaches it, which is why the issue is reported for jets and helicopters alike. Verified on a VC6 release build: the captured wreck falls to the ground in ~3 seconds instead of circling, and the 10-replay 1.04 corpus still completes 10/10 with zero CRC errors, byte-identical to the unfixed run. --- .../Source/GameLogic/Object/Update/AIUpdate.cpp | 17 +++++++++++++++++ .../Source/GameLogic/Object/Update/AIUpdate.cpp | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index c8fcd2f3221..dbb9653fe76 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -826,6 +826,23 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) const LocomotorTemplateVector* set = getAIUpdateModuleData()->findLocomotorTemplateVector(wst); if (set) { + // TheSuperHackers @bugfix Do not rebuild the locomotor set of a dead aircraft. + // + // A slow death module disables flight by mutating the CURRENT Locomotor instance - + // JetSlowDeathBehavior::beginSlowDeath() applies a negative maxLift and a zero + // maxTurnRate to getCurLocomotor(). Rebuilding the set here deleteInstance()s that + // Locomotor and constructs replacements from template, and the Locomotor constructor + // resets m_maxLift and m_maxTurnRate to BIGNUM. The wreck therefore regains full lift + // and full turn rate and keeps flying, circling forever because DestructionDelay is + // effectively infinite. + // + // This lives in AIUpdateInterface rather than in a slow death module because every + // aircraft reaches it, which is why the symptom is reported for jets and helicopters + // alike. + Object* obj = getObject(); + if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) + return FALSE; + m_locomotorSet.clear(); m_curLocomotor = nullptr; for (size_t i = 0; i < set->size(); ++i) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 2eed5b09878..8a8e4e825dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -832,6 +832,23 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) const LocomotorTemplateVector* set = getAIUpdateModuleData()->findLocomotorTemplateVector(wst); if (set) { + // TheSuperHackers @bugfix Do not rebuild the locomotor set of a dead aircraft. + // + // A slow death module disables flight by mutating the CURRENT Locomotor instance - + // JetSlowDeathBehavior::beginSlowDeath() applies a negative maxLift and a zero + // maxTurnRate to getCurLocomotor(). Rebuilding the set here deleteInstance()s that + // Locomotor and constructs replacements from template, and the Locomotor constructor + // resets m_maxLift and m_maxTurnRate to BIGNUM. The wreck therefore regains full lift + // and full turn rate and keeps flying, circling forever because DestructionDelay is + // effectively infinite. + // + // This lives in AIUpdateInterface rather than in a slow death module because every + // aircraft reaches it, which is why the symptom is reported for jets and helicopters + // alike. + Object* obj = getObject(); + if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) + return FALSE; + m_locomotorSet.clear(); m_curLocomotor = nullptr; for (size_t i = 0; i < set->size(); ++i) From 117104ec4b23420f0ce7f0f526efe6b846af9dfc Mon Sep 17 00:00:00 2001 From: wh1ter0se <62149665+wh1ter0se69@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:31:08 +0300 Subject: [PATCH 2/3] Guard the fix behind RETAIL_COMPATIBLE_CRC This changes simulation behaviour, so per the policy in GameDefines.h it must not be active in a retail compatible build. It is compiled out by default and becomes live when the project flips RETAIL_COMPATIBLE_CRC. --- .../Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp | 2 ++ .../Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index dbb9653fe76..c4b823e97aa 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -839,9 +839,11 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) // This lives in AIUpdateInterface rather than in a slow death module because every // aircraft reaches it, which is why the symptom is reported for jets and helicopters // alike. +#if !RETAIL_COMPATIBLE_CRC Object* obj = getObject(); if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) return FALSE; +#endif m_locomotorSet.clear(); m_curLocomotor = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 8a8e4e825dd..356ae342343 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -845,9 +845,11 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) // This lives in AIUpdateInterface rather than in a slow death module because every // aircraft reaches it, which is why the symptom is reported for jets and helicopters // alike. +#if !RETAIL_COMPATIBLE_CRC Object* obj = getObject(); if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) return FALSE; +#endif m_locomotorSet.clear(); m_curLocomotor = nullptr; From 18df0e293c0d0be77491ced54a943ca513e112db Mon Sep 17 00:00:00 2001 From: wh1ter0se <62149665+wh1ter0se69@users.noreply.github.com> Date: Mon, 10 Aug 2026 04:46:21 +0300 Subject: [PATCH 3/3] Address review: shorten the comment, and refuse rebuilds only Keeps the comment to three lines with the rationale in one sentence, per review. Also adds m_curLocomotorSet != LOCOMOTORSET_INVALID so the guard refuses a rebuild but never the initial build - AIUpdateInterface:: loadPostProcess() deliberately sets the current set to INVALID before re-choosing it when loading a pre-version-4 save, and without this a dead aircraft in such a save would be left with no locomotor set at all. --- .../GameLogic/Object/Update/AIUpdate.cpp | 20 ++++++------------- .../GameLogic/Object/Update/AIUpdate.cpp | 20 ++++++------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index c4b823e97aa..9addc760c72 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -826,22 +826,14 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) const LocomotorTemplateVector* set = getAIUpdateModuleData()->findLocomotorTemplateVector(wst); if (set) { - // TheSuperHackers @bugfix Do not rebuild the locomotor set of a dead aircraft. - // - // A slow death module disables flight by mutating the CURRENT Locomotor instance - - // JetSlowDeathBehavior::beginSlowDeath() applies a negative maxLift and a zero - // maxTurnRate to getCurLocomotor(). Rebuilding the set here deleteInstance()s that - // Locomotor and constructs replacements from template, and the Locomotor constructor - // resets m_maxLift and m_maxTurnRate to BIGNUM. The wreck therefore regains full lift - // and full turn rate and keeps flying, circling forever because DestructionDelay is - // effectively infinite. - // - // This lives in AIUpdateInterface rather than in a slow death module because every - // aircraft reaches it, which is why the symptom is reported for jets and helicopters - // alike. + // TheSuperHackers @bugfix wh1ter0se69 10/08/2026 Do not rebuild the locomotor set of a dead + // aircraft. Rebuilding discards the Locomotor instance that a slow death module mutated to + // ground it, so the wreck regains full lift from template and keeps flying. #if !RETAIL_COMPATIBLE_CRC Object* obj = getObject(); - if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) + // LOCOMOTORSET_INVALID means no set has been built yet, so this refuses rebuilds only. + if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT) + && m_curLocomotorSet != LOCOMOTORSET_INVALID) return FALSE; #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 356ae342343..a7175f4476f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -832,22 +832,14 @@ Bool AIUpdateInterface::chooseLocomotorSetExplicit(LocomotorSetType wst) const LocomotorTemplateVector* set = getAIUpdateModuleData()->findLocomotorTemplateVector(wst); if (set) { - // TheSuperHackers @bugfix Do not rebuild the locomotor set of a dead aircraft. - // - // A slow death module disables flight by mutating the CURRENT Locomotor instance - - // JetSlowDeathBehavior::beginSlowDeath() applies a negative maxLift and a zero - // maxTurnRate to getCurLocomotor(). Rebuilding the set here deleteInstance()s that - // Locomotor and constructs replacements from template, and the Locomotor constructor - // resets m_maxLift and m_maxTurnRate to BIGNUM. The wreck therefore regains full lift - // and full turn rate and keeps flying, circling forever because DestructionDelay is - // effectively infinite. - // - // This lives in AIUpdateInterface rather than in a slow death module because every - // aircraft reaches it, which is why the symptom is reported for jets and helicopters - // alike. + // TheSuperHackers @bugfix wh1ter0se69 10/08/2026 Do not rebuild the locomotor set of a dead + // aircraft. Rebuilding discards the Locomotor instance that a slow death module mutated to + // ground it, so the wreck regains full lift from template and keeps flying. #if !RETAIL_COMPATIBLE_CRC Object* obj = getObject(); - if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT)) + // LOCOMOTORSET_INVALID means no set has been built yet, so this refuses rebuilds only. + if (obj != nullptr && obj->isEffectivelyDead() && obj->isKindOf(KINDOF_AIRCRAFT) + && m_curLocomotorSet != LOCOMOTORSET_INVALID) return FALSE; #endif