feat(chat): Campaign CH slice CH6c — window opacity + transparency setting

Retail's ChatInterface::SetOpacity (0x004F3120) fades the WHOLE composited
window surface with one alpha; UiRenderContext.ApplyAlpha already gated
DrawSprite/DrawRect/DrawFill (since 1da697ec, pre-CH6) but DrawStringDat and
DrawString still passed applyAlpha:false, so text stayed sharp over a
translucent window. Both now route through the same chokepoint.

RetailWindowOpacityController (new) subscribes to a new
RetailWindowManager.WindowRegistered event and drives every registered
window's live Opacity from keyboard-focus state, applied to EVERY window
(chat, floaties, vitals, toolbar, ...) rather than retail's ChatInterface-only
scope — register row AP-190, retiring the stale AP-40 "fixed 0.75, no focus
transition" row in the same commit.

Verified retail's shipped opacity defaults from the decomp (constructor
literals, no cdb needed): the base ChatInterface ctor sets
DefaultOpacity=0.5/ActiveOpacity=1.0, kept unmodified by the four floating
windows; gmMainChatUI's own ctor overrides the main window to 1.0/1.0
(always fully opaque). acdream ships one shared global default (0.5/1.0)
rather than replicating the per-class override — also AP-190. The linking
invariant (raising default above active drags active UP; lowering active
below default drags default DOWN — never a clamp) is ported verbatim as
ChatOpacityLink in AcDream.UI.Abstractions, shared by the live controller
and the new Settings -> Chat tab's two linked opacity sliders.

Persistence: ChatSettings.DefaultOpacity/ActiveOpacity round-trip through
SettingsStore; Save pushes both through IRuntimeSettingsTargets.SetChatOpacity
into the live controller, no restart required.

Rider (CH6a/b re-review): strengthened the grip-media regression guard past
a bare SpriteFile != 0 check — ChatLayoutConformanceTests now drives each
live grip through a real UiRenderContext/TextRenderer (backed by the
in-memory RecordingGpuDevice test double) and asserts the draw call chain
actually queued sprite geometry, via a new TextRenderer.DebugSpriteSegments
test-only accessor.

Full Release suite 12,459 passed / 4 skipped / 0 failed (baseline
12,420/4/0). No subagents, no client launches (session hard constraints);
pending the next connected user gate for visual confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 14:00:55 +02:00
parent ccab53d9a1
commit a819687cf0
23 changed files with 1174 additions and 38 deletions

File diff suppressed because one or more lines are too long

View file

