fix #379: chat opacity fade was scoped to every window, not just chat
Root cause: RetailWindowOpacityController applied the Default/Active opacity fade to EVERY registered UiRoot window (vitals, toolbar, inventory, spellbook, radar, even the Options panel itself), but retail's ChatInterface::SetDefaultOpacity/SetActiveOpacity are only ever called by gmMainChatUI/gmFloatyChatUI — the mechanism is chat-only in retail, not a global window-opacity feature. Fix: scope the controller's catch-up loop, OnWindowRegistered, ReapplyAll, and Dispose to WindowNames.Chat/ChatWindow1-4 only; every other registered window now stays fully opaque regardless of slider position, matching retail's own scope. Regressed by RetailWindowOpacityControllerTests. OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels (registers vitals/toolbar/chat/a floating chat window and asserts the non-chat windows never move off 1.0 while chat windows still track Default/ Active correctly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c121842664
commit
c0b3d8f233
4 changed files with 177 additions and 56 deletions
|
|
@ -6,8 +6,22 @@ namespace AcDream.App.Tests.UI;
|
|||
/// Campaign CH slice CH6c: <see cref="RetailWindowOpacityController"/> — the live
|
||||
/// per-window opacity mechanism ported from <c>ChatInterface::SetOpacity/
|
||||
/// SetDefaultOpacity/SetActiveOpacity</c> (<c>0x004F3120</c>/<c>0x004F3BC0</c>/
|
||||
/// <c>0x004F3C40</c>), extended to every <see cref="RetailWindowManager"/>-registered
|
||||
/// window rather than retail's ChatInterface-only scope (register row AP-190).
|
||||
/// <c>0x004F3C40</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// #379 fix (2026-08-11, gate 4): the CH6c-era "extended to every
|
||||
/// RetailWindowManager-registered window" scope (formerly register row AP-190)
|
||||
/// was a bug, not a deliberate divergence — retail's ONLY two
|
||||
/// <c>ChatInterface::SetDefaultOpacity</c>/<c>SetActiveOpacity</c> callers are
|
||||
/// the main chat window and the four floaty chat windows (see the class's own
|
||||
/// doc comment). These tests now register windows under their REAL
|
||||
/// <see cref="WindowNames"/> so the scope check is exercised by name, exactly
|
||||
/// like the production wiring (<see cref="RetailUiRuntime"/> registers every
|
||||
/// window under its <see cref="WindowNames"/> constant, never an arbitrary
|
||||
/// string) — the pre-fix tests used placeholder names ("Vitals", "A", "B",
|
||||
/// ...) that happened to fall inside the (bugged) unscoped set and would now
|
||||
/// silently fall outside the real scope for the wrong reason.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class RetailWindowOpacityControllerTests
|
||||
{
|
||||
|
|
@ -30,7 +44,7 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
public void Construction_AttachesToAlreadyRegisteredWindows_AtDefaultOpacity()
|
||||
{
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, "Vitals");
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
|
|
@ -51,9 +65,9 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
|
||||
// The window is mounted AFTER the controller exists — proves the
|
||||
// RetailWindowManager.WindowRegistered subscription (not just the ctor's
|
||||
// catch-up loop over already-registered windows) is what applies retail's
|
||||
// GLOBAL opacity scope to every future Mount* call too.
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, "Toolbar");
|
||||
// catch-up loop over already-registered windows) is what applies the
|
||||
// chat-scoped opacity to every future Mount* call too.
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.ChatWindow1);
|
||||
|
||||
Assert.Equal(0.3f, handle.Opacity);
|
||||
}
|
||||
|
|
@ -62,7 +76,7 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
public void FocusEnteringAWindow_SwitchesToActiveOpacity_LeavingSwitchesBack()
|
||||
{
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat");
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
Assert.Equal(0.5f, handle.Opacity);
|
||||
|
|
@ -75,26 +89,55 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
GC.KeepAlive(controller);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #379 regression: pins the EXACT applied-window set to the five real
|
||||
/// <see cref="WindowNames"/> chat windows. Before the fix, this same setup
|
||||
/// left <c>vitals</c>/<c>toolbar</c> at 0.4 (the DefaultOpacity) instead of
|
||||
/// their own authored 1.0 — the user's literal complaint ("only the
|
||||
/// chatwindows shall change not the other panels").
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OpacityFade_AppliesToEveryRegisteredWindow_NotJustChat()
|
||||
public void OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels()
|
||||
{
|
||||
// The CH6c scope extension: retail's ChatInterface::SetOpacity only ever
|
||||
// runs on chat-derived windows. acdream applies the SAME mechanism to
|
||||
// every RetailWindowManager window — vitals, toolbar, whatever else is
|
||||
// mounted — matching the task's GLOBAL-option framing.
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle vitals, _) = RegisterWindow(root, "Vitals");
|
||||
(RetailWindowHandle toolbar, UiElement toolbarChild) = RegisterWindow(root, "Toolbar");
|
||||
(RetailWindowHandle vitals, _) = RegisterWindow(root, WindowNames.Vitals);
|
||||
(RetailWindowHandle toolbar, UiElement toolbarChild) = RegisterWindow(root, WindowNames.Toolbar);
|
||||
(RetailWindowHandle chat, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
(RetailWindowHandle floaty1, UiElement floaty1Child) = RegisterWindow(root, WindowNames.ChatWindow1);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.4f, activeOpacity: 1.0f);
|
||||
|
||||
Assert.Equal(0.4f, vitals.Opacity);
|
||||
Assert.Equal(0.4f, toolbar.Opacity);
|
||||
// Non-chat windows never had Apply called on them at all — they sit at
|
||||
// UiElement.Opacity's own 1.0 ctor default, untouched by the slider.
|
||||
Assert.Equal(1.0f, vitals.Opacity);
|
||||
Assert.Equal(1.0f, toolbar.Opacity);
|
||||
// The two chat windows DID get the DefaultOpacity applied.
|
||||
Assert.Equal(0.4f, chat.Opacity);
|
||||
Assert.Equal(0.4f, floaty1.Opacity);
|
||||
|
||||
root.SetKeyboardFocus(toolbarChild);
|
||||
|
||||
Assert.Equal(0.4f, vitals.Opacity); // unrelated window: still unfocused
|
||||
Assert.Equal(1.0f, toolbar.Opacity); // the focused one: active
|
||||
// Focusing a NON-chat window's descendant still does not touch its own
|
||||
// opacity (that window was never subscribed to focus changes either)
|
||||
// NOR does it bleed the ACTIVE opacity onto the chat windows — they
|
||||
// stay at DefaultOpacity since neither of THEM has focus.
|
||||
Assert.Equal(1.0f, vitals.Opacity);
|
||||
Assert.Equal(1.0f, toolbar.Opacity);
|
||||
Assert.Equal(0.4f, chat.Opacity);
|
||||
Assert.Equal(0.4f, floaty1.Opacity);
|
||||
|
||||
root.SetKeyboardFocus(floaty1Child);
|
||||
controller.SetActiveOpacity(0.6f);
|
||||
|
||||
// A live re-apply (SetActiveOpacity -> ReapplyAll) is ALSO scoped — the
|
||||
// two non-chat windows remain untouched throughout, proving ReapplyAll
|
||||
// itself (not just the initial Attach) respects the scope. The FOCUSED
|
||||
// chat window picks up the new active value; the unfocused one stays
|
||||
// at the (unchanged) default.
|
||||
Assert.Equal(1.0f, vitals.Opacity);
|
||||
Assert.Equal(1.0f, toolbar.Opacity);
|
||||
Assert.Equal(0.4f, chat.Opacity);
|
||||
Assert.Equal(0.6f, floaty1.Opacity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -104,8 +147,8 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
// raising DEFAULT above the current ACTIVE value drags active UP to
|
||||
// match — it never clamps the default down instead.
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle unfocused, _) = RegisterWindow(root, "A");
|
||||
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B");
|
||||
(RetailWindowHandle unfocused, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, WindowNames.ChatWindow1);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.3f, activeOpacity: 0.5f);
|
||||
root.SetKeyboardFocus(focusedChild);
|
||||
|
|
@ -125,8 +168,8 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
{
|
||||
// Symmetric case (ChatInterface::SetActiveOpacity @0x004F3C40).
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle unfocused, _) = RegisterWindow(root, "A");
|
||||
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, "B");
|
||||
(RetailWindowHandle unfocused, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
(RetailWindowHandle focused, UiElement focusedChild) = RegisterWindow(root, WindowNames.ChatWindow1);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 0.7f);
|
||||
root.SetKeyboardFocus(focusedChild);
|
||||
|
|
@ -145,7 +188,7 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
// UpdateFromPlayerModule (0x004CE3F0) reads/applies Default first, then
|
||||
// Active — the shape used to push a freshly loaded ChatSettings pair.
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, "Chat");
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
|
||||
|
|
@ -174,7 +217,7 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
public void Dispose_UnsubscribesFromFocusChanges()
|
||||
{
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat");
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
|
||||
|
|
@ -198,14 +241,14 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
// kind a lingering external reference to the handle could still fire)
|
||||
// must not reach the controller anymore.
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat");
|
||||
(RetailWindowHandle handle, UiElement child) = RegisterWindow(root, WindowNames.Chat);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
|
||||
root.SetKeyboardFocus(child);
|
||||
Assert.Equal(1.0f, handle.Opacity);
|
||||
|
||||
root.WindowManager.Unregister("Chat");
|
||||
root.WindowManager.Unregister(WindowNames.Chat);
|
||||
|
||||
// Unregister hides the outer frame, which drops keyboard focus off
|
||||
// the now-invisible child — the manager's (still-live at that point)
|
||||
|
|
@ -228,7 +271,7 @@ public sealed class RetailWindowOpacityControllerTests
|
|||
public void SetMutators_AfterDispose_AreNoOps()
|
||||
{
|
||||
UiRoot root = NewRoot();
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, "Chat");
|
||||
(RetailWindowHandle handle, _) = RegisterWindow(root, WindowNames.Chat);
|
||||
var controller = new RetailWindowOpacityController(
|
||||
root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue