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>
87 lines
3.4 KiB
C#
87 lines
3.4 KiB
C#
using AcDream.UI.Abstractions.Panels.Settings;
|
|
|
|
namespace AcDream.UI.Abstractions.Tests.Panels.Settings;
|
|
|
|
/// <summary>
|
|
/// Campaign CH slice CH6c: pins <see cref="ChatOpacityLink"/>'s port of retail's
|
|
/// linked default/active opacity invariant (<c>ChatInterface::SetDefaultOpacity
|
|
/// @0x004F3BC0</c> / <c>SetActiveOpacity @0x004F3C40</c>) — active >= default,
|
|
/// ALWAYS, enforced by dragging the OTHER value rather than clamping the one
|
|
/// being set.
|
|
/// </summary>
|
|
public sealed class ChatOpacityLinkTests
|
|
{
|
|
[Fact]
|
|
public void SetDefault_BelowCurrentActive_LeavesActiveUnchanged()
|
|
{
|
|
// The ordinary case: lowering the background opacity while it's already
|
|
// below the active value doesn't need to touch active at all.
|
|
var (def, active) = ChatOpacityLink.SetDefault(currentActive: 1.0f, newDefault: 0.3f);
|
|
Assert.Equal(0.3f, def);
|
|
Assert.Equal(1.0f, active);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetDefault_AboveCurrentActive_DragsActiveUp_DoesNotClampDefault()
|
|
{
|
|
// The decomp-verified answer to "does raising default above active drag
|
|
// active up, or clamp default?": ChatInterface::SetDefaultOpacity always
|
|
// WRITES this->m_fDefaultOpacity = arg2 first, THEN calls SetActiveOpacity
|
|
// when active < default. Default is never clamped down.
|
|
var (def, active) = ChatOpacityLink.SetDefault(currentActive: 0.4f, newDefault: 0.9f);
|
|
Assert.Equal(0.9f, def);
|
|
Assert.Equal(0.9f, active);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetActive_AboveCurrentDefault_LeavesDefaultUnchanged()
|
|
{
|
|
var (def, active) = ChatOpacityLink.SetActive(currentDefault: 0.2f, newActive: 0.9f);
|
|
Assert.Equal(0.2f, def);
|
|
Assert.Equal(0.9f, active);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetActive_BelowCurrentDefault_DragsDefaultDown()
|
|
{
|
|
// Symmetric case: ChatInterface::SetActiveOpacity writes m_fActiveOpacity
|
|
// first, then calls SetDefaultOpacity when default > active.
|
|
var (def, active) = ChatOpacityLink.SetActive(currentDefault: 0.7f, newActive: 0.3f);
|
|
Assert.Equal(0.3f, def);
|
|
Assert.Equal(0.3f, active);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1f, 0f)]
|
|
[InlineData(2f, 1f)]
|
|
public void SetDefault_ClampsInputToUnitRange(float rawInput, float expectedDefault)
|
|
{
|
|
var (def, _) = ChatOpacityLink.SetDefault(currentActive: 1f, newDefault: rawInput);
|
|
Assert.Equal(expectedDefault, def);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(-1f, 0f)]
|
|
[InlineData(2f, 1f)]
|
|
public void SetActive_ClampsInputToUnitRange(float rawInput, float expectedActive)
|
|
{
|
|
var (_, active) = ChatOpacityLink.SetActive(currentDefault: 0f, newActive: rawInput);
|
|
Assert.Equal(expectedActive, active);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetDefault_ThenSetActive_NeverProducesActiveBelowDefault()
|
|
{
|
|
// A short sequence exercising the invariant through several moves, the
|
|
// way a user dragging both sliders back and forth would.
|
|
(float def, float active) = (0.5f, 1.0f);
|
|
(def, active) = ChatOpacityLink.SetDefault(active, 0.9f);
|
|
Assert.True(active >= def);
|
|
(def, active) = ChatOpacityLink.SetActive(def, 0.1f);
|
|
Assert.True(active >= def);
|
|
(def, active) = ChatOpacityLink.SetDefault(active, 0.6f);
|
|
Assert.True(active >= def);
|
|
Assert.Equal(0.6f, def);
|
|
Assert.Equal(0.6f, active);
|
|
}
|
|
}
|