Skip to content

tweak(Controlbar): Allow replay observer to view contained units if not following player - #3122

Open
Mr-Sheerlock wants to merge 3 commits into
TheSuperHackers:mainfrom
Mr-Sheerlock:add-observer-inventory
Open

tweak(Controlbar): Allow replay observer to view contained units if not following player#3122
Mr-Sheerlock wants to merge 3 commits into
TheSuperHackers:mainfrom
Mr-Sheerlock:add-observer-inventory

Conversation

@Mr-Sheerlock

@Mr-Sheerlock Mr-Sheerlock commented Aug 13, 2026

Copy link
Copy Markdown

This PR is an enhancement/suggestion for observer Controlbar functionality. It allows an observer to view exact units contained by a building/unit.

I added the m_observerLookAtPlayer null check because otherwise when following a player and the player selects a containing building/unit the UI doesn't support a way to stop following the player.

Verification:
1: For replays:

  • Checked civilian buildings, technicals/battle busses, china bunkers, GLA multiple tunnels and palace in ZH and Generals.
  • Verified Evacuation & Stop Commands don't execute when pressing on any unit.
  • Tested buttons and hotkeys from observer point of view
  • Tested switching from observer to skirmish player

2: For multiplayer

  • Verified observers can't see any inventory (ally or enemy)
  • Verified no regressions regarding seeing an enemy team inventory as an active player

Potential issues:

  • Some buildings like barracks show slots despite not being garrison-able (probably because units can enter them anyway)

and also a limitation I just thought about: would be lovely if we can select a garrisoned unit and view its own garrisoned units too.

Edit:
Added more tests in the verification.

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

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

Copy link
Copy Markdown

PR Summary by Qodo

Observer ControlBar: read-only structure inventory view when not following a player

✨ Enhancement 🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Enable observers to open structure inventory UI for containers when not following a player.
• Make observer command bar read-only, disabling inventory selection and evacuate/stop actions.
• Prevent multiplayer inventory peeking via neutrality check on apparent controlling player.
Diagram

graph TD
  cb["Observer ControlBar update"] --> obj[("Selected object")] --> has{"Contain slots?"} -->|"No"| olist["Observer list"]
  has -->|"Yes"| allow{"Not following & neutral?"} -->|"Yes"| inv["Inventory view (read-only)"]
  allow -->|"No"| deny["No inventory (return)"]

  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _data[("Data")] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Explicit replay-only gate (game-mode based)
  • ➕ Simple mental model: observers can inspect inventory only in replay/single-player contexts
  • ➕ Avoids relying on relationship semantics that may vary by multiplayer observer implementation
  • ➖ Requires correct game-mode detection across replay/online/LAN cases
  • ➖ Less flexible if future spectator modes should allow partial visibility
2. Dedicated observer-inventory context (separate from Structure Inventory)
  • ➕ Cleaner separation: no risk of accidentally re-enabling commands in a shared context
  • ➕ Can tailor UI affordances (tooltips, highlighting) specifically for observers
  • ➖ More UI code and additional context wiring to maintain
  • ➖ Potential duplication of existing inventory rendering logic

Recommendation: Current approach (reuse Structure Inventory with a read-only flag + neutrality gating) is a good tradeoff: minimal UI duplication while preventing command execution. During review, double-check that the early-return on neutrality failure cannot leave a stale inventory context visible, and confirm all observer-entry paths set m_isReadOnly consistently (e.g., any observer scheme set via template vs player).

Files changed (3) +48 / -16

Enhancement (2) +46 / -14
ControlBar.hAdd read-only observer state and neutrality-check helper declaration +2/-0

Add read-only observer state and neutrality-check helper declaration

• Introduces a new helper API to evaluate neutrality against the apparent controlling player. Adds a ControlBar read-only flag used to prevent observers from issuing commands.

Core/GameEngine/Include/GameClient/ControlBar.h

ControlBar.cppObserver update flow now supports read-only inventory context with neutrality gating +44/-14

Observer update flow now supports read-only inventory context with neutrality gating

• Adds observer-mode logic to switch into structure-inventory context when selecting a container and not following a player. Centralizes relationship checks in isApparentControllingPlayerNeutral(), and marks observer control bars as read-only when loading observer schemes.

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp

Bug fix (1) +2 / -2
ControlBarStructureInventory.cppDisable inventory interaction and evacuate/stop commands when control bar is read-only +2/-2

Disable inventory interaction and evacuate/stop commands when control bar is read-only

