Skip to content

bugfix(tunnel): Restore retail compatibility after changes to TunnelTracker::onTunnelDestroyed() and TunnelContain::onCapture() - #3242

Open
Caball009 wants to merge 3 commits into
TheSuperHackers:mainfrom
Caball009:Caball009/bugfix_on_tunnel_destroyed
Open

bugfix(tunnel): Restore retail compatibility after changes to TunnelTracker::onTunnelDestroyed() and TunnelContain::onCapture()#3242
Caball009 wants to merge 3 commits into
TheSuperHackers:mainfrom
Caball009:Caball009/bugfix_on_tunnel_destroyed

Conversation

@Caball009

@Caball009 Caball009 commented Aug 31, 2026

Copy link
Copy Markdown

This PR makes 2 changes:

    • Restore retail compatibility: adding TunnelContain::onCapture to Generals is not retail compatible so this needs to be behind the RETAIL_COMPATIBLE_CRC macro.
    • Restore retail compatibility: the early return in TunnelTracker::onTunnelDestroyed is not retail compatible so this had to be changed.
    • Fix a potential crash. A side effect of the early return is that the members of the contain list (m_containList) hold on to a pointer to their container. This can lead to use-after-free bugs and crashes. The early return is removed.

The m_tunnelCount integer underflow cannot be fixed with retail compatibility enabled, but the code should be well-defined and safe now.

See commits for clean diffs.


Object *validTunnel = TheGameLogic->findObjectByID( m_tunnelIDs.front() );

I'm not entirely familiar with the STLPort implementation of std::list<T>, but accessing the front element of an empty std::list appears to result reliably in T{} for trivial types; INVALID_ID in this context. As a result, all contained objects have their container pointer updated to nullptr, neatly avoiding the use-after-free bugs and resulting crash.


Crash reproduction with the current main branch (VC6 / VS22, RETAIL_COMPATIBLE_CRC == 1), using Zero Hour GLA campaign mission 3:

Video and call stack
gen_zh_md_gla03_campaign_tunnel_crash.mp4
generalszh.exe!AIUpdateInterface::getNextMoodTarget(bool calledByAI, bool calledDuringIdle) Line 4557
generalszh.exe!AIIdleState::update() Line 1443
generalszh.exe!StateMachine::updateStateMachine() Line 439
generalszh.exe!AIStateMachine::updateStateMachine() Line 884
generalszh.exe!AIUpdateInterface::update() Line 1015
generalszh.exe!GameLogic::update() Line 3873
generalszh.exe!SubsystemInterface::UPDATE() Line 131
generalszh.exe!GameEngine::update() Line 921
generalszh.exe!Win32GameEngine::update() Line 91
generalszh.exe!GameEngine::execute() Line 983
generalszh.exe!GameMain() Line 55
generalszh.exe!WinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, char * lpCmdLine, int nCmdShow) Line 929

TODO:

  • Replicate last commit to Generals.
  • Update PR description to include the changes after the second commit.

@Caball009 Caball009 added Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour ThisProject The issue was introduced by this project, or this task is specific to this project Crash This is a crash, very bad labels Aug 31, 2026
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 31, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Restore retail-compatible tunnel lifecycle and passenger cleanup

🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Restores retail-compatible tunnel lifecycle behavior in Generals and Zero Hour.
• Clears stale passenger container pointers when no replacement tunnel remains.
• Derives modern tunnel state from IDs while preserving retail counters and saves.
Diagram

graph TD
  Contain["Tunnel lifecycle"] --> Tracker["TunnelTracker"] --> Mode{"Retail build?"}
  Mode -->|Yes| Retail["Retail counter"] --> Destroy["Destroy handling"] --> Passengers["Passenger links"]
  Mode -->|No| Modern["Tunnel ID list"] --> Destroy
  Tracker --> Save["Save transfer"]
  Modern --> Save
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always derive state from tunnel IDs
  • ➕ Eliminates duplicated counter state and underflow risk.
  • ➕ Provides one consistent destruction path for all builds.
  • ➖ Breaks retail CRC compatibility.
  • ➖ Changes legacy save and runtime behavior.
2. Retain early return with targeted cleanup
  • ➕ Minimizes changes to the existing destruction routine.
  • ➕ Could prevent stale passenger pointers after invalid removals.
  • ➖ Still diverges from retail control flow.
  • ➖ Leaves duplicated counter and ID-list state vulnerable to desynchronization.

Recommendation: Keep the PR's split implementation: preserve legacy counter behavior only where retail compatibility requires it, while making the ID list authoritative in modern builds. This limits compatibility risk and removes redundant state where possible; reviewers should closely validate both macro configurations and legacy save loading.

Files changed (8) +116 / -27

Bug fix (5) +106 / -25
TunnelContain.hConditionally declare the tunnel capture override +2/-0

Conditionally declare the tunnel capture override

• Excludes 'TunnelContain::onCapture' from retail-compatible Generals builds to restore the original virtual interface and CRC behavior.

Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h

TunnelTracker.cppPreserve destruction cleanup without unsafe empty-list access +13/-13

Preserve destruction cleanup without unsafe empty-list access

• Replaces the missing-tunnel early return with assertions and retail-compatible removal behavior. It safely clears passenger container pointers when the tunnel count and ID list diverge and no replacement tunnel exists.

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp

TunnelContain.cppUse last-tunnel query and guard capture handling +3/-1

Use last-tunnel query and guard capture handling

• Uses 'isLastTunnel()' when selling a tunnel and conditionally compiles the capture implementation outside retail-compatible Generals builds.

Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp

TunnelTracker.cppSplit retail and modern tunnel destruction paths +85/-10

Split retail and modern tunnel destruction paths

• Preserves legacy counter-based destruction in retail builds while modern builds use the ID list as authoritative state. It also adds safe passenger reassignment, registration diagnostics, and versioned transfer support for counter-free saves.

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp

TunnelContain.cppAdopt last-tunnel query and conditional capture logic +3/-1

Adopt last-tunnel query and conditional capture logic

• Switches selling behavior to 'isLastTunnel()' and conditionally compiles the capture implementation according to retail compatibility requirements.

GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp

Refactor (2) +8 / -2
TunnelTracker.hReplace raw tunnel-count access with last-tunnel query +1/-1

Replace raw tunnel-count access with last-tunnel query

• Replaces the exposed counter getter with an intent-specific 'isLastTunnel()' method used during tunnel sales.

Generals/Code/GameEngine/Include/Common/TunnelTracker.h

TunnelTracker.hMake tunnel tracking state build-mode dependent +7/-1

Make tunnel tracking state build-mode dependent

• Keeps the legacy tunnel counter only for retail-compatible builds. Modern builds determine last-tunnel status directly from the tunnel ID list.

GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h

Other (1) +2 / -0
TunnelContain.hGuard the tunnel capture override for compatibility +2/-0

Guard the tunnel capture override for compatibility

• Applies the retail-compatibility condition to the 'onCapture' declaration while retaining it for applicable Zero Hour builds.

GeneralsMD/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h

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

qodo-free-for-open-source-projects Bot commented Aug 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Tunnel creation assertion inverted 🐞 Bug ≡ Correctness
Description
onTunnelCreated asserts that a new tunnel ID is already tracked, so every normal first-time
registration triggers DEBUG_CRASHING builds while duplicate registrations pass. The ID is only
appended after this check, and the normal creation callback performs no earlier insertion.
Code

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[R229-230]

+	DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), newTunnel->getID()) != m_tunnelIDs.end(),
+		("TunnelTracker::onTunnelCreated - New tunnel was already added; this shouldn't happen"));
Evidence
The assertion tests find(...) != end() before push_back adds the ID, while
TunnelContain::onObjectCreated directly calls this registration method for newly created tunnels.
DEBUG_ASSERTCRASH invokes DEBUG_CRASH when its condition is false.

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[227-235]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[460-477]
Core/GameEngine/Include/Common/Debug.h[186-198]

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

## Issue description
`TunnelTracker::onTunnelCreated` uses an inverted assertion: it requires the new tunnel ID to already exist, causing normal registrations to trigger a debug crash while allowing duplicates.
## Issue Context
The tunnel ID is appended only after the assertion. The normal `TunnelContain::onObjectCreated` path calls this method without inserting the ID first.
## Fix Focus Areas
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[229-230]

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


2. Untracked destruction corrupts count 🐞 Bug ≡ Correctness
Description
onTunnelDestroyed now decrements m_tunnelCount even when remove found no matching tunnel or
the count is already zero, so a wrong-tracker notification can underflow the count or make the
tracker cave in while valid tunnels remain. This is reachable in retail Generals after
Team::setControllingPlayer: capture callbacks are skipped, but later tunnel deletion resolves the
new owner's tracker and reports an ID that tracker never registered.
Code

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247]

+	m_tunnelCount--;
Evidence
onTunnelCreated is the operation that increments the count and appends the ID, but the changed
destroy path diagnoses a failed removal and nevertheless decrements the unsigned count. In retail
mode, team reassignment only updates partition state rather than invoking capture callbacks; the
tunnel remains marked registered, and its deletion looks up the current owner's tracker, providing a
concrete path to failed removal. DEBUG_CRASH is compiled to a no-op when debug crashing is
disabled, so it does not prevent the release-state corruption.

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[224-268]
Generals/Code/GameEngine/Source/Common/RTS/Team.cpp[1391-1418]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[359-374]
Core/GameEngine/Include/Common/Debug.h[175-206]

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

## Issue description
`TunnelTracker::onTunnelDestroyed` unconditionally decrements the unsigned tunnel count after detecting that the tunnel was not registered. This can underflow or corrupt a different owner's tracker; retain the containment-pointer cleanup needed by this PR without applying registration bookkeeping when removal failed.
## Issue Context
The retail `Team::setControllingPlayer` path skips `Object::onCapture`, while `TunnelContain::onDelete` obtains the tracker from the object's current owner. Therefore an untracked destruction is a reachable state, not merely a defensive assertion case. Keep the Generals and GeneralsMD implementations behaviorally aligned.
## Fix Focus Areas
- Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[234-268]
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[235-269]

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



Remediation recommended

3. Missing crash format argument 🐞 Bug ☼ Reliability
Description
The failed-removal assertion passes a %s format specifier to DebugCrash without supplying the
tunnel name argument. When this diagnostic executes, the variadic formatter reads an absent
argument, potentially crashing or emitting corrupt diagnostic output instead of reporting the
tracker failure.
Code

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[R251-252]

+	DEBUG_ASSERTCRASH(oldSize != m_tunnelIDs.size(),
+		("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel"));
Evidence
The assertion's message contains %s but no second argument, whereas the equivalent non-retail
diagnostic supplies deadTunnel->getName().str(). The debug macro forwards the tuple directly to
variadic DebugCrash, so no argument is added automatically.

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247-252]
GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[284-289]
Core/GameEngine/Include/Common/Debug.h[175-198]

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 failed tunnel-removal assertion contains a `%s` placeholder but does not pass the corresponding tunnel name, resulting in invalid variadic formatting when the assertion fires.
## Issue Context
`DEBUG_ASSERTCRASH` forwards its message tuple directly to the variadic `DebugCrash` function. The non-retail branch immediately below demonstrates the intended call with `deadTunnel->getName().str()`.
## Fix Focus Areas
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[251-252]

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


Grey Divider

Context sources

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

Previous reviews

Review updated until commit 4faa5ea ⚖️ Balanced

Results up to commit 4faa5ea


🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)


Action required
1. Tunnel creation assertion inverted 🐞 Bug ≡ Correctness ⭐ New
Description
onTunnelCreated asserts that a new tunnel ID is already tracked, so every normal first-time
registration triggers DEBUG_CRASHING builds while duplicate registrations pass. The ID is only
appended after this check, and the normal creation callback performs no earlier insertion.
Code

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[R229-230]

+	DEBUG_ASSERTCRASH(std::find(m_tunnelIDs.begin(), m_tunnelIDs.end(), newTunnel->getID()) != m_tunnelIDs.end(),
+		("TunnelTracker::onTunnelCreated - New tunnel was already added; this shouldn't happen"));
Evidence
The assertion tests find(...) != end() before push_back adds the ID, while
TunnelContain::onObjectCreated directly calls this registration method for newly created tunnels.
DEBUG_ASSERTCRASH invokes DEBUG_CRASH when its condition is false.

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[227-235]
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[460-477]
Core/GameEngine/Include/Common/Debug.h[186-198]

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

## Issue description
`TunnelTracker::onTunnelCreated` uses an inverted assertion: it requires the new tunnel ID to already exist, causing normal registrations to trigger a debug crash while allowing duplicates.

## Issue Context
The tunnel ID is appended only after the assertion. The normal `TunnelContain::onObjectCreated` path calls this method without inserting the ID first.

## Fix Focus Areas
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[229-230]

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


2. Untracked destruction corrupts count 🐞 Bug ≡ Correctness
Description
onTunnelDestroyed now decrements m_tunnelCount even when remove found no matching tunnel or
the count is already zero, so a wrong-tracker notification can underflow the count or make the
tracker cave in while valid tunnels remain. This is reachable in retail Generals after
Team::setControllingPlayer: capture callbacks are skipped, but later tunnel deletion resolves the
new owner's tracker and reports an ID that tracker never registered.
Code

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247]

+	m_tunnelCount--;
Evidence
onTunnelCreated is the operation that increments the count and appends the ID, but the changed
destroy path diagnoses a failed removal and nevertheless decrements the unsigned count. In retail
mode, team reassignment only updates partition state rather than invoking capture callbacks; the
tunnel remains marked registered, and its deletion looks up the current owner's tracker, providing a
concrete path to failed removal. DEBUG_CRASH is compiled to a no-op when debug crashing is
disabled, so it does not prevent the release-state corruption.

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[224-268]
Generals/Code/GameEngine/Source/Common/RTS/Team.cpp[1391-1418]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[359-374]
Core/GameEngine/Include/Common/Debug.h[175-206]

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

## Issue description
`TunnelTracker::onTunnelDestroyed` unconditionally decrements the unsigned tunnel count after detecting that the tunnel was not registered. This can underflow or corrupt a different owner's tracker; retain the containment-pointer cleanup needed by this PR without applying registration bookkeeping when removal failed.
## Issue Context
The retail `Team::setControllingPlayer` path skips `Object::onCapture`, while `TunnelContain::onDelete` obtains the tracker from the object's current owner. Therefore an untracked destruction is a reachable state, not merely a defensive assertion case. Keep the Generals and GeneralsMD implementations behaviorally aligned.
## Fix Focus Areas
- Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[234-268]
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[235-269]

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



Remediation recommended
3. Missing crash format argument 🐞 Bug ☼ Reliability ⭐ New
Description
The failed-removal assertion passes a %s format specifier to DebugCrash without supplying the
tunnel name argument. When this diagnostic executes, the variadic formatter reads an absent
argument, potentially crashing or emitting corrupt diagnostic output instead of reporting the
tracker failure.
Code

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[R251-252]

+	DEBUG_ASSERTCRASH(oldSize != m_tunnelIDs.size(),
+		("TunnelTracker::onTunnelDestroyed - Attempting to remove object '%s' that has never been tracked as a tunnel"));
Evidence
The assertion's message contains %s but no second argument, whereas the equivalent non-retail
diagnostic supplies deadTunnel->getName().str(). The debug macro forwards the tuple directly to
variadic DebugCrash, so no argument is added automatically.

GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247-252]
GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[284-289]
Core/GameEngine/Include/Common/Debug.h[175-198]

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 failed tunnel-removal assertion contains a `%s` placeholder but does not pass the corresponding tunnel name, resulting in invalid variadic formatting when the assertion fires.

## Issue Context
`DEBUG_ASSERTCRASH` forwards its message tuple directly to the variadic `DebugCrash` function. The non-retail branch immediately below demonstrates the intended call with `deadTunnel->getName().str()`.

## Fix Focus Areas
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[251-252]

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


Context sources
Results up to commit 342ce12


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)


Action required
1. Untracked destruction corrupts count 🐞 Bug ≡ Correctness
Description
onTunnelDestroyed now decrements m_tunnelCount even when remove found no matching tunnel or
the count is already zero, so a wrong-tracker notification can underflow the count or make the
tracker cave in while valid tunnels remain. This is reachable in retail Generals after
Team::setControllingPlayer: capture callbacks are skipped, but later tunnel deletion resolves the
new owner's tracker and reports an ID that tracker never registered.
Code

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247]

+	m_tunnelCount--;
Evidence
onTunnelCreated is the operation that increments the count and appends the ID, but the changed
destroy path diagnoses a failed removal and nevertheless decrements the unsigned count. In retail
mode, team reassignment only updates partition state rather than invoking capture callbacks; the
tunnel remains marked registered, and its deletion looks up the current owner's tracker, providing a
concrete path to failed removal. DEBUG_CRASH is compiled to a no-op when debug crashing is
disabled, so it does not prevent the release-state corruption.

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[224-268]
Generals/Code/GameEngine/Source/Common/RTS/Team.cpp[1391-1418]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[359-374]
Core/GameEngine/Include/Common/Debug.h[175-206]

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

## Issue description
`TunnelTracker::onTunnelDestroyed` unconditionally decrements the unsigned tunnel count after detecting that the tunnel was not registered. This can underflow or corrupt a different owner's tracker; retain the containment-pointer cleanup needed by this PR without applying registration bookkeeping when removal failed.
## Issue Context
The retail `Team::setControllingPlayer` path skips `Object::onCapture`, while `TunnelContain::onDelete` obtains the tracker from the object's current owner. Therefore an untracked destruction is a reachable state, not merely a defensive assertion case. Keep the Generals and GeneralsMD implementations behaviorally aligned.
## Fix Focus Areas
- Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[234-268]
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[235-269]

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


Context sources
Results up to commit 1d42639


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)


Action required
1. Untracked destruction corrupts count 🐞 Bug ≡ Correctness
Description
onTunnelDestroyed now decrements m_tunnelCount even when remove found no matching tunnel or
the count is already zero, so a wrong-tracker notification can underflow the count or make the
tracker cave in while valid tunnels remain. This is reachable in retail Generals after
Team::setControllingPlayer: capture callbacks are skipped, but later tunnel deletion resolves the
new owner's tracker and reports an ID that tracker never registered.
Code

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[247]

+	m_tunnelCount--;
Evidence
onTunnelCreated is the operation that increments the count and appends the ID, but the changed
destroy path diagnoses a failed removal and nevertheless decrements the unsigned count. In retail
mode, team reassignment only updates partition state rather than invoking capture callbacks; the
tunnel remains marked registered, and its deletion looks up the current owner's tracker, providing a
concrete path to failed removal. DEBUG_CRASH is compiled to a no-op when debug crashing is
disabled, so it does not prevent the release-state corruption.

Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[224-268]
Generals/Code/GameEngine/Source/Common/RTS/Team.cpp[1391-1418]
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp[359-374]
Core/GameEngine/Include/Common/Debug.h[175-206]

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

## Issue description
`TunnelTracker::onTunnelDestroyed` unconditionally decrements the unsigned tunnel count after detecting that the tunnel was not registered. This can underflow or corrupt a different owner's tracker; retain the containment-pointer cleanup needed by this PR without applying registration bookkeeping when removal failed.

## Issue Context
The retail `Team::setControllingPlayer` path skips `Object::onCapture`, while `TunnelContain::onDelete` obtains the tracker from the object's current owner. Therefore an untracked destruction is a reachable state, not merely a defensive assertion case. Keep the Generals and GeneralsMD implementations behaviorally aligned.

## Fix Focus Areas
- Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[234-268]
- GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[235-269]

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


Context sources

Grey Divider

Qodo Logo

@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown

Greptile Summary

The PR restores retail-compatible tunnel behavior while making tunnel destruction safe when bookkeeping diverges.

  • Guards TunnelContain::onCapture behind the retail-compatibility condition in both variants.
  • Separates retail and non-retail Zero Hour tunnel bookkeeping and destruction behavior.
  • Versions Zero Hour tunnel save transfer after removing the redundant non-retail tunnel count.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Removes the unsafe early return and safely clears containment links when retail-compatible bookkeeping leaves no remaining tunnel ID.
Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp Excludes the non-retail capture override from retail-compatible Generals builds.
GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Corrects the previously reported assertions and iterator usage, separates retail destruction behavior, and versions tunnel-count serialization.
GeneralsMD/Code/GameEngine/Include/Common/TunnelTracker.h Retains the explicit tunnel count only for retail-compatible CRC builds and derives it from the ID list otherwise.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp Applies the capture override only where retail compatibility does not prohibit it.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Tunnel destroyed] --> B{Retail-compatible CRC?}
    B -->|Yes| C[Remove ID and preserve retail count behavior]
    B -->|No| D[Remove tracked ID using list iterator]
    C --> E{Tunnel count is zero?}
    D --> F{ID list is empty?}
    E -->|Yes| G[Cave in and clear occupants]
    F -->|Yes| G
    E -->|No| H[Reassign containedBy to remaining tunnel or null]
    F -->|No| I[Reassign containedBy to remaining tunnel]
Loading

Reviews (12): Last reviewed commit: "Added complete retail incompatible versi..." | Re-trigger Greptile

Comment thread Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp
Comment thread Generals/Code/GameEngine/Include/GameLogic/Module/TunnelContain.h Outdated
Comment thread Generals/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp Outdated
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 1d42639 to 001ee15 Compare August 31, 2026 20:50
@Caball009
Caball009 marked this pull request as draft August 31, 2026 21:04
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 001ee15 to 342ce12 Compare August 31, 2026 21:13
@Caball009
Caball009 marked this pull request as ready for review August 31, 2026 21:14
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 342ce12

@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 342ce12 to a3b7cb8 Compare August 31, 2026 21:22
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from a3b7cb8 to a34726c Compare September 1, 2026 14:09
Comment thread Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp
@OmarAglan

Copy link
Copy Markdown

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: a34726cff5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 3d34f16 to ddc9b76 Compare September 1, 2026 19:17
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from ddc9b76 to 8545c86 Compare September 1, 2026 19:20
@Caball009
Caball009 marked this pull request as draft September 1, 2026 19:21
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 8545c86 to 4faa5ea Compare September 1, 2026 19:23
@Caball009
Caball009 marked this pull request as ready for review September 1, 2026 19:24
@Caball009
Caball009 marked this pull request as draft September 1, 2026 19:26
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4faa5ea

@Caball009
Caball009 marked this pull request as ready for review September 1, 2026 19:27
@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from 4faa5ea to f7a763e Compare September 1, 2026 19:29
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4faa5ea

@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from f7a763e to d77b627 Compare September 1, 2026 19:36
Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/TunnelContain.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp
Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
}

if( m_tunnelCount == 0 )
m_needsFullHealTimeUpdate = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This only needs to be set true if m_tunnelIDs is modified.

@Caball009 Caball009 Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think it only needs to be set to true if !m_tunnelIDs.empty() after the erase operation. I'll change it to that.

Comment thread GeneralsMD/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp Outdated
}
else
{
m_tunnelCount = m_tunnelIDs.size();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe this should only be set on Xfer Load?

@Caball009 Caball009 Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Why is that?

Ah, we may not want to modify m_tunnelCount on saving. Is there a downside to doing it like this, though?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If everything goes well it essentially would be a no-op. But if the sizes are out of sync, then the Xfer Save silently repairs it, which is unexpected? ;-)

@Caball009 Caball009 Sep 2, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

True, it's unexpected, but it seems like a good thing to me :) If we keep it, I think I should add a comment, though. I'll just add an XFER_LOAD check, that seems better on second thought.

@Caball009
Caball009 force-pushed the Caball009/bugfix_on_tunnel_destroyed branch from d77b627 to 322dbfd Compare September 2, 2026 17:57
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 Crash This is a crash, very bad Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker ThisProject The issue was introduced by this project, or this task is specific to this project ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generals replay incompatibility

4 participants