From e1e9c0e053ef3030bdc0c0885bddf64ce60f1962 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 27 Sep 2026 13:45:36 -0700 Subject: [PATCH 01/11] bugfix(ai): Fix isSupplySourceAttacked SCAN_RATE using frames instead of seconds --- Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 7 ++++++- .../Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index fc9385a7b68..439a4717803 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -928,7 +928,12 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) //------------------------------------------------------------------------------------------------- Bool AIPlayer::isSupplySourceAttacked() { - const Int SCAN_RATE = 10; // don't scan more often than every 10 seconds. + // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for just 10 frames. +#if RETAIL_COMPATIBLE_CRC + const Int SCAN_RATE = 10; // 10 frames. +#else + const Int SCAN_RATE = 10 * LOGICFRAMES_PER_SECOND; // don't scan more often than every 10 seconds. +#endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 10e5faa96af..42341875ffc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -935,7 +935,12 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) //------------------------------------------------------------------------------------------------- Bool AIPlayer::isSupplySourceAttacked() { - const Int SCAN_RATE = 10; // don't scan more often than every 10 seconds. + // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for just 10 frames. +#if RETAIL_COMPATIBLE_CRC + const Int SCAN_RATE = 10; // 10 frames. +#else + const Int SCAN_RATE = 10 * LOGICFRAMES_PER_SECOND; // don't scan more often than every 10 seconds. +#endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; From fb38594596386fa73dcac4cde0c7b524c5ff1e4b Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Tue, 29 Sep 2026 14:29:31 -0700 Subject: [PATCH 02/11] bugfix(ai): Separate SCAN_RATE into REFRESH_RATE and SCAN_WINDOW --- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 16 +++++++++------- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 439a4717803..d43357ea360 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -928,25 +928,27 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) //------------------------------------------------------------------------------------------------- Bool AIPlayer::isSupplySourceAttacked() { - // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for just 10 frames. + // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. + // Original EA comment: "don't scan more often than every 10 seconds." + const Int REFRESH_RATE = 10; // 10 frames. #if RETAIL_COMPATIBLE_CRC - const Int SCAN_RATE = 10; // 10 frames. + const Int SCAN_WINDOW = 10; #else - const Int SCAN_RATE = 10 * LOGICFRAMES_PER_SECOND; // don't scan more often than every 10 seconds. + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; // 10 seconds of attack history. #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { - m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; + m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; return false; // can't be attacked on first frame. } m_attackedSupplyCenter = INVALID_ID; if (curFrame < m_supplySourceAttackCheckFrame) { return false; } - if (m_player->getAttackedFrame()+SCAN_RATE < curFrame) { + if (m_player->getAttackedFrame() + SCAN_WINDOW < curFrame) { return false; // haven't been attacked recently. } - m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; + m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; // Scan my units. Player::PlayerTeamList::const_iterator it; @@ -973,7 +975,7 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + SCAN_RATE > curFrame) { + if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 42341875ffc..745507e8f95 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -935,25 +935,27 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) //------------------------------------------------------------------------------------------------- Bool AIPlayer::isSupplySourceAttacked() { - // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for just 10 frames. + // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. + // Original EA comment: "don't scan more often than every 10 seconds." + const Int REFRESH_RATE = 10; // 10 frames. #if RETAIL_COMPATIBLE_CRC - const Int SCAN_RATE = 10; // 10 frames. + const Int SCAN_WINDOW = 10; #else - const Int SCAN_RATE = 10 * LOGICFRAMES_PER_SECOND; // don't scan more often than every 10 seconds. + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; // 10 seconds of attack history. #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { - m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; + m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; return false; // can't be attacked on first frame. } m_attackedSupplyCenter = INVALID_ID; if (curFrame < m_supplySourceAttackCheckFrame) { return false; } - if (m_player->getAttackedFrame()+SCAN_RATE < curFrame) { + if (m_player->getAttackedFrame() + SCAN_WINDOW < curFrame) { return false; // haven't been attacked recently. } - m_supplySourceAttackCheckFrame = curFrame+SCAN_RATE; + m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; // Scan my units. Player::PlayerTeamList::const_iterator it; @@ -980,7 +982,7 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + SCAN_RATE > curFrame) { + if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; From 9f1a75a6a78e125b41f8bf38954a00ac91f6ee40 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Tue, 29 Sep 2026 15:28:28 -0700 Subject: [PATCH 03/11] bugfix(ai): Ignore undamaged units in supply attack checks --- Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 6 ++++++ GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index d43357ea360..98d9d8098ba 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -975,6 +975,12 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } +#if !RETAIL_COMPATIBLE_CRC + // Ignore undamaged units. + if (body->getLastDamageTimestamp() == 0xffffffff) { + continue; + } +#endif if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 745507e8f95..fbf24769728 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -982,6 +982,12 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } +#if !RETAIL_COMPATIBLE_CRC + // Ignore undamaged units. + if (body->getLastDamageTimestamp() == 0xffffffff) { + continue; + } +#endif if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); From cf0408efe39f594785eda10fbc9ebbb95b50d276 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Thu, 1 Oct 2026 16:11:17 -0700 Subject: [PATCH 04/11] bugfix(ai): Create InvalidBodyTimestamp constant and hasLastDamageTimestamp function --- .../Code/GameEngine/Include/GameLogic/Module/ActiveBody.h | 3 +++ .../Code/GameEngine/Include/GameLogic/Module/BodyModule.h | 2 ++ Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 8 ++++---- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 4 ++-- .../Code/GameEngine/Include/GameLogic/Module/ActiveBody.h | 3 +++ .../Code/GameEngine/Include/GameLogic/Module/BodyModule.h | 2 ++ .../Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 2 +- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 4 ++-- .../Source/GameLogic/Object/Update/StealthUpdate.cpp | 4 ++-- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 9c80b361d93..f5d93e54da7 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -40,6 +40,8 @@ class BodyParticleSystem; class ParticleSystemTemplate; +constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet + //------------------------------------------------------------------------------------------------- /** Active body module */ //------------------------------------------------------------------------------------------------- @@ -80,6 +82,7 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual UnsignedInt getLastDamageTimestamp() const override { return m_lastDamageTimestamp; } ///< return frame of last damage dealt + virtual Bool hasLastDamageTimestamp() const override { return m_lastDamageTimestamp != InvalidBodyTimestamp; } ///< return whether a frame of last damage has been recorded virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index f201ca1a777..cdd714f4ca1 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -159,6 +159,7 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual UnsignedInt getLastDamageTimestamp() const = 0; + virtual Bool hasLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -213,6 +214,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object virtual UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt + virtual Bool hasLastDamageTimestamp() const override { return FALSE; } ///< return whether a frame of last damage has been recorded virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 98d9d8098ba..34722f93b57 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -929,12 +929,12 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) Bool AIPlayer::isSupplySourceAttacked() { // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. - // Original EA comment: "don't scan more often than every 10 seconds." - const Int REFRESH_RATE = 10; // 10 frames. + // A prior EA comment indicated that the intent was to look for 10 seconds into the attack history. + const Int REFRESH_RATE = 10; #if RETAIL_COMPATIBLE_CRC const Int SCAN_WINDOW = 10; #else - const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; // 10 seconds of attack history. + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { @@ -977,7 +977,7 @@ Bool AIPlayer::isSupplySourceAttacked() } #if !RETAIL_COMPATIBLE_CRC // Ignore undamaged units. - if (body->getLastDamageTimestamp() == 0xffffffff) { + if (!body->hasLastDamageTimestamp()) { continue; } #endif diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index bea29c00a96..9f614cdaf0b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -149,8 +149,8 @@ ActiveBody::ActiveBody( Thing *thing, const ModuleData* moduleData ) : m_curArmorSet(nullptr), m_frontCrushed(false), m_backCrushed(false), - m_lastDamageTimestamp(0xffffffff),// So we don't think we just got damaged on the first frame - m_lastHealingTimestamp(0xffffffff),// So we don't think we just got healed on the first frame + m_lastDamageTimestamp(InvalidBodyTimestamp),// So we don't think we just got damaged on the first frame + m_lastHealingTimestamp(InvalidBodyTimestamp),// So we don't think we just got healed on the first frame m_curDamageState(BODY_PRISTINE), m_nextDamageFXTime(0), m_lastDamageFXDone((DamageType)-1), diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index f29567555f7..26d95c7a0a0 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -40,6 +40,8 @@ class BodyParticleSystem; class ParticleSystemTemplate; +constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet + //------------------------------------------------------------------------------------------------- /** Active body module */ //------------------------------------------------------------------------------------------------- @@ -88,6 +90,7 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual UnsignedInt getLastDamageTimestamp() const override { return m_lastDamageTimestamp; } ///< return frame of last damage dealt + virtual Bool hasLastDamageTimestamp() const override { return m_lastDamageTimestamp != InvalidBodyTimestamp; } ///< return whether a frame of last damage has been recorded virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 015b5e743ed..f26249211d8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -169,6 +169,7 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual UnsignedInt getLastDamageTimestamp() const = 0; + virtual Bool hasLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -229,6 +230,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object virtual UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt + virtual Bool hasLastDamageTimestamp() const override { return FALSE; } ///< return whether a frame of last damage has been recorded virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index fbf24769728..3b84f012d2f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -984,7 +984,7 @@ Bool AIPlayer::isSupplySourceAttacked() } #if !RETAIL_COMPATIBLE_CRC // Ignore undamaged units. - if (body->getLastDamageTimestamp() == 0xffffffff) { + if (!body->hasLastDamageTimestamp()) { continue; } #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 21ce7bd81ae..8736aff8492 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -160,8 +160,8 @@ ActiveBody::ActiveBody( Thing *thing, const ModuleData* moduleData ) : m_curArmorSet(nullptr), m_frontCrushed(false), m_backCrushed(false), - m_lastDamageTimestamp(0xffffffff),// So we don't think we just got damaged on the first frame - m_lastHealingTimestamp(0xffffffff),// So we don't think we just got healed on the first frame + m_lastDamageTimestamp(InvalidBodyTimestamp),// So we don't think we just got damaged on the first frame + m_lastHealingTimestamp(InvalidBodyTimestamp),// So we don't think we just got healed on the first frame m_curDamageState(BODY_PRISTINE), m_nextDamageFXTime(0), m_lastDamageFXDone((DamageType)-1), diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index b1a82a0ead8..ed31ced0bc4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -319,9 +319,9 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const #endif { //Can't stealth if we just took damage in the last frame or two. - if( self->getBodyModule()->getLastDamageTimestamp() != 0xffffffff ) + if( self->getBodyModule()->hasLastDamageTimestamp() ) { - //But it's initialized to 0xffffffff so we don't think we took damage on the first frame. + //But it's initialized to InvalidBodyTimestamp so we don't think we took damage on the first frame. return FALSE; } } From afc4fc4ec379e4946a6390c2c80c94b2233badbc Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sat, 3 Oct 2026 11:21:11 -0700 Subject: [PATCH 05/11] bugfix(ai): Update getLastDamageTimestamp to return nullptr when no damage is recorded --- .../Include/GameLogic/Module/ActiveBody.h | 5 +---- .../Include/GameLogic/Module/BodyModule.h | 6 ++---- .../GameEngine/Source/GameClient/Drawable.cpp | 5 +++-- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 9 ++------- .../GameEngine/Source/GameLogic/AI/AITNGuard.cpp | 3 ++- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 14 ++++++++++++++ .../GameLogic/Object/Contain/TunnelContain.cpp | 3 ++- .../Include/GameLogic/Module/ActiveBody.h | 5 +---- .../Include/GameLogic/Module/BodyModule.h | 6 ++---- .../GameEngine/Source/GameClient/Drawable.cpp | 5 +++-- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 15 +++++---------- .../GameEngine/Source/GameLogic/AI/AITNGuard.cpp | 3 ++- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 14 ++++++++++++++ .../GameLogic/Object/Contain/TunnelContain.cpp | 3 ++- .../GameLogic/Object/Update/StealthUpdate.cpp | 9 +++------ 15 files changed, 58 insertions(+), 47 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index f5d93e54da7..99cf612a9ee 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -40,8 +40,6 @@ class BodyParticleSystem; class ParticleSystemTemplate; -constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet - //------------------------------------------------------------------------------------------------- /** Active body module */ //------------------------------------------------------------------------------------------------- @@ -81,8 +79,7 @@ class ActiveBody : public BodyModule virtual void setAflame( Bool setting ) override;///< This is a major change like a damage state. virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object - virtual UnsignedInt getLastDamageTimestamp() const override { return m_lastDamageTimestamp; } ///< return frame of last damage dealt - virtual Bool hasLastDamageTimestamp() const override { return m_lastDamageTimestamp != InvalidBodyTimestamp; } ///< return whether a frame of last damage has been recorded + virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index cdd714f4ca1..e98f964f48c 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -158,8 +158,7 @@ class BodyModuleInterface virtual void clearArmorSetFlag(ArmorSetType ast) = 0; virtual const DamageInfo *getLastDamageInfo() const = 0; - virtual UnsignedInt getLastDamageTimestamp() const = 0; - virtual Bool hasLastDamageTimestamp() const = 0; + virtual const UnsignedInt *getLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -213,8 +212,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt - virtual Bool hasLastDamageTimestamp() const override { return FALSE; } ///< return whether a frame of last damage has been recorded + virtual const UnsignedInt *getLastDamageTimestamp() const override { return nullptr; } ///< return frame of last damage dealt virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 046c31ec844..520fda14131 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1812,7 +1812,8 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI const DamageInfo *damageInfo = obj->getBodyModule()->getLastDamageInfo(); if (damageInfo) { - if (obj->getBodyModule()->getLastDamageTimestamp() > m_lastDamageTimestamp && damageInfo->in.m_amount > RECOIL_DAMAGE) + const UnsignedInt *lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp > m_lastDamageTimestamp && damageInfo->in.m_amount > RECOIL_DAMAGE) { Object *attacker = TheGameLogic->getObject( damageInfo->in.m_sourceID ); if (attacker) @@ -1831,7 +1832,7 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_accelerationRollRate -= recoil * lateral; } - m_lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); + m_lastDamageTimestamp = *lastDamageTimestamp; } } #endif diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 34722f93b57..10f104ebd61 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -975,13 +975,8 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } -#if !RETAIL_COMPATIBLE_CRC - // Ignore undamaged units. - if (!body->hasLastDamageTimestamp()) { - continue; - } -#endif - if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp index d1ec1c6fc3d..76d858f05b2 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp @@ -230,7 +230,8 @@ Bool AITNGuardMachine::lookForInnerTarget() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + TheAI->getAiData()->m_guardEnemyScanRate > TheGameLogic->getFrame()) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + TheAI->getAiData()->m_guardEnemyScanRate > TheGameLogic->getFrame()) { // winner. ObjectID attackerID = info->in.m_sourceID; Object *attacker = TheGameLogic->findObjectByID(attackerID); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 9f614cdaf0b..883aeed285e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -60,6 +60,8 @@ #define YELLOW_DAMAGE_PERCENT (0.25f) +constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet + // FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// // ------------------------------------------------------------------------------------------------ @@ -1002,6 +1004,18 @@ void ActiveBody::internalChangeHealth( Real delta ) } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +const UnsignedInt *ActiveBody::getLastDamageTimestamp() const +{ +#if RETAIL_COMPATIBLE_CRC + return &m_lastDamageTimestamp; +#else + // TheSuperHackers @fix WebbontheWeb 03/10/2026 Avoid treating never-damaged sentinels as an attack timestamp. + return m_lastDamageTimestamp != InvalidBodyTimestamp ? &m_lastDamageTimestamp : nullptr; +#endif +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Real ActiveBody::getHealth() const diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp index c84d73a7caf..e1c19cbb237 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp @@ -464,7 +464,8 @@ UpdateSleepTime TunnelContain::update() if (body) { const DamageInfo *info = body->getLastDamageInfo(); if (info) { - if (body->getLastDamageTimestamp() + LOGICFRAMES_PER_SECOND > TheGameLogic->getFrame()) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + LOGICFRAMES_PER_SECOND > TheGameLogic->getFrame()) { // winner. ObjectID attackerID = info->in.m_sourceID; Object *attacker = TheGameLogic->findObjectByID(attackerID); diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 26d95c7a0a0..346ee250af4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -40,8 +40,6 @@ class BodyParticleSystem; class ParticleSystemTemplate; -constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet - //------------------------------------------------------------------------------------------------- /** Active body module */ //------------------------------------------------------------------------------------------------- @@ -89,8 +87,7 @@ class ActiveBody : public BodyModule virtual Real getCurrentSubdualDamageAmount() const override { return m_currentSubdualDamage; } virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object - virtual UnsignedInt getLastDamageTimestamp() const override { return m_lastDamageTimestamp; } ///< return frame of last damage dealt - virtual Bool hasLastDamageTimestamp() const override { return m_lastDamageTimestamp != InvalidBodyTimestamp; } ///< return whether a frame of last damage has been recorded + virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index f26249211d8..0f2ed72ff74 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -168,8 +168,7 @@ class BodyModuleInterface virtual Bool testArmorSetFlag(ArmorSetType ast) = 0; virtual const DamageInfo *getLastDamageInfo() const = 0; - virtual UnsignedInt getLastDamageTimestamp() const = 0; - virtual Bool hasLastDamageTimestamp() const = 0; + virtual const UnsignedInt *getLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -229,8 +228,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt - virtual Bool hasLastDamageTimestamp() const override { return FALSE; } ///< return whether a frame of last damage has been recorded + virtual const UnsignedInt *getLastDamageTimestamp() const override { return nullptr; } ///< return frame of last damage dealt virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 7ed234e99d8..e5b54e41861 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -1814,7 +1814,8 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI const DamageInfo *damageInfo = obj->getBodyModule()->getLastDamageInfo(); if (damageInfo) { - if (obj->getBodyModule()->getLastDamageTimestamp() > m_lastDamageTimestamp && damageInfo->in.m_amount > RECOIL_DAMAGE) + const UnsignedInt *lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp > m_lastDamageTimestamp && damageInfo->in.m_amount > RECOIL_DAMAGE) { Object *attacker = TheGameLogic->getObject( damageInfo->in.m_sourceID ); if (attacker) @@ -1833,7 +1834,7 @@ void Drawable::calcPhysicsXformTreads( const Locomotor *locomotor, PhysicsXformI m_locoInfo->m_accelerationRollRate -= recoil * lateral; } - m_lastDamageTimestamp = obj->getBodyModule()->getLastDamageTimestamp(); + m_lastDamageTimestamp = *lastDamageTimestamp; } } #endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 3b84f012d2f..3c199dc2d4c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -936,12 +936,12 @@ void AIPlayer::guardSupplyCenter( Team *team, Int minSupplies ) Bool AIPlayer::isSupplySourceAttacked() { // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. - // Original EA comment: "don't scan more often than every 10 seconds." - const Int REFRESH_RATE = 10; // 10 frames. + // A prior EA comment indicated that the intent was to look for 10 seconds into the attack history. + const Int REFRESH_RATE = 10; #if RETAIL_COMPATIBLE_CRC const Int SCAN_WINDOW = 10; #else - const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; // 10 seconds of attack history. + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { @@ -982,13 +982,8 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } -#if !RETAIL_COMPATIBLE_CRC - // Ignore undamaged units. - if (!body->hasLastDamageTimestamp()) { - continue; - } -#endif - if (body->getLastDamageTimestamp() + SCAN_WINDOW > curFrame) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + SCAN_WINDOW > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp index a475add6e1e..196f7c711d0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AITNGuard.cpp @@ -230,7 +230,8 @@ Bool AITNGuardMachine::lookForInnerTarget() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + TheAI->getAiData()->m_guardEnemyScanRate > TheGameLogic->getFrame()) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + TheAI->getAiData()->m_guardEnemyScanRate > TheGameLogic->getFrame()) { // winner. ObjectID attackerID = info->in.m_sourceID; Object *attacker = TheGameLogic->findObjectByID(attackerID); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 8736aff8492..a63add78d9c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -64,6 +64,8 @@ #define YELLOW_DAMAGE_PERCENT (0.25f) +constexpr const UnsignedInt InvalidBodyTimestamp = ~0u; ///< damage or healing timestamp that has not been recorded yet + // FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// // ------------------------------------------------------------------------------------------------ @@ -1338,6 +1340,18 @@ Bool ActiveBody::isSubdued() const #endif } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +const UnsignedInt *ActiveBody::getLastDamageTimestamp() const +{ +#if RETAIL_COMPATIBLE_CRC + return &m_lastDamageTimestamp; +#else + // TheSuperHackers @fix WebbontheWeb 03/10/2026 Avoid treating never-damaged sentinels as an attack timestamp. + return m_lastDamageTimestamp != InvalidBodyTimestamp ? &m_lastDamageTimestamp : nullptr; +#endif +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Real ActiveBody::getHealth() const diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp index b2f2884bb6c..5c408b6524f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp @@ -576,7 +576,8 @@ UpdateSleepTime TunnelContain::update() if (body) { const DamageInfo *info = body->getLastDamageInfo(); if (info) { - if (body->getLastDamageTimestamp() + LOGICFRAMES_PER_SECOND > TheGameLogic->getFrame()) { + const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); + if (lastDamageTimestamp && *lastDamageTimestamp + LOGICFRAMES_PER_SECOND > TheGameLogic->getFrame()) { // winner. ObjectID attackerID = info->in.m_sourceID; Object *attacker = TheGameLogic->findObjectByID(attackerID); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index ed31ced0bc4..b2625e853f8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -311,7 +311,8 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const return FALSE; } - if( flags & STEALTH_NOT_WHILE_TAKING_DAMAGE && self->getBodyModule()->getLastDamageTimestamp() >= now - 1 ) + const UnsignedInt *lastDamageTimestamp = self->getBodyModule()->getLastDamageTimestamp(); + if( flags & STEALTH_NOT_WHILE_TAKING_DAMAGE && lastDamageTimestamp && *lastDamageTimestamp >= now - 1 && *lastDamageTimestamp <= now ) { #if RETAIL_COMPATIBLE_CRC || PRESERVE_STRUCTURE_STEALTH_DURING_REPAIR //Only if it's not healing damage. @@ -319,11 +320,7 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const #endif { //Can't stealth if we just took damage in the last frame or two. - if( self->getBodyModule()->hasLastDamageTimestamp() ) - { - //But it's initialized to InvalidBodyTimestamp so we don't think we took damage on the first frame. - return FALSE; - } + return FALSE; } } From 6fe083ea4cafa4fb54fdad300f8ef0e0dc8d771f Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 03:21:01 -0700 Subject: [PATCH 06/11] bugfix(ai): Return static zero from BodyModule::getLastDamageTimestamp for retail compatibility --- .../GameEngine/Include/GameLogic/Module/BodyModule.h | 10 +++++++++- .../GameEngine/Include/GameLogic/Module/BodyModule.h | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index e98f964f48c..36332da2456 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -212,7 +212,15 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override { return nullptr; } ///< return frame of last damage dealt + virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none (only in non-retail builds) + { +#if RETAIL_COMPATIBLE_CRC + static const UnsignedInt timestamp = 0; + return ×tamp; +#else + return nullptr; +#endif + } virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 0f2ed72ff74..086e5265fd8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -228,7 +228,15 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override { return nullptr; } ///< return frame of last damage dealt + virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none (only in non-retail builds) + { +#if RETAIL_COMPATIBLE_CRC + static const UnsignedInt timestamp = 0; + return ×tamp; +#else + return nullptr; +#endif + } virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } From 3d86e443565f6b35bab97db16827aa4810750ba2 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 03:47:04 -0700 Subject: [PATCH 07/11] bugfix(ai): Update getLastHealingTimestamp to return nullptr when no healing is recorded --- .../GameEngine/Include/GameLogic/Module/ActiveBody.h | 2 +- .../GameEngine/Include/GameLogic/Module/BodyModule.h | 12 ++++++++++-- .../Code/GameEngine/Source/GameClient/Drawable.cpp | 4 +++- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 12 ++++++++++++ .../GameEngine/Include/GameLogic/Module/ActiveBody.h | 2 +- .../GameEngine/Include/GameLogic/Module/BodyModule.h | 12 ++++++++++-- .../Code/GameEngine/Source/GameClient/Drawable.cpp | 4 +++- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 12 ++++++++++++ 8 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 99cf612a9ee..5ea5e713e65 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -80,7 +80,7 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) - virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt + virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 36332da2456..66f7413b9b6 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -159,7 +159,7 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual const UnsignedInt *getLastDamageTimestamp() const = 0; - virtual UnsignedInt getLastHealingTimestamp() const = 0; + virtual const UnsignedInt *getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; virtual Bool getFrontCrushed() const = 0; @@ -221,7 +221,15 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } - virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt + virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) + { +#if RETAIL_COMPATIBLE_CRC + static const UnsignedInt timestamp = 0; + return ×tamp; +#else + return nullptr; +#endif + } virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } virtual Bool getFrontCrushed() const override { return false; } diff --git a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp index 520fda14131..97eb84504d8 100644 --- a/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -3178,8 +3178,10 @@ void Drawable::drawHealing(const IRegion2D* healthBarRegion) // if( lastDamage != nullptr && lastDamage->in.m_damageType == DAMAGE_HEALING // &&(TheGameLogic->getFrame() - body->getLastHealingTimestamp()) <= HEALING_ICON_DISPLAY_TIME // ) + const UnsignedInt *lastHealingTimestamp = body->getLastHealingTimestamp(); if ( TheGameLogic->getFrame() > HEALING_ICON_DISPLAY_TIME && // because so many things init health early in game - (TheGameLogic->getFrame() - body->getLastHealingTimestamp() <= HEALING_ICON_DISPLAY_TIME) ) + lastHealingTimestamp != nullptr && + (TheGameLogic->getFrame() - *lastHealingTimestamp <= HEALING_ICON_DISPLAY_TIME) ) showHealing = TRUE; } diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 883aeed285e..292b60cc528 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -1016,6 +1016,18 @@ const UnsignedInt *ActiveBody::getLastDamageTimestamp() const #endif } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +const UnsignedInt *ActiveBody::getLastHealingTimestamp() const +{ +#if RETAIL_COMPATIBLE_CRC + return &m_lastHealingTimestamp; +#else + // TheSuperHackers @fix WebbontheWeb 04/10/2026 Avoid treating never-healed sentinels as a healing timestamp. + return m_lastHealingTimestamp != InvalidBodyTimestamp ? &m_lastHealingTimestamp : nullptr; +#endif +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Real ActiveBody::getHealth() const diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 346ee250af4..375092e3884 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -88,7 +88,7 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) - virtual UnsignedInt getLastHealingTimestamp() const override { return m_lastHealingTimestamp; } ///< return frame of last damage dealt + virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 086e5265fd8..8e0a7becbfc 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -169,7 +169,7 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual const UnsignedInt *getLastDamageTimestamp() const = 0; - virtual UnsignedInt getLastHealingTimestamp() const = 0; + virtual const UnsignedInt *getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; virtual Bool getFrontCrushed() const = 0; @@ -237,7 +237,15 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } - virtual UnsignedInt getLastHealingTimestamp() const override { return 0; } ///< return frame of last healing dealt + virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) + { +#if RETAIL_COMPATIBLE_CRC + static const UnsignedInt timestamp = 0; + return ×tamp; +#else + return nullptr; +#endif + } virtual ObjectID getClearableLastAttacker() const override { return INVALID_ID; } virtual void clearLastAttacker() override { } virtual Bool getFrontCrushed() const override { return false; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index e5b54e41861..13c758db7d5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -3183,8 +3183,10 @@ void Drawable::drawHealing(const IRegion2D* healthBarRegion) // if( lastDamage != nullptr && lastDamage->in.m_damageType == DAMAGE_HEALING // &&(TheGameLogic->getFrame() - body->getLastHealingTimestamp()) <= HEALING_ICON_DISPLAY_TIME // ) + const UnsignedInt *lastHealingTimestamp = body->getLastHealingTimestamp(); if ( TheGameLogic->getFrame() > HEALING_ICON_DISPLAY_TIME && // because so many things init health early in game - (TheGameLogic->getFrame() - body->getLastHealingTimestamp() <= HEALING_ICON_DISPLAY_TIME) ) + lastHealingTimestamp != nullptr && + (TheGameLogic->getFrame() - *lastHealingTimestamp <= HEALING_ICON_DISPLAY_TIME) ) showHealing = TRUE; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index a63add78d9c..9689ce9065a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -1352,6 +1352,18 @@ const UnsignedInt *ActiveBody::getLastDamageTimestamp() const #endif } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +const UnsignedInt *ActiveBody::getLastHealingTimestamp() const +{ +#if RETAIL_COMPATIBLE_CRC + return &m_lastHealingTimestamp; +#else + // TheSuperHackers @fix WebbontheWeb 04/10/2026 Avoid treating never-healed sentinels as a healing timestamp. + return m_lastHealingTimestamp != InvalidBodyTimestamp ? &m_lastHealingTimestamp : nullptr; +#endif +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Real ActiveBody::getHealth() const From a14fe1815f394cea95ce9c5f37586da30058bae5 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 08:46:50 -0700 Subject: [PATCH 08/11] bugfix(ai): Update RefreshRate and ScanWindow to PascalCase --- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 14 +++++++------- .../GameEngine/Source/GameLogic/AI/AIPlayer.cpp | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 10f104ebd61..d7c97c00060 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -930,25 +930,25 @@ Bool AIPlayer::isSupplySourceAttacked() { // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. // A prior EA comment indicated that the intent was to look for 10 seconds into the attack history. - const Int REFRESH_RATE = 10; + const Int RefreshRate = 10; #if RETAIL_COMPATIBLE_CRC - const Int SCAN_WINDOW = 10; + const Int ScanWindow = 10; #else - const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; + const Int ScanWindow = 10 * LOGICFRAMES_PER_SECOND; #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { - m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; + m_supplySourceAttackCheckFrame = curFrame + RefreshRate; return false; // can't be attacked on first frame. } m_attackedSupplyCenter = INVALID_ID; if (curFrame < m_supplySourceAttackCheckFrame) { return false; } - if (m_player->getAttackedFrame() + SCAN_WINDOW < curFrame) { + if (m_player->getAttackedFrame() + ScanWindow < curFrame) { return false; // haven't been attacked recently. } - m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; + m_supplySourceAttackCheckFrame = curFrame + RefreshRate; // Scan my units. Player::PlayerTeamList::const_iterator it; @@ -976,7 +976,7 @@ Bool AIPlayer::isSupplySourceAttacked() continue; } const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); - if (lastDamageTimestamp && *lastDamageTimestamp + SCAN_WINDOW > curFrame) { + if (lastDamageTimestamp && *lastDamageTimestamp + ScanWindow > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp index 3c199dc2d4c..9108b9708c3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -937,25 +937,25 @@ Bool AIPlayer::isSupplySourceAttacked() { // TheSuperHackers @bugfix WebbontheWeb 27/09/2026 No longer scans for supply source attacks for just the last 10 frames. // A prior EA comment indicated that the intent was to look for 10 seconds into the attack history. - const Int REFRESH_RATE = 10; + const Int RefreshRate = 10; #if RETAIL_COMPATIBLE_CRC - const Int SCAN_WINDOW = 10; + const Int ScanWindow = 10; #else - const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; + const Int ScanWindow = 10 * LOGICFRAMES_PER_SECOND; #endif UnsignedInt curFrame = TheGameLogic->getFrame(); if (curFrame==0) { - m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; + m_supplySourceAttackCheckFrame = curFrame + RefreshRate; return false; // can't be attacked on first frame. } m_attackedSupplyCenter = INVALID_ID; if (curFrame < m_supplySourceAttackCheckFrame) { return false; } - if (m_player->getAttackedFrame() + SCAN_WINDOW < curFrame) { + if (m_player->getAttackedFrame() + ScanWindow < curFrame) { return false; // haven't been attacked recently. } - m_supplySourceAttackCheckFrame = curFrame + REFRESH_RATE; + m_supplySourceAttackCheckFrame = curFrame + RefreshRate; // Scan my units. Player::PlayerTeamList::const_iterator it; @@ -983,7 +983,7 @@ Bool AIPlayer::isSupplySourceAttacked() continue; } const UnsignedInt *lastDamageTimestamp = body->getLastDamageTimestamp(); - if (lastDamageTimestamp && *lastDamageTimestamp + SCAN_WINDOW > curFrame) { + if (lastDamageTimestamp && *lastDamageTimestamp + ScanWindow > curFrame) { // winner. m_attackedSupplyCenter = obj->getID(); return true; From 79ec4908feef49b2f1858ffa9e4c83f978956cdd Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 08:52:53 -0700 Subject: [PATCH 09/11] bugfix(ai): Add getRecordedLastDamageTimestamp for StealthUpdate damage check --- .../GameEngine/Include/GameLogic/Module/ActiveBody.h | 1 + .../GameEngine/Include/GameLogic/Module/BodyModule.h | 2 ++ .../Source/GameLogic/Object/Body/ActiveBody.cpp | 9 ++++++++- .../Source/GameLogic/Object/Update/StealthUpdate.cpp | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 375092e3884..35dbc275602 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -88,6 +88,7 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getRecordedLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (in all builds) virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 8e0a7becbfc..cb27e7f0c99 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -169,6 +169,7 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual const UnsignedInt *getLastDamageTimestamp() const = 0; + virtual const UnsignedInt *getRecordedLastDamageTimestamp() const = 0; virtual const UnsignedInt *getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -237,6 +238,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } + virtual const UnsignedInt *getRecordedLastDamageTimestamp() const override { return getLastDamageTimestamp(); } ///< return frame of last damage, or nullptr if none (in all builds) virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) { #if RETAIL_COMPATIBLE_CRC diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 9689ce9065a..3bfc26d0c67 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -1348,10 +1348,17 @@ const UnsignedInt *ActiveBody::getLastDamageTimestamp() const return &m_lastDamageTimestamp; #else // TheSuperHackers @fix WebbontheWeb 03/10/2026 Avoid treating never-damaged sentinels as an attack timestamp. - return m_lastDamageTimestamp != InvalidBodyTimestamp ? &m_lastDamageTimestamp : nullptr; + return getRecordedLastDamageTimestamp(); #endif } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +const UnsignedInt *ActiveBody::getRecordedLastDamageTimestamp() const +{ + return m_lastDamageTimestamp != InvalidBodyTimestamp ? &m_lastDamageTimestamp : nullptr; +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- const UnsignedInt *ActiveBody::getLastHealingTimestamp() const diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index b2625e853f8..f29bde03481 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -311,8 +311,8 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const return FALSE; } - const UnsignedInt *lastDamageTimestamp = self->getBodyModule()->getLastDamageTimestamp(); - if( flags & STEALTH_NOT_WHILE_TAKING_DAMAGE && lastDamageTimestamp && *lastDamageTimestamp >= now - 1 && *lastDamageTimestamp <= now ) + const UnsignedInt *lastDamageTimestamp = self->getBodyModule()->getRecordedLastDamageTimestamp(); + if( flags & STEALTH_NOT_WHILE_TAKING_DAMAGE && lastDamageTimestamp && *lastDamageTimestamp >= now - 1 ) { #if RETAIL_COMPATIBLE_CRC || PRESERVE_STRUCTURE_STEALTH_DURING_REPAIR //Only if it's not healing damage. From 3ad204de2cb1a563dbc12b1603af10400bd4cee4 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 09:28:14 -0700 Subject: [PATCH 10/11] bugfix(ai): Restore retail never-damaged check in StealthUpdate --- .../GameEngine/Include/GameLogic/Module/ActiveBody.h | 1 - .../GameEngine/Include/GameLogic/Module/BodyModule.h | 2 -- .../Source/GameLogic/Object/Body/ActiveBody.cpp | 9 +-------- .../Source/GameLogic/Object/Update/StealthUpdate.cpp | 12 +++++++++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 35dbc275602..375092e3884 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -88,7 +88,6 @@ class ActiveBody : public BodyModule virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) - virtual const UnsignedInt *getRecordedLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (in all builds) virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index cb27e7f0c99..8e0a7becbfc 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -169,7 +169,6 @@ class BodyModuleInterface virtual const DamageInfo *getLastDamageInfo() const = 0; virtual const UnsignedInt *getLastDamageTimestamp() const = 0; - virtual const UnsignedInt *getRecordedLastDamageTimestamp() const = 0; virtual const UnsignedInt *getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -238,7 +237,6 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } - virtual const UnsignedInt *getRecordedLastDamageTimestamp() const override { return getLastDamageTimestamp(); } ///< return frame of last damage, or nullptr if none (in all builds) virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) { #if RETAIL_COMPATIBLE_CRC diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 3bfc26d0c67..9689ce9065a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -1348,15 +1348,8 @@ const UnsignedInt *ActiveBody::getLastDamageTimestamp() const return &m_lastDamageTimestamp; #else // TheSuperHackers @fix WebbontheWeb 03/10/2026 Avoid treating never-damaged sentinels as an attack timestamp. - return getRecordedLastDamageTimestamp(); -#endif -} - -//------------------------------------------------------------------------------------------------- -//------------------------------------------------------------------------------------------------- -const UnsignedInt *ActiveBody::getRecordedLastDamageTimestamp() const -{ return m_lastDamageTimestamp != InvalidBodyTimestamp ? &m_lastDamageTimestamp : nullptr; +#endif } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp index f29bde03481..bba50bcdc10 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/StealthUpdate.cpp @@ -311,7 +311,7 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const return FALSE; } - const UnsignedInt *lastDamageTimestamp = self->getBodyModule()->getRecordedLastDamageTimestamp(); + const UnsignedInt *lastDamageTimestamp = self->getBodyModule()->getLastDamageTimestamp(); if( flags & STEALTH_NOT_WHILE_TAKING_DAMAGE && lastDamageTimestamp && *lastDamageTimestamp >= now - 1 ) { #if RETAIL_COMPATIBLE_CRC || PRESERVE_STRUCTURE_STEALTH_DURING_REPAIR @@ -319,8 +319,14 @@ Bool StealthUpdate::allowedToStealth( Object *stealthOwner ) const if( self->getBodyModule()->getLastDamageInfo()->in.m_damageType != DAMAGE_HEALING ) #endif { - //Can't stealth if we just took damage in the last frame or two. - return FALSE; +#if RETAIL_COMPATIBLE_CRC + //But it's initialized to 0xffffffff so we don't think we took damage on the first frame. + if( *lastDamageTimestamp != 0xffffffff ) +#endif + { + //Can't stealth if we just took damage in the last frame or two. + return FALSE; + } } } From 5a7d4f45bbacd33681191fda82f16bb8e25d4043 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 4 Oct 2026 09:48:16 -0700 Subject: [PATCH 11/11] bugfix(ai): Remove non-retail behaviour disclaimers --- .../Code/GameEngine/Include/GameLogic/Module/ActiveBody.h | 4 ++-- .../Code/GameEngine/Include/GameLogic/Module/BodyModule.h | 4 ++-- .../Code/GameEngine/Include/GameLogic/Module/ActiveBody.h | 4 ++-- .../Code/GameEngine/Include/GameLogic/Module/BodyModule.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 5ea5e713e65..fdffe305281 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -79,8 +79,8 @@ class ActiveBody : public BodyModule virtual void setAflame( Bool setting ) override;///< This is a major change like a damage state. virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) - virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none + virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 66f7413b9b6..d4e6fc5288b 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -212,7 +212,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none { #if RETAIL_COMPATIBLE_CRC static const UnsignedInt timestamp = 0; @@ -221,7 +221,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } - virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none { #if RETAIL_COMPATIBLE_CRC static const UnsignedInt timestamp = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 375092e3884..54434a6d3a8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -87,8 +87,8 @@ class ActiveBody : public BodyModule virtual Real getCurrentSubdualDamageAmount() const override { return m_currentSubdualDamage; } virtual const DamageInfo *getLastDamageInfo() const override { return &m_lastDamageInfo; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none (only in non-retail builds) - virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastDamageTimestamp() const override; ///< return frame of last damage, or nullptr if none + virtual const UnsignedInt *getLastHealingTimestamp() const override; ///< return frame of last healing, or nullptr if none virtual ObjectID getClearableLastAttacker() const override { return (m_lastDamageCleared ? INVALID_ID : m_lastDamageInfo.in.m_sourceID); } virtual void clearLastAttacker() override { m_lastDamageCleared = true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h index 8e0a7becbfc..d9d1c2736b7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -228,7 +228,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface virtual Real getInitialHealth() const override {return 0.0f;} // return initial health virtual const DamageInfo *getLastDamageInfo() const override { return nullptr; } ///< return info on last damage dealt to this object - virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastDamageTimestamp() const override ///< return frame of last damage, or nullptr if none { #if RETAIL_COMPATIBLE_CRC static const UnsignedInt timestamp = 0; @@ -237,7 +237,7 @@ class BodyModule : public BehaviorModule, public BodyModuleInterface return nullptr; #endif } - virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none (only in non-retail builds) + virtual const UnsignedInt *getLastHealingTimestamp() const override ///< return frame of last healing, or nullptr if none { #if RETAIL_COMPATIBLE_CRC static const UnsignedInt timestamp = 0;