From e1e9c0e053ef3030bdc0c0885bddf64ce60f1962 Mon Sep 17 00:00:00 2001 From: Chris Webb Date: Sun, 27 Sep 2026 13:45:36 -0700 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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; } }