fix(D.5.1): occupancy-gated slot numbers (empty=0x1000005e bg digit) + bottom-right rect probe
FIX 1: UIElement_UIItem::SetShortcutNum (decomp 229481) has a three-way source branch: occupied+peace -> 0x10000042 (peace digit set), occupied+war -> 0x10000043 (war digit set), empty (ItemId==0) -> 0x1000005e (background digit, stance-independent). acdream previously only had the peace/war pair and drew them regardless of occupancy. Changes: - GameWindow.cs: read property 0x1000005e into toolbarEmptyDigits (no fallback; null is safe). Logs entry count. Passes emptyDigits to Bind. Adds [D.5.1 probe] block logging screen pos + size of 7 bottom-right element ids via ScreenPosition. - ToolbarController.cs: adds _emptyDigits field, emptyDigits ctor+Bind param (null default). RestampShortcutNumbers sets cell.EmptyDigits. Comments cite decomp 229481. - UiItemSlot.cs: adds EmptyDigits property + ActiveDigitArray() internal testable seam (occupied -> peace/war by stance; empty -> EmptyDigits). OnDraw uses it. Comment updated with three-way source table. - Tests: 5 new UiItemSlotTests (ActiveDigitArray occupancy), 2 new ToolbarControllerTests (emptyDigits injection + null-safe). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d5a88cd15
commit
a7cad5566b
5 changed files with 200 additions and 24 deletions
|
|
@ -1913,12 +1913,15 @@ public sealed class GameWindow : IDisposable
|
||||||
// (gmToolbarUI). Mirrors the vitals/chat import+bind+mount pattern above.
|
// (gmToolbarUI). Mirrors the vitals/chat import+bind+mount pattern above.
|
||||||
|
|
||||||
// Read the shortcut-slot digit sprite DID arrays from LayoutDesc 0x21000037
|
// Read the shortcut-slot digit sprite DID arrays from LayoutDesc 0x21000037
|
||||||
// (the UIItem cell template): element 0x1000034A under composite 0x10000346,
|
// (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);
|
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||||
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
|
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
|
||||||
|
// Occupancy branch (decomp 229481):
|
||||||
|
// occupied → StateDesc.Properties[0x10000042] (peace) / [0x10000043] (war)
|
||||||
|
// empty → StateDesc.Properties[0x1000005e] (background digit, stance-independent)
|
||||||
uint[]? toolbarPeaceDigits = null;
|
uint[]? toolbarPeaceDigits = null;
|
||||||
uint[]? toolbarWarDigits = null;
|
uint[]? toolbarWarDigits = null;
|
||||||
|
uint[]? toolbarEmptyDigits = null;
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
{
|
{
|
||||||
var uiItemLd = _dats!.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
var uiItemLd = _dats!.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
||||||
|
|
@ -1948,6 +1951,19 @@ public sealed class GameWindow : IDisposable
|
||||||
if (arrWar.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d)
|
if (arrWar.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d)
|
||||||
toolbarWarDigits[i] = d.Value;
|
toolbarWarDigits[i] = d.Value;
|
||||||
}
|
}
|
||||||
|
// Empty-slot background digit: property 0x1000005e, stance-independent.
|
||||||
|
// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
|
||||||
|
// else branch when m_elem_Icon->m_state == 0x1000001c (empty state).
|
||||||
|
// No fallback constants — if absent, empty slots draw no digit (safe).
|
||||||
|
if (props.TryGetValue(0x1000005Eu, out var rawEmpty)
|
||||||
|
&& rawEmpty is DatReaderWriter.Types.ArrayBaseProperty arrEmpty)
|
||||||
|
{
|
||||||
|
toolbarEmptyDigits = new uint[arrEmpty.Value.Count];
|
||||||
|
for (int i = 0; i < arrEmpty.Value.Count; i++)
|
||||||
|
if (arrEmpty.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d)
|
||||||
|
toolbarEmptyDigits[i] = d.Value;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"[D.5.1] empty digit array: {toolbarEmptyDigits?.Length ?? 0} entries.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1966,7 +1982,7 @@ public sealed class GameWindow : IDisposable
|
||||||
{ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
{ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
||||||
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u };
|
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u };
|
||||||
// Report the arrays actually used (after any fallback substitution).
|
// Report the arrays actually used (after any fallback substitution).
|
||||||
Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={toolbarPeaceDigits.Length}, war={toolbarWarDigits.Length} entries.");
|
Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={toolbarPeaceDigits.Length}, war={toolbarWarDigits.Length}, empty={toolbarEmptyDigits?.Length ?? 0} entries.");
|
||||||
|
|
||||||
AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
|
AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
|
|
@ -1980,8 +1996,9 @@ public sealed class GameWindow : IDisposable
|
||||||
iconIds: (type, icon, under, over) => iconComposer.GetIcon(type, icon, under, over),
|
iconIds: (type, icon, under, over) => iconComposer.GetIcon(type, icon, under, over),
|
||||||
useItem: guid => UseItemByGuid(guid),
|
useItem: guid => UseItemByGuid(guid),
|
||||||
combatState: Combat,
|
combatState: Combat,
|
||||||
peaceDigits: toolbarPeaceDigits,
|
peaceDigits: toolbarPeaceDigits,
|
||||||
warDigits: toolbarWarDigits);
|
warDigits: toolbarWarDigits,
|
||||||
|
emptyDigits: toolbarEmptyDigits);
|
||||||
|
|
||||||
var toolbarRoot = toolbarLayout.Root;
|
var toolbarRoot = toolbarLayout.Root;
|
||||||
toolbarRoot.Left = 10; toolbarRoot.Top = 300;
|
toolbarRoot.Left = 10; toolbarRoot.Top = 300;
|
||||||
|
|
@ -1993,6 +2010,25 @@ public sealed class GameWindow : IDisposable
|
||||||
toolbarRoot.ClickThrough = false;
|
toolbarRoot.ClickThrough = false;
|
||||||
toolbarRoot.Draggable = true;
|
toolbarRoot.Draggable = true;
|
||||||
_uiHost.Root.AddChild(toolbarRoot);
|
_uiHost.Root.AddChild(toolbarRoot);
|
||||||
|
|
||||||
|
// [D.5.1 PROBE] Bottom-right geometry rect dump — temporary diagnostic.
|
||||||
|
// Localises the bottom-right mismatch reported by the user; remove once fixed.
|
||||||
|
// ScreenPosition walks Parent chain (UiElement.cs:54-63); Left/Top are parent-relative.
|
||||||
|
// IDs: root=0x10000191, backpack-btn=0x100001B1, backpack-drag=0x1000046C,
|
||||||
|
// last top slot=0x100001AF, last bottom slot=0x100006BF,
|
||||||
|
// row1 right-cap=0x100001B0, row2 right-cap=0x100006C0.
|
||||||
|
{
|
||||||
|
uint[] probeIds = { 0x10000191u, 0x100001B1u, 0x1000046Cu, 0x100001AFu, 0x100006BFu, 0x100001B0u, 0x100006C0u };
|
||||||
|
foreach (var pid in probeIds)
|
||||||
|
{
|
||||||
|
var pe = toolbarLayout.FindElement(pid);
|
||||||
|
if (pe is not null)
|
||||||
|
Console.WriteLine($"[D.5.1 probe] 0x{pid:X8} ({pe.GetType().Name}): screen=({pe.ScreenPosition.X:F1},{pe.ScreenPosition.Y:F1}) left={pe.Left:F1} top={pe.Top:F1} w={pe.Width:F1} h={pe.Height:F1}");
|
||||||
|
else
|
||||||
|
Console.WriteLine($"[D.5.1 probe] 0x{pid:X8}: not found in layout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016).");
|
Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016).");
|
||||||
}
|
}
|
||||||
else Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found.");
|
else Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found.");
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,15 @@ public sealed class ToolbarController
|
||||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||||
|
|
||||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
// 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.
|
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
|
||||||
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||||
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
|
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
|
||||||
|
// Occupancy branch (decomp 229481):
|
||||||
|
// occupied → peace 0x10000042 / war 0x10000043 (split by stance)
|
||||||
|
// empty → background digit 0x1000005e (stance-independent)
|
||||||
private uint[]? _peaceDigits;
|
private uint[]? _peaceDigits;
|
||||||
private uint[]? _warDigits;
|
private uint[]? _warDigits;
|
||||||
|
private uint[]? _emptyDigits;
|
||||||
private bool _peace = true; // true = NonCombat (peace), false = any war stance
|
private bool _peace = true; // true = NonCombat (peace), false = any war stance
|
||||||
|
|
||||||
private ToolbarController(
|
private ToolbarController(
|
||||||
|
|
@ -71,7 +74,8 @@ public sealed class ToolbarController
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState,
|
CombatState? combatState,
|
||||||
uint[]? peaceDigits,
|
uint[]? peaceDigits,
|
||||||
uint[]? warDigits)
|
uint[]? warDigits,
|
||||||
|
uint[]? emptyDigits)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_shortcuts = shortcuts;
|
_shortcuts = shortcuts;
|
||||||
|
|
@ -79,6 +83,7 @@ public sealed class ToolbarController
|
||||||
_useItem = useItem;
|
_useItem = useItem;
|
||||||
_peaceDigits = peaceDigits;
|
_peaceDigits = peaceDigits;
|
||||||
_warDigits = warDigits;
|
_warDigits = warDigits;
|
||||||
|
_emptyDigits = emptyDigits;
|
||||||
|
|
||||||
for (int i = 0; i < SlotIds.Length; i++)
|
for (int i = 0; i < SlotIds.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -133,6 +138,12 @@ public sealed class ToolbarController
|
||||||
/// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
/// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="warDigits">War-mode digit DID array (property 0x10000043, same element).</param>
|
/// <param name="warDigits">War-mode digit DID array (property 0x10000043, same element).</param>
|
||||||
|
/// <param name="emptyDigits">
|
||||||
|
/// Empty-slot background digit DID array (property 0x1000005e, stance-independent).
|
||||||
|
/// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||||||
|
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state).
|
||||||
|
/// Null if the dat lookup failed (empty slots draw no digit, which is safe).
|
||||||
|
/// </param>
|
||||||
public static ToolbarController Bind(
|
public static ToolbarController Bind(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ItemRepository repo,
|
ItemRepository repo,
|
||||||
|
|
@ -141,10 +152,11 @@ public sealed class ToolbarController
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState = null,
|
CombatState? combatState = null,
|
||||||
uint[]? peaceDigits = null,
|
uint[]? peaceDigits = null,
|
||||||
uint[]? warDigits = null)
|
uint[]? warDigits = null,
|
||||||
|
uint[]? emptyDigits = null)
|
||||||
{
|
{
|
||||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||||
peaceDigits, warDigits);
|
peaceDigits, warDigits, emptyDigits);
|
||||||
c.Populate();
|
c.Populate();
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
@ -176,8 +188,9 @@ public sealed class ToolbarController
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-stamp slot number labels after any item change.
|
// Re-stamp slot number labels after any item change.
|
||||||
// Numbers show on ALL top-row slots regardless of item occupancy —
|
// Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481):
|
||||||
// the user's retail screenshot confirms numbers on empty top-row slots.
|
// occupied → peace 0x10000042 / war 0x10000043; empty → background 0x1000005e.
|
||||||
|
// The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511).
|
||||||
RestampShortcutNumbers();
|
RestampShortcutNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,12 +228,14 @@ public sealed class ToolbarController
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Push digit-array references and shortcut-number state into every slot cell.
|
/// Push digit-array references and shortcut-number state into every slot cell.
|
||||||
/// Top row (indices 0–8): SetShortcutNum(i, _peace) — numbers 1–9 on ALL slots
|
/// Top row (indices 0–8): SetShortcutNum(i, _peace) — numbers 1–9 always shown
|
||||||
/// including empty ones (confirmed from user's retail screenshot; the numbers are
|
/// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite
|
||||||
/// slot LABELS, not item indicators).
|
/// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481).
|
||||||
/// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there.
|
/// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there.
|
||||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||||
/// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
|
/// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
|
||||||
|
/// Occupancy → source: occupied → peace 0x10000042 / war 0x10000043;
|
||||||
|
/// empty → background 0x1000005e (decomp 229481/229493).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RestampShortcutNumbers()
|
private void RestampShortcutNumbers()
|
||||||
{
|
{
|
||||||
|
|
@ -230,6 +245,7 @@ public sealed class ToolbarController
|
||||||
if (cell is null) continue;
|
if (cell is null) continue;
|
||||||
cell.PeaceDigits = _peaceDigits;
|
cell.PeaceDigits = _peaceDigits;
|
||||||
cell.WarDigits = _warDigits;
|
cell.WarDigits = _warDigits;
|
||||||
|
cell.EmptyDigits = _emptyDigits;
|
||||||
if (i < 9)
|
if (i < 9)
|
||||||
cell.SetShortcutNum(i, _peace); // top row: slot label digits 1–9 always shown
|
cell.SetShortcutNum(i, _peace); // top row: slot label digits 1–9 always shown
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,20 @@ public sealed class UiItemSlot : UiElement
|
||||||
public bool ShortcutPeace { get; private set; } = true;
|
public bool ShortcutPeace { get; private set; } = true;
|
||||||
|
|
||||||
/// <summary>Peace digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
/// <summary>Peace digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
||||||
/// Injected by the controller after reading LayoutDesc 0x21000037.</summary>
|
/// Injected by the controller after reading LayoutDesc 0x21000037.
|
||||||
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
||||||
|
/// property 0x10000042 (peace) or 0x10000043 (war) by stance.</summary>
|
||||||
public uint[]? PeaceDigits { get; set; }
|
public uint[]? PeaceDigits { get; set; }
|
||||||
|
|
||||||
/// <summary>War digit DID array. Same layout as PeaceDigits.</summary>
|
/// <summary>War digit DID array. Same layout as PeaceDigits.
|
||||||
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229493) — war stance.</summary>
|
||||||
public uint[]? WarDigits { get; set; }
|
public uint[]? WarDigits { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Empty-slot digit DID array (property 0x1000005e, stance-independent).
|
||||||
|
/// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||||||
|
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).</summary>
|
||||||
|
public uint[]? EmptyDigits { get; set; }
|
||||||
|
|
||||||
/// <summary>Set the slot's shortcut position and combat stance so the correct digit
|
/// <summary>Set the slot's shortcut position and combat stance so the correct digit
|
||||||
/// is drawn. Call with index 0..8 for the top row; pass peace=true for NonCombat.</summary>
|
/// is drawn. Call with index 0..8 for the top row; pass peace=true for NonCombat.</summary>
|
||||||
public void SetShortcutNum(int index, bool peace)
|
public void SetShortcutNum(int index, bool peace)
|
||||||
|
|
@ -69,6 +77,20 @@ public sealed class UiItemSlot : UiElement
|
||||||
/// <summary>Clear the shortcut number label (hides the digit).</summary>
|
/// <summary>Clear the shortcut number label (hides the digit).</summary>
|
||||||
public void ClearShortcutNum() { ShortcutNum = -1; }
|
public void ClearShortcutNum() { ShortcutNum = -1; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
|
||||||
|
/// branch in UIElement_UIItem::SetShortcutNum (decomp 229481):
|
||||||
|
/// occupied (ItemId != 0) → ShortcutPeace ? PeaceDigits : WarDigits (0x10000042/43)
|
||||||
|
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
|
||||||
|
/// Exposed as an internal method so unit tests can assert array selection without
|
||||||
|
/// needing a real render context.
|
||||||
|
/// </summary>
|
||||||
|
internal uint[]? ActiveDigitArray()
|
||||||
|
{
|
||||||
|
bool occupied = ItemId != 0;
|
||||||
|
return occupied ? (ShortcutPeace ? PeaceDigits : WarDigits) : EmptyDigits;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Events / draw ─────────────────────────────────────────────────────────
|
// ── Events / draw ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Invoked by <see cref="OnEvent"/> when a left-button-down lands on
|
/// <summary>Invoked by <see cref="OnEvent"/> when a left-button-down lands on
|
||||||
|
|
@ -84,10 +106,8 @@ public sealed class UiItemSlot : UiElement
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
{
|
{
|
||||||
// Draw the icon (filled slot) or the empty-slot border. Both paths fall
|
// Draw the icon (filled slot) or the empty-slot border. Both paths fall through
|
||||||
// through to the digit draw below, so the slot label shows on all top-row
|
// to the digit draw below; the slot label always shows on top-row slots.
|
||||||
// slots regardless of whether they hold an item (retail screenshot confirms
|
|
||||||
// numbers on empty slots).
|
|
||||||
if (ItemId != 0 && IconTexture != 0)
|
if (ItemId != 0 && IconTexture != 0)
|
||||||
{
|
{
|
||||||
ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||||
|
|
@ -100,11 +120,14 @@ public sealed class UiItemSlot : UiElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
// Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||||
// Each digit image is corner-baked (glyph in top-left, rest alpha=0) so we
|
// Occupancy branch (decomp 229481):
|
||||||
// draw it full-cell size and the transparent region is invisible. DrawMode=Alphablend.
|
// occupied (ItemId != 0) → peace/war digit set 0x10000042/43, split by stance
|
||||||
|
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
|
||||||
|
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
|
||||||
|
// full-cell Alphablend so the transparent region is invisible.
|
||||||
if (ShortcutNum >= 0 && SpriteResolve is not null)
|
if (ShortcutNum >= 0 && SpriteResolve is not null)
|
||||||
{
|
{
|
||||||
var arr = ShortcutPeace ? PeaceDigits : WarDigits;
|
var arr = ActiveDigitArray();
|
||||||
if (arr is not null && ShortcutNum < arr.Length)
|
if (arr is not null && ShortcutNum < arr.Length)
|
||||||
{
|
{
|
||||||
uint did = arr[ShortcutNum];
|
uint did = arr[ShortcutNum];
|
||||||
|
|
|
||||||
|
|
@ -171,9 +171,11 @@ public class ToolbarControllerTests
|
||||||
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||||
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
|
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
|
||||||
|
|
||||||
// Fake digit arrays: 9 peace entries (0x10..0x18), 9 war entries (0x20..0x28).
|
// Fake digit arrays: 9 peace entries (0x10..0x18), 9 war entries (0x20..0x28),
|
||||||
|
// 9 empty (background) entries (0x30..0x38).
|
||||||
private static readonly uint[] FakePeace = { 0x10u,0x11u,0x12u,0x13u,0x14u,0x15u,0x16u,0x17u,0x18u };
|
private static readonly uint[] FakePeace = { 0x10u,0x11u,0x12u,0x13u,0x14u,0x15u,0x16u,0x17u,0x18u };
|
||||||
private static readonly uint[] FakeWar = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u };
|
private static readonly uint[] FakeWar = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u };
|
||||||
|
private static readonly uint[] FakeEmpty = { 0x30u,0x31u,0x32u,0x33u,0x34u,0x35u,0x36u,0x37u,0x38u };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// After Bind with peace/war digit arrays, top-row cells (indices 0–8) have
|
/// After Bind with peace/war digit arrays, top-row cells (indices 0–8) have
|
||||||
|
|
@ -272,4 +274,44 @@ public class ToolbarControllerTests
|
||||||
Assert.Same(FakeWar, slots[id].Cell.WarDigits);
|
Assert.Same(FakeWar, slots[id].Cell.WarDigits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EmptyDigits (0x1000005e background digit) is injected into every slot cell.
|
||||||
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — empty-slot branch.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ShortcutNumbers_emptyDigitArrayInjected()
|
||||||
|
{
|
||||||
|
var (layout, slots, _) = FakeToolbar();
|
||||||
|
var repo = new ItemRepository();
|
||||||
|
|
||||||
|
ToolbarController.Bind(layout, repo,
|
||||||
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
|
iconIds: (_,_,_,_) => 0u, useItem: _ => { },
|
||||||
|
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: FakeEmpty);
|
||||||
|
|
||||||
|
foreach (var id in Row1)
|
||||||
|
Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits);
|
||||||
|
foreach (var id in Row2)
|
||||||
|
Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When emptyDigits is null, cells have EmptyDigits == null (no digit on empty slots).
|
||||||
|
/// This is the safe fallback when the dat property 0x1000005e is absent.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ShortcutNumbers_nullEmptyDigits_cellsHaveNullEmptyDigits()
|
||||||
|
{
|
||||||
|
var (layout, slots, _) = FakeToolbar();
|
||||||
|
var repo = new ItemRepository();
|
||||||
|
|
||||||
|
ToolbarController.Bind(layout, repo,
|
||||||
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
|
iconIds: (_,_,_,_) => 0u, useItem: _ => { },
|
||||||
|
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: null);
|
||||||
|
|
||||||
|
foreach (var id in Row1)
|
||||||
|
Assert.Null(slots[id].Cell.EmptyDigits);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,4 +82,63 @@ public class UiItemSlotTests
|
||||||
s.ClearShortcutNum();
|
s.ClearShortcutNum();
|
||||||
Assert.Equal(-1, s.ShortcutNum);
|
Assert.Equal(-1, s.ShortcutNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
|
||||||
|
|
||||||
|
private static readonly uint[] Peace = { 0x10u, 0x11u, 0x12u };
|
||||||
|
private static readonly uint[] War = { 0x20u, 0x21u, 0x22u };
|
||||||
|
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless
|
||||||
|
/// of ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
|
||||||
|
/// else branch when m_elem_Icon->m_state == 0x1000001c (empty).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
|
||||||
|
{
|
||||||
|
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
||||||
|
s.SetShortcutNum(0, peace: true);
|
||||||
|
// ItemId == 0 → EmptyDigits
|
||||||
|
Assert.Same(Empty, s.ActiveDigitArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveDigitArray_emptySlot_warStance_stillReturnsEmptyDigits()
|
||||||
|
{
|
||||||
|
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
||||||
|
s.SetShortcutNum(0, peace: false);
|
||||||
|
// ItemId == 0 → EmptyDigits regardless of stance
|
||||||
|
Assert.Same(Empty, s.ActiveDigitArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When ItemId != 0 (occupied), ActiveDigitArray returns PeaceDigits or WarDigits
|
||||||
|
/// depending on ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ActiveDigitArray_occupiedSlot_peaceStance_returnsPeaceDigits()
|
||||||
|
{
|
||||||
|
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
||||||
|
s.SetItem(0x5001u, 0x99u);
|
||||||
|
s.SetShortcutNum(0, peace: true);
|
||||||
|
Assert.Same(Peace, s.ActiveDigitArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveDigitArray_occupiedSlot_warStance_returnsWarDigits()
|
||||||
|
{
|
||||||
|
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
||||||
|
s.SetItem(0x5001u, 0x99u);
|
||||||
|
s.SetShortcutNum(0, peace: false);
|
||||||
|
Assert.Same(War, s.ActiveDigitArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
|
||||||
|
{
|
||||||
|
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = null };
|
||||||
|
s.SetShortcutNum(0, peace: true);
|
||||||
|
Assert.Null(s.ActiveDigitArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue