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 e7dd96b8f48..6232e2c016d 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4826,6 +4826,22 @@ 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. + if (!contain) + { + source->friend_removeFromTunnelContain(); + + // destroy this object, because it serves no purpose in its invalid state + TheGameLogic->destroyObject(source); + + return STATE_FAILURE; + } +#endif + if (victim) { inFiringRange = contain->attemptBestFirePointPosition( source, weapon, victim ); 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 54f8920a92a..87045e024df 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4318,6 +4318,22 @@ 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()) + { + obj->friend_removeFromTunnelContain(); + + // destroy this object, because it serves no purpose in its invalid state + TheGameLogic->destroyObject(obj); + + return nullptr; + } +#endif + if( !container->getContain()->isPassengerAllowedToFire() ) { //Container doesn't allow for passenger to shoot.