acdream/src/AcDream.App/UI/RetailPanelCatalog.cs
Erik ec6eeb120d feat(quest): QT5/QT6 — the Journal panel, and the button that was already there
The quest log is on screen. Rows come from the live tracker joined to the
authored catalog, the Status column runs QT4's port of FillProgressString, and
the detail pane shows contact, locations, description and the other timer.

Two things measured rather than assumed, each now pinned by an installed-DAT
test rather than left to the commit message:

The tab pairing is read from the authored 0x2E table, not inferred from
x-order — the FA campaign had to correct exactly that mistake, and Contracts
turns out to be the authored DEFAULT tab (0x32 = True), so opening on the
wrong one would have looked like an empty panel.

The open path needed no keybind at all. Toolbar button 0x1000055A authors
0x10000029 = 0x19 and has been sitting in ToolbarController.PanelButtonIds
since the toolbar was ported — it just had no panel behind it, so clicking it
did nothing. Registering slot 25 finished a wiring that was already
three-quarters present.

The list rebuild is revision-gated while the repeat countdown is not: nothing
on the wire changes as a cooldown runs down, so a rebuild-gated timer would
freeze on screen, and a per-frame rebuild would reset the player's scroll under
them. Both directions have a test.

Deliberately inert: the Abandon button (retail's abandon path is a
contract-registry command this campaign did not port — authored and visible,
but wiring a no-op handler would look responsive and lie), and the Journal
notes and Page List tabs, which are their own feature.

Campaign QT slices 5 and 6 of 6 — code-complete, connected gate owed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:12:05 +02:00

130 lines
5.5 KiB
C#

namespace AcDream.App.UI;
/// <summary>
/// Retail <c>gmPanelUI</c> panel ids carried by DAT property <c>0x10000029</c>.
/// The toolbar exposes only a subset of these ids; Helpful/Harmful effects use
/// the authored <c>gmUIElement_EffectsIndicator</c> buttons instead.
/// </summary>
public static class RetailPanelCatalog
{
public const uint CharacterInformation = 3u;
public const uint PositiveEffects = 4u;
public const uint NegativeEffects = 5u;
public const uint Inventory = 7u;
public const uint LinkStatus = 8u;
public const uint MiniGame = 9u;
public const uint Character = 11u;
public const uint Magic = 13u;
public const uint Vitae = 15u;
/// <summary>
/// The retail Options panel's <c>gmPanelUI</c> slot key — byte-verified
/// from the toolbar options button's OWN authored property
/// <c>0x10000029 = 10</c> (fixture <c>toolbar_21000016.json</c>,
/// element <c>0x1000019B</c>) and independently corroborated by
/// <c>docs/research/2026-08-10-options-panel-structure.md</c> §1.4's
/// decompiled slot-key table (host <c>0x2100006E</c>, slot
/// <c>0x1000018D</c> → key 10). Campaign OP slice OP3.
/// </summary>
public const uint Options = 10u;
/// <summary>
/// Campaign FA slice FA3: the four-tab Friends/Allegiance/Fellowship/
/// Squelch panel's <c>gmPanelUI</c> slot key — byte-verified from the
/// live installed DATs (host <c>0x2100006E</c> slot <c>0x1000018F</c>'s
/// own authored <c>0x10000029 = 12</c>, <c>FaPanelSlotProbeTests</c>).
/// No toolbar button authors this id (lane A §6.1: the open path is
/// keyboard-only, F3/F4) — <see cref="SocialPanel"/> is therefore in
/// <see cref="Mounted"/> only, not <see cref="Toolbar"/>, the same
/// convention <see cref="PositiveEffects"/>/<see cref="NegativeEffects"/>/
/// <see cref="LinkStatus"/>/<see cref="MiniGame"/>/<see cref="Vitae"/>
/// already use for their own non-toolbar open paths.
/// </summary>
public const uint SocialPanel = 12u;
/// <summary>
/// Batch C (overnight hover/UI round, 2026-08-17): the two-tab Map/House
/// panel's <c>gmPanelUI</c> slot key — byte-verified from the live
/// installed DATs (host <c>0x2100006E</c> slot <c>0x1000018C</c>'s own
/// authored <c>0x10000029 = 16</c>, and the toolbar Map/House button
/// <c>0x1000019A</c>'s own authored <c>0x10000029 = 16</c>,
/// <c>MapHousePanelSlotProbeTests</c>). Already independently named by
/// the FA campaign's full 16-slot dump
/// (<c>docs/research/2026-08-11-fa-panel-structure.md:927-933</c>,
/// "gmMapUI+gmHouseUI pages"). Unlike <see cref="SocialPanel"/>, a real
/// toolbar button opens this one — it is in BOTH <see cref="Mounted"/>
/// and <see cref="Toolbar"/>.
/// </summary>
public const uint MapHouse = 16u;
/// <summary>
/// Campaign QT slice QT5: the three-tab Journal panel (Contracts /
/// Journal / Page List) — <c>gmPanelUI</c> slot key byte-verified from the
/// live installed DATs (host <c>0x2100006E</c> slot <c>0x10000559</c>'s own
/// authored <c>0x10000029 = 0x19</c>). Toolbar button <c>0x1000055A</c>
/// authors the same id, so this one is in BOTH <see cref="Mounted"/> and
/// <see cref="Toolbar"/> — like <see cref="MapHouse"/>, unlike
/// <see cref="SocialPanel"/>.
/// </summary>
public const uint Journal = 25u;
private static readonly (uint PanelId, string WindowName)[] Mounted =
{
(CharacterInformation, WindowNames.CharacterInformation),
(PositiveEffects, WindowNames.PositiveEffects),
(NegativeEffects, WindowNames.NegativeEffects),
(Inventory, WindowNames.Inventory),
(LinkStatus, WindowNames.LinkStatus),
(MiniGame, WindowNames.MiniGame),
(Character, WindowNames.Character),
(Magic, WindowNames.Spellbook),
(Vitae, WindowNames.Vitae),
(Options, WindowNames.Options),
(SocialPanel, WindowNames.SocialPanel),
(MapHouse, WindowNames.MapHouse),
(Journal, WindowNames.Journal),
};
private static readonly (uint PanelId, string WindowName)[] Toolbar =
{
(Inventory, WindowNames.Inventory),
(Character, WindowNames.Character),
(Magic, WindowNames.Spellbook),
(Options, WindowNames.Options),
(MapHouse, WindowNames.MapHouse),
// Campaign QT slice QT5: toolbar button 0x1000055A authors
// 0x10000029 = 0x19 and has been in ToolbarController.PanelButtonIds
// all along — it simply had no panel behind it, so clicking it did
// nothing.
(Journal, WindowNames.Journal),
};
public static IReadOnlyList<(uint PanelId, string WindowName)> MountedPanels => Mounted;
public static IReadOnlyList<(uint PanelId, string WindowName)> ToolbarPanels => Toolbar;
public static bool TryGetWindowName(uint panelId, out string windowName)
{
foreach (var entry in Mounted)
{
if (entry.PanelId != panelId) continue;
windowName = entry.WindowName;
return true;
}
windowName = string.Empty;
return false;
}
public static bool TryGetPanelId(string windowName, out uint panelId)
{
foreach (var entry in Mounted)
{
if (!string.Equals(entry.WindowName, windowName, StringComparison.Ordinal)) continue;
panelId = entry.PanelId;
return true;
}
panelId = 0;
return false;
}
}