From 0035a9c8d72bb5d431c11b7bcde3e79590a1ef5d Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:56:13 +0200 Subject: [PATCH 1/3] bugfix(contain): Avoid Generals crashes for units in tunnels after transfer to ally. --- .../Code/GameEngine/Source/GameLogic/AI/AIStates.cpp | 12 ++++++++++++ .../Source/GameLogic/Object/Update/AIUpdate.cpp | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index e7dd96b8f48..c60a2951f6b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4826,6 +4826,18 @@ StateReturnType AIAttackAimAtTargetState::onEnter() if( containedBy && weapon ) { ContainModuleInterface *contain = containedBy->getContain(); + +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Caball009 22/09/2026 Return early to prevent a crash. + // This is needed when a Tunnel Network with units is transferred to another player and then sold or destroyed. + // The units are not properly transferred and are left in an invalid state. + // Returning a failure prevents units from firing while hidden and indestructible. + if (!contain) + { + return STATE_FAILURE; + } +#endif + if (victim) { inFiringRange = contain->attemptBestFirePointPosition( source, weapon, victim ); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 54f8920a92a..fbd9d2f75f6 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4318,6 +4318,17 @@ Object* AIUpdateInterface::getNextMoodTarget( Bool calledByAI, Bool calledDuring //Not contained return nullptr; } + +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix Caball009 22/09/2026 Return early to prevent a crash. + // This is needed when a Tunnel Network with units is transferred to another player and then sold or destroyed. + // The units are not properly transferred and are left in an invalid state. + if (!container->getContain()) + { + return nullptr; + } +#endif + if( !container->getContain()->isPassengerAllowedToFire() ) { //Container doesn't allow for passenger to shoot. From d6b31fab0fc8197fd2bbdd6ce97f3a4e7fe46b2b Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:36:39 +0200 Subject: [PATCH 2/3] Destroy object when in invalid state. --- Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp | 3 +++ .../GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index c60a2951f6b..ecf2548d19c 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4834,6 +4834,9 @@ StateReturnType AIAttackAimAtTargetState::onEnter() // Returning a failure prevents units from firing while hidden and indestructible. if (!contain) { + // destroy this object, because it serves no purpose in its invalid state + TheGameLogic->destroyObject(source); + return STATE_FAILURE; } #endif diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index fbd9d2f75f6..50da8acd04f 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4325,6 +4325,9 @@ Object* AIUpdateInterface::getNextMoodTarget( Bool calledByAI, Bool calledDuring // The units are not properly transferred and are left in an invalid state. if (!container->getContain()) { + // destroy this object, because it serves no purpose in its invalid state + TheGameLogic->destroyObject(obj); + return nullptr; } #endif From f196b177df2c99ab4fb7199472ae193d30b827e0 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:36:29 +0200 Subject: [PATCH 3/3] Improved clean up code for object in invalid state. --- .../GameEngine/Include/Common/TunnelTracker.h | 2 +- .../GameEngine/Include/GameLogic/Object.h | 3 +++ .../Source/Common/RTS/TunnelTracker.cpp | 5 ++++- .../Source/GameLogic/AI/AIStates.cpp | 3 ++- .../Source/GameLogic/Object/Object.cpp | 19 +++++++++++++++++++ .../GameLogic/Object/Update/AIUpdate.cpp | 2 ++ 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/TunnelTracker.h b/Generals/Code/GameEngine/Include/Common/TunnelTracker.h index f66b7285ee6..50241906ab8 100644 --- a/Generals/Code/GameEngine/Include/Common/TunnelTracker.h +++ b/Generals/Code/GameEngine/Include/Common/TunnelTracker.h @@ -51,7 +51,7 @@ class TunnelTracker : public MemoryPoolObject, Bool isValidContainerFor(const Object* obj, Bool checkCapacity) const; void addToContainList( Object *obj ); ///< add 'obj' to contain list - void removeFromContain( Object *obj, Bool exposeStealthUnits = FALSE ); ///< remove 'obj' from contain list + Bool removeFromContain( Object *obj, Bool exposeStealthUnits = FALSE ); ///< remove 'obj' from contain list Bool isInContainer( Object *obj ); ///< Is this thing inside? void onTunnelCreated( const Object *newTunnel ); ///< A tunnel was made diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 54ca8613dc8..a1f4480687b 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -425,6 +425,9 @@ class Object : public Thing, public Snapshot void onRemovedFrom( Object *removedFrom ); Int getTransportSlotCount() const; void friend_setContainedBy( Object *containedBy ); +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + void friend_removeFromTunnelContain(); +#endif const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. const Object* getOuterObject() const; ///< Get the top-level object diff --git a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 2a79732cb85..a8561f39c36 100644 --- a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -197,7 +197,7 @@ void TunnelTracker::addToContainList( Object *obj ) } // ------------------------------------------------------------------------ -void TunnelTracker::removeFromContain( Object *obj, Bool exposeStealthUnits ) +Bool TunnelTracker::removeFromContain( Object *obj, Bool exposeStealthUnits ) { ContainedItemsList::iterator it = std::find(m_containList.begin(), m_containList.end(), obj); @@ -212,8 +212,11 @@ void TunnelTracker::removeFromContain( Object *obj, Bool exposeStealthUnits ) DEBUG_ASSERTCRASH(m_heroUnitsContained > 0, ("TunnelTracker::removeFromContain - Removing hero but hero count is %d", m_heroUnitsContained)); --m_heroUnitsContained; } + + return true; } + return false; } // ------------------------------------------------------------------------ diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index ecf2548d19c..6232e2c016d 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4831,9 +4831,10 @@ StateReturnType AIAttackAimAtTargetState::onEnter() // TheSuperHackers @bugfix Caball009 22/09/2026 Return early to prevent a crash. // This is needed when a Tunnel Network with units is transferred to another player and then sold or destroyed. // The units are not properly transferred and are left in an invalid state. - // Returning a failure prevents units from firing while hidden and indestructible. if (!contain) { + source->friend_removeFromTunnelContain(); + // destroy this object, because it serves no purpose in its invalid state TheGameLogic->destroyObject(source); diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 7aa4a442db6..a5df7c3dd62 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -44,6 +44,9 @@ #include "Common/Team.h" #include "Common/ThingFactory.h" #include "Common/ThingTemplate.h" +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +#include "Common/TunnelTracker.h" +#endif #include "Common/Upgrade.h" #include "Common/WellKnownKeys.h" #include "Common/Xfer.h" @@ -636,6 +639,22 @@ void Object::onRemovedFrom( Object *removedFrom ) m_containedByFrame = 0; } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +void Object::friend_removeFromTunnelContain() +{ + for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) + { + TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); + if (tracker && tracker->removeFromContain(this)) + { + break; + } + } + + onRemovedFrom(nullptr); +} +#endif + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Int Object::getTransportSlotCount() const diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 50da8acd04f..87045e024df 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4325,6 +4325,8 @@ Object* AIUpdateInterface::getNextMoodTarget( Bool calledByAI, Bool calledDuring // The units are not properly transferred and are left in an invalid state. if (!container->getContain()) { + obj->friend_removeFromTunnelContain(); + // destroy this object, because it serves no purpose in its invalid state TheGameLogic->destroyObject(obj);