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:
parent
ccab53d9a1
commit
a819687cf0
23 changed files with 1174 additions and 38 deletions
|
|
@ -0,0 +1,52 @@
|
|||
namespace AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's linked default/active window-opacity invariant: active >= default,
|
||||
/// ALWAYS — enforced by dragging the OTHER value, never by clamping the one being
|
||||
/// set. Verbatim port of <c>ChatInterface::SetDefaultOpacity @0x004F3BC0</c> /
|
||||
/// <c>SetActiveOpacity @0x004F3C40</c> (docs/research/2026-08-09-chat-retail-window-shell.md
|
||||
/// §3): raising the default ABOVE the current active value drags active UP to match;
|
||||
/// lowering the active value BELOW the current default drags default DOWN to match.
|
||||
/// Retail's options-page sliders share a <c>DualHash</c> link
|
||||
/// (<c>gmChatOptionsUI::InitOptions @0x0049FC60</c>) — this is why they visibly track
|
||||
/// each other while dragging.
|
||||
///
|
||||
/// <para>
|
||||
/// Shared by two independent consumers so both apply the identical link math:
|
||||
/// <c>RetailWindowOpacityController</c> (AcDream.App — the live per-window
|
||||
/// mechanism, mutating its own two float fields) and <see cref="SettingsPanel"/>'s
|
||||
/// Chat tab (the draft slider UI, mutating a <see cref="ChatSettings"/> record).
|
||||
/// Pure functions — no window/render/state dependency — so both layers can call
|
||||
/// them without violating the App→Abstractions dependency direction.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ChatOpacityLink
|
||||
{
|
||||
/// <summary>
|
||||
/// Port of <c>ChatInterface::SetDefaultOpacity @0x004F3BC0</c>: set the default
|
||||
/// (unfocused) opacity to <paramref name="newDefault"/>, dragging
|
||||
/// <paramref name="currentActive"/> UP if it would otherwise fall below the new
|
||||
/// default. Never returns an (default, active) pair with active < default.
|
||||
/// </summary>
|
||||
public static (float DefaultOpacity, float ActiveOpacity) SetDefault(
|
||||
float currentActive, float newDefault)
|
||||
{
|
||||
newDefault = System.Math.Clamp(newDefault, 0f, 1f);
|
||||
float active = currentActive < newDefault ? newDefault : currentActive;
|
||||
return (newDefault, active);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>ChatInterface::SetActiveOpacity @0x004F3C40</c>: set the active
|
||||
/// (focused) opacity to <paramref name="newActive"/>, dragging
|
||||
/// <paramref name="currentDefault"/> DOWN if it would otherwise exceed the new
|
||||
/// active value. Never returns an (default, active) pair with active < default.
|
||||
/// </summary>
|
||||
public static (float DefaultOpacity, float ActiveOpacity) SetActive(
|
||||
float currentDefault, float newActive)
|
||||
{
|
||||
newActive = System.Math.Clamp(newActive, 0f, 1f);
|
||||
float def = currentDefault > newActive ? newActive : currentDefault;
|
||||
return (def, newActive);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,17 @@ public sealed record ChatSettings(
|
|||
ulong ChatWindow1Filter = 0x0000101Cu, // Speech, Tell, Speech_Direct_Send, Emote
|
||||
ulong ChatWindow2Filter = 0x00040C00u, // Social, Social_Send, Allegiance
|
||||
ulong ChatWindow3Filter = 0x00080000u, // Fellowship
|
||||
ulong ChatWindow4Filter = 0x78000000u) // Turbine General/Trade/LFG/Roleplay
|
||||
ulong ChatWindow4Filter = 0x78000000u, // Turbine General/Trade/LFG/Roleplay
|
||||
// Campaign CH slice CH6c: retail's two GLOBAL window-opacity options
|
||||
// (Option_DefaultOpacity_Property 0x10000080 / Option_ActiveOpacity_Property
|
||||
// 0x10000081, docs/research/2026-08-09-chat-retail-window-shell.md §3).
|
||||
// DefaultOpacity applies while a window's descendant does NOT have keyboard
|
||||
// focus; ActiveOpacity while it does. Always active >= default — enforced by
|
||||
// ChatOpacityLink at every setter, not by clamping here. acdream applies this
|
||||
// GLOBALLY to every RetailWindowManager-registered window (register row
|
||||
// AP-190), where retail scopes it to ChatInterface-derived windows only.
|
||||
float DefaultOpacity = 0.5f,
|
||||
float ActiveOpacity = 1.0f)
|
||||
{
|
||||
/// <summary>
|
||||
/// N4 (CH3 Opus review): matches ACE's ACTUAL
|
||||
|
|
|
|||
|
|
@ -428,11 +428,40 @@ public sealed class SettingsPanel : IPanel
|
|||
if (renderer.SliderFloat("Font size (pt)", ref fontSize, 10f, 20f))
|
||||
_vm.SetChat(c with { FontSize = fontSize });
|
||||
|
||||
renderer.Spacing();
|
||||
renderer.Text("Window transparency");
|
||||
renderer.Separator();
|
||||
|
||||
// Campaign CH slice CH6c: retail's two linked opacity sliders
|
||||
// (gmChatOptionsUI::InitOptions @0x0049FC60's DualHash pair). Dragging
|
||||
// Background above Active drags Active UP to match; dragging Active below
|
||||
// Background drags Background DOWN — ChatOpacityLink is the shared port of
|
||||
// ChatInterface::SetDefaultOpacity/SetActiveOpacity (0x004F3BC0/0x004F3C40)
|
||||
// that both this draft and the live RetailWindowOpacityController use, so the
|
||||
// sliders track each other exactly like retail's options page.
|
||||
float defaultOpacity = c.DefaultOpacity;
|
||||
if (renderer.SliderFloat("Background opacity (unfocused)", ref defaultOpacity, 0f, 1f))
|
||||
{
|
||||
var (def, active) = ChatOpacityLink.SetDefault(c.ActiveOpacity, defaultOpacity);
|
||||
_vm.SetChat(c with { DefaultOpacity = def, ActiveOpacity = active });
|
||||
}
|
||||
|
||||
float activeOpacity = c.ActiveOpacity;
|
||||
if (renderer.SliderFloat("Active opacity (typing / focused)", ref activeOpacity, 0f, 1f))
|
||||
{
|
||||
var (def, active) = ChatOpacityLink.SetActive(c.DefaultOpacity, activeOpacity);
|
||||
_vm.SetChat(c with { DefaultOpacity = def, ActiveOpacity = active });
|
||||
}
|
||||
|
||||
renderer.Spacing();
|
||||
renderer.TextWrapped(
|
||||
"Channel filters hide messages from the chat window without "
|
||||
+ "changing your server-side subscriptions. Save persists; "
|
||||
+ "Cancel reverts.");
|
||||
+ "changing your server-side subscriptions. Window transparency "
|
||||
+ "applies to every retained window (chat, floaties, vitals, "
|
||||
+ "toolbar, inventory...) and fades whichever window doesn't "
|
||||
+ "currently have keyboard focus; Active opacity can never be "
|
||||
+ "lower than Background — dragging one past the other drags "
|
||||
+ "the other along. Save persists; Cancel reverts.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,9 @@ public sealed class SettingsStore
|
|||
ChatWindow1Filter: ReadULong(chat, "chatWindow1Filter", d.ChatWindow1Filter),
|
||||
ChatWindow2Filter: ReadULong(chat, "chatWindow2Filter", d.ChatWindow2Filter),
|
||||
ChatWindow3Filter: ReadULong(chat, "chatWindow3Filter", d.ChatWindow3Filter),
|
||||
ChatWindow4Filter: ReadULong(chat, "chatWindow4Filter", d.ChatWindow4Filter));
|
||||
ChatWindow4Filter: ReadULong(chat, "chatWindow4Filter", d.ChatWindow4Filter),
|
||||
DefaultOpacity: ReadFloat(chat, "defaultOpacity", d.DefaultOpacity),
|
||||
ActiveOpacity: ReadFloat(chat, "activeOpacity", d.ActiveOpacity));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -541,11 +543,13 @@ public sealed class SettingsStore
|
|||
private static SortedDictionary<string, object> BuildChatObject(ChatSettings c)
|
||||
=> new(StringComparer.Ordinal)
|
||||
{
|
||||
["activeOpacity"] = c.ActiveOpacity,
|
||||
["appearOffline"] = c.AppearOffline,
|
||||
["chatWindow1Filter"] = c.ChatWindow1Filter,
|
||||
["chatWindow2Filter"] = c.ChatWindow2Filter,
|
||||
["chatWindow3Filter"] = c.ChatWindow3Filter,
|
||||
["chatWindow4Filter"] = c.ChatWindow4Filter,
|
||||
["defaultOpacity"] = c.DefaultOpacity,
|
||||
["filterProfanity"] = c.FilterProfanity,
|
||||
["fontSize"] = c.FontSize,
|
||||
["hearGeneralChat"] = c.HearGeneralChat,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue