fix(chat): CH6c review fixes — opaque default, opacity-transition register clauses
BLOCKER: ChatSettings.DefaultOpacity shipped retail's base ChatInterface value (0.5) as ONE shared global default applied to every RetailWindowManager-registered window, not just the four floating chat windows retail itself fades. That faded the whole out-of-box registered UI (radar, vitals, toolbar, main chat, ...) to 50% opacity, including several windows that can never take keyboard focus and so were stuck at 0.5 permanently. Fixed to gmMainChatUI's 1.0/1.0 override (0x004CD0F0) instead — retail-identical opaque presentation for the 11 non-chat windows and the main chat window; only the four floating chat windows now diverge from retail's 0.5-while-idle default, and the Settings -> Chat transparency slider remains fully user-settable. AP-190 reworded and gains two new decomp-verified clauses: (3) retail eases opacity toward its target by 5% of the delta per tick (ChatInterface::ListenToGlobalMessage @0x004F3840, armed from the focus element-messages at @0x004F5275) where acdream snaps -- deferred, needs a UI frame-tick hook the opacity controller doesn't have; (4) retail's focus predicate is the chat ENTRY FIELD specifically (ChatInterface::IsTextEntryFocused @0x004F30A0) where acdream uses any-focusable-descendant. Both findings + the pre-existing UiMenu.cs PushAlphaAbsolute(1f) popup bypass are folded into the window-shell research doc's opacity section. NITs: fixed the stale "text bypasses the alpha" comment in UiElement.DrawSelfAndChildren (CH6c already routed DrawStringDat/ DrawString through the same ApplyAlpha chokepoint as sprites/rects); added RetailWindowManager.WindowUnregistered + wired RetailWindowOpacityController to detach and forget a window unregistered while it held focus (previously only Dispose detached, leaking any window unregistered mid-focus for the rest of the session); added post-Dispose no-op guards to the three Set* opacity mutators; added a DrawString (BitmapFont path) alpha regression test and a DrawStringDat outline/background-pass alpha test (the existing tests only ever exercised the foreground/fill pass). Also fixes RuntimeSettingsControllerTests.SettingsViewModelSavePreserves SectionAndTargetOrder's now-stale "target-chat-opacity:0.5:1" expectation (caught by the full-suite run this fix requires) to match the new 1.0 default. Campaign ledger CH6c row updated to APPROVE-WITH-FIXES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
964af62e25
commit
cc58289967
14 changed files with 319 additions and 18 deletions
|
|
@ -115,6 +115,21 @@ public sealed class TextRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only snapshot of the current frame's queued NORMAL-layer BITMAP FONT
|
||||
/// text buffer (<see cref="DrawString"/>/<see cref="DrawStringClipped"/>,
|
||||
/// the <see cref="AcDream.App.UI.UiRenderContext.DrawString"/> path used for
|
||||
/// D.6 world-space HUD text): (vertex count, alpha of the first emitted
|
||||
/// vertex — color.W at float index 7 of the 8-float vertex layout). Unlike
|
||||
/// <see cref="DebugSpriteSegments"/> this buffer is not split per-texture —
|
||||
/// bitmap-font glyphs all sample the one atlas — so there is exactly one
|
||||
/// (count, alpha) pair to check. CH6c review NIT: pins that
|
||||
/// <see cref="UiRenderContext.DrawString"/>'s alpha chokepoint is guarded
|
||||
/// the same way <see cref="DrawStringDat"/>'s already was.
|
||||
/// </summary>
|
||||
internal (int VertexCount, float Alpha) DebugTextBuffer
|
||||
=> (_textVerts, _textBuf.Count > 0 ? _textBuf[7] : 0f);
|
||||
|
||||
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
||||
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
||||
// rect panel backgrounds (which otherwise always win because rects flush after
|
||||
|
|
|
|||
|
|
@ -43,6 +43,17 @@ public sealed class RetailWindowManager : IDisposable
|
|||
/// </summary>
|
||||
public event Action<RetailWindowHandle>? WindowRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a window is REMOVED from the registry (<see cref="Unregister"/>),
|
||||
/// after teardown (<c>NotifyClosed</c>/<c>DisposeController</c>) but before the
|
||||
/// handle is discarded. CH6c review NIT: <see cref="RetailWindowOpacityController"/>
|
||||
/// subscribes here so it can drop its per-handle <c>DescendantFocusChanged</c>
|
||||
/// subscription and forget the handle from its focused-window set — without this,
|
||||
/// a window unregistered while it held keyboard focus stayed referenced by the
|
||||
/// controller for the rest of the session.
|
||||
/// </summary>
|
||||
public event Action<RetailWindowHandle>? WindowUnregistered;
|
||||
|
||||
public RetailWindowHandle Register(
|
||||
string name,
|
||||
UiElement outerFrame,
|
||||
|
|
@ -215,6 +226,7 @@ public sealed class RetailWindowManager : IDisposable
|
|||
_defaultInputs.Remove(handle);
|
||||
handle.NotifyClosed();
|
||||
handle.DisposeController();
|
||||
WindowUnregistered?.Invoke(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,9 +29,24 @@ namespace AcDream.App.UI;
|
|||
/// (<c>0x004F4550</c>, DefaultOpacity=0.5/ActiveOpacity=1.0) to DefaultOpacity=1.0
|
||||
/// (always fully opaque); <c>gmFloatyChatUI::Create</c> (<c>0x004CE2C0</c>) calls
|
||||
/// <c>ChatInterface::ChatInterface</c> directly with no override, so the four
|
||||
/// floating windows keep the base 0.5/1.0. acdream ships ONE shared default (the
|
||||
/// base ChatInterface value, 0.5/1.0) applied uniformly, including to the main
|
||||
/// chat window — a simplification recorded alongside the scope extension above.
|
||||
/// floating windows keep the base 0.5/1.0. <b>CH6c review fix:</b> acdream ships ONE
|
||||
/// shared default — <c>gmMainChatUI</c>'s 1.0/1.0 override, not the base
|
||||
/// ChatInterface value — applied uniformly to every window including the four
|
||||
/// floating chat windows. Shipping the base 0.5/1.0 pair globally (the original
|
||||
/// CH6c behavior) faded the WHOLE registered UI to 50% opacity out of the box,
|
||||
/// including windows that can never take keyboard focus and so were stuck at 0.5
|
||||
/// permanently; 1.0/1.0 is retail-identical for the 11 non-chat windows and the
|
||||
/// main chat window, and only the four floaties diverge from retail's
|
||||
/// 0.5-while-idle default now — user-settable via the same slider (register row
|
||||
/// AP-190).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Retail also EASES the live opacity toward its target by 5% of the delta per
|
||||
/// tick (<c>ChatInterface::ListenToGlobalMessage @0x004F3840</c>) rather than
|
||||
/// snapping, and its focus predicate is the chat ENTRY FIELD specifically
|
||||
/// (<c>ChatInterface::IsTextEntryFocused @0x004F30A0</c>), not "any descendant has
|
||||
/// focus". Both are recorded as AP-190 residuals — CH6c review, not yet ported.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class RetailWindowOpacityController : IDisposable
|
||||
|
|
@ -55,6 +70,7 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
System.Math.Clamp(activeOpacity, 0f, 1f));
|
||||
|
||||
_manager.WindowRegistered += OnWindowRegistered;
|
||||
_manager.WindowUnregistered += OnWindowUnregistered;
|
||||
foreach (RetailWindowHandle handle in _manager.Windows)
|
||||
Attach(handle);
|
||||
}
|
||||
|
|
@ -72,6 +88,7 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
/// </summary>
|
||||
public void SetDefaultOpacity(float value)
|
||||
{
|
||||
if (_disposed) return;
|
||||
(DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetDefault(ActiveOpacity, value);
|
||||
ReapplyAll();
|
||||
}
|
||||
|
|
@ -82,6 +99,7 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
/// </summary>
|
||||
public void SetActiveOpacity(float value)
|
||||
{
|
||||
if (_disposed) return;
|
||||
(DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetActive(DefaultOpacity, value);
|
||||
ReapplyAll();
|
||||
}
|
||||
|
|
@ -94,6 +112,7 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
/// </summary>
|
||||
public void SetOpacity(float defaultOpacity, float activeOpacity)
|
||||
{
|
||||
if (_disposed) return;
|
||||
(DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetDefault(ActiveOpacity, defaultOpacity);
|
||||
(DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetActive(DefaultOpacity, activeOpacity);
|
||||
ReapplyAll();
|
||||
|
|
@ -101,6 +120,19 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
|
||||
private void OnWindowRegistered(RetailWindowHandle handle) => Attach(handle);
|
||||
|
||||
/// <summary>
|
||||
/// CH6c review NIT: without this, a window unregistered while it held
|
||||
/// keyboard focus stayed in <see cref="_focused"/> — and its
|
||||
/// <see cref="RetailWindowHandle.DescendantFocusChanged"/> subscription
|
||||
/// stayed live — for the rest of the session, because the only prior
|
||||
/// detach point was <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
private void OnWindowUnregistered(RetailWindowHandle handle)
|
||||
{
|
||||
handle.DescendantFocusChanged -= OnDescendantFocusChanged;
|
||||
_focused.Remove(handle);
|
||||
}
|
||||
|
||||
private void Attach(RetailWindowHandle handle)
|
||||
{
|
||||
handle.DescendantFocusChanged += OnDescendantFocusChanged;
|
||||
|
|
@ -133,7 +165,9 @@ public sealed class RetailWindowOpacityController : IDisposable
|
|||
_disposed = true;
|
||||
|
||||
_manager.WindowRegistered -= OnWindowRegistered;
|
||||
_manager.WindowUnregistered -= OnWindowUnregistered;
|
||||
foreach (RetailWindowHandle handle in _manager.Windows)
|
||||
handle.DescendantFocusChanged -= OnDescendantFocusChanged;
|
||||
_focused.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -460,7 +460,10 @@ public abstract class UiElement
|
|||
if (!Visible) return;
|
||||
|
||||
// Translate into our local space + push this window's opacity (multiplies into
|
||||
// descendants' sprite/rect draws; text bypasses the alpha so it stays sharp).
|
||||
// descendants' sprite, rect, AND text draws — CH6c ported DrawStringDat/DrawString
|
||||
// through the same ApplyAlpha chokepoint as sprites/rects, matching retail's
|
||||
// ChatInterface::SetOpacity (0x004F3120), which fades the whole composited window
|
||||
// surface, chrome and glyphs together, not text-stays-sharp over a translucent panel).
|
||||
ctx.PushTransform(Left, Top);
|
||||
ctx.PushAlpha(Opacity);
|
||||
try
|
||||
|
|
|
|||
|
|
@ -70,7 +70,20 @@ public sealed record ChatSettings(
|
|||
// 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,
|
||||
//
|
||||
// CH6c review fix (2026-08-10): the base ChatInterface ctor's 0.5/1.0 pair
|
||||
// is retail-correct ONLY for the four floating chat windows —
|
||||
// gmMainChatUI overrides to 1.0/1.0 (0x004CD0F0), and every other
|
||||
// RetailWindowManager-registered window (radar, vitals, toolbar, ...) has
|
||||
// no retail opacity fade at all, so applying 0.5 to them out of the box
|
||||
// rendered the whole registered UI half-transparent forever, including
|
||||
// several windows that can never take keyboard focus and so were
|
||||
// PERMANENTLY stuck at 0.5. DefaultOpacity now ships 1.0, matching
|
||||
// retail-identical opaque presentation for the 11 non-chat windows and
|
||||
// the main chat window; the four floating chat windows lose their
|
||||
// retail 0.5-while-idle fade by default, but the Settings → Chat
|
||||
// transparency slider remains fully user-settable (AP-190).
|
||||
float DefaultOpacity = 1.0f,
|
||||
float ActiveOpacity = 1.0f)
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue