fix(chat): CH6a/b rework — grip media, retail window-id model, floaty fixture
Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
56b84deeab
commit
1aa7709988
23 changed files with 6284 additions and 248 deletions
|
|
@ -8,7 +8,13 @@ namespace AcDream.Core.Chat;
|
|||
/// id <c>0</c> is the main chat window, ids <c>1</c>-<c>4</c> are the four
|
||||
/// floating chat windows (Campaign CH slice CH6b,
|
||||
/// <c>docs/research/2026-08-09-chat-retail-window-shell.md</c> §1.2 and
|
||||
/// <c>docs/research/2026-08-09-chat-retail-color-table.md</c> §4).
|
||||
/// <c>docs/research/2026-08-09-chat-retail-color-table.md</c> §4). These are
|
||||
/// acdream's OWN compact indices, not retail's raw <c>m_eWindowID</c> values:
|
||||
/// retail's actual main window is <c>m_eWindowID == 8</c>, its floaties are
|
||||
/// <c>2</c>-<c>5</c>, and <c>m_eWindowID == 0</c> is retail's ctor-default
|
||||
/// "unauthored" sentinel — none of that is <c>0</c>-<c>4</c> (CH6a/b
|
||||
/// REJECT-review SHOULD-FIX 2,
|
||||
/// <c>docs/research/2026-08-10-ch6ab-review-findings.md</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// Ports two retail mechanisms exactly:
|
||||
|
|
@ -16,38 +22,45 @@ namespace AcDream.Core.Chat;
|
|||
/// <item><c>ChatInterface::PostInit @0x004F3DD0</c>'s <c>m_oldState</c>
|
||||
/// switch seeds each window's default 64-bit
|
||||
/// <c>m_llTextTypeFilter</c> (color-table doc §4's table — the constants
|
||||
/// below are byte-identical to that table).</item>
|
||||
/// below are byte-identical to that table; retail's own main-window default
|
||||
/// 0xFBFFFFFF is shared by <c>m_eWindowID</c> 1 AND 8, and the floaty
|
||||
/// defaults come from retail <c>m_eWindowID</c> 2-5, one higher than
|
||||
/// acdream's compact 1-4).</item>
|
||||
/// <item><c>ChatInterface::RecvNotice_DisplayFinalStringInfo
|
||||
/// @0x004F4640</c>'s display predicate: a line shows in window
|
||||
/// <c>W</c> when the message's target window id equals <c>W</c>
|
||||
/// (explicit addressing) OR the message is broadcast (target id
|
||||
/// <c>0</c>) AND <c>W</c>'s filter accepts the line's
|
||||
/// <see cref="RetailLogTextType"/> (<c>ChatInterface::TypeIsActive
|
||||
/// (explicit addressing) OR the message is broadcast
|
||||
/// (<see cref="BroadcastTargetWindow"/>) AND <c>W</c>'s filter accepts the
|
||||
/// line's <see cref="RetailLogTextType"/> (<c>ChatInterface::TypeIsActive
|
||||
/// @0x004F2F10</c>).</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <c>UpdateFromPlayerModule @0x004F3920</c> early-returns for window id
|
||||
/// <c>0</c> — the main window never has a user-settable filter. That
|
||||
/// invariant needs no special case here: for <c>windowId == 0</c>,
|
||||
/// <see cref="ShouldDisplay"/>'s first branch (<c>targetWindowId ==
|
||||
/// windowId</c>) is already true for every broadcast line (target id
|
||||
/// <c>0</c>), so the main window's filter is never actually consulted —
|
||||
/// exactly matching retail's "no user filter" behavior without a guard.
|
||||
/// <see cref="SetFilter"/> and <see cref="SetOpen"/> are still no-ops for
|
||||
/// window <c>0</c> (it is always open and its seeded filter is inert), for
|
||||
/// the same reason retail's setters gate on <c>m_eWindowID != 0</c>
|
||||
/// (persistence doc §4.2).
|
||||
/// <see cref="BroadcastTargetWindow"/> is a sentinel distinct from every real
|
||||
/// window id (<c>0</c>-<c>4</c>) — the acdream-internal analogue of retail's
|
||||
/// wire <c>arg5 == 0</c> broadcast marker, deliberately kept separate from
|
||||
/// window id <c>0</c> (main). Earlier CH6b code conflated the two (both were
|
||||
/// literal <c>0</c>), so <see cref="ShouldDisplay"/>'s explicit-addressing
|
||||
/// branch (<c>targetWindowId == windowId</c>) short-circuited true for EVERY
|
||||
/// broadcast line evaluated against the main window, regardless of its own
|
||||
/// filter — the CH6a/b REJECT-review BLOCKER. With the sentinel separated
|
||||
/// out, the main window's filter is genuinely consulted for broadcast lines,
|
||||
/// <see cref="SetFilter"/> is a real (not inert) write for window <c>0</c>
|
||||
/// too, and a future explicit <c>targetWindowId == MainWindowId</c> (AP-180's
|
||||
/// <c>m_idCurrentCommandSource</c> per-window echo) stays distinguishable
|
||||
/// from an ordinary broadcast line. <see cref="SetOpen"/>/<see cref="Toggle"/>
|
||||
/// keep their own, UNRELATED no-op for window <c>0</c> — retail's main window
|
||||
/// is simply never closable, independent of the broadcast-sentinel fix.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// No production <see cref="ChatEntry"/> carries an explicit target window
|
||||
/// id yet (register row AP-180 — the <c>windowId</c> dual-destination echo
|
||||
/// is deferred); every current line is effectively broadcast
|
||||
/// (<c>targetWindowId == 0</c>). <see cref="ShouldDisplay"/> still accepts
|
||||
/// the full retail shape so the routing predicate does not need to change
|
||||
/// shape when AP-180 lands.
|
||||
/// (<c>targetWindowId == </c><see cref="BroadcastTargetWindow"/>).
|
||||
/// <see cref="ShouldDisplay"/> still accepts the full retail shape so the
|
||||
/// routing predicate does not need to change shape when AP-180 lands.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ChatWindowState
|
||||
|
|
@ -56,6 +69,15 @@ public sealed class ChatWindowState
|
|||
public const int MinFloatingWindowId = 1;
|
||||
public const int MaxFloatingWindowId = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Sentinel <c>targetWindowId</c> meaning "broadcast to every window,
|
||||
/// subject to each window's own filter" — the acdream-internal analogue
|
||||
/// of retail's wire <c>arg5 == 0</c>. Deliberately outside the
|
||||
/// <c>0</c>-<c>4</c> real-window-id range (see the class doc) so it can
|
||||
/// never collide with <see cref="MainWindowId"/>.
|
||||
/// </summary>
|
||||
public const uint BroadcastTargetWindow = uint.MaxValue;
|
||||
|
||||
private const int WindowCount = MaxFloatingWindowId + 1;
|
||||
|
||||
private readonly object _gate = new();
|
||||
|
|
@ -83,8 +105,9 @@ public sealed class ChatWindowState
|
|||
{
|
||||
lock (_gate)
|
||||
{
|
||||
// "everything 0x00-0x1F except 0x1A" (m_oldState 1/8) — inert for
|
||||
// routing (see class doc) but seeded for fidelity/inspection.
|
||||
// "everything 0x00-0x1F except 0x1A" (m_oldState 1/8) — genuinely
|
||||
// consulted for broadcast lines now that BroadcastTargetWindow is
|
||||
// distinct from MainWindowId (see class doc).
|
||||
_filters[0] = 0xFBFFFFFFu;
|
||||
// Speech, Tell, Speech_Direct_Send, Emote (m_oldState 2).
|
||||
_filters[1] = 0x0000101Cu;
|
||||
|
|
@ -110,13 +133,15 @@ public sealed class ChatWindowState
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set window <paramref name="windowId"/>'s 64-bit type filter. No-op for
|
||||
/// the main window (id <c>0</c>) — see the class doc.
|
||||
/// Set window <paramref name="windowId"/>'s 64-bit type filter — including
|
||||
/// the main window (id <c>0</c>): CH6a/b REJECT-review SHOULD-FIX 2 drops
|
||||
/// the old no-op (see the class doc's <see cref="BroadcastTargetWindow"/>
|
||||
/// paragraph). Retail's main window IS user-settable through the options
|
||||
/// page; only the settings UI to drive this is future work.
|
||||
/// </summary>
|
||||
public void SetFilter(int windowId, ulong filter)
|
||||
{
|
||||
ValidateWindowId(windowId);
|
||||
if (windowId == MainWindowId) return;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_filters[windowId] == filter) return;
|
||||
|
|
@ -182,12 +207,15 @@ public sealed class ChatWindowState
|
|||
/// <summary>
|
||||
/// <c>ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640</c>'s
|
||||
/// exact display predicate — see the class doc for the two branches.
|
||||
/// <paramref name="targetWindowId"/> is either a real window id
|
||||
/// (<c>0</c>-<c>4</c>, explicit addressing) or
|
||||
/// <see cref="BroadcastTargetWindow"/> (broadcast, filtered per window).
|
||||
/// </summary>
|
||||
public bool ShouldDisplay(int windowId, uint targetWindowId, uint logTextType)
|
||||
{
|
||||
ValidateWindowId(windowId);
|
||||
if (targetWindowId == (uint)windowId) return true;
|
||||
return targetWindowId == 0u && TypeIsActive(windowId, logTextType);
|
||||
return targetWindowId == BroadcastTargetWindow && TypeIsActive(windowId, logTextType);
|
||||
}
|
||||
|
||||
private static void ValidateWindowId(int windowId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue