From a4f6a80eb1fd8989c355dca961dd7721552aaa30 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 17 Sep 2026 15:28:58 -0400 Subject: [PATCH 1/9] bugfix(object): Fix crash when destroying an occupant of an already destroyed container --- .../Source/GameLogic/Object/Object.cpp | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index afd6ee9660d..e513636f60b 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -44,6 +44,7 @@ #include "Common/Team.h" #include "Common/ThingFactory.h" #include "Common/ThingTemplate.h" +#include "Common/TunnelTracker.h" #include "Common/Upgrade.h" #include "Common/WellKnownKeys.h" #include "Common/Xfer.h" @@ -164,6 +165,20 @@ AsciiString DebugDescribeObject(const Object *obj) return ret; } +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +static Bool isLiveObject( const Object *obj ) +{ + for( const Object *other = TheGameLogic->getFirstObject(); other; other = other->getNextObject() ) + { + if( other == obj ) + { + return TRUE; + } + } + return FALSE; +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatusMask, Team *team ) : @@ -693,9 +708,30 @@ void Object::onDestroy() { // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. - if( m_containedBy && m_containedBy->getContain() ) + if( m_containedBy ) { - m_containedBy->getContain()->removeFromContain( this ); + // TheSuperHackers @bugfix bobtista 17/09/2026 The container may already be destroyed without this + // object knowing, for example a Tunnel Network that changed owner through the asset transfer of a + // surrendering ally. Only call into a container that still exists, and drop the stale link otherwise. + if( isLiveObject( m_containedBy ) ) + { + if( m_containedBy->getContain() ) + { + m_containedBy->getContain()->removeFromContain( this ); + } + } + else + { + for( Int i = 0; i < ThePlayerList->getPlayerCount(); ++i ) + { + TunnelTracker *tracker = ThePlayerList->getNthPlayer( i )->getTunnelSystem(); + if( tracker ) + { + tracker->removeFromContain( this ); + } + } + onRemovedFrom( nullptr ); + } } // From c0efa7a70eff8fa0ccd3355947e4802c4aa59d8e Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:57:42 +0200 Subject: [PATCH 2/9] refactor: clear the object hash table after destroyAllObjectsImmediate The use of GameLogic::findObjectByID relies on this. --- .../Source/GameLogic/System/GameLogic.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 932caa24f82..7c4a66ae604 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -410,14 +410,6 @@ void GameLogic::reset() m_thingTemplateBuildableOverrides.clear(); m_controlBarOverrides.clear(); - // set the hash to be rather large. We need to optimize this value later. - m_objHash.clear(); -#if USING_STLPORT - m_objHash.resize(OBJ_HASH_SIZE); -#else - m_objHash.reserve(OBJ_HASH_SIZE); -#endif - m_pauseFrame = 0; m_pauseSound = FALSE; m_pauseMusic = FALSE; @@ -432,6 +424,14 @@ void GameLogic::reset() // destroy all objects destroyAllObjectsImmediate(); + // set the hash to be rather large. We need to optimize this value later. + m_objHash.clear(); +#if USING_STLPORT + m_objHash.resize(OBJ_HASH_SIZE); +#else + m_objHash.reserve(OBJ_HASH_SIZE); +#endif + m_nextObjID = (ObjectID)1; m_frameObjectsChangedTriggerAreas = 0; From a25c611749858ba0acd10fcef5a211d89eb72173 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:58:14 +0200 Subject: [PATCH 3/9] refactor: Improve performance --- .../GameEngine/Include/Common/TunnelTracker.h | 2 +- .../Source/Common/RTS/TunnelTracker.cpp | 6 ++- .../Source/GameLogic/Object/Object.cpp | 43 ++++++------------- 3 files changed, 19 insertions(+), 32 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/Source/Common/RTS/TunnelTracker.cpp b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 2a79732cb85..8b2073fd815 100644 --- a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -197,9 +197,8 @@ 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); if (it != m_containList.end()) { @@ -212,8 +211,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/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index e513636f60b..8ee60f098c1 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -165,20 +165,6 @@ AsciiString DebugDescribeObject(const Object *obj) return ret; } -//------------------------------------------------------------------------------------------------- -//------------------------------------------------------------------------------------------------- -static Bool isLiveObject( const Object *obj ) -{ - for( const Object *other = TheGameLogic->getFirstObject(); other; other = other->getNextObject() ) - { - if( other == obj ) - { - return TRUE; - } - } - return FALSE; -} - //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatusMask, Team *team ) : @@ -706,31 +692,30 @@ const Object* Object::getOuterObject() const //------------------------------------------------------------------------------------------------- void Object::onDestroy() { - // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. - if( m_containedBy ) + if (m_containedBy && m_containedBy->getContain()) { - // TheSuperHackers @bugfix bobtista 17/09/2026 The container may already be destroyed without this +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 The container may already be destroyed without this // object knowing, for example a Tunnel Network that changed owner through the asset transfer of a // surrendering ally. Only call into a container that still exists, and drop the stale link otherwise. - if( isLiveObject( m_containedBy ) ) + if (!TheGameLogic->findObjectByID(m_containedBy->getID())) { - if( m_containedBy->getContain() ) + for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) { - m_containedBy->getContain()->removeFromContain( this ); + TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); + if (tracker && tracker->removeFromContain(this)) + { + break; + } } + + onRemovedFrom(nullptr); } else +#endif { - for( Int i = 0; i < ThePlayerList->getPlayerCount(); ++i ) - { - TunnelTracker *tracker = ThePlayerList->getNthPlayer( i )->getTunnelSystem(); - if( tracker ) - { - tracker->removeFromContain( this ); - } - } - onRemovedFrom( nullptr ); + m_containedBy->getContain()->removeFromContain(this); } } From 59f318f9313ff1ea998034e9ba435231b2b4d01e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 22 Sep 2026 14:53:54 -0400 Subject: [PATCH 4/9] refactor(object): Extract tunnel containment cleanup --- .../GameEngine/Include/GameLogic/Object.h | 1 + .../Source/GameLogic/Object/Object.cpp | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 54ca8613dc8..30a813436b8 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -423,6 +423,7 @@ class Object : public Thing, public Snapshot inline Bool isContained() const { return m_containedBy != nullptr; } void onContainedBy( Object *containedBy ); void onRemovedFrom( Object *removedFrom ); + void removeFromTunnelContain(); ///< Remove from the tunnel tracker and clear containment without accessing the container. Int getTransportSlotCount() const; void friend_setContainedBy( Object *containedBy ); const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 8ee60f098c1..436b8482711 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -637,6 +637,21 @@ void Object::onRemovedFrom( Object *removedFrom ) m_containedByFrame = 0; } +//------------------------------------------------------------------------------------------------- +void Object::removeFromTunnelContain() +{ + for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) + { + TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); + if (tracker && tracker->removeFromContain(this)) + { + break; + } + } + + onRemovedFrom(nullptr); +} + //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Int Object::getTransportSlotCount() const @@ -701,16 +716,7 @@ void Object::onDestroy() // surrendering ally. Only call into a container that still exists, and drop the stale link otherwise. if (!TheGameLogic->findObjectByID(m_containedBy->getID())) { - for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) - { - TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); - if (tracker && tracker->removeFromContain(this)) - { - break; - } - } - - onRemovedFrom(nullptr); + removeFromTunnelContain(); } else #endif From d3c76f9a734b1194ffe6ef1de3df8476f7057f5f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Thu, 24 Sep 2026 16:38:07 -0400 Subject: [PATCH 5/9] refactor(generals): Keep tunnel cleanup private and clarify its limits --- Generals/Code/GameEngine/Include/GameLogic/Object.h | 2 +- .../Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp | 1 + .../Code/GameEngine/Source/GameLogic/Object/Object.cpp | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 30a813436b8..3d81e21eca9 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -423,7 +423,6 @@ class Object : public Thing, public Snapshot inline Bool isContained() const { return m_containedBy != nullptr; } void onContainedBy( Object *containedBy ); void onRemovedFrom( Object *removedFrom ); - void removeFromTunnelContain(); ///< Remove from the tunnel tracker and clear containment without accessing the container. Int getTransportSlotCount() const; void friend_setContainedBy( Object *containedBy ); const Object* getEnclosingContainedBy() const; ///< Find the first enclosing container in the containment chain. @@ -650,6 +649,7 @@ class Object : public Thing, public Snapshot virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; private: + void removeFromTunnelContain(); // yes, private. No, really. Private. Don't expose. enum ObjectPrivateStatusBits diff --git a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 8b2073fd815..a8561f39c36 100644 --- a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -199,6 +199,7 @@ void TunnelTracker::addToContainList( Object *obj ) // ------------------------------------------------------------------------ Bool TunnelTracker::removeFromContain( Object *obj, Bool exposeStealthUnits ) { + ContainedItemsList::iterator it = std::find(m_containList.begin(), m_containList.end(), obj); if (it != m_containList.end()) { diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 436b8482711..72098c3f88c 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -707,13 +707,13 @@ const Object* Object::getOuterObject() const //------------------------------------------------------------------------------------------------- void Object::onDestroy() { + // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. - if (m_containedBy && m_containedBy->getContain()) + if( m_containedBy && m_containedBy->getContain() ) { #if RTS_GENERALS && RETAIL_COMPATIBLE_CRC - // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 The container may already be destroyed without this - // object knowing, for example a Tunnel Network that changed owner through the asset transfer of a - // surrendering ally. Only call into a container that still exists, and drop the stale link otherwise. + // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 Remove stranded tunnel occupants during destruction. + // The lookup detects unregistered IDs, but cannot detect reuse of the freed container memory. if (!TheGameLogic->findObjectByID(m_containedBy->getID())) { removeFromTunnelContain(); From c7c29a679e27c5492204446b79e54421b22266c6 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 2 Oct 2026 12:45:43 -0400 Subject: [PATCH 6/9] refactor(gamelogic): Clear the Zero Hour object lookup table after destroying all objects --- .../GameEngine/Source/GameLogic/System/GameLogic.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 60bae04f186..e7bea4711ea 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -420,12 +420,6 @@ void GameLogic::reset() m_thingTemplateBuildableOverrides.clear(); m_controlBarOverrides.clear(); - // set the hash to be rather large. We need to optimize this value later. -// m_objHash.clear(); -// m_objHash.resize(OBJ_HASH_SIZE); - m_objVector.clear(); - m_objVector.resize(OBJ_HASH_SIZE, nullptr); - m_pauseFrame = 0; m_pauseSound = FALSE; m_pauseMusic = FALSE; @@ -440,6 +434,12 @@ void GameLogic::reset() // destroy all objects destroyAllObjectsImmediate(); + // set the hash to be rather large. We need to optimize this value later. +// m_objHash.clear(); +// m_objHash.resize(OBJ_HASH_SIZE); + m_objVector.clear(); + m_objVector.resize(OBJ_HASH_SIZE, nullptr); + m_nextObjID = (ObjectID)1; m_frameObjectsChangedTriggerAreas = 0; From 555259ab790235510f2d3a9eeb542feae8bdbc3d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 4 Oct 2026 20:59:23 -0400 Subject: [PATCH 7/9] unify(object): Gate the Generals tunnel occupant cleanup and mirror it to Zero Hour --- .../GameEngine/Include/GameLogic/Object.h | 2 ++ .../Source/GameLogic/Object/Object.cpp | 4 +++ .../GameEngine/Include/Common/TunnelTracker.h | 2 +- .../GameEngine/Include/GameLogic/Object.h | 3 ++ .../Source/Common/RTS/TunnelTracker.cpp | 5 ++- .../Source/GameLogic/Object/Object.cpp | 33 ++++++++++++++++++- 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index 3d81e21eca9..ecd0e71561d 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -649,7 +649,9 @@ class Object : public Thing, public Snapshot virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; private: +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC void removeFromTunnelContain(); +#endif // yes, private. No, really. Private. Don't expose. enum ObjectPrivateStatusBits diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 72098c3f88c..3c014f8a399 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -44,7 +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" @@ -637,6 +639,7 @@ void Object::onRemovedFrom( Object *removedFrom ) m_containedByFrame = 0; } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC //------------------------------------------------------------------------------------------------- void Object::removeFromTunnelContain() { @@ -651,6 +654,7 @@ void Object::removeFromTunnelContain() onRemovedFrom(nullptr); } +#endif //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h b/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h index 722aab0a54c..851ae5bef26 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h +++ b/GeneralsMD/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/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 13019ab3fa7..160f9dc4bbe 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -685,6 +685,9 @@ class Object : public Thing, public Snapshot virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; private: +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + void removeFromTunnelContain(); +#endif // yes, private. No, really. Private. Don't expose. enum ObjectPrivateStatusBits diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 11d987d0570..62a7bd1a112 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -198,7 +198,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); @@ -213,8 +213,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/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index f99f83dfd27..11a072451f2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/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" @@ -711,6 +714,23 @@ void Object::onRemovedFrom( Object *removedFrom ) } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +//------------------------------------------------------------------------------------------------- +void Object::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 @@ -770,7 +790,18 @@ void Object::onDestroy() // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up. if( m_containedBy && m_containedBy->getContain() ) { - m_containedBy->getContain()->removeFromContain( this ); +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 Remove stranded tunnel occupants during destruction. + // The lookup detects unregistered IDs, but cannot detect reuse of the freed container memory. + if (!TheGameLogic->findObjectByID(m_containedBy->getID())) + { + removeFromTunnelContain(); + } + else +#endif + { + m_containedBy->getContain()->removeFromContain(this); + } } // From 6fd4b0a4d870de989b311449eccfa88f7b1530ae Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 5 Oct 2026 17:45:41 -0400 Subject: [PATCH 8/9] refactor(object): Make removeFromTunnelContain protected --- Generals/Code/GameEngine/Include/GameLogic/Object.h | 3 ++- GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Object.h b/Generals/Code/GameEngine/Include/GameLogic/Object.h index ecd0e71561d..ea38ddbc835 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Object.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Object.h @@ -648,11 +648,12 @@ class Object : public Thing, public Snapshot virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; -private: #if RTS_GENERALS && RETAIL_COMPATIBLE_CRC void removeFromTunnelContain(); #endif +private: + // yes, private. No, really. Private. Don't expose. enum ObjectPrivateStatusBits { diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 160f9dc4bbe..214abbf58f8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -684,11 +684,12 @@ class Object : public Thing, public Snapshot virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; -private: #if RTS_GENERALS && RETAIL_COMPATIBLE_CRC void removeFromTunnelContain(); #endif +private: + // yes, private. No, really. Private. Don't expose. enum ObjectPrivateStatusBits { From 8f376995348f379df7e3618e7e6c4276d6e23ca8 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 5 Oct 2026 17:50:01 -0400 Subject: [PATCH 9/9] docs(object): Explain the stale tunnel pointer read --- Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp | 5 ++++- .../Code/GameEngine/Source/GameLogic/Object/Object.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 3c014f8a399..587f1f9b387 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -717,7 +717,10 @@ void Object::onDestroy() { #if RTS_GENERALS && RETAIL_COMPATIBLE_CRC // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 Remove stranded tunnel occupants during destruction. - // The lookup detects unregistered IDs, but cannot detect reuse of the freed container memory. + // Surrendering can transfer a tunnel without updating its tunnel tracker. If that tunnel is destroyed, + // m_containedBy still points to the freed tunnel, and its ID is read here through that stale pointer. + // An unregistered ID means the tunnel is gone, so this object is removed from the tunnel trackers directly. + // This is a limited workaround that keeps retail compatibility. It cannot detect reuse of the freed memory. if (!TheGameLogic->findObjectByID(m_containedBy->getID())) { removeFromTunnelContain(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 11a072451f2..e32873ffad2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -792,7 +792,10 @@ void Object::onDestroy() { #if RTS_GENERALS && RETAIL_COMPATIBLE_CRC // TheSuperHackers @bugfix bobtista / Caball009 17/09/2026 Remove stranded tunnel occupants during destruction. - // The lookup detects unregistered IDs, but cannot detect reuse of the freed container memory. + // Surrendering can transfer a tunnel without updating its tunnel tracker. If that tunnel is destroyed, + // m_containedBy still points to the freed tunnel, and its ID is read here through that stale pointer. + // An unregistered ID means the tunnel is gone, so this object is removed from the tunnel trackers directly. + // This is a limited workaround that keeps retail compatibility. It cannot detect reuse of the freed memory. if (!TheGameLogic->findObjectByID(m_containedBy->getID())) { removeFromTunnelContain();