feat(D.5.1): faithful toolbar slot numbers 1-9 (SetShortcutNum digit sprites, peace/war)

Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465) and
gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). The 9 top-row toolbar slots
show digit labels 1-9 at all times (even when empty — confirmed from the user''s
retail screenshot). The digit sprites are real 32x32 PFID_A8R8G8B8 RenderSurfaces
with glyphs baked into the top-left corner (rest alpha=0), drawn Alphablend over
the slot icon/empty sprite.

Digit DID arrays (peace: property 0x10000042, war: 0x10000043) are read at startup
from LayoutDesc 0x21000037 element 0x1000034A under composite 0x10000346 using the
same ArrayBaseProperty{DataIdBaseProperty} pattern as LayoutImporter.ReadState.
A cited-constant fallback (same confirmed dat ids) is used if the dat navigation
fails. The war glyph set (darker/golden glyphs) switches on any combat stance;
peace glyphs (lighter) restore on NonCombat — re-stamped by RestampShortcutNumbers()
called from both Populate() and SetCombatMode().

Changes:
- UiItemSlot: ShortcutNum/ShortcutPeace/PeaceDigits/WarDigits state; SetShortcutNum/
  ClearShortcutNum; OnDraw restructured (no early return) so digit draws after icon.
- ToolbarController: _peaceDigits/_warDigits/_peace fields; Bind() gains peaceDigits/
  warDigits optional params; RestampShortcutNumbers() helper; Populate() and
  SetCombatMode() both call RestampShortcutNumbers().
- GameWindow: reads digit arrays under _datLock from LayoutDesc 0x21000037, passes to
  Bind(); cited constants as fallback.
- Tests: 5 new UiItemSlotTests (SetShortcutNum/ClearShortcutNum state); 4 new
  ToolbarControllerTests (top-row/bottom-row labels, peace/war switch, array injection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-17 13:52:50 +02:00
parent f21dbfad80
commit b2a812d1fa
5 changed files with 328 additions and 6 deletions

View file

@ -1911,6 +1911,64 @@ public sealed class GameWindow : IDisposable
// Phase D.5.1 — toolbar window, data-driven from LayoutDesc 0x21000016
// (gmToolbarUI). Mirrors the vitals/chat import+bind+mount pattern above.
// Read the shortcut-slot digit sprite DID arrays from LayoutDesc 0x21000037
// (the UIItem cell template): element 0x1000034A under composite 0x10000346,
// StateDesc.Properties[0x10000042] = peace digits, [0x10000043] = war digits.
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
uint[]? toolbarPeaceDigits = null;
uint[]? toolbarWarDigits = null;
lock (_datLock)
{
var uiItemLd = _dats!.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
if (uiItemLd is not null
&& uiItemLd.Elements.TryGetValue(0x10000346u, out var composite)
&& composite.Children.TryGetValue(0x1000034Au, out var shortcutNumElem)
&& shortcutNumElem.StateDesc is { } sd
&& sd.Properties is { } props)
{
// Mirror LayoutImporter.ReadState: Properties[key] is ArrayBaseProperty
// containing DataIdBaseProperty entries. Each DataIdBaseProperty.Value is
// the RenderSurface DID for that digit.
// Peace: property 0x10000042; War: property 0x10000043.
if (props.TryGetValue(0x10000042u, out var rawPeace)
&& rawPeace is DatReaderWriter.Types.ArrayBaseProperty arrPeace)
{
toolbarPeaceDigits = new uint[arrPeace.Value.Count];
for (int i = 0; i < arrPeace.Value.Count; i++)
if (arrPeace.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d)
toolbarPeaceDigits[i] = d.Value;
}
if (props.TryGetValue(0x10000043u, out var rawWar)
&& rawWar is DatReaderWriter.Types.ArrayBaseProperty arrWar)
{
toolbarWarDigits = new uint[arrWar.Value.Count];
for (int i = 0; i < arrWar.Value.Count; i++)
if (arrWar.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d)
toolbarWarDigits[i] = d.Value;
}
Console.WriteLine(toolbarPeaceDigits is not null
? $"[D.5.1] digit arrays loaded: peace={toolbarPeaceDigits.Length}, war={toolbarWarDigits?.Length ?? 0} entries."
: "[D.5.1] digit arrays: property 0x10000042 not found in element 0x1000034A — falling back to cited constants.");
}
else
{
Console.WriteLine("[D.5.1] digit arrays: element 0x1000034A/0x10000346 not found in LayoutDesc 0x21000037 — falling back to cited constants.");
}
}
// Cited-constant fallback (UIElement_UIItem::SetShortcutNum, decomp 229465 + dat probe).
// Used when the dat navigation above fails (e.g. missing LayoutDesc in older dat).
if (toolbarPeaceDigits is null)
toolbarPeaceDigits = new uint[]
{ 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u,
0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u };
if (toolbarWarDigits is null)
toolbarWarDigits = new uint[]
{ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u };
AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
lock (_datLock)
toolbarLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
@ -1922,7 +1980,9 @@ public sealed class GameWindow : IDisposable
() => Shortcuts,
iconIds: (type, icon, under, over) => iconComposer.GetIcon(type, icon, under, over),
useItem: guid => UseItemByGuid(guid),
combatState: Combat);
combatState: Combat,
peaceDigits: toolbarPeaceDigits,
warDigits: toolbarWarDigits);
var toolbarRoot = toolbarLayout.Root;
toolbarRoot.Left = 10; toolbarRoot.Top = 300;