From 2ac671d25ac12df0ba113f73db097ce29b63171d Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:10:35 +0200 Subject: [PATCH 1/3] Put Generals function 'TunnelContain::onCapture' behind non-retail compatibility macro. --- .../Code/GameEngine/Include/GameLogic/Module/TunnelContain.h | 2 ++ .../Source/GameLogic/Object/Contain/TunnelContain.cpp | 2 ++ .../Code/GameEngine/Include/GameLogic/Module/TunnelContain.h | 2 ++ .../Source/GameLogic/Object/Contain/TunnelContain.cpp | 2 ++ 4 files changed, 8 insertions(+) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h b/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h index 8d20de4e80c..f95930d830a 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h @@ -94,7 +94,9 @@ class TunnelContain : public OpenContain, public CreateModuleInterface virtual void onContaining( Object *obj ) override; ///< object now contains 'obj' virtual void onRemoving( Object *obj ) override; ///< object no longer contains 'obj' virtual void onSelling() override;///< Container is being sold. Tunnel responds by kicking people out if this is the last tunnel. +#if !(RTS_GENERALS && RETAIL_COMPATIBLE_CRC) virtual void onCapture( Player *oldOwner, Player *newOwner ) override; // Need to change who we are registered with. +#endif virtual void orderAllPassengersToExit( CommandSourceType commandSource ) override; ///< All of the smarts of exiting are in the passenger's AIExit. removeAllFrommContain is a last ditch system call, this is the game Evacuate diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp index ba20b9f78a7..c84d73a7caf 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp @@ -400,6 +400,7 @@ void TunnelContain::onBuildComplete() // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ +#if !(RTS_GENERALS && RETAIL_COMPATIBLE_CRC) void TunnelContain::onCapture( Player *oldOwner, Player *newOwner ) { if( m_isCurrentlyRegistered ) @@ -421,6 +422,7 @@ void TunnelContain::onCapture( Player *oldOwner, Player *newOwner ) // extend base class OpenContain::onCapture( oldOwner, newOwner ); } +#endif //------------------------------------------------------------------------------------------------- void TunnelContain::orderAllPassengersToExit( CommandSourceType commandSource ) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h index d539559d42e..41cb104ac18 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h @@ -97,7 +97,9 @@ class TunnelContain : public OpenContain, public CreateModuleInterface virtual void onContaining( Object *obj, Bool wasSelected ) override; ///< object now contains 'obj' virtual void onRemoving( Object *obj ) override; ///< object no longer contains 'obj' virtual void onSelling() override;///< Container is being sold. Tunnel responds by kicking people out if this is the last tunnel. +#if !(RTS_GENERALS && RETAIL_COMPATIBLE_CRC) virtual void onCapture( Player *oldOwner, Player *newOwner ) override; // Need to change who we are registered with. +#endif virtual void orderAllPassengersToExit( CommandSourceType commandSource, Bool instantly ) override; ///< All of the smarts of exiting are in the passenger's AIExit. removeAllFrommContain is a last ditch system call, this is the game Evacuate virtual void orderAllPassengersToIdle( CommandSourceType commandSource ) override; ///< Just like it sounds diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp index 46f0b537a73..b2f2884bb6c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp @@ -502,6 +502,7 @@ void TunnelContain::onBuildComplete() // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ +#if !(RTS_GENERALS && RETAIL_COMPATIBLE_CRC) void TunnelContain::onCapture( Player *oldOwner, Player *newOwner ) { if( m_isCurrentlyRegistered ) @@ -523,6 +524,7 @@ void TunnelContain::onCapture( Player *oldOwner, Player *newOwner ) // extend base class OpenContain::onCapture( oldOwner, newOwner ); } +#endif //------------------------------------------------------------------------------------------------- void TunnelContain::orderAllPassengersToExit( CommandSourceType commandSource, Bool instantly ) From a34726cff543119c6a291ef5f48b89fd0ed48bb3 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:21:33 +0200 Subject: [PATCH 2/3] Fixed retail incompatibility and crash issue. --- .../Source/Common/RTS/TunnelTracker.cpp | 26 +++++++++---------- .../Source/Common/RTS/TunnelTracker.cpp | 26 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 91fc49b7f13..14313a2160a 100644 --- a/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -231,18 +231,14 @@ void TunnelTracker::onTunnelCreated( const Object *newTunnel ) // ------------------------------------------------------------------------ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) { - { - std::list::iterator it = std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()); - if (it == m_tunnelIDs.end()) - { - DEBUG_CRASH(("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", deadTunnel->getName().str())); - return; - } + DEBUG_ASSERTCRASH(static_cast(m_tunnelCount) > 0 && m_tunnelCount == m_tunnelIDs.size(), + ("TunnelTracker::onTunnelDestroyed - Tunnel count is unexpected")); + DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()) != m_tunnelIDs.end(), + ("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", deadTunnel->getName().str())); - m_tunnelCount--; - m_tunnelIDs.erase(it); - m_needsFullHealTimeUpdate = true; - } + m_tunnelCount--; + m_tunnelIDs.remove( deadTunnel->getID() ); + m_needsFullHealTimeUpdate = true; if( m_tunnelCount == 0 ) { @@ -253,7 +249,11 @@ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) } else { - Object *validTunnel = TheGameLogic->findObjectByID( m_tunnelIDs.front() ); + // TheSuperHackers @bugfix Caball009 01/09/2026 Check if container is empty before accessing it. + // The tunnel count may diverge from the container size, because of bugs that cannot be fixed with + // retail compatibility enabled. Expected retail behavior: empty container access results in a nullptr. + Object *tunnel = m_tunnelIDs.empty() ? nullptr : TheGameLogic->findObjectByID( m_tunnelIDs.front() ); + // Otherwise, make sure nobody inside remembers the dead tunnel as the one they entered // (scripts need to use so there must be something valid here) for(ContainedItemsList::iterator it = m_containList.begin(); it != m_containList.end(); ) @@ -261,7 +261,7 @@ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) Object* obj = *it; ++it; if( obj->getContainedBy() == deadTunnel ) - obj->onContainedBy( validTunnel ); + obj->onContainedBy( tunnel ); } } } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 114c70088f3..18fbf1190e8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -232,18 +232,14 @@ void TunnelTracker::onTunnelCreated( const Object *newTunnel ) // ------------------------------------------------------------------------ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) { - { - std::list::iterator it = std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()); - if (it == m_tunnelIDs.end()) - { - DEBUG_CRASH(("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", deadTunnel->getName().str())); - return; - } + DEBUG_ASSERTCRASH(static_cast(m_tunnelCount) > 0 && m_tunnelCount == m_tunnelIDs.size(), + ("TunnelTracker::onTunnelDestroyed - Tunnel count is unexpected")); + DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()) != m_tunnelIDs.end(), + ("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", deadTunnel->getName().str())); - m_tunnelCount--; - m_tunnelIDs.erase(it); - m_needsFullHealTimeUpdate = true; - } + m_tunnelCount--; + m_tunnelIDs.remove( deadTunnel->getID() ); + m_needsFullHealTimeUpdate = true; if( m_tunnelCount == 0 ) { @@ -254,7 +250,11 @@ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) } else { - Object *validTunnel = TheGameLogic->findObjectByID( m_tunnelIDs.front() ); + // TheSuperHackers @bugfix Caball009 01/09/2026 Check if container is empty before accessing it. + // The tunnel count may diverge from the container size, because of bugs that cannot be fixed with + // retail compatibility enabled. Expected retail behavior: empty container access results in a nullptr. + Object *tunnel = m_tunnelIDs.empty() ? nullptr : TheGameLogic->findObjectByID( m_tunnelIDs.front() ); + // Otherwise, make sure nobody inside remembers the dead tunnel as the one they entered // (scripts need to use so there must be something valid here) for(ContainedItemsList::iterator it = m_containList.begin(); it != m_containList.end(); ) @@ -262,7 +262,7 @@ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) Object* obj = *it; ++it; if( obj->getContainedBy() == deadTunnel ) - obj->onContainedBy( validTunnel ); + obj->onContainedBy( tunnel ); } } } From e30337a81935689a09ae26ffe375436a0ac891c2 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:56:39 +0200 Subject: [PATCH 3/3] Added complete retail incompatible version for Zero Hour. --- .../GameEngine/Include/Common/TunnelTracker.h | 6 ++ .../Source/Common/RTS/TunnelTracker.cpp | 88 +++++++++++++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h b/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h index b044ebc00cd..722aab0a54c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h @@ -66,7 +66,11 @@ class TunnelTracker : public MemoryPoolObject, void healObjects(); ///< heal all objects within the tunnel #endif +#if RETAIL_COMPATIBLE_CRC UnsignedInt friend_getTunnelCount() const {return m_tunnelCount;}///< TunnelContains are allowed to ask if they are the last one ahead of deletion time +#else + UnsignedInt friend_getTunnelCount() const { return m_tunnelIDs.size(); }///< TunnelContains are allowed to ask if they are the last one ahead of deletion time +#endif const std::list< ObjectID > *getContainerList() const {return &m_tunnelIDs;} @@ -87,7 +91,9 @@ class TunnelTracker : public MemoryPoolObject, std::list< ObjectID > m_xferContainList;///< for loading of m_containList during post processing Int m_containListSize; ///< size of the contain list UnsignedInt m_heroUnitsContained; ///< cached hero count +#if RETAIL_COMPATIBLE_CRC UnsignedInt m_tunnelCount; ///< How many tunnels have registered so we know when we should kill our contain list +#endif UnsignedInt m_framesForFullHeal; ///< How many frames it takes to fully heal a unit Bool m_needsFullHealTimeUpdate; ///< Set to true when needing to recalc full heal time to batch the operation diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp index 18fbf1190e8..11d987d0570 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp @@ -50,7 +50,9 @@ // ------------------------------------------------------------------------ TunnelTracker::TunnelTracker() { +#if RETAIL_COMPATIBLE_CRC m_tunnelCount = 0; +#endif m_containListSize = 0; m_heroUnitsContained = 0; m_curNemesisID = INVALID_ID; @@ -224,7 +226,12 @@ Bool TunnelTracker::isInContainer( Object *obj ) // ------------------------------------------------------------------------ void TunnelTracker::onTunnelCreated( const Object *newTunnel ) { + DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), newTunnel->getID()) == m_tunnelIDs.end(), + ("TunnelTracker::onTunnelCreated - New tunnel was already added; this shouldn't happen")); + +#if RETAIL_COMPATIBLE_CRC m_tunnelCount++; +#endif m_tunnelIDs.push_back( newTunnel->getID() ); m_needsFullHealTimeUpdate = true; } @@ -232,14 +239,19 @@ void TunnelTracker::onTunnelCreated( const Object *newTunnel ) // ------------------------------------------------------------------------ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) { - DEBUG_ASSERTCRASH(static_cast(m_tunnelCount) > 0 && m_tunnelCount == m_tunnelIDs.size(), +#if RETAIL_COMPATIBLE_CRC + + DEBUG_ASSERTLOG(static_cast(m_tunnelCount) > 0 && m_tunnelCount == m_tunnelIDs.size(), ("TunnelTracker::onTunnelDestroyed - Tunnel count is unexpected")); - DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()) != m_tunnelIDs.end(), + + const size_t oldSize = m_tunnelIDs.size(); + m_tunnelIDs.remove(deadTunnel->getID()); + + (void)oldSize; + DEBUG_ASSERTLOG(oldSize != m_tunnelIDs.size(), ("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", deadTunnel->getName().str())); m_tunnelCount--; - m_tunnelIDs.remove( deadTunnel->getID() ); - m_needsFullHealTimeUpdate = true; if( m_tunnelCount == 0 ) { @@ -265,6 +277,45 @@ void TunnelTracker::onTunnelDestroyed( const Object *deadTunnel ) obj->onContainedBy( tunnel ); } } + +#else + + const std::list::iterator it = std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), deadTunnel->getID()); + if (it != m_tunnelIDs.end()) + { + m_tunnelIDs.erase(it); + + m_needsFullHealTimeUpdate = true; + } + else + { + DEBUG_CRASH(("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel", + deadTunnel->getName().str())); + } + + if( m_tunnelIDs.empty() ) + { + // Kill everyone in our contain list. Cave in! + iterateContained( destroyObject, nullptr, FALSE ); + m_containList.clear(); + m_containListSize = 0; + } + else + { + Object *validTunnel = TheGameLogic->findObjectByID( m_tunnelIDs.front() ); + + // Otherwise, make sure nobody inside remembers the dead tunnel as the one they entered + // (scripts need to use so there must be something valid here) + for(ContainedItemsList::iterator it = m_containList.begin(); it != m_containList.end(); ) + { + Object* obj = *it; + ++it; + if( obj->getContainedBy() == deadTunnel ) + obj->onContainedBy( validTunnel ); + } + } + +#endif // RETAIL_COMPATIBLE_CRC } // ------------------------------------------------------------------------ @@ -379,13 +430,19 @@ void TunnelTracker::crc( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: - * 1: Initial version */ + * 1: Initial version + * 2: TheSuperHackers @tweak Removed m_tunnelCount (should always be equal to m_tunnelIDs.size()) + */ // ------------------------------------------------------------------------------------------------ void TunnelTracker::xfer( Xfer *xfer ) { // version +#if RETAIL_COMPATIBLE_XFER_SAVE XferVersion currentVersion = 1; +#else + XferVersion currentVersion = 2; +#endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -424,8 +481,25 @@ void TunnelTracker::xfer( Xfer *xfer ) m_needsFullHealTimeUpdate = true; } - // tunnel count - xfer->xferUnsignedInt( &m_tunnelCount ); +#if RETAIL_COMPATIBLE_CRC + if (version <= 1) + { + xfer->xferUnsignedInt(&m_tunnelCount); + } + else + { + if (xfer->getXferMode() == XFER_LOAD) + { + m_tunnelCount = m_tunnelIDs.size(); + } + } +#else + if (version <= 1) + { + UnsignedInt tunnelCount = m_tunnelIDs.size(); + xfer->xferUnsignedInt(&tunnelCount); + } +#endif }