From 352445cf45757fe6254552d72f494eda1500c572 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Thu, 1 Oct 2026 17:10:50 +1000 Subject: [PATCH 1/3] bugfix: Prevent cases where weapons would partially fire and require reloading without actually firing a shot --- .../GameEngine/Source/GameLogic/AI/AIStates.cpp | 14 ++++++++++++++ .../GameEngine/Source/GameLogic/AI/AIStates.cpp | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index b14c99de5a0..d0e84170c36 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -5084,6 +5084,20 @@ StateReturnType AIAttackFireWeaponState::update() return STATE_FAILURE; } +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Stubbjax 28/09/2026 The target may have moved out of range since we entered this + // state, so we check the range again to avoid partially firing the weapon. + if (!weapon->hasLeechRange()) + { + Bool inRange = m_att->isAttackingObject() + ? weapon->isWithinAttackRange(obj, victim) + : weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + + if (!inRange) + return STATE_FAILURE; + } +#endif + /** this is the weird case where we have multi turrets, and turret 'a' wants to fire, but someone has changed the current weapon to be one not on him. diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index 0075d169a3f..8a114ab30ed 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -5240,6 +5240,20 @@ StateReturnType AIAttackFireWeaponState::update() return STATE_FAILURE; } +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Stubbjax 28/09/2026 The target may have moved out of range since we entered this + // state, so we check the range again to avoid partially firing the weapon. + if (!weapon->hasLeechRange()) + { + Bool inRange = m_att->isAttackingObject() + ? weapon->isWithinAttackRange(obj, victim) + : weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + + if (!inRange) + return STATE_FAILURE; + } +#endif + /** this is the weird case where we have multi turrets, and turret 'a' wants to fire, but someone has changed the current weapon to be one not on him. From 503ed60bb65cd14f881875e8a811f9d6907c1641 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Tue, 6 Oct 2026 20:23:31 +1100 Subject: [PATCH 2/3] chore: Add coverage for linked turrets --- .../GameEngine/Source/GameLogic/AI/AIStates.cpp | 2 +- .../GameEngine/Source/GameLogic/AI/AIStates.cpp | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index d0e84170c36..a5188fab0dc 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -5085,7 +5085,7 @@ StateReturnType AIAttackFireWeaponState::update() } #if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @bugfix Stubbjax 28/09/2026 The target may have moved out of range since we entered this + // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this // state, so we check the range again to avoid partially firing the weapon. if (!weapon->hasLeechRange()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index 8a114ab30ed..fa29f39e745 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -5241,7 +5241,7 @@ StateReturnType AIAttackFireWeaponState::update() } #if !RETAIL_COMPATIBLE_CRC - // TheSuperHackers @bugfix Stubbjax 28/09/2026 The target may have moved out of range since we entered this + // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this // state, so we check the range again to avoid partially firing the weapon. if (!weapon->hasLeechRange()) { @@ -5332,6 +5332,18 @@ StateReturnType AIAttackFireWeaponState::update() Weapon *weapon = obj->getWeaponInWeaponSlot( (WeaponSlotType)slot ); if ( weapon ) { +#if !RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this + // state, so we check the range again to avoid partially firing the weapon. + if (!weapon->hasLeechRange()) + { + Bool inRange = weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + + if (!inRange) + continue; + } +#endif + if ( weapon->fireWeapon(obj, getMachineGoalPosition()) ) //fire() returns 'reloaded' obj->releaseWeaponLock(LOCKED_TEMPORARILY);// unlock, 'cause we're loaded From c9b4c668130f08dc8d3fc34637c77397c51c7ab1 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Wed, 7 Oct 2026 22:14:27 +1100 Subject: [PATCH 3/3] refactor: Add convenience functions for handling leech ranges --- .../GameEngine/Include/GameLogic/Weapon.h | 5 ++++ .../Source/GameLogic/AI/AIStates.cpp | 15 +++++------- .../Source/GameLogic/Object/Weapon.cpp | 18 ++++++++++++++ .../GameEngine/Include/GameLogic/Weapon.h | 5 ++++ .../Source/GameLogic/AI/AIStates.cpp | 24 +++++++------------ .../Source/GameLogic/Object/Weapon.cpp | 18 ++++++++++++++ 6 files changed, 61 insertions(+), 24 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Weapon.h b/Generals/Code/GameEngine/Include/GameLogic/Weapon.h index 6f5f44336c4..c6e70a56c81 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Weapon.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Weapon.h @@ -613,6 +613,11 @@ class Weapon : public MemoryPoolObject, Bool isWithinAttackRange(const Object *source, const Object *target) const; Bool isWithinAttackRange(const Object *source, const Coord3D* pos) const; + /** return true if the target is within attack range or the weapon has leech range, false otherwise. + */ + Bool isWithinEffectiveAttackRange(const Object* source, const Object* target) const; + Bool isWithinEffectiveAttackRange(const Object* source, const Coord3D* pos) const; + Bool isTooClose(const Object *source, const Object *target) const; Bool isTooClose(const Object *source, const Coord3D *pos) const; diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index a5188fab0dc..838e86139b1 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -1138,7 +1138,7 @@ Bool outOfWeaponRangeObject( State *thisState, void* userData ) // victim->getID(), victim->getTemplate()->getName().str())); return true; } - if (!weapon->hasLeechRange() && !weapon->isWithinAttackRange(obj, victim)) + if (!weapon->isWithinEffectiveAttackRange(obj, victim)) { //CRCDEBUG_LOG(("outOfWeaponRangeObject() - object %d (%s) is out of range for attacking %d (%s)", // obj->getID(), obj->getTemplate()->getName().str(), @@ -5087,15 +5087,12 @@ StateReturnType AIAttackFireWeaponState::update() #if !RETAIL_COMPATIBLE_CRC // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this // state, so we check the range again to avoid partially firing the weapon. - if (!weapon->hasLeechRange()) - { - Bool inRange = m_att->isAttackingObject() - ? weapon->isWithinAttackRange(obj, victim) - : weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + Bool inRange = m_att->isAttackingObject() + ? weapon->isWithinEffectiveAttackRange(obj, victim) + : weapon->isWithinEffectiveAttackRange(obj, getMachineGoalPosition()); - if (!inRange) - return STATE_FAILURE; - } + if (!inRange) + return STATE_FAILURE; #endif /** diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index 2c8cc417691..59e480545ce 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -2137,6 +2137,24 @@ Bool Weapon::isWithinAttackRange(const Object *source, const Object *target) con return false; } +//------------------------------------------------------------------------------------------------- +Bool Weapon::isWithinEffectiveAttackRange(const Object* source, const Object* target) const +{ + if (hasLeechRange()) + return true; + + return isWithinAttackRange(source, target); +} + +//------------------------------------------------------------------------------------------------- +Bool Weapon::isWithinEffectiveAttackRange(const Object* source, const Coord3D* pos) const +{ + if (hasLeechRange()) + return true; + + return isWithinAttackRange(source, pos); +} + //------------------------------------------------------------------------------------------------- Bool Weapon::isTooClose(const Object *source, const Object *target) const { diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h index c5997707ba7..e299dbb94fa 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h @@ -632,6 +632,11 @@ class Weapon : public MemoryPoolObject, Bool isWithinAttackRange(const Object *source, const Object *target) const; Bool isWithinAttackRange(const Object *source, const Coord3D* pos) const; + /** return true if the target is within attack range or the weapon has leech range, false otherwise. + */ + Bool isWithinEffectiveAttackRange(const Object* source, const Object* target) const; + Bool isWithinEffectiveAttackRange(const Object* source, const Coord3D* pos) const; + Bool isTooClose(const Object *source, const Object *target) const; Bool isTooClose(const Object *source, const Coord3D *pos) const; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index fa29f39e745..dd38be8e202 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -1143,7 +1143,7 @@ Bool outOfWeaponRangeObject( State *thisState, void* userData ) // victim->getID(), victim->getTemplate()->getName().str())); return true; } - if (!weapon->hasLeechRange() && !weapon->isWithinAttackRange(obj, victim)) + if (!weapon->isWithinEffectiveAttackRange(obj, victim)) { //CRCDEBUG_LOG(("outOfWeaponRangeObject() - object %d (%s) is out of range for attacking %d (%s)", // obj->getID(), obj->getTemplate()->getName().str(), @@ -5243,15 +5243,12 @@ StateReturnType AIAttackFireWeaponState::update() #if !RETAIL_COMPATIBLE_CRC // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this // state, so we check the range again to avoid partially firing the weapon. - if (!weapon->hasLeechRange()) - { - Bool inRange = m_att->isAttackingObject() - ? weapon->isWithinAttackRange(obj, victim) - : weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + Bool inRange = m_att->isAttackingObject() + ? weapon->isWithinEffectiveAttackRange(obj, victim) + : weapon->isWithinEffectiveAttackRange(obj, getMachineGoalPosition()); - if (!inRange) - return STATE_FAILURE; - } + if (!inRange) + return STATE_FAILURE; #endif /** @@ -5335,13 +5332,10 @@ StateReturnType AIAttackFireWeaponState::update() #if !RETAIL_COMPATIBLE_CRC // TheSuperHackers @bugfix Stubbjax 28/09/2026 The weapon may have gone out of range since we entered this // state, so we check the range again to avoid partially firing the weapon. - if (!weapon->hasLeechRange()) - { - Bool inRange = weapon->isWithinAttackRange(obj, getMachineGoalPosition()); + Bool inRange = weapon->isWithinEffectiveAttackRange(obj, getMachineGoalPosition()); - if (!inRange) - continue; - } + if (!inRange) + continue; #endif if ( weapon->fireWeapon(obj, getMachineGoalPosition()) ) //fire() returns 'reloaded' diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index 0cd912d485c..66c1e52d34f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -2323,6 +2323,24 @@ Bool Weapon::isWithinAttackRange(const Object *source, const Object *target) con return false; } +//------------------------------------------------------------------------------------------------- +Bool Weapon::isWithinEffectiveAttackRange(const Object* source, const Object *target) const +{ + if (hasLeechRange()) + return true; + + return isWithinAttackRange(source, target); +} + +//------------------------------------------------------------------------------------------------- +Bool Weapon::isWithinEffectiveAttackRange(const Object* source, const Coord3D* pos) const +{ + if (hasLeechRange()) + return true; + + return isWithinAttackRange(source, pos); +} + //------------------------------------------------------------------------------------------------- Bool Weapon::isTooClose(const Object *source, const Object *target) const {