From a5d5fe607eb23dc22b946e8b100f35ca3ad05992 Mon Sep 17 00:00:00 2001 From: Okladnoj Date: Thu, 13 Aug 2026 22:45:31 +0300 Subject: [PATCH] bugfix(aiupdate): Let Tunnel Network passengers exit from any tunnel Follow-up to #3089. The exit command is rejected unless the addressed object is the passenger's own container, but a GLA Tunnel Network shares one contents list across every tunnel, so only the tunnel a unit entered would release it. Ask the addressed container whether it holds the unit instead. --- .../Include/GameLogic/Module/CaveContain.h | 1 + .../Include/GameLogic/Module/ContainModule.h | 1 + .../Include/GameLogic/Module/OpenContain.h | 1 + .../Include/GameLogic/Module/TunnelContain.h | 1 + .../GameLogic/Object/Update/AIUpdate.cpp | 24 ++++++++++++--- .../Include/GameLogic/Module/CaveContain.h | 1 + .../Include/GameLogic/Module/ContainModule.h | 1 + .../Include/GameLogic/Module/OpenContain.h | 1 + .../Include/GameLogic/Module/TunnelContain.h | 1 + .../GameLogic/Object/Update/AIUpdate.cpp | 30 ++++++++++++++----- 10 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/CaveContain.h b/Generals/Code/GameEngine/Include/GameLogic/Module/CaveContain.h index 3663ae85fa4..c8bf87e45f9 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/CaveContain.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/CaveContain.h @@ -100,6 +100,7 @@ class CaveContain : public OpenContain, public CreateModuleInterface, public Cav virtual UnsignedInt getContainCount() const override; virtual Int getContainMax() const override; virtual const ContainedItemsList* getContainedItemsList() const override; + virtual Bool isSharedContainer() const override { return TRUE; } virtual Bool isKickOutOnCapture() override { return FALSE; }///< Caves and Tunnels don't kick out on capture. // override the onDie we inherit from OpenContain diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/ContainModule.h b/Generals/Code/GameEngine/Include/GameLogic/Module/ContainModule.h index 32be50719d7..90989c34ae0 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/ContainModule.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/ContainModule.h @@ -84,6 +84,7 @@ class ContainModuleInterface virtual Bool isSpecialZeroSlotContainer() const = 0; virtual Bool isHealContain() const = 0; virtual Bool isTunnelContain() const = 0; + virtual Bool isSharedContainer() const = 0; virtual Bool isImmuneToClearBuildingAttacks() const = 0; diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/OpenContain.h b/Generals/Code/GameEngine/Include/GameLogic/Module/OpenContain.h index cf145317cd1..83303ebff3f 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/OpenContain.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/OpenContain.h @@ -190,6 +190,7 @@ class OpenContain : public UpdateModule, virtual Bool isGarrisonable() const override { return false; } ///< can this unit be Garrisoned? (ick) virtual Bool isHealContain() const override { return false; } ///< true when container only contains units while healing (not a transport!) virtual Bool isTunnelContain() const override { return FALSE; } + virtual Bool isSharedContainer() const override { return FALSE; } virtual Bool isSpecialZeroSlotContainer() const override { return false; } virtual Bool isImmuneToClearBuildingAttacks() const override { return true; } diff --git a/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h b/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h index bf99a748e6e..3c62838120e 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h @@ -89,6 +89,7 @@ class TunnelContain : public OpenContain, public CreateModuleInterface virtual OpenContain *asOpenContain() override { return this; } ///< treat as open container virtual Bool isGarrisonable() const override { return false; } ///< can this unit be Garrisoned? (ick) virtual Bool isHealContain() const override { return false; } ///< true when container only contains units while healing (not a transport!) + virtual Bool isSharedContainer() const override { return TRUE; } virtual Bool isImmuneToClearBuildingAttacks() const override { return true; } virtual void onContaining( Object *obj ) override; ///< object now contains 'obj' diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 047e0a0c00e..a80f0be7834 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -3658,6 +3658,24 @@ void AIUpdateInterface::privateCombatDrop( Object *target, const Coord3D& pos, C } } +//---------------------------------------------------------------------------------------- +#if !RETAIL_COMPATIBLE_CRC +static Bool isContainedBy( const Object *us, const Object *container ) +{ + if (us->getContainedBy() == container) + return TRUE; + + // A shared container holds one passenger list for the whole network, so a passenger is contained + // by the endpoint it entered and not by the one that was ordered to unload. + const ContainModuleInterface *contain = container->getContain(); + if (contain == nullptr || !contain->isSharedContainer()) + return FALSE; + + const ContainedItemsList *items = contain->getContainedItemsList(); + return items != nullptr && std::find(items->begin(), items->end(), us) != items->end(); +} +#endif + //---------------------------------------------------------------------------------------- /** * Get out of whatever it is inside of @@ -3674,12 +3692,10 @@ void AIUpdateInterface::privateExit( Object *objectToExit, CommandSourceType cmd } else { - // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, + // TheSuperHackers @bugfix Caball009 / Okladnoj 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) + if (!isContainedBy(us, objectToExit)) return; #endif } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CaveContain.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CaveContain.h index 677efcbe848..19633700b36 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CaveContain.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CaveContain.h @@ -101,6 +101,7 @@ class CaveContain : public OpenContain, public CreateModuleInterface, public Cav virtual UnsignedInt getContainCount() const override; virtual Int getContainMax() const override; virtual const ContainedItemsList* getContainedItemsList() const override; + virtual Bool isSharedContainer() const override { return TRUE; } virtual Bool isKickOutOnCapture() override { return FALSE; }///< Caves and Tunnels don't kick out on capture. // override the onDie we inherit from OpenContain diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ContainModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ContainModule.h index c1a9b932f31..89c52d1fbf1 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ContainModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ContainModule.h @@ -97,6 +97,7 @@ class ContainModuleInterface virtual Bool isSpecialZeroSlotContainer() const = 0; virtual Bool isHealContain() const = 0; virtual Bool isTunnelContain() const = 0; + virtual Bool isSharedContainer() const = 0; virtual Bool isRiderChangeContain() const = 0; virtual Bool isImmuneToClearBuildingAttacks() const = 0; virtual Bool isSpecialOverlordStyleContainer() const = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OpenContain.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OpenContain.h index c3c3f1f99bb..31e5fd35bb9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OpenContain.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OpenContain.h @@ -201,6 +201,7 @@ class OpenContain : public UpdateModule, virtual Bool isBustable() const override { return false; } ///< can this container get busted by a bunkerbuster virtual Bool isHealContain() const override { return false; } ///< true when container only contains units while healing (not a transport!) virtual Bool isTunnelContain() const override { return FALSE; } + virtual Bool isSharedContainer() const override { return FALSE; } virtual Bool isRiderChangeContain() const override { return FALSE; } virtual Bool isSpecialZeroSlotContainer() const override { return false; } virtual Bool isImmuneToClearBuildingAttacks() const override { return true; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h index 5e295cd9603..3d7b570a5d2 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h @@ -91,6 +91,7 @@ class TunnelContain : public OpenContain, public CreateModuleInterface virtual Bool isBustable() const override { return TRUE; } ///< can this container get busted by a bunkerbuster virtual Bool isHealContain() const override { return false; } ///< true when container only contains units while healing (not a transport!) virtual Bool isTunnelContain() const override { return TRUE; } + virtual Bool isSharedContainer() const override { return TRUE; } virtual Bool isImmuneToClearBuildingAttacks() const override { return true; } virtual Bool isSpecialOverlordStyleContainer() const override {return FALSE;} diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp index 617e387003b..6d16ef15792 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp @@ -3813,6 +3813,24 @@ void AIUpdateInterface::privateCombatDrop( Object *target, const Coord3D& pos, C } } +//---------------------------------------------------------------------------------------- +#if !RETAIL_COMPATIBLE_CRC +static Bool isContainedBy( const Object *us, const Object *container ) +{ + if (us->getContainedBy() == container) + return TRUE; + + // A shared container holds one passenger list for the whole network, so a passenger is contained + // by the endpoint it entered and not by the one that was ordered to unload. + const ContainModuleInterface *contain = container->getContain(); + if (contain == nullptr || !contain->isSharedContainer()) + return FALSE; + + const ContainedItemsList *items = contain->getContainedItemsList(); + return items != nullptr && std::find(items->begin(), items->end(), us) != items->end(); +} +#endif + //---------------------------------------------------------------------------------------- /** * Get out of whatever it is inside of @@ -3829,12 +3847,10 @@ void AIUpdateInterface::privateExit( Object *objectToExit, CommandSourceType cmd } else { - // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, + // TheSuperHackers @bugfix Caball009 / Okladnoj 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) + if (!isContainedBy(us, objectToExit)) return; #endif } @@ -3868,12 +3884,10 @@ void AIUpdateInterface::privateExitInstantly( Object *objectToExit, CommandSourc } else { - // TheSuperHackers @bugfix Caball009 10/08/2026 Don't process invalid exit commands, + // TheSuperHackers @bugfix Caball009 / Okladnoj 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) + if (!isContainedBy(us, objectToExit)) return; #endif }