• Gates inventory button enablement behind m_isReadOnly to prevent observers from selecting/issuing exit actions. Ensures Evacuate/Stop are only enabled when not read-only and the container is non-empty.

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Read-only state not reset ✓ Resolved 🐞 Bug ≡ Correctness
Description
m_isReadOnly is set to TRUE when switching to observer command bar via
setControlBarSchemeByPlayer(), but it is never set back to FALSE when switching back to an active
player, and it is never set to TRUE when observer mode is entered via
setControlBarSchemeByPlayerTemplate(). This can leave active players unable to use
structure-inventory actions (exit/evacuate/stop) or leave observer-template users with enabled
inventory controls.
Code

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[R2799-2802]

if( !p->isPlayerActive() )
{
  m_isObserverCommandBar = TRUE;
+		m_isReadOnly = TRUE;
Evidence
The PR makes inventory button enabling depend on m_isReadOnly, but m_isReadOnly is only set TRUE in
one observer-entry path and not cleared on exit or set in the observer-template path used for
inactive/defeated players, so it can be incorrect depending on the transition path.

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[2787-2831]
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[2845-2874]
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[63-90]
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[168-177]
Core/GameEngine/Source/Common/GameUtility.cpp[102-111]
Generals/Code/GameEngine/Source/Common/RTS/Player.cpp[1987-1997]

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

## Issue description
`m_isReadOnly` is introduced to disable inventory commands for observers, but it is only set to `TRUE` in `setControlBarSchemeByPlayer()` when the player is inactive. It is never cleared in the active-player path, and the observer-template path (`setControlBarSchemeByPlayerTemplate`) never sets it at all.
This leaves the flag in a stale/incorrect state depending on how observer mode is entered/exited.
## Issue Context
- Active/inactive transitions happen without necessarily calling `ControlBar::reset()` (e.g. `rts::changeLocalPlayer`).
- Defeated/inactive players explicitly use `setControlBarSchemeByPlayerTemplate(FactionObserver)`.
- Structure inventory buttons/evacuate/stop are now enabled/disabled based on `m_isReadOnly`.
## Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[2787-2831]
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[2833-2876]
## Suggested fix
1. In `setControlBarSchemeByPlayer(Player* p)`, explicitly set `m_isReadOnly = TRUE` for inactive players and `m_isReadOnly = FALSE` for active players.
2. In `setControlBarSchemeByPlayerTemplate(const PlayerTemplate* pt)`, set `m_isReadOnly = TRUE` when `pt` is the observer template, and set `m_isReadOnly = FALSE` otherwise.
3. (Optional) Consider making `m_isReadOnly` derived from `m_isObserverCommandBar` (or consolidating state) to prevent future drift.

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


2. Inventory slots overflow 🐞 Bug ☼ Reliability
Description
ControlBar::update() now switches observers into CB_CONTEXT_STRUCTURE_INVENTORY for any selected
object with getContainMax() > 0, but the structure inventory UI only supports 10 occupant slots. If
a container ever has >10 contained objects (e.g., tunnel networks when MaxTunnelCapacity is
configured above 10), populateStructureInventory() will call populateButtonProc() past the supported
slot count, tripping the MAX_STRUCTURE_INVENTORY_BUTTONS assert and/or overwriting non-inventory
buttons.
Code

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[R1483-1486]

+		ContainModuleInterface* observerContain = obj ? obj->getContain() : nullptr;
+		Bool showObserverInventory = (observerContain != nullptr && observerContain->getContainMax() > 0);
+
+		if (showObserverInventory && m_observerLookAtPlayer == nullptr)
Evidence
The new observer routing uses only getContainMax() > 0 to enter structure inventory, but the
structure inventory implementation asserts that it can only populate 10 occupant buttons and still
iterates over all contained objects. Tunnel networks can be configured to have capacities above 10
via GlobalData, making this path reachable for observers.

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1451-1499]
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[63-91]
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[179-196]
Generals/Code/GameEngine/Source/Common/RTS/TunnelTracker.cpp[107-112]
Generals/Code/GameEngine/Source/Common/GlobalData.cpp[395-400]

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

