From 62d06421a48ae79090c60d7af4139cec3e1d811c Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:50:13 +0200 Subject: [PATCH 1/3] bugfix(contain): Avoid Generals crashes for units in tunnels after transfer to ally. --- .../GameEngine/Include/GameLogic/GameLogic.h | 4 ++ .../Source/GameLogic/AI/AIStates.cpp | 14 +++++++ .../GameLogic/Object/Update/AIUpdate.cpp | 14 +++++++ .../Source/GameLogic/System/GameLogic.cpp | 39 ++++++++++++++++++- 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h index 9e2513986ce..e6ed8ae6d9b 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -249,6 +249,10 @@ class GameLogic : public SubsystemInterface, public Snapshot Bool isQuitToDesktopRequested() const { return m_quitToDesktopAfterMatch; } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + void friend_destroyInTunnelContain(const Object* strandedObject); +#endif + protected: // snapshot methods diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index 2b77abfb794..4ce8ac5b2cf 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4832,6 +4832,20 @@ 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) + { + // remove all such units from the tunnel tracker and destroy them + TheGameLogic->friend_destroyInTunnelContain(source); + + 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 526516e21e7..e1752fa3db4 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4318,6 +4318,20 @@ 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()) + { + // remove all such units from the tunnel tracker and destroy them + TheGameLogic->friend_destroyInTunnelContain(obj); + + return nullptr; + } +#endif + if( !container->getContain()->isPassengerAllowedToFire() ) { //Container doesn't allow for passenger to shoot. diff --git a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 09048b600d6..0659bac5ec9 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -54,7 +54,9 @@ #include "Common/ThingFactory.h" #include "Common/Team.h" #include "Common/ThingTemplate.h" -#include "GameClient/Water.h" +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +#include "Common/TunnelTracker.h" +#endif #include "Common/WellKnownKeys.h" #include "Common/Xfer.h" #include "Common/XferCRC.h" @@ -73,6 +75,7 @@ #include "GameClient/View.h" #include "GameClient/CampaignManager.h" #include "GameClient/GameWindowTransitions.h" +#include "GameClient/Water.h" #include "GameLogic/AI.h" #include "GameLogic/AIPathfind.h" @@ -2829,6 +2832,40 @@ void GameLogic::friend_awakenUpdateModule(Object* obj, UpdateModulePtr u, Unsign } } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +// ------------------------------------------------------------------------------------------------ +// Find the tunnel tracker that contains the object and destroy all objects in the contain list +// ------------------------------------------------------------------------------------------------ +void GameLogic::friend_destroyInTunnelContain(const Object* strandedObject) +{ + for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) + { + TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); + if (!tracker) + continue; + + const ContainedItemsList& list = *tracker->getContainedItemsList(); + + const ContainedItemsList::const_iterator it = std::find(list.begin(), list.end(), strandedObject); + if (it == list.end()) + continue; + + while (!list.empty()) + { + Object* obj = list.front(); + + tracker->removeFromContain(obj); + + obj->onRemovedFrom(nullptr); + + destroyObject(obj); + } + + break; + } +} +#endif + // ------------------------------------------------------------------------------------------------ #ifdef DO_UNIT_TIMINGS enum {TIME_FRAMES=100}; From a5a8ce8c65d64b70b3540a06f8b20abff4dbe017 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:51:23 +0200 Subject: [PATCH 2/3] refactor: Modify Generals code in preparation to replicate code to Zero Hour. --- .../Code/GameEngine/Source/GameLogic/AI/AIStates.cpp | 9 +++++++-- .../Source/GameLogic/Object/Update/AIUpdate.cpp | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index 4ce8ac5b2cf..3b69eed6050 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4829,10 +4829,14 @@ StateReturnType AIAttackAimAtTargetState::onEnter() //to move to the best fire point, check if it's in firing range, and if not //move it back so another unit with longer range can! Object *containedBy = source->getContainedBy(); + ContainModuleInterface *contain = containedBy ? containedBy->getContain() : nullptr; + +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC if( containedBy && weapon ) +#else + if( containedBy && weapon && contain && contain->isEnclosingContainerFor( source ) ) +#endif { - 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. @@ -4846,6 +4850,7 @@ StateReturnType AIAttackAimAtTargetState::onEnter() } #endif + // non enclosing garrison containers do not use firepoints. Lorenzen, 6/11/03 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 e1752fa3db4..227bd3c49bf 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4334,7 +4334,8 @@ Object* AIUpdateInterface::getNextMoodTarget( Bool calledByAI, Bool calledDuring if( !container->getContain()->isPassengerAllowedToFire() ) { - //Container doesn't allow for passenger to shoot. + // Sorry, stealthed and not allowed to idle fire when stealthed. + // Being in a firing container is an exception to this veto. return nullptr; } } From 3289e16d3af7d6a356aeda2168ccb40ec4cda45b Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:07:09 +0200 Subject: [PATCH 3/3] Replicated to Zero Hour. --- .../GameEngine/Include/GameLogic/GameLogic.h | 4 +++ .../Source/GameLogic/AI/AIStates.cpp | 17 +++++++++ .../GameLogic/Object/Update/AIUpdate.cpp | 30 +++++++++++++--- .../Source/GameLogic/System/GameLogic.cpp | 35 +++++++++++++++++-- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h index 15276811ccd..6219c1663ae 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -252,6 +252,10 @@ class GameLogic : public SubsystemInterface, public Snapshot Bool isQuitToDesktopRequested() const { return m_quitToDesktopAfterMatch; } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + void friend_destroyInTunnelContain(const Object* strandedObject); +#endif + protected: // snapshot methods diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp index dbfddab2192..86f43fecc65 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIStates.cpp @@ -4953,8 +4953,25 @@ StateReturnType AIAttackAimAtTargetState::onEnter() Object *containedBy = source->getContainedBy(); ContainModuleInterface *contain = containedBy ? containedBy->getContain() : nullptr; +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC + if( containedBy && weapon ) +#else if( containedBy && weapon && contain && contain->isEnclosingContainerFor( source ) ) +#endif { +#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) + { + // remove all such units from the tunnel tracker and destroy them + TheGameLogic->friend_destroyInTunnelContain(source); + + return STATE_FAILURE; + } +#endif + // non enclosing garrison containers do not use firepoints. Lorenzen, 6/11/03 if (victim) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 195fcf552f9..07c89b71541 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -4559,13 +4559,33 @@ Object* AIUpdateInterface::getNextMoodTarget( Bool calledByAI, Bool calledDuring { if( !canAutoAcquireWhileStealthed() ) { - const Object *container = obj->getContainedBy(); - if( ! (container && container->getContain()->isPassengerAllowedToFire()) ) - { + //He's stealthed and idle, but if he's garrisoned, then that's a whole different matter.... + const Object *container = obj->getContainedBy(); + if( !container ) + { + //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()) + { + // remove all such units from the tunnel tracker and destroy them + TheGameLogic->friend_destroyInTunnelContain(obj); + + return nullptr; + } +#endif + + if( !container->getContain()->isPassengerAllowedToFire() ) + { // Sorry, stealthed and not allowed to idle fire when stealthed. // Being in a firing container is an exception to this veto. - return nullptr; - } + return nullptr; + } } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp index 0d8d4e64692..86fa19d0241 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp @@ -55,8 +55,9 @@ #include "Common/ThingFactory.h" #include "Common/Team.h" #include "Common/ThingTemplate.h" -#include "GameClient/Water.h" -#include "GameClient/Snow.h" +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +#include "Common/TunnelTracker.h" +#endif #include "Common/WellKnownKeys.h" #include "Common/Xfer.h" #include "Common/XferCRC.h" @@ -73,8 +74,10 @@ #include "GameClient/MapUtil.h" #include "GameClient/Mouse.h" #include "GameClient/ParticleSys.h" +#include "GameClient/Snow.h" #include "GameClient/TerrainVisual.h" #include "GameClient/View.h" +#include "GameClient/Water.h" #include "GameClient/CampaignManager.h" #include "GameClient/GameWindowTransitions.h" @@ -3154,11 +3157,39 @@ void GameLogic::friend_awakenUpdateModule(Object* obj, UpdateModulePtr u, Unsign } } +#if RTS_GENERALS && RETAIL_COMPATIBLE_CRC +// ------------------------------------------------------------------------------------------------ +// Find the tunnel tracker that contains the object and destroy all objects in the contain list +// ------------------------------------------------------------------------------------------------ +void GameLogic::friend_destroyInTunnelContain(const Object* strandedObject) +{ + for (Int i = 0; i < ThePlayerList->getPlayerCount(); ++i) + { + TunnelTracker* tracker = ThePlayerList->getNthPlayer(i)->getTunnelSystem(); + if (!tracker) + continue; + const ContainedItemsList& list = *tracker->getContainedItemsList(); + const ContainedItemsList::const_iterator it = std::find(list.begin(), list.end(), strandedObject); + if (it == list.end()) + continue; + + while (!list.empty()) + { + Object* obj = list.front(); + tracker->removeFromContain(obj); + obj->onRemovedFrom(nullptr); + + destroyObject(obj); + } + break; + } +} +#endif // ------------------------------------------------------------------------------------------------ #ifdef DO_UNIT_TIMINGS