diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 047e0a0c00e..4d61060da22 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -63,6 +63,7 @@ #include "GameLogic/Module/DeliverPayloadAIUpdate.h" #include "GameLogic/Module/HackInternetAIUpdate.h" #include "GameLogic/Module/HordeUpdate.h" +#include "GameLogic/Module/BehaviorModule.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/PolygonTrigger.h" @@ -73,6 +74,41 @@ #define SLEEPY_AI +// TheSuperHackers @bugfix arazmj 15/08/2026 Allow same-player tunnel and cave networks to exit through another endpoint. +static Bool isCaveContainer(const Object *obj) +{ + if (!obj) + return FALSE; + + for (BehaviorModule **module = obj->getBehaviorModules(); *module; ++module) + { + if ((*module)->getCaveInterface() != nullptr) + return TRUE; + } + + return FALSE; +} + +static Bool isSharedNetworkExitContainer(const Object *fromContainer, const Object *toContainer) +{ + if (!fromContainer || !toContainer) + return FALSE; + + const ContainModuleInterface *fromContain = fromContainer->getContain(); + const ContainModuleInterface *toContain = toContainer->getContain(); + if (!fromContain || !toContain) + return FALSE; + + const Player *fromPlayer = fromContainer->getControllingPlayer(); + const Player *toPlayer = toContainer->getControllingPlayer(); + if (!fromPlayer || fromPlayer != toPlayer) + return FALSE; + + if (fromContain->isTunnelContain() && toContain->isTunnelContain()) + return TRUE; + + return isCaveContainer(fromContainer) && isCaveContainer(toContainer); +} //------------------------------------------------------------------------------------------------- AIUpdateModuleData::AIUpdateModuleData() @@ -3677,10 +3713,14 @@ void AIUpdateInterface::privateExit( Object *objectToExit, CommandSourceType cmd // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, // because an object should not attempt to exit something it's not contained by. #if !RETAIL_COMPATIBLE_CRC - // @todo Remove function parameter 'objectToExit' because it's become obsolete. - if (us->getContainedBy() != objectToExit) - return; + { + const Player *unitPlayer = us->getControllingPlayer(); + const Player *exitPlayer = objectToExit ? objectToExit->getControllingPlayer() : nullptr; + if (!unitPlayer || unitPlayer != exitPlayer + || !isSharedNetworkExitContainer(us->getContainedBy(), objectToExit)) + return; + } #endif } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 617e387003b..7dd867fcacb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -63,6 +63,7 @@ #include "GameLogic/Module/DeliverPayloadAIUpdate.h" #include "GameLogic/Module/HackInternetAIUpdate.h" #include "GameLogic/Module/HordeUpdate.h" +#include "GameLogic/Module/BehaviorModule.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/PolygonTrigger.h" @@ -73,6 +74,41 @@ #define SLEEPY_AI +// TheSuperHackers @bugfix arazmj 15/08/2026 Allow same-player tunnel and cave networks to exit through another endpoint. +static Bool isCaveContainer(const Object *obj) +{ + if (!obj) + return FALSE; + + for (BehaviorModule **module = obj->getBehaviorModules(); *module; ++module) + { + if ((*module)->getCaveInterface() != nullptr) + return TRUE; + } + + return FALSE; +} + +static Bool isSharedNetworkExitContainer(const Object *fromContainer, const Object *toContainer) +{ + if (!fromContainer || !toContainer) + return FALSE; + + const ContainModuleInterface *fromContain = fromContainer->getContain(); + const ContainModuleInterface *toContain = toContainer->getContain(); + if (!fromContain || !toContain) + return FALSE; + + const Player *fromPlayer = fromContainer->getControllingPlayer(); + const Player *toPlayer = toContainer->getControllingPlayer(); + if (!fromPlayer || fromPlayer != toPlayer) + return FALSE; + + if (fromContain->isTunnelContain() && toContain->isTunnelContain()) + return TRUE; + + return isCaveContainer(fromContainer) && isCaveContainer(toContainer); +} //------------------------------------------------------------------------------------------------- AIUpdateModuleData::AIUpdateModuleData() @@ -3832,10 +3868,14 @@ void AIUpdateInterface::privateExit( Object *objectToExit, CommandSourceType cmd // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, // because an object should not attempt to exit something it's not contained by. #if !RETAIL_COMPATIBLE_CRC - // @todo Remove function parameter 'objectToExit' because it's become obsolete. - if (us->getContainedBy() != objectToExit) - return; + { + const Player *unitPlayer = us->getControllingPlayer(); + const Player *exitPlayer = objectToExit ? objectToExit->getControllingPlayer() : nullptr; + if (!unitPlayer || unitPlayer != exitPlayer + || !isSharedNetworkExitContainer(us->getContainedBy(), objectToExit)) + return; + } #endif } @@ -3871,10 +3911,14 @@ void AIUpdateInterface::privateExitInstantly( Object *objectToExit, CommandSourc // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, // because an object should not attempt to exit something it's not contained by. #if !RETAIL_COMPATIBLE_CRC - // @todo Remove function parameter 'objectToExit' because it's become obsolete. - if (us->getContainedBy() != objectToExit) - return; + { + const Player *unitPlayer = us->getControllingPlayer(); + const Player *exitPlayer = objectToExit ? objectToExit->getControllingPlayer() : nullptr; + if (!unitPlayer || unitPlayer != exitPlayer + || !isSharedNetworkExitContainer(us->getContainedBy(), objectToExit)) + return; + } #endif }