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

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