## Issue description
Observer mode now routes any selectable container (ContainMax > 0) into `CB_CONTEXT_STRUCTURE_INVENTORY`. The structure inventory UI is hard-limited to `MAX_STRUCTURE_INVENTORY_BUTTONS` (10). If `iterateContained()` yields more than 10 occupants, `populateButtonProc()` hits its `DEBUG_ASSERTCRASH` (and in non-assert builds can start repurposing the Stop/Evacuate buttons and potentially go beyond UI expectations).
### Issue Context
- `ControlBar::update()` (observer branch) uses only `getContainMax() > 0` as the gate.
- `populateStructureInventory()` iterates *all* contained objects and calls `populateButtonProc()`.
- `populateButtonProc()` asserts `buttonIndex < MAX_STRUCTURE_INVENTORY_BUTTONS`.
- Tunnel network capacity is configurable via `GlobalData::m_maxTunnelCapacity` (INI: `MaxTunnelCapacity`), so it can exceed 10.
### Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1483-1496]
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[63-90]
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[179-196]
### What to change
Implement *one* of these safe guards (preferably both A and B):
1. **A (UI-level hardening):** In `populateButtonProc()`, if `buttonIndex >= MAX_STRUCTURE_INVENTORY_BUTTONS`, return early (do not write into `m_containData` / do not enable controls). This prevents asserts/crashes and prevents Stop/Evacuate slots from being repurposed.
2. **B (observer routing guard):** In observer `update()`, only route to `CB_CONTEXT_STRUCTURE_INVENTORY` when `observerContain->getContainCount() <= MAX_STRUCTURE_INVENTORY_BUTTONS` (or clamp display to 10 with a clear rule). If count exceeds, fall back to `CB_CONTEXT_OBSERVER_LIST` or add paging/scrolling support.
Include an explicit comment explaining the 10-slot UI limitation so future changes to tunnel capacity don’t reintroduce the problem.

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



Remediation recommended

3. Observer inventory early return 🐞 Bug ≡ Correctness
Description
In observer-mode ControlBar::update(), selecting a container that fails
isApparentControllingPlayerNeutral(obj) returns immediately, bypassing the fallback that switches
the UI back to CB_CONTEXT_OBSERVER_LIST. This can leave stale structure-inventory UI visible after
selecting a disallowed container.
Code

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[R1488-1492]

+		if (showObserverInventory && m_observerLookAtPlayer == nullptr)
+		{
+			if (!isApparentControllingPlayerNeutral(obj)) {
+				return;
+			}
Evidence
The new early return is placed inside the observer-only update path before the existing
observer-list fallback branch, so a failed neutral check can prevent the context from being
restored.

Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1453-1504]

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

## Issue description
Observer-mode `ControlBar::update()` returns early when a selected container is not neutral. That return occurs before the code that restores `CB_CONTEXT_OBSERVER_LIST`, so the control bar can remain in a previous context (e.g., structure inventory) even though the current selection is disallowed.
## Issue Context
This is in the `if (m_isObserverCommandBar)` update path and only triggers for selected objects with a contain module.
## Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1485-1502]
## Suggested fix
Replace the early `return` with a controlled fallback:
- Either switch to `CB_CONTEXT_OBSERVER_LIST` (or `CB_CONTEXT_NONE`) before returning, or
- Restructure the logic so the existing `else if (m_currContext != CB_CONTEXT_OBSERVER_LIST)` branch remains reachable when the neutral check fails.

ⓘ 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 tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1483 to +1486
ContainModuleInterface* observerContain = obj ? obj->getContain() : nullptr;
Bool showObserverInventory = (observerContain != nullptr && observerContain->getContainMax() > 0);

if (showObserverInventory && m_observerLookAtPlayer == nullptr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Inventory slots overflow 🐞 Bug ☼ Reliability

ControlBar::update() now switches observers into CB_CONTEXT_STRUCTURE_INVENTORY for any selected
object with getContainMax() > 0, but the structure inventory UI only supports 10 occupant slots. If
a container ever has >10 contained objects (e.g., tunnel networks when MaxTunnelCapacity is
configured above 10), populateStructureInventory() will call populateButtonProc() past the supported
slot count, tripping the MAX_STRUCTURE_INVENTORY_BUTTONS assert and/or overwriting non-inventory
buttons.
Agent Prompt
### Issue description
Observer mode now routes any selectable container (ContainMax > 0) into `CB_CONTEXT_STRUCTURE_INVENTORY`. The structure inventory UI is hard-limited to `MAX_STRUCTURE_INVENTORY_BUTTONS` (10). If `iterateContained()` yields more than 10 occupants, `populateButtonProc()` hits its `DEBUG_ASSERTCRASH` (and in non-assert builds can start repurposing the Stop/Evacuate buttons and potentially go beyond UI expectations).

### Issue Context
- `ControlBar::update()` (observer branch) uses only `getContainMax() > 0` as the gate.
- `populateStructureInventory()` iterates *all* contained objects and calls `populateButtonProc()`.
- `populateButtonProc()` asserts `buttonIndex < MAX_STRUCTURE_INVENTORY_BUTTONS`.
- Tunnel network capacity is configurable via `GlobalData::m_maxTunnelCapacity` (INI: `MaxTunnelCapacity`), so it can exceed 10.

### Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1483-1496]
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[63-90]
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp[179-196]

### What to change
Implement *one* of these safe guards (preferably both A and B):
1. **A (UI-level hardening):** In `populateButtonProc()`, if `buttonIndex >= MAX_STRUCTURE_INVENTORY_BUTTONS`, return early (do not write into `m_containData` / do not enable controls). This prevents asserts/crashes and prevents Stop/Evacuate slots from being repurposed.
2. **B (observer routing guard):** In observer `update()`, only route to `CB_CONTEXT_STRUCTURE_INVENTORY` when `observerContain->getContainCount() <= MAX_STRUCTURE_INVENTORY_BUTTONS` (or clamp display to 10 with a clear rule). If count exceeds, fall back to `CB_CONTEXT_OBSERVER_LIST` or add paging/scrolling support.

Include an explicit comment explaining the 10-slot UI limitation so future changes to tunnel capacity don’t reintroduce the problem.

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

if (showObserverInventory && m_observerLookAtPlayer == nullptr)
{
if (m_currContext != CB_CONTEXT_STRUCTURE_INVENTORY || m_currentSelectedDrawable != drawToEvaluateFor)
switchToContext(CB_CONTEXT_STRUCTURE_INVENTORY, drawToEvaluateFor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could the observer inventory be made read-only at the UI boundary? populateStructureInventory() enables the occupant, Evacuate, and Stop buttons, and assigning a real m_currentSelectedDrawable lets clicks reach processCommandUI() and emit MSG_EXIT, MSG_EVACUATE, or MSG_DO_STOP.
To be fair, this doesn't currently do anything, but coincidentally so - would be good to make that intentional. Qodo also commented about this

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.

Thank you for the review. After the new push, none of the buttons are enabled or can be pressed from the observer's POV.

@xezon

xezon commented Aug 15, 2026

Copy link
Copy Markdown

The buttons should not be clickable for an observer / non-owning player.

Please fix other issues when there are.

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from f3f0ec5 to cdd3316 Compare August 17, 2026 04:03
@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from cdd3316 to fd01ff8 Compare August 17, 2026 11:25
@Mr-Sheerlock
Mr-Sheerlock marked this pull request as draft August 17, 2026 19:33
@Mr-Sheerlock
Mr-Sheerlock marked this pull request as ready for review August 17, 2026 23:00
@Mr-Sheerlock Mr-Sheerlock changed the title tweak(Controlbar): Allow observer to view contained units if not following player tweak(Controlbar): Allow replay observer to view contained units if not following player Aug 17, 2026
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp
Comment on lines +1488 to +1492
if (showObserverInventory && m_observerLookAtPlayer == nullptr)
{
if (!isApparentControllingPlayerNeutral(obj)) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Observer inventory early return 🐞 Bug ≡ Correctness

In observer-mode ControlBar::update(), selecting a container that fails
isApparentControllingPlayerNeutral(obj) returns immediately, bypassing the fallback that switches
the UI back to CB_CONTEXT_OBSERVER_LIST. This can leave stale structure-inventory UI visible after
selecting a disallowed container.
Agent Prompt
## Issue description
Observer-mode `ControlBar::update()` returns early when a selected container is not neutral. That return occurs before the code that restores `CB_CONTEXT_OBSERVER_LIST`, so the control bar can remain in a previous context (e.g., structure inventory) even though the current selection is disallowed.

## Issue Context
This is in the `if (m_isObserverCommandBar)` update path and only triggers for selected objects with a contain module.

## Fix Focus Areas
- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp[1485-1502]

## Suggested fix
Replace the early `return` with a controlled fallback:
- Either switch to `CB_CONTEXT_OBSERVER_LIST` (or `CB_CONTEXT_NONE`) before returning, or
- Restructure the logic so the existing `else if (m_currContext != CB_CONTEXT_OBSERVER_LIST)` branch remains reachable when the neutral check fails.

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9660d40

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 9660d40 to 97e5f11 Compare August 17, 2026 23:22
@Mr-Sheerlock

Copy link
Copy Markdown
Author

Okay I think the PR should be ready for review.
I added neutrality checks to prevent any observer to see inventory during multiplayer.
The replay observer can't click any inventory buttons.

@Mr-Sheerlock

Copy link
Copy Markdown
Author

However I also found one detail regarding the Jarmen Kell and didn't find it anywhere in the issues.
You can't view the inventory of the building occupied by an enemy Jarmen (although the mouse hover suggests it can be garrisoned) which can be a tell sign in multiplayer. Idk if this should be discussed in an issue.

else
{
switchToContext( CB_CONTEXT_NONE, nullptr );
m_isObserverCommandBar = FALSE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should clear m_isReadOnly here too right?

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.

Yes, thank you.
Fixed.

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 97e5f11 to 0d4cf8d Compare August 21, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants