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

@ -54,18 +54,31 @@ public sealed class ToolbarController
private readonly Func<ItemType, uint, uint, uint, uint> _iconIds; // (itemType, iconId, underlayId, overlayId) → GL tex
private readonly Action<uint> _useItem; // guid → fire UseObject
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
// Peace set: property 0x10000042; war set: property 0x10000043.
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
private uint[]? _peaceDigits;
private uint[]? _warDigits;
private bool _peace = true; // true = NonCombat (peace), false = any war stance
private ToolbarController(
ImportedLayout layout,
ItemRepository repo,
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
Func<ItemType, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState)
CombatState? combatState,
uint[]? peaceDigits,
uint[]? warDigits)
{
_repo = repo;
_shortcuts = shortcuts;
_iconIds = iconIds;
_useItem = useItem;
_peaceDigits = peaceDigits;
_warDigits = warDigits;
for (int i = 0; i < SlotIds.Length; i++)
{
@ -113,15 +126,25 @@ public sealed class ToolbarController
/// combat-mode indicator elements accordingly.
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
/// </param>
/// <param name="peaceDigits">
/// Peace-mode digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element
/// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id.
/// Null if the dat lookup failed (no digits drawn). Retail reference:
/// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
/// </param>
/// <param name="warDigits">War-mode digit DID array (property 0x10000043, same element).</param>
public static ToolbarController Bind(
ImportedLayout layout,
ItemRepository repo,
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
Func<ItemType, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState = null)
CombatState? combatState = null,
uint[]? peaceDigits = null,
uint[]? warDigits = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState);
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
peaceDigits, warDigits);
c.Populate();
return c;
}
@ -151,6 +174,11 @@ public sealed class ToolbarController
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId);
list.Cell.SetItem(sc.ObjectGuid, tex);
}
// Re-stamp slot number labels after any item change.
// Numbers show on ALL top-row slots regardless of item occupancy —
// the user's retail screenshot confirms numbers on empty top-row slots.
RestampShortcutNumbers();
}
/// <summary>
@ -178,6 +206,35 @@ public sealed class ToolbarController
if (_combatIndicators[i] is { } e)
e.Visible = show[i];
}
// Re-stamp digit set: peace glyphs in NonCombat, war glyphs in any combat stance.
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196610-196621).
_peace = (mode == CombatMode.NonCombat);
RestampShortcutNumbers();
}
/// <summary>
/// Push digit-array references and shortcut-number state into every slot cell.
/// Top row (indices 08): SetShortcutNum(i, _peace) — numbers 19 on ALL slots
/// including empty ones (confirmed from user's retail screenshot; the numbers are
/// slot LABELS, not item indicators).
/// Bottom row (indices 917): ClearShortcutNum() — retail shows no numbers there.
/// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
/// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
/// </summary>
private void RestampShortcutNumbers()
{
for (int i = 0; i < _slots.Length; i++)
{
var cell = _slots[i]?.Cell;
if (cell is null) continue;
cell.PeaceDigits = _peaceDigits;
cell.WarDigits = _warDigits;
if (i < 9)
cell.SetShortcutNum(i, _peace); // top row: slot label digits 19 always shown
else
cell.ClearShortcutNum(); // bottom row: no slot labels
}
}
/// <summary>