Skip to content

bugfix(aiupdate): Let Tunnel Network passengers exit from any tunnel - #3136

Open
Okladnoj wants to merge 1 commit into
TheSuperHackers:mainfrom
Okladnoj:okji/bugfix/tunnel-exit-any-tunnel
Open

bugfix(aiupdate): Let Tunnel Network passengers exit from any tunnel#3136
Okladnoj wants to merge 1 commit into
TheSuperHackers:mainfrom
Okladnoj:okji/bugfix/tunnel-exit-any-tunnel

Conversation

@Okladnoj

Copy link
Copy Markdown

Follow-up to #3089 — problem: evacuating a Tunnel Network does not work (build two
tunnels, load units through the first one, press Evacuate on the second one — nobody
comes out).

A Tunnel Network keeps a single contents list shared by every tunnel, while a
passenger's getContainedBy() points at the tunnel it entered, so #3089 rejects
everyone addressed to a different tunnel. Now we ask the addressed container whether it
holds the unit, instead of comparing container objects.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix Tunnel Network evacuation by validating containment via shared contents list

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Fix exit command validation to support Tunnel Networks with shared contents lists.
• Reject exit requests only when the addressed tunnel does not actually contain the unit.
• Apply the same fix to both Generals and GeneralsMD engine variants.
Diagram

graph TD
  A["Exit/Evacuate command"] --> B["AIUpdateInterface::privateExit"] --> C["Read container's ContainModule"] --> D[("Contained items list")] --> E{"Contains unit?"} --> F["Allow exit processing"]
  E --> G["Reject/return"]
  subgraph Legend
    direction LR
    _p["Process"] ~~~ _d{"Decision"} ~~~ _ds[("Data")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fix containment ownership pointer for Tunnel Networks
  • ➕ Preserves the original identity-based check (us->getContainedBy() == addressed container).
  • ➕ Could reduce future surprises where shared-list containers behave differently.
  • ➖ Higher risk: changes core containment semantics and potentially many callers.
  • ➖ Harder to guarantee correctness if multiple tunnel objects intentionally share state.
2. Add a container API like contains(Object*)
  • ➕ Centralizes containment validation behind an interface method (no list peeking at call sites).
  • ➕ Allows specialized containers (Tunnel Network) to override containment logic cleanly.
  • ➖ Requires broader refactor across exit paths and other containment checks.
  • ➖ More code churn than this targeted fix.

Recommendation: The PR’s approach is the best low-risk fix for the regression described in #3089: validate exit commands by verifying the addressed container actually contains the unit (via the shared contents list). Consider a follow-up refactor to introduce a dedicated contains() query on the containment module to avoid repeating list-based checks at call sites.

Files changed (2) +12 / -6

Bug fix (2) +12 / -6
AIUpdate.cppValidate exit against addressed container contents (Tunnel Network-safe) +4/-2

Validate exit against addressed container contents (Tunnel Network-safe)

• Updates non-retail exit-command validation to check whether the addressed container’s contained-items list includes the unit. This prevents false rejection when Tunnel Network tunnels share a single contents list across multiple tunnel objects.

Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp

AIUpdate.cppMirror Tunnel Network exit validation fix for MD (normal + instant exit) +8/-4

Mirror Tunnel Network exit validation fix for MD (normal + instant exit)

• Applies the same containment-by-list validation to both privateExit and privateExitInstantly in the MD variant. Ensures evacuate/exit works regardless of which tunnel instance is targeted.

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. O(n²) tunnel evacuate 🐞 Bug ➹ Performance
Description
In !RETAIL_COMPATIBLE_CRC builds, privateExit/privateExitInstantly now do a linear std::find over
ContainedItemsList for every exit command. When OpenContain::orderAllPassengersToExit iterates that
same list and issues one exit per passenger (TunnelContain uses a shared tunnel-system list), this
adds an extra scan per passenger and can turn a single evacuate into O(n²) list traversal.
Code

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[R3836-3839]

+		const ContainModuleInterface* contain = objectToExit->getContain();
+		const ContainedItemsList* items = contain != nullptr ? contain->getContainedItemsList() : nullptr;

-		if (us->getContainedBy() != objectToExit)
+		if (items == nullptr || std::find(items->begin(), items->end(), us) == items->end())
Evidence
The PR adds a std::find membership check in AIUpdate. OpenContain evacuation loops over
getContainedItemsList() and calls aiExit/aiExitInstantly once per rider, so the new code adds
one extra linear scan per passenger. TunnelContain redirects getContainedItemsList() to a shared
tunnel-system list, making repeated scans over the same list likely during tunnel evacuation.

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3832-3841]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3873-3882]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp[1428-1450]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[328-336]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp[1330-1344]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[251-259]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AIUpdateInterface::privateExit` / `privateExitInstantly` validate the addressed container by scanning its `ContainedItemsList` with `std::find`. During `OpenContain::orderAllPassengersToExit`, this validation is invoked once per passenger while iterating the same list, producing avoidable repeated list scans.

### Issue Context
This path is used by Tunnel Networks because `TunnelContain::getContainedItemsList()` returns the shared `TunnelTracker` list.

### Fix Focus Areas
- Prefer O(1) fast-paths before `std::find`:
 - If `us->getContainedBy() == objectToExit`, accept without scanning.
 - If `us->getContainedBy()` has a `ContainModuleInterface` and its `getContainedItemsList()` pointer equals the addressed container’s `items` pointer (shared tunnel list), accept without scanning.
 - Only fall back to `std::find` when the above checks fail.

- file/path[start_line-end_line]
 - Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3677-3685]
 - GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3832-3840]
 - GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3873-3881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Null list blocks exit 🐞 Bug ☼ Reliability
Description
The new validation returns early when getContainedItemsList() is nullptr, even if the unit is
legitimately contained by the addressed container (previously it only compared us->getContainedBy()
vs objectToExit). This is a behavioral change and can silently drop exit commands in cases where a
contain module returns a null list (e.g., TunnelContain when its tunnel system is unavailable).
Code

Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[R3682-3685]

+		const ContainedItemsList* items = contain != nullptr ? contain->getContainedItemsList() : nullptr;

-		if (us->getContainedBy() != objectToExit)
+		if (items == nullptr || std::find(items->begin(), items->end(), us) == items->end())
			return;
Evidence
AIUpdate now returns when items == nullptr. TunnelContain’s getContainedItemsList() explicitly
returns nullptr when the owning player or tunnel system is missing, so this new behavior can cause
exit commands to be rejected solely due to list unavailability.

Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3679-3686]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[251-259]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[328-336]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The updated exit-command validation treats `items == nullptr` as “not contained” and returns. Previously, a passenger could still exit when `us->getContainedBy() == objectToExit` regardless of list access.

### Issue Context
At least one container implementation can return `nullptr` from `getContainedItemsList()` when its backing structure is unavailable (TunnelContain returns `nullptr` if `owningPlayer` or `owningPlayer->getTunnelSystem()` is null).

### Fix Focus Areas
- Preserve prior behavior as a safe fallback:
 - If `items == nullptr`, fall back to `if (us->getContainedBy() != objectToExit) return;` instead of unconditional return.
 - (Optional, complements perf fix) Accept when the addressed container shares the same `ContainedItemsList*` pointer as `us->getContainedBy()`.

- file/path[start_line-end_line]
 - Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3679-3685]
 - GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3834-3840]
 - GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp[3875-3881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
@Okladnoj
Okladnoj force-pushed the okji/bugfix/tunnel-exit-any-tunnel branch from 3f4abcc to 6e75c21 Compare August 14, 2026 01:49
Okladnoj added a commit to Okladnoj/GeneralsGameCode that referenced this pull request Aug 14, 2026
…t check

Addresses review findings on TheSuperHackers#3136: skip the list scan when the addressed object already is our container, and fall back to the previous identity check when the container exposes no contents list.
Okladnoj added a commit to OKJID/GameClient that referenced this pull request Aug 14, 2026
…t check

Addresses review findings on TheSuperHackers#3136: skip the list scan when the addressed object already is our container, and fall back to the previous identity check when the container exposes no contents list.

@Caball009 Caball009 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find. I overlooked this.

@Caball009 Caball009 added Critical Severity: Minor < Major < Critical < Blocker ThisProject The issue was introduced by this project, or this task is specific to this project NoRetail This fix or change is not applicable with Retail game compatibility Gen Relates to Generals ZH Relates to Zero Hour Bug Something is not working right, typically is user facing Unit AI Is related to unit behavior labels Aug 14, 2026
@Okladnoj
Okladnoj force-pushed the okji/bugfix/tunnel-exit-any-tunnel branch from 6e75c21 to 6ce04ca Compare August 14, 2026 02:43
Okladnoj added a commit to Okladnoj/GeneralsGameCode that referenced this pull request Aug 14, 2026
Okladnoj added a commit to OKJID/GameClient that referenced this pull request Aug 14, 2026
bobtista
bobtista previously approved these changes Aug 14, 2026

@bobtista bobtista left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
The only nit I can see is some if () returns without braces, which is just formatting preference.

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments and questions

Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
@Okladnoj
Okladnoj force-pushed the okji/bugfix/tunnel-exit-any-tunnel branch from 6ce04ca to 06eece5 Compare August 14, 2026 20:41
Okladnoj added a commit to Okladnoj/GeneralsGameCode that referenced this pull request Aug 14, 2026
…t check

Addresses review notes from xezon on TheSuperHackers#3136:
- restore the original @BugFix date, the fix itself is unchanged
- drop the double null test in favour of an early return
- explain why a Tunnel Network needs the contain list check

Behaviour is unchanged.
Okladnoj added a commit to OKJID/GameClient that referenced this pull request Aug 14, 2026
…t check

Addresses review notes from xezon on TheSuperHackers#3136:
- restore the original @BugFix date, the fix itself is unchanged
- drop the double null test in favour of an early return
- explain why a Tunnel Network needs the contain list check

Behaviour is unchanged.
@Okladnoj

Copy link
Copy Markdown
Author

@xezon
Date restored, double null test replaced with an early return, comment reworded.

On isTunnelContain(). That won't work — CaveContain returns FALSE from it but hands out the same shared list. The fix currently covers caves as well, and the gate would leave them broken.

There is no extra iteration anyway: the outer containedBy != objectToExit check already filters out ordinary containers.

On unbinding the container pointer earlier. That changes game logic and would show up in the CRC, so better as a separate PR.

@xezon

xezon commented Aug 14, 2026

Copy link
Copy Markdown

Can we perhaps add bool isTunnelContain() { return true; } to CaveContain or will that break anything?

@Okladnoj

Copy link
Copy Markdown
Author

Can we perhaps add bool isTunnelContain() { return true; } to CaveContain or will that break anything?

Probably will — isTunnelContain() currently doubles as a type test before a static_cast.

TunnelTracker::updateFullHealTime(), both trees:

if (!contain->isTunnelContain())
    continue;
const TunnelContain* tunnelContain = static_cast<const TunnelContain*>(contain);

Caves do reach that loop — they register through onTunnelCreated() (CaveContain.cpp:256, :272). With TRUE they would be cast to TunnelContain and getFullTimeForHeal() would read the wrong module data. The path sits behind PRESERVE_TUNNEL_HEAL_STACKING, but that flag is meant to go to 0.

Second one is AcademyStats.cpp:413: caves would count as a Tunnel Network. Not a crash, but there is no guard, so it would affect RETAIL too.

@xezon

xezon commented Aug 15, 2026

Copy link
Copy Markdown

Ok. How about we add a new virtual function to contain modules bool isSharedContainer(), and make that true for TunnelContain and CaveContain. And then we can test the containment if the container is shared. Basically the idea here is to avoid unnecessary contain list inspection when not needed.

Okladnoj added a commit to Okladnoj/GeneralsGameCode that referenced this pull request Aug 15, 2026
Follows xezon's suggestion on TheSuperHackers#3136: a dedicated predicate
tells whether a container shares its contents list with the rest of its
network, so the exit check only inspects the list when it can matter.

TRUE for TunnelContain and CaveContain, FALSE for every other container.
@Okladnoj
Okladnoj force-pushed the okji/bugfix/tunnel-exit-any-tunnel branch from 06eece5 to d445878 Compare August 15, 2026 18:58
@Okladnoj

Copy link
Copy Markdown
Author

Ok. How about we add a new virtual function to contain modules bool isSharedContainer(), and make that true for TunnelContain and CaveContain. And then we can test the containment if the container is shared. Basically the idea here is to avoid unnecessary contain list inspection when not needed.

Done. TRUE for TunnelContain and CaveContain, FALSE by default in OpenContain — no other module needed touching. Thanks for the architectural hint :)

Ran a ~95,700 frame replay, all good.

Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate.cpp Outdated
@Caball009
Caball009 dismissed bobtista’s stale review August 16, 2026 15:17

Implementation change since review

Follow-up to TheSuperHackers#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.
@Okladnoj
Okladnoj force-pushed the okji/bugfix/tunnel-exit-any-tunnel branch from d445878 to a5d5fe6 Compare August 16, 2026 16:52
Okladnoj added a commit to OKJID/GameClient that referenced this pull request Aug 16, 2026
Follows xezon's suggestion on TheSuperHackers#3136: a dedicated predicate
tells whether a container shares its contents list with the rest of its
network, so the exit check only inspects the list when it can matter.

TRUE for TunnelContain and CaveContain, FALSE for every other container.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Critical Severity: Minor < Major < Critical < Blocker Gen Relates to Generals NoRetail This fix or change is not applicable with Retail game compatibility ThisProject The issue was introduced by this project, or this task is specific to this project Unit AI Is related to unit behavior ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants