diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h index 9c80b361d93..99cf612a9ee 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -79,7 +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 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 f201ca1a777..e98f964f48c 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -158,7 +158,7 @@ class BodyModuleInterface virtual void clearArmorSetFlag(ArmorSetType ast) = 0; virtual const DamageInfo *getLastDamageInfo() const = 0; - virtual UnsignedInt getLastDamageTimestamp() const = 0; + virtual const UnsignedInt *getLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -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 UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt + 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 fc9385a7b68..10f104ebd61 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -928,20 +928,27 @@ 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 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; +#if RETAIL_COMPATIBLE_CRC + const Int SCAN_WINDOW = 10; +#else + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; +#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; @@ -968,7 +975,8 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + SCAN_RATE > 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 bea29c00a96..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 ///////////////////////////////////////////////////////////////////////////// // ------------------------------------------------------------------------------------------------ @@ -149,8 +151,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), @@ -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 f29567555f7..346ee250af4 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ActiveBody.h @@ -87,7 +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 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 015b5e743ed..0f2ed72ff74 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BodyModule.h @@ -168,7 +168,7 @@ class BodyModuleInterface virtual Bool testArmorSetFlag(ArmorSetType ast) = 0; virtual const DamageInfo *getLastDamageInfo() const = 0; - virtual UnsignedInt getLastDamageTimestamp() const = 0; + virtual const UnsignedInt *getLastDamageTimestamp() const = 0; virtual UnsignedInt getLastHealingTimestamp() const = 0; virtual ObjectID getClearableLastAttacker() const = 0; virtual void clearLastAttacker() = 0; @@ -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 UnsignedInt getLastDamageTimestamp() const override { return 0; } ///< return frame of last damage dealt + 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 10e5faa96af..3c199dc2d4c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPlayer.cpp @@ -935,20 +935,27 @@ 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 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; +#if RETAIL_COMPATIBLE_CRC + const Int SCAN_WINDOW = 10; +#else + const Int SCAN_WINDOW = 10 * LOGICFRAMES_PER_SECOND; +#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; @@ -975,7 +982,8 @@ Bool AIPlayer::isSupplySourceAttacked() if (info->out.m_noEffect) { continue; } - if (body->getLastDamageTimestamp() + SCAN_RATE > 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 21ce7bd81ae..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 ///////////////////////////////////////////////////////////////////////////// // ------------------------------------------------------------------------------------------------ @@ -160,8 +162,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), @@ -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 b1a82a0ead8..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()->getLastDamageTimestamp() != 0xffffffff ) - { - //But it's initialized to 0xffffffff so we don't think we took damage on the first frame. - return FALSE; - } + return FALSE; } }