Skip to content

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

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

tweak(Controlbar): Allow observer to view units and structures if not following player#3122
Mr-Sheerlock wants to merge 14 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:

  • 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

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

@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 (0) 📘 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 ✓ Resolved 🐞 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 ✓ Resolved 🐞 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 copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
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 Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
@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

if (showObserverInventory && m_observerLookAtPlayer == nullptr)
{
if (!isApparentControllingPlayerNeutral(obj)) {

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 return sits above the else if that restores CB_CONTEXT_OBSERVER_LIST, so a failed neutrality check leaves the previous container's inventory on screen. It's reachable for a defeated player who becomes an observer, though I don't think for a replay observer if that helps.

Maybe

Bool showObserverInventory = observerContain != nullptr
                     && observerContain->getContainMax() > 0
                     && m_observerLookAtPlayer == nullptr
                     && isApparentControllingPlayerNeutral(obj);
if (showObserverInventory)

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.

Fixed

showRallyPoint(exitPosition);

ContainModuleInterface* observerContain = obj ? obj->getContain() : nullptr;
Bool showObserverInventory = (observerContain != nullptr && observerContain->getContainMax() > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is including non-garrisonable containers intentional? If yes, the fixed 10-slot layout needs bounding (there's a bot comment on populateButtonProc). If not, matching the isGarrisonable() gate resolves both.

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.

No, I missed this case.

I will go with the bounding solution for generality.


// Enable the button
info->inventoryButtons[ info->buttonIndex ]->winEnable( TRUE );
info->inventoryButtons[ info->buttonIndex ]->winEnable( !info->self->m_isReadOnly );

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The DEBUG_ASSERTCRASH at line 68 is compiled out in release, so buttonIndex keeps incrementing past MAX_STRUCTURE_INVENTORY_BUTTONS (10).
An early return once buttonIndex >= MAX_STRUCTURE_INVENTORY_BUTTONS would make the limit enforced rather than asserted.

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.
Fixed.

Color m_buildUpClockColor;

Bool m_isObserverCommandBar; ///< If this is true, the command bar behaves greatly different
Bool m_isReadOnly; ///< If this is true, the command bar will not allow any commands to be issued

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

m_isReadOnly tracks m_isObserverCommandBar - both are set at the same six sites, always to the same value. Could we have the two inventory call sites use m_isObserverCommandBar directly (or isObserverControlBarOn()) and drop this member?

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.
Fixed

@xezon

xezon commented Sep 3, 2026

Copy link
Copy Markdown

Comments are left unaddressed. Needs push.

@Mr-Sheerlock

Copy link
Copy Markdown
Author

Will try to push asap

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 0d4cf8d to 1576210 Compare September 4, 2026 13:32
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown

Greptile Summary

This PR expands the observer control bar so observers can inspect selected units, structures, contained units, command sets, and production queues while rendering their controls as read-only.

  • Adds observer-aware context selection and fallback behavior.
  • Disables command, inventory, queue, evacuation, stop, and construction controls while observing.
  • Extracts neutral-player checks into reusable helpers.
  • Makes observer-state accessors const-correct.

Confidence Score: 2/5

The PR does not appear safe to merge because active-player construction controls can remain disabled after leaving observer mode, and live observers can still access private inventory and production information.

The changes since the previous review only add const qualification and do not address the outstanding findings. The reused Cancel Construction button is still disabled and retains its observer-only color state when control returns to an active player. The observer exemption also still applies to live multiplayer observers, allowing them to inspect contained units, command state, and production queues without an additional visibility restriction. The lower-impact persistent observer coloring on Evacuate and Stop also remains.

Files Needing Attention: Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp, Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp, Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameClient/ControlBar.h Declares observer-aware context helpers and makes observer-state accessors const-correct.
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Extends context evaluation to observer selections, although the previously reported live-observer information disclosure remains outstanding.
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp Populates observer-visible command and production controls in a disabled state.
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp Makes container contents read-only for observers, but the previously reported persistent observer color state remains.
Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp Disables cancellation while observing, but the previously reported failure to restore the reused button remains.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Selection changes] --> B{Observer control bar active?}
    B -- No --> C[Apply ownership and relationship checks]
    B -- Yes --> D{Following a player?}
    D -- Yes --> E[Show followed-player observer context]
    D -- No --> F[Evaluate selected object context]
    F --> G[Show inventory, commands, or construction details]
    G --> H[Populate controls in disabled read-only state]
    C --> I[Show normal player context]
Loading

Reviews (11): Last reviewed commit: "tweak(Controlbar): Relax neutrality cons..." | Re-trigger Greptile

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 1576210 to b8bc9c5 Compare September 4, 2026 13:49
@OmarAglan

Copy link
Copy Markdown

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8bc9c5851

ℹ️ 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 on lines +1484 to +1487
Bool showObserverInventory = observerContain != nullptr
&& observerContain->getContainMax() > 0
&& m_observerLookAtPlayer == nullptr
&& isApparentControllingPlayerNeutral(obj);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict free-observer inventories to replay playback

In a live multiplayer match, an observer who is not following a player can satisfy this condition for a civilian building occupied exclusively by undetected stealth units. GarrisonContain::getApparentControllingPlayer() deliberately reports the building's original neutral owner to non-allies in that case, so this check opens the inventory and reveals the hidden occupants' images and veterancy. Add an explicit replay/playback guard, or verify the actual controller before exposing the inventory, to preserve the stealth-garrison mechanic for live observers.

Useful? React with 👍 / 👎.

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 the only unit that triggers such behavior is Jarmen Kell (When it's the only unit garrisoned in building) whose place can be discovered using other cues.

You can't view the inventory of the building occupied by an enemy Jarmen

Otherwise all stealth units trigger building garrison apparent to other players too.

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.

Fixed

Comment on lines +1492 to +1493
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.

P2 Badge Preserve multi-slot occupancy in observer transport inventories

When a replay transport carries a passenger whose TransportSlotCount is greater than one, this routes it through populateStructureInventory(), which displays getContainMax() slots without subtracting getExtraSlotsInUse(). The existing transport path in doTransportInventoryUI() explicitly performs that subtraction, so the observer view now shows too many empty slots and misrepresents the transport's remaining capacity. Use the transport inventory accounting here or hide the corresponding extra slots.

Useful? React with 👍 / 👎.

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.

Fixed

@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 7219d2d to dde9ebc Compare September 4, 2026 19:47
Comment on lines +178 to +182
if(isObserverControlBarOn())
{
m_commandWindows[ EVACUATE_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
m_commandWindows[ STOP_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Observer color state persists

When replay observation sets WIN_STATUS_ALWAYS_COLOR on the Evacuate and Stop controls, the non-observer path never clears it. The same command windows are reused when switching to an active player, so disabled Evacuate and Stop controls keep the observer-specific colored rendering instead of their normal disabled appearance. This is a non-blocking visual inconsistency that can make their state less clear.

Suggested change
if(isObserverControlBarOn())
{
m_commandWindows[ EVACUATE_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
m_commandWindows[ STOP_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
}
if(isObserverControlBarOn())
{
m_commandWindows[ EVACUATE_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
m_commandWindows[ STOP_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
}
else
{
m_commandWindows[ EVACUATE_ID ]->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
m_commandWindows[ STOP_ID ]->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp
Line: 178-182

Comment:
**Observer color state persists**

When replay observation sets `WIN_STATUS_ALWAYS_COLOR` on the Evacuate and Stop controls, the non-observer path never clears it. The same command windows are reused when switching to an active player, so disabled Evacuate and Stop controls keep the observer-specific colored rendering instead of their normal disabled appearance. This is a non-blocking visual inconsistency that can make their state less clear.

```suggestion
	if(isObserverControlBarOn())
	{
		m_commandWindows[ EVACUATE_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
		m_commandWindows[ STOP_ID ]->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
	}
	else
	{
		m_commandWindows[ EVACUATE_ID ]->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
		m_commandWindows[ STOP_ID ]->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
	}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@Caball009

Copy link
Copy Markdown

Verified observers can't see any inventory (ally or enemy)

Does it matter if observers can see that? They can already see the number of pips if there are any occupants.

Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarStructureInventory.cpp Outdated
@xezon

xezon commented Sep 5, 2026

Copy link
Copy Markdown

Verified observers can't see any inventory (ally or enemy)

This begs the question whether it is ok that Observer in Multiplayer should behave different from Observer in Replay. It probably should not. If we want to restrict Observer capabilities in Multiplayer, then this likely will need to be a much broader feature that confines an observer to a team view.

Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp Outdated
Comment thread Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarCommand.cpp Outdated

showRallyPoint(exitPosition);

ContainModuleInterface* observerContain = obj ? obj->getContain() : 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.

I am surprised by this code block here. Why does it not go through the regular code path to see the inventories?

The issue with replicating all the UI stuff for Observer is that another layer of complexity is added, plus anything not covered here will remain unsupported, for example: CB_CONTEXT_OCL_TIMER, CB_CONTEXT_UNDER_CONSTRUCTION and whatever else.

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.

tried to address this issue in the latest push.

@Mr-Sheerlock

Copy link
Copy Markdown
Author

@Caball009 @xezon

I think it's up for vote. My personal opinion would be be on the more conservative side: to make it opaque for enemy observer in multiplayer to limit communication on voice channels during gameplay.

@xezon

xezon commented Sep 6, 2026

Copy link
Copy Markdown

I think it's up for vote. My personal opinion would be be on the more conservative side: to make it opaque for enemy observer in multiplayer to limit communication on voice channels during gameplay.

What we need is a real Observer mode for competitive play, which is a separate concern (unrelated to this change).

For the regular Observer, I do expect that it needs to behave the same in Replay and Match. For example we would also like a Live Streamer to benefit from the same new Observer abilities, right? Otherwise he could only present the full experience in Replay Mode.

return localPlayer->getRelationship(otherPlayer->getDefaultTeam()) == NEUTRAL;
}

Bool ControlBar::isControllingPlayerNeutral(const Object* obj) const

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I find these 2 isolated function quite a bit confusing by their name. They sound as if they do almost the same, but they also look substantially different. Do their name accurately represent what they do?

I am asking because if these portions intend to do the same thing, then the code can be streamlined more.

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 their names correspond to what they do.
In case of Apparent, it takes into consideration whether the stealth unit inside it wouldn't change the behaviour for an observing player (active and non-active)

Bool ControlBar::isControllingPlayerNeutral(const Object* obj) const
{
const Player* player = ThePlayerList->getLocalPlayer();
return player->getRelationship(obj->getTeam()) == NEUTRAL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is the different with getRelationship(otherPlayer->getDefaultTeam()) and getRelationship(obj->getTeam()) between the two function intentional? What is the difference between that (ignoring getApparentControllingPlayer)?

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.

No I just abstracted the both of the original implementations before the PR into a function it as is because I was using it elsewhere.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I see that, but I wonder if there is more to do. The way the functions differ is quite confusing. I wonder if isControllingPlayerNeutral can be a subset of isApparentControllingPlayerNeutral.

@xezon xezon Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Example:

Bool ControlBar::isApparentControllingPlayerNeutral(const Object* obj) const
{
	if (ContainModuleInterface* contain = obj->getContain()) 
	{
		Player* localPlayer = ThePlayerList->getLocalPlayer();
		if (const Player* otherPlayer = contain->getApparentControllingPlayer(localPlayer))
			return localPlayer->getRelationship(otherPlayer->getDefaultTeam()) == NEUTRAL;
	}

	return isControllingPlayerNeutral(obj);
}

Bool ControlBar::isControllingPlayerNeutral(const Object* obj) const 
{
	const Player* player = ThePlayerList->getLocalPlayer();
	return player->getRelationship(obj->getTeam()) == NEUTRAL; 
}

Not clear to me if identical. Maybe it is.

@Mr-Sheerlock

Copy link
Copy Markdown
Author

For the regular Observer, I do expect that it needs to behave the same in Replay and Match. For example we would also like a Live Streamer to benefit from the same new Observer abilities, right? Otherwise he could only present the full experience in Replay Mode.

Ok I will remove neutrality check on observers if so.

Comment on lines +90 to +93
if (!isControlEnabled()) {
win->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
win->winEnable(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.

P1 Cancel Button Stays Disabled

When control switches from replay-observer mode to an active player, this reused cancel-construction button remains disabled and retains WIN_STATUS_ALWAYS_COLOR. The observer branch sets both states, but the active-player path does not call winEnable(TRUE) or clear the status. As a result, selecting an under-construction object after the switch can leave Cancel Construction unusable.

Suggested change
if (!isControlEnabled()) {
win->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
win->winEnable(FALSE);
}
if (!isControlEnabled())
{
win->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
win->winEnable(FALSE);
}
else
{
win->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
win->winEnable(TRUE);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp
Line: 90-93

Comment:
**Cancel Button Stays Disabled**

When control switches from replay-observer mode to an active player, this reused cancel-construction button remains disabled and retains `WIN_STATUS_ALWAYS_COLOR`. The observer branch sets both states, but the active-player path does not call `winEnable(TRUE)` or clear the status. As a result, selecting an under-construction object after the switch can leave Cancel Construction unusable.

```suggestion
	if (!isControlEnabled())
	{
		win->winSetStatus(WIN_STATUS_ALWAYS_COLOR);
		win->winEnable(FALSE);
	}
	else
	{
		win->winClearStatus(WIN_STATUS_ALWAYS_COLOR);
		win->winEnable(TRUE);
	}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines 1917 to +1927
@@ -1927,16 +1924,16 @@ void ControlBar::evaluateContextUI()
}
else if( obj->getCommandSetString().isEmpty() == FALSE )
{

switchToContext( CB_CONTEXT_COMMAND, drawToEvaluateFor );
if (obj->isLocallyControlled() == TRUE || isObserverControlBarOn() || isControllingPlayerNeutral(obj))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Live Observers Reveal Inventories

The observer-mode exemption also applies to live multiplayer observers. When one selects an allied or enemy container, this condition opens its structure-inventory context, the update guard is bypassed, and the inventory UI displays each contained unit's image and veterancy without another visibility check. The same exemption exposes command and production-queue state, leaking player information that multiplayer observers should not be able to inspect.

How this was verified: Live observers enter observer-control-bar mode, which bypasses the ownership checks before the inventory iterator renders each contained object.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp
Line: 1917-1927

Comment:
**Live Observers Reveal Inventories**

The observer-mode exemption also applies to live multiplayer observers. When one selects an allied or enemy container, this condition opens its structure-inventory context, the update guard is bypassed, and the inventory UI displays each contained unit's image and veterancy without another visibility check. The same exemption exposes command and production-queue state, leaking player information that multiplayer observers should not be able to inspect.

**How this was verified:** Live observers enter observer-control-bar mode, which bypasses the ownership checks before the inventory iterator renders each contained object.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@Mr-Sheerlock Mr-Sheerlock changed the title tweak(Controlbar): Allow replay observer to view contained units if not following player tweak(Controlbar): Allow replay observer to view units and structures if not following player Sep 7, 2026
@Mr-Sheerlock Mr-Sheerlock changed the title tweak(Controlbar): Allow replay observer to view units and structures if not following player tweak(Controlbar): Allow observer to view units and structures if not following player Sep 7, 2026
Comment thread Core/GameEngine/Include/GameClient/ControlBar.h Outdated
@Mr-Sheerlock
Mr-Sheerlock force-pushed the add-observer-inventory branch from 5a726f9 to ff1024c Compare September 7, 2026 09:48
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.

5 participants