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

@ -45,9 +45,12 @@ public sealed class UiRenderContext
private readonly System.Collections.Generic.List<UiClipRect?> _clipStack = new();
private UiClipRect? _clip;
// Alpha (opacity) stack — a window pushes its Opacity so its background/sprite
// draws fade (retail's translucent-chat effect). Text draws bypass this (they go
// straight to TextRenderer), so text stays sharp over a translucent background.
// Alpha (opacity) stack — a window pushes its Opacity so EVERY draw under it
// (sprite, rect/fill, AND text) fades together. Retail's ChatInterface::SetOpacity
// (0x004F3120) sets one alpha on the window's whole composited render surface —
// chrome, background, and glyphs all fade as one unit, not text-stays-sharp over a
// translucent panel. Campaign CH slice CH6c ported this: DrawStringDat and
// DrawString both route through ApplyAlpha exactly like DrawSprite/DrawRect/DrawFill.
private readonly System.Collections.Generic.List<float> _alphaStack = new();
private float _alpha = 1f;
@ -188,14 +191,15 @@ public sealed class UiRenderContext
if (f is null) return;
float screenX = _current.X + x;
float screenY = _current.Y + y;
Vector4 alphaColor = ApplyAlpha(color);
if (_clip is { } clip)
{
TextRenderer.DrawStringClipped(
f, text, screenX, screenY, color,
f, text, screenX, screenY, alphaColor,
clip.Left, clip.Top, clip.Right, clip.Bottom);
return;
}
TextRenderer.DrawString(f, text, screenX, screenY, color);
TextRenderer.DrawString(f, text, screenX, screenY, alphaColor);
}
/// <summary>
@ -262,6 +266,9 @@ public sealed class UiRenderContext
// Background (outline) atlas pass, tinted black — drawn behind. Gated by
// `outline` (retail's per-element m_bitField & 0x10); off by default so UI
// text is crisp fill-only and free of the grey halo over solid panels.
// Both passes route through ApplyAlpha (applyAlpha: true) so a window's
// opacity fades glyphs exactly like its chrome/background sprites — retail's
// ChatInterface::SetOpacity (0x004F3120) fades the whole composited surface.
if (outline && font.BackgroundTexture != 0)
{
var (bu0, bv0, bu1, bv1) = AtlasUv(
@ -269,7 +276,7 @@ public sealed class UiRenderContext
font.BackgroundWidth, font.BackgroundHeight);
DrawSpriteAbsolute(
font.BackgroundTexture, gx, gy, gw, gh,
bu0, bv0, bu1, bv1, outlineTint, applyAlpha: false);
bu0, bv0, bu1, bv1, outlineTint, applyAlpha: true);
}
// Foreground (fill) atlas pass, tinted with the requested color.
@ -278,7 +285,7 @@ public sealed class UiRenderContext
font.ForegroundWidth, font.ForegroundHeight);
DrawSpriteAbsolute(
font.ForegroundTexture, gx, gy, gw, gh,
fu0, fv0, fu1, fv1, color, applyAlpha: false);
fu0, fv0, fu1, fv1, color, applyAlpha: true);
}
pen += UiDatFont.GlyphAdvance(g);