@ -45,11 +45,14 @@ font instead of the unwired 15px debug fallback; bare `/help` and
`DoHelp` shape — two scroll entries in the right order, not one
acdream-invented blob. **CH6b (floating chat windows 1-4) landed
CODE-COMPLETE 2026-08-10** under this session's hard constraints (no
subagents, no client launches) — see its ledger row and Slices bullet;
CH6c (opacity) remains not started. Status stays CODE-COMPLETE pending
the next user gate round (still needed for CH6a's own visual
confirmation, CH6b's keybind/mirror/filter behavior, CH6c, round 3's
fixes, and a final in-client visual pass on everything fixed so far).
subagents, no client launches) — see its ledger row and Slices bullet.
**CH6c (opacity) landed CODE-COMPLETE 2026-08-10**, same constraints —
see its ledger row and Slices bullet; the full three-sub-slice CH6 window
shell is now CODE-COMPLETE. Status stays CODE-COMPLETE pending the next
user gate round (still needed for CH6a's own visual confirmation, CH6b's
keybind/mirror/filter behavior, CH6c's focus-driven fade and Settings
slider, round 3's fixes, and a final in-client visual pass on everything
fixed so far).
**Why now:** first track of the alpha-release program (chat is the most
visible daily surface for the friend-alpha). User-directed 2026-08-09.
@ -133,8 +136,9 @@ implementer per slice against a pinned contract (per
commands implemented family-by-family.
- **CH5 — closeout.** Register sweep, ledger flip, ISSUES updates,
in-client test script for the user gate.
- **CH6 — chat-window shell parity (filed 2026-08-09 at user gate round
1; research complete:
- **CH6 — chat-window shell parity — all three sub-slices CODE-COMPLETE
2026-08-10, pending the connected user gate (filed 2026-08-09 at user
gate round 1; research complete:
`docs/research/2026-08-09-chat-retail-window-shell.md`).** Three
sub-slices:
- **CH6a — correct main-window import + resize.** Swap the wrong
@ -222,6 +226,40 @@ implementer per slice against a pinned contract (per
the retail per-window option array `0x1000008C` is stored by ACE as
an opaque byte[] it never parses, so the wire format is its own
deferred slice.
**CODE-COMPLETE 2026-08-10 (hard constraint: no subagents, no client
launches).** The sprite/rect chokepoint (`UiRenderContext.ApplyAlpha`) had
quietly existed since `1da697ec`, well before CH6 — the gap was narrower
than the plan assumed: `DrawStringDat`/`DrawString` still passed
`applyAlpha: false`, so text stayed sharp over a translucent window;
CH6c routes both through `ApplyAlpha` too, matching retail's
whole-surface `SetOpacity` fade. `RetailWindowOpacityController`
(new, `src/AcDream.App/UI/RetailWindowOpacityController.cs`) subscribes to
a new `RetailWindowManager.WindowRegistered` event and drives every
registered window's live `Opacity` from keyboard-focus state — deliberately
EVERY window (chat, floaties, vitals, toolbar, ...), not just retail's
`ChatInterface`-scoped mechanism (register row AP-190, retiring the stale
AP-40 "fixed 0.75, no focus transition" row in the same commit). Verified
retail defaults from the decomp (constructor-literal, no cdb needed):
the base `ChatInterface` ctor sets DefaultOpacity=0.5/ActiveOpacity=1.0,
which the four floating windows keep unmodified, but `gmMainChatUI`'s own
ctor overrides the main window to 1.0/1.0 (always fully opaque); acdream
ships ONE shared global default (the base 0.5/1.0) rather than
replicating the per-class override — also AP-190. The linking invariant
(raising default above active drags active UP; lowering active below
default drags default DOWN — never a clamp) is decomp-verified and ported
as `ChatOpacityLink` in `AcDream.UI.Abstractions`, shared by the live
controller and the Settings → Chat tab's two new linked sliders
(`SettingsPanel.RenderChatTab`). Persistence: `ChatSettings.DefaultOpacity`/
`ActiveOpacity` round-trip through `SettingsStore`; Save pushes both
through `IRuntimeSettingsTargets.SetChatOpacity` into the live controller —
no restart. Rider (CH6a/b re-review): strengthened the grip-media
regression guard past a bare `SpriteFile != 0` check —
`ChatLayoutConformanceTests` now drives each live grip through a real
`UiRenderContext`/`TextRenderer` (backed by the in-memory
`RecordingGpuDevice` test double) and asserts the draw call chain actually
queued sprite geometry, via a new `TextRenderer.DebugSpriteSegments`
test-only accessor. Full Release suite 12,459 passed / 4 skipped / 0
failed (baseline 12,420/4/0).
## Gates
@ -249,7 +287,7 @@ implementer per slice against a pinned contract (per
| Jump-in-air root cause (round-2 item 1, resolved) | `a5a7eb4f` | Runtime tests 1,323/0 | — | round-3 probe evidence pinpointed a missing `OnInterfaceText` wire on the production controller-commit path (`RuntimeLocalPlayerMovementState.CommitRuntimeOwnedController`); FIXED, regression test added |
| User gate round 3 | `98de4f5a` | Debug (all projects): 12,329 passed / 4 skipped / 1 failed (pre-existing #351 Debug-only flake — reproduces identically on the pristine pre-round-3 commit, not a regression); Release (every project reachable while a live `AcDream.App.exe` client — PID 15064, must not be killed per project policy — holds its own Release binaries locked, blocking `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` specifically): `AcDream.UI.Abstractions.Tests` (the layer this round's `/help` fix lives in) 867/867, plus `Core.Net.Tests` 823/823, `Runtime.Tests` 1,323/1,323, `Content.Tests` 130/130, `Headless.Tests` 89/89, `Bake.Tests` 15/15, `Cli.Tests` 4/4 — all 0 failed | — | findings (a)-(c) fixed this commit — SpewBox flush-top + retail dat font, `/help`/`/help death` exact retail print sequence (see "User gate — round 3" below) |
| CH6b floating windows 14 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions |
| CH6c opacity | not started | — | — | not started |
| CH6c opacity | `PENDING-SHA` | 12,459 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) |
### CH4 closeout (2026-08-09)
@ -590,7 +628,7 @@ round's scope and filed as slice CH6.
| G | The chat input line overflows the window's right edge when the window is resized. | **FIXED this SHA.** The input field's right edge no longer holds a fixed absolute pixel position across a resize (retail edge-mode 0's "frozen at current" fallback, or the `AnchorEdges` default with no `Right` bit) — `ChatWindowController.Bind` now upgrades it to retail edge-mode 1 (`UiLayoutPolicy`) or the equivalent `AnchorEdges.Right` stretch, so the right edge tracks every resize instead of only the bind-time/channel-change recompute. |
| H | Extra/duplicate chat windows appear on number keys 1/2/3/4. | **STILL CH6b** (not this commit) — retail's real floating windows 14 (`0x2100005B` ×4) and their `ToggleFloatingChatWindow1..4` keybinds are a separate slice; CH6a only fixed the shell (import/resize) of the main window. |
| I | Resizing the chat window only works from one corner, not every corner. | **FIXED at CH6a `1fd51543`.** Root cause: the main window imported the WRONG LayoutDesc (`0x21000006`, an unrelated layout whose root/resize-bar appear nowhere in the EoR gameplay UI) instead of retail's real `0x2100006F`; every symptom (crop hacks, the dropped resize bar, the one-corner-only resize) was downstream of that. The swap + a new `LayoutImporter` case for element type 9 (`UIElement_Resizebar`, `UiResizeGrip`) + `UiRoot` grip-priority hit-testing now resize from all 4 edges and all 4 corners, while the top strip (a Type-2 Dragbar, not a grip) correctly remains a move-only affordance. |
| J | The chat window has transparency issues / visual artifacts, and the user wants a transparency setting eventually. | **Artifacts FIXED at CH6a `1fd51543`** — the reported visual glitches were downstream of importing the wrong LayoutDesc (stray unparented siblings, the hand-cropped content width, the 9px patch); all retired with the correct import, and the two hard-coded translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. **Real opacity (the future transparency SETTING) remains CH6c** — `UiRenderContext.AlphaMod` still has no draw-path consumer. |
| J | The chat window has transparency issues / visual artifacts, and the user wants a transparency setting eventually. | **Artifacts FIXED at CH6a `1fd51543`** — the reported visual glitches were downstream of importing the wrong LayoutDesc (stray unparented siblings, the hand-cropped content width, the 9px patch); all retired with the correct import, and the two hard-coded translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. **Real opacity + the transparency SETTING landed at CH6c** — `UiRenderContext` now applies window alpha to sprite, rect, AND text draws; `RetailWindowOpacityController` drives every window's opacity from keyboard focus; the Settings → Chat tab carries two linked sliders. Pending the next connected round for visual confirmation. |
Findings AG's evidence: this commit's diff + the new/updated tests in
`tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs`,

View file

@ -463,12 +463,54 @@ and application is one call on the window's own render surface —
text — with one alpha, not a per-widget background tint.** This is the shape the
future acdream user setting should take.
UNVERIFIED: retail's *shipped default values* for the two sliders. They are not
constants in code (the only in-code fallback is 1.0f); they arrive from the
server's `GameplayOptions` blob or from the options page's authored slider
defaults. Cheapest resolution: one cdb breakpoint on
`ChatInterface::SetDefaultOpacity` at login and read `st(0)` — or dump the
slider template defaults from the options LayoutDesc (`0x2100002B`).
**RESOLVED at Campaign CH slice CH6c (2026-08-10) by static decomp, not cdb —
the values are constructor-literal, so no live attach was needed.** Retail's
shipped defaults are PER WINDOW CLASS, not one constant:
```
004f4550 ChatInterface::ChatInterface(this, arg2, arg3) // BASE ctor
004f459f this->m_fDefaultOpacity = 0.5f;
004f45a5 this->m_fCurrentOpacity = 0.5f;
004f45ab this->m_fActiveOpacity = 1f;
004cd0f0 gmMainChatUI::gmMainChatUI(this, arg2, arg3) // derived, calls base first
004cd0ff ChatInterface::ChatInterface(this, arg2, arg3);
004cd148 this->m_fDefaultOpacity = 1f; // OVERRIDES base
004cd14e this->m_fCurrentOpacity = 1f;
// m_fActiveOpacity left at base's 1f
004ce2c0 gmFloatyChatUI::Create(arg1, arg2) // the 4 floating windows
004ce2e0 ChatInterface::ChatInterface(eax, arg1, arg2); // NO override — keeps base 0.5/1.0
```
`gmFloatyMainChatUI` (element class `0x10000050`, the concrete class actually
instantiated for the retail main chat window — its `DynamicCast` accepts both
`0x10000050` and `0x10000041`) calls `gmMainChatUI::gmMainChatUI` as its own
base constructor (`0x004D22B0`) and adds no opacity override of its own, so it
inherits `gmMainChatUI`'s 1.0/1.0.
**So: the main chat window is ALWAYS FULLY OPAQUE in both states (Default=1.0,
Active=1.0) unless a saved `GameplayOptions` value overrides it via
`UpdateFromPlayerModule`; the four floating windows default to
Default=0.5/Active=1.0 (translucent when idle, opaque once the chat entry has
focus).** These are IN-MEMORY CONSTRUCTED starting values for each window
INSTANCE's fields — they get overwritten the moment `UpdateFromPlayerModule`
successfully reads a persisted `0x10000080`/`0x10000081` value from
`PlayerModule::InqOption` (the SAME global option for every window instance),
which is why the two options are still correctly described as GLOBAL rather
than per-window: only a NEVER-SAVED option (a fresh character, nothing in the
`GameplayOptions` blob yet) lets the per-class constructed defaults show
through, and even then only until the user's first slider drag pushes one
shared value into every live window via `RecvNotice_GameplayOptionChanged`.
acdream ships ONE shared global default — the base `ChatInterface` value,
0.5/1.0 — applied uniformly to every window including the main chat window
(register row AP-190 in `docs/architecture/retail-divergence-register.md`),
rather than replicating `gmMainChatUI`'s per-class 1.0/1.0 override. The
linking invariant (active >= default, restored by dragging the OTHER value —
verified from `SetDefaultOpacity`/`SetActiveOpacity`'s own bodies, matching
the summary already recorded above) is ported exactly regardless of which
default seeds it.
---
@ -635,7 +677,26 @@ per-grip bool properties `0x2A`/`0x2B`/`0x2C`/`0x2D`. Additionally,
for `Resizebar` in `src/AcDream.App/UI` returns nothing — so authored grips
would be imported as inert sprites today.
**G3 — Window opacity is completely inert.**
**G3 — Window opacity is completely inert. CLOSED at Campaign CH slice CH6c
(2026-08-10).** `UiRenderContext.ApplyAlpha` already gated `DrawRect`/
`DrawFill`/`DrawSprite` before this slice (added back at `1da697ec`, well
before CH6 — the "zero consumers" framing below described the PUBLIC
`AlphaMod` property specifically, not the private `_alpha`/`ApplyAlpha` pair
those three draws already used); the actual gap was narrower than originally
scoped: (a) `DrawStringDat`/`DrawString` still passed `applyAlpha: false`, so
TEXT stayed sharp over a translucent window against retail's whole-surface
`SetOpacity` semantics — CH6c fixed both; (b) nothing ever SET a window's
`Opacity` below its 1f default, since `RetailUiRuntime.MountChat` deliberately
left it at 1f pending this slice. CH6c added
`RetailWindowOpacityController` (`src/AcDream.App/UI/RetailWindowOpacityController.cs`),
which drives every `RetailWindowManager`-registered window's live `Opacity`
from keyboard-focus state and the two retail-linked Default/Active floats,
now exposed as a Settings → Chat tab transparency slider pair
(`SettingsPanel.RenderChatTab`). See the verified defaults + linking
behavior above (§3) and register row AP-190. The original paragraph below is
kept verbatim as the historical record of what CH6a/b actually shipped —
do not re-run this investigation.
`RetailWindowFrame.cs:157` sets `outerFrame.Opacity`, and
`UiElement.DrawSelfAndChildren` (`src/AcDream.App/UI/UiElement.cs:465`) and
`DrawOverlays` (`:513`) push it onto `UiRenderContext`'s alpha stack. But
@ -743,8 +804,16 @@ run alongside either.
- **Retires** any row asserting "chat window is not resizable from the top" once
CH6a lands.
- **New row** if CH6b keeps text out of the window alpha (retail's
`ChatInterface::SetOpacity @0x004F3120` fades the whole surface).
- **CLOSED at CH6c**: text now respects window alpha — `DrawStringDat`/
`DrawString` route through `ApplyAlpha` exactly like `DrawSprite`/`DrawRect`/
`DrawFill`, matching `ChatInterface::SetOpacity`'s whole-surface fade. No
divergence row needed for this part.
- **New row AP-190** (CH6c): acdream applies the two opacity options to EVERY
`RetailWindowManager` window (chat + floaties + vitals + toolbar +
everything else), where retail's mechanism only ever runs from
`ChatInterface`-derived windows; and ships ONE shared default (the base
`ChatInterface` ctor's 0.5/1.0) rather than `gmMainChatUI`'s per-class
1.0/1.0 override for the main window specifically.
- **New row** for local-only chat-window persistence until CH6f, since retail
stores this server-side in `GameplayOptions`.