feat(ui): Campaign OP slice OP5 — the Chat tab

Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and
OP3's per-page OptionPage model: the General Options header + two
DualHash-linked opacity sliders (Option_DefaultOpacity_Property
0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on
drag through RetailWindowOpacityController, defaults read from the
installed DAT's DBProperties collection at DID 0x78000001 via
ChatOptionsDatDefaults), and the five per-window text-filter blocks
(main window 12 rows minus Gameplay, four floaties 13 rows each — the
byte-verified authored order cross-checked against the raw
gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not
just the research doc's own table) writing AcDream.Core.Chat.
ChatWindowState directly, the same state CH6's chat windows already
read.

AP-195 retired: ported both halves left open at the OP2 re-review —
the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by
the block-level P0x10000082/P0x10000083 sprites now threaded through
ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail
(UiCheckboxBitfield64.Height grows with its stacked row content; the
enclosing ListBox reflows around the block's FINAL height via the new
UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking
rather than a third stacking path). AP-187 broadened to cover the main
window's own filter (previously only the four floaties) and the new
live-editing write path.

The main chat window's filter (retail window id 8, ChatWindowState id
0) gains its own settings.json persistence (ChatSettings.
ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields;
opacity persistence is now wired on every live slider change, not only
through the old dev-scaffold Settings panel.

Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the
new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all
19 committed layout fixtures — purely additive, confirmed against the
live installed DAT (0x10000520's own 0x82/0x83 properties resolve to
0x06004D17/0x06004D19 exactly as AP-195 documented).

Conformance: FilterRows/FilterBlocks pinned against the byte-verified
authored order and ChatWindowState's own default constants; the AP-195
LED swap and self-sizing behavior; the DAT opacity-default extraction
against the live installed DAT; live filter/opacity writes reaching
ChatWindowState/RetailWindowOpacityController; OnShown re-seed and
Reset/Defaults ghosting per the OP4 binding-pattern discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-11 06:25:59 +02:00
parent 4798302226
commit e71e5a9614
52 changed files with 4540 additions and 40 deletions

View file

@ -657,19 +657,18 @@ public sealed class RetailUiRuntime : IDisposable
};
/// <summary>
/// Capture the four floating windows' current text-type filters into
/// the local settings store (research doc §4.4/§6.1 — local-only until
/// CH6f's wire format). No-op when no store was wired.
/// Capture the main window's and four floating windows' current text-type
/// filters into the local settings store (research doc §4.4/§6.1 — local-only
/// until a future retail <c>0x1000008C</c> wire format). No-op when no store was
/// wired.
///
/// <para>
/// CH6a/b REJECT-review NIT 1: unlike window geometry/visibility (which
/// <see cref="_persistence"/> auto-saves on every change), a filter
/// change is only captured here — i.e. only when <see cref="SaveLayout"/>
/// runs, which today is exclusively the explicit <c>/saveautoui</c>
/// command. Harmless in practice (nothing currently mutates a window's
/// filter live — no options UI exists yet), but worth tightening to
/// auto-save-on-change once CH6e/CH6f gives filters a live settings
/// surface a user can actually edit mid-session.
/// CH6a/b REJECT-review NIT 1 — CLOSED by Campaign OP slice OP5: filters are now
/// ALSO captured on every live edit through the Chat tab's five filter blocks
/// (each block's <c>BitfieldOptionRow.apply</c> closure calls this directly), not
/// only when <see cref="SaveLayout"/> runs via the explicit <c>/saveautoui</c>
/// command — OP5 is the "live settings surface a user can actually edit
/// mid-session" this NIT was waiting on.
/// </para>
/// </summary>
private void SaveChatWindowFilters()
@ -679,6 +678,7 @@ public sealed class RetailUiRuntime : IDisposable
ChatSettings current = store.LoadChat();
store.SaveChat(current with
{
ChatWindowMainFilter = windows.GetFilter(ChatWindowState.MainWindowId),
ChatWindow1Filter = windows.GetFilter(1),
ChatWindow2Filter = windows.GetFilter(2),
ChatWindow3Filter = windows.GetFilter(3),
@ -686,6 +686,25 @@ public sealed class RetailUiRuntime : IDisposable
});
}
/// <summary>
/// Campaign OP slice OP5: persists the Chat tab's two opacity-slider values —
/// the "worth tightening to auto-save-on-change" gap
/// <see cref="SaveChatWindowFilters"/>'s own doc flagged before a live options UI
/// existed to edit them. Called from the Chat tab's slider apply closures, so
/// every drag tick is captured (matching the filter blocks' own auto-save-on-
/// change wiring below, not the old "only on /saveautoui" schedule).
/// </summary>
private void SaveChatOpacity()
{
if (_bindings.Chat.Store is not { } store) return;
ChatSettings current = store.LoadChat();
store.SaveChat(current with
{
DefaultOpacity = WindowOpacity.DefaultOpacity,
ActiveOpacity = WindowOpacity.ActiveOpacity,
});
}
public void CloseWindow(string name)
{
if (RetailPanelCatalog.TryGetPanelId(name, out uint panelId))
@ -898,6 +917,16 @@ public sealed class RetailUiRuntime : IDisposable
return;
}
// Campaign OP slice OP5: the main window's own filter (id 0) is now
// user-settable through the Chat tab's main-window filter block
// (research doc §5.2) — seed it from the local store here, the same
// "local-only persistence" leg MountFloatingChatWindows already runs
// for windows 1-4 (research doc §4.4/§6.1 — the retail 0x1000008C
// gameplay-options wire remains explicitly deferred).
if (_bindings.Chat.Store is { } chatStore)
_bindings.Chat.Windows.SetFilter(
ChatWindowState.MainWindowId, chatStore.LoadChat().ChatWindowMainFilter);
ChatWindowController? controller = ChatWindowController.Bind(
info,
layout,
@ -2040,6 +2069,68 @@ public sealed class RetailUiRuntime : IDisposable
Console.WriteLine("[UI] options panel: Character tab rows did not bind.");
}
// Campaign OP slice OP5 (2026-08-11): the Chat tab's 6 headers, 2 linked
// opacity sliders, and 5 per-window filter blocks. Same "runs before
// ActivateTabs, own dat-lock scope" shape as the Character-tab block above.
lock (_bindings.Assets.DatLock)
{
var strings = new DatStringResolver(_bindings.Assets.Dats);
// U1's Chat-tab-specific consumer (this class's own doc trace): the
// two sliders' Defaults values come from the DAT DBProperties
// collection, resolved ONCE — the installed DAT does not change
// within a session.
if (!Layout.ChatOptionsDatDefaults.TryRead(
_bindings.Assets.Dats, out float datDefaultOpacity, out float datActiveOpacity))
{
Console.WriteLine(
"[UI] options panel: Chat tab opacity DAT defaults did not resolve "
+ "(DID 0x78000001) — falling back to retail's ChatInterface base-"
+ "constructor values (0.5/1.0).");
}
bool chatBound = Layout.ChatOptionsPageController.Bind(
layout,
controller.ChatPage,
templateResolver: (templateLayoutId, templateElementId) =>
{
ElementInfo? info = LayoutImporter.ImportInfos(
_bindings.Assets.Dats, templateLayoutId, templateElementId);
return info is null
? null
: LayoutImporter.Build(
info,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont,
strings.Resolve).Root;
},
resolveString: (tableId, stringId) => strings.Resolve(tableId, stringId),
new Layout.ChatOptionsPageController.Bindings(
CurrentDefaultOpacity: () => WindowOpacity.DefaultOpacity,
CurrentActiveOpacity: () => WindowOpacity.ActiveOpacity,
SetDefaultOpacity: value =>
{
WindowOpacity.SetDefaultOpacity(value);
SaveChatOpacity();
},
SetActiveOpacity: value =>
{
WindowOpacity.SetActiveOpacity(value);
SaveChatOpacity();
},
DefaultOpacityDatDefault: datDefaultOpacity,
ActiveOpacityDatDefault: datActiveOpacity,
CurrentFilter: windowId => _bindings.Chat.Windows.GetFilter(windowId),
SetFilter: (windowId, value) =>
{
_bindings.Chat.Windows.SetFilter(windowId, value);
SaveChatWindowFilters();
}));
if (!chatBound)
Console.WriteLine("[UI] options panel: Chat tab rows did not bind.");
}
controller.ActivateTabs();
RetailWindowHandle handle = RetailWindowFrame.Mount(