From ffd944f1d53209cdbac35076e6ae8e8e6669c994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prof=2E=20Zsolt=20Zolt=C3=A1n=20Feh=C3=A9r=20Dr=2E?= Date: Mon, 20 Jul 2026 12:09:53 +0200 Subject: [PATCH 1/2] Fix PoisonedBehavior damage-over-time ignoring POISON resistance (#1616) PoisonedBehavior::update()'s periodic damage-over-time tick dealt DAMAGE_UNRESISTABLE instead of DAMAGE_POISON, so a target's actual POISON resistance (e.g. Chemical Suits) was never applied to it, even though the effect is conceptually poison damage. The existing developer comment explained this was to avoid PoisonedBehavior::onDamage() re-arming/ re-triggering itself off its own tick. Changed the periodic tick to deal real DAMAGE_POISON (gated behind #if !RETAIL_COMPATIBLE_CRC, since this changes actual damage dealt - a CRC-relevant simulation outcome - so it stays dormant by default for multiplayer/replay compatibility with retail 1.04/1.08). Replaced the old damage-type workaround with an explicit reentrancy guard: a new m_dealingPeriodicDamage member is set for the duration of the synchronous getObject()->attemptDamage() call in update(), and onDamage() ignores POISON damage while that flag is set. This could not be done by comparing damageInfo->in.m_sourceID against getObject()->getID() (self), since the periodic tick's damage.in.m_sourceID is m_poisonSource - the original poisoner, kept for XP-credit purposes (see the existing Stubbjax fix in startPoisonedEffects) - not the poisoned object itself; a sourceID-based self-check would never match and would also incorrectly suppress legitimate re-poisoning by repeated attacks from the same source. Not Core-shared; applied identically to both Generals and GeneralsMD. TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Deal real POISON damage on the periodic tick so POISON resistance applies, guarded by a reentrancy flag instead of DAMAGE_UNRESISTABLE so it no longer re-triggers itself. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit 48078fb0c24ec2198080db1c79298ab15db6ae39) --- .../GameLogic/Module/PoisonedBehavior.h | 5 ++++ .../Object/Behavior/PoisonedBehavior.cpp | 24 +++++++++++++++++++ .../GameLogic/Module/PoisonedBehavior.h | 5 ++++ .../Object/Behavior/PoisonedBehavior.cpp | 24 +++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h b/Generals/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h index 17d2a95021e..790a10edbd7 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h @@ -91,5 +91,10 @@ class PoisonedBehavior : public UpdateModule, Real m_poisonDamageAmount; ObjectID m_poisonSource; DeathType m_deathType; + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Reentrancy guard so our own periodic poison + // damage-over-time tick (see update()) does not re-trigger onDamage() on ourselves now that + // it deals real POISON damage. damage.in.m_sourceID cannot be used for this since it is set + // to the original poisoner (for XP credit), which could legitimately re-poison us again. + Bool m_dealingPeriodicDamage; }; diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp index 5483e20dd84..3b110d25f8e 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp @@ -72,6 +72,7 @@ PoisonedBehavior::PoisonedBehavior( Thing *thing, const ModuleData* moduleData ) m_poisonDamageAmount = 0.0f; m_poisonSource = INVALID_ID; m_deathType = DEATH_POISONED; + m_dealingPeriodicDamage = FALSE; setWakeFrame(getObject(), UPDATE_SLEEP_FOREVER); } @@ -87,7 +88,16 @@ PoisonedBehavior::~PoisonedBehavior() void PoisonedBehavior::onDamage( DamageInfo *damageInfo ) { if( damageInfo->in.m_damageType == DAMAGE_POISON ) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Our own periodic poison damage-over-time tick + // (see update()) now deals real POISON damage so that POISON resistance applies to it. Ignore + // it here so it does not re-arm/re-trigger this same PoisonedBehavior instance on ourselves. + if( m_dealingPeriodicDamage ) + return; +#endif startPoisonedEffects( damageInfo ); + } } // ------------------------------------------------------------------------------------------------ @@ -119,9 +129,23 @@ UpdateSleepTime PoisonedBehavior::update() DamageInfo damage; damage.in.m_amount = m_poisonDamageAmount; damage.in.m_sourceID = m_poisonSource; +#if RETAIL_COMPATIBLE_CRC damage.in.m_damageType = DAMAGE_UNRESISTABLE; // Not poison, as that will infect us again +#else + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Deal actual POISON damage on the periodic tick + // so the target's POISON resistance (e.g. Chemical Suits) is properly applied. This used to be + // UNRESISTABLE to avoid re-triggering ourselves; onDamage() now guards against that explicitly + // via a self-source check instead. + damage.in.m_damageType = DAMAGE_POISON; +#endif damage.in.m_deathType = m_deathType; +#if !RETAIL_COMPATIBLE_CRC + m_dealingPeriodicDamage = TRUE; +#endif getObject()->attemptDamage( &damage ); +#if !RETAIL_COMPATIBLE_CRC + m_dealingPeriodicDamage = FALSE; +#endif m_poisonDamageFrame = now + d->m_poisonDamageIntervalData; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h index 5897092528a..aba56d52137 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PoisonedBehavior.h @@ -91,5 +91,10 @@ class PoisonedBehavior : public UpdateModule, Real m_poisonDamageAmount; ObjectID m_poisonSource; DeathType m_deathType; + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Reentrancy guard so our own periodic poison + // damage-over-time tick (see update()) does not re-trigger onDamage() on ourselves now that + // it deals real POISON damage. damage.in.m_sourceID cannot be used for this since it is set + // to the original poisoner (for XP credit), which could legitimately re-poison us again. + Bool m_dealingPeriodicDamage; }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp index 1cadfe55df4..62dfd0b8fc3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp @@ -72,6 +72,7 @@ PoisonedBehavior::PoisonedBehavior( Thing *thing, const ModuleData* moduleData ) m_poisonDamageAmount = 0.0f; m_poisonSource = INVALID_ID; m_deathType = DEATH_POISONED; + m_dealingPeriodicDamage = FALSE; setWakeFrame(getObject(), UPDATE_SLEEP_FOREVER); } @@ -87,7 +88,16 @@ PoisonedBehavior::~PoisonedBehavior() void PoisonedBehavior::onDamage( DamageInfo *damageInfo ) { if( damageInfo->in.m_damageType == DAMAGE_POISON ) + { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Our own periodic poison damage-over-time tick + // (see update()) now deals real POISON damage so that POISON resistance applies to it. Ignore + // it here so it does not re-arm/re-trigger this same PoisonedBehavior instance on ourselves. + if( m_dealingPeriodicDamage ) + return; +#endif startPoisonedEffects( damageInfo ); + } } // ------------------------------------------------------------------------------------------------ @@ -119,10 +129,24 @@ UpdateSleepTime PoisonedBehavior::update() DamageInfo damage; damage.in.m_amount = m_poisonDamageAmount; damage.in.m_sourceID = m_poisonSource; +#if RETAIL_COMPATIBLE_CRC damage.in.m_damageType = DAMAGE_UNRESISTABLE; // Not poison, as that will infect us again +#else + // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Deal actual POISON damage on the periodic tick + // so the target's POISON resistance (e.g. Chemical Suits) is properly applied. This used to be + // UNRESISTABLE to avoid re-triggering ourselves; onDamage() now guards against that explicitly + // via a self-source check instead. + damage.in.m_damageType = DAMAGE_POISON; +#endif damage.in.m_damageFXOverride = DAMAGE_POISON; // but this will ensure that the right effect is played damage.in.m_deathType = m_deathType; +#if !RETAIL_COMPATIBLE_CRC + m_dealingPeriodicDamage = TRUE; +#endif getObject()->attemptDamage( &damage ); +#if !RETAIL_COMPATIBLE_CRC + m_dealingPeriodicDamage = FALSE; +#endif m_poisonDamageFrame = now + d->m_poisonDamageIntervalData; } From 7a97ad03acb1dd40294f0f6d34d84d56e3377168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prof=2E=20Zsolt=20Zolt=C3=A1n=20Feh=C3=A9r=20Dr=2E?= Date: Mon, 20 Jul 2026 12:28:30 +0200 Subject: [PATCH 2/2] Fix stale comment in PoisonedBehavior::update() (#1616 follow-up) A fable-model review of commit 48078fb0c pointed out that the comment on the DAMAGE_POISON assignment still described the abandoned "self-source check" approach (comparing damageInfo->in.m_sourceID against the object's own ID), which was already known to be unsound - the periodic tick's damage.in.m_sourceID is m_poisonSource (the original attacker, kept for XP credit), not the poisoned object's own ID, so that comparison could never match. The actually-committed fix uses the m_dealingPeriodicDamage reentrancy flag instead; the code was already correct, only this one comment was left describing the wrong mechanism. Reviewed-by: fable (code-reviewer) TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Comment described the abandoned self-source-check approach instead of the actual m_dealingPeriodicDamage reentrancy guard that was committed. Co-Authored-By: Claude Sonnet 5 (cherry picked from commit ea5ce8a602fe39b0d5831bb58e89ed5cdfb3c6c0) --- .../Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp | 2 +- .../Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp index 3b110d25f8e..a476c883067 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp @@ -135,7 +135,7 @@ UpdateSleepTime PoisonedBehavior::update() // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Deal actual POISON damage on the periodic tick // so the target's POISON resistance (e.g. Chemical Suits) is properly applied. This used to be // UNRESISTABLE to avoid re-triggering ourselves; onDamage() now guards against that explicitly - // via a self-source check instead. + // via the m_dealingPeriodicDamage reentrancy flag instead. damage.in.m_damageType = DAMAGE_POISON; #endif damage.in.m_deathType = m_deathType; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp index 62dfd0b8fc3..225f328dd0e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/PoisonedBehavior.cpp @@ -135,7 +135,7 @@ UpdateSleepTime PoisonedBehavior::update() // TheSuperHackers @bugfix ZsoltFeher 07/20/2026 Deal actual POISON damage on the periodic tick // so the target's POISON resistance (e.g. Chemical Suits) is properly applied. This used to be // UNRESISTABLE to avoid re-triggering ourselves; onDamage() now guards against that explicitly - // via a self-source check instead. + // via the m_dealingPeriodicDamage reentrancy flag instead. damage.in.m_damageType = DAMAGE_POISON; #endif damage.in.m_damageFXOverride = DAMAGE_POISON; // but this will ensure that the right effect is played