diff --git a/docs/ISSUES.md b/docs/ISSUES.md index d498e965..7313b8ba 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,9 +46,39 @@ Copy this block when adding a new issue: --- -## #211 — Login-equipped missile weapon incorrectly requests melee combat +## #212 — Toolbar shortcut numbers turn gray in physical combat **Status:** IN-PROGRESS — implementation complete 2026-07-13, pending user gate +**Severity:** LOW +**Component:** retained UI / toolbar + +**Description:** Entering melee or missile combat changed the numbered overlay +on occupied toolbar shortcuts from the gold mesh to the gray mesh. + +**Root cause:** The port interpreted `UIElement_UIItem::SetShortcutNum`'s Boolean +as peace versus war. Retail names the stored value `m_bShortcutGhosted`, and +`gmToolbarUI::RecvNotice_SetCombatMode` passes true only for Magic combat mode. +The false interpretation therefore ghosted every occupied shortcut in every +combat stance. + +**Resolution:** Shortcut state and digit arrays now use retail's regular/ghosted +terminology. NonCombat, Melee, and Missile select property `0x10000042`; Magic +alone selects ghosted property `0x10000043`. Empty slots continue to use the +stance-independent `0x1000005E` array. + +**Research:** `docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md` +§2.4; `UIElement_UIItem::SetShortcutNum @ 0x004E1590`; +`gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610`. + +**Acceptance:** Compare an occupied numbered toolbar slot in peace, melee, and +missile modes: the mesh remains gold/yellow. Enter Magic mode: physical-item +shortcuts use the gray ghosted mesh. + +--- + +## #211 — Login-equipped missile weapon incorrectly requests melee combat + +**Status:** DONE — 2026-07-13, user confirmed combat entry works (`ab98cda2`) **Severity:** HIGH **Component:** inventory projection / combat mode diff --git a/docs/research/2026-06-17-stateful-icon-system-handoff.md b/docs/research/2026-06-17-stateful-icon-system-handoff.md index e1c952d5..ca23069a 100644 --- a/docs/research/2026-06-17-stateful-icon-system-handoff.md +++ b/docs/research/2026-06-17-stateful-icon-system-handoff.md @@ -40,7 +40,7 @@ Plus a special case at `407546` (`0058d1ee`): **`IsThePlayer`** → `m_idIcon = - **`src/AcDream.Core/Items/ItemInstance.cs`** — has `IconId`, `IconUnderlayId`, `IconOverlayId`, `Type`. **No `Effects`/`UiEffects` field yet.** - **`src/AcDream.Core/Items/ItemRepository.cs`** — `EnrichItem(objectId, iconId, name, type, iconOverlayId=0, iconUnderlayId=0)` writes the typed icon ids onto an existing item + fires `ItemPropertiesUpdated`. Threaded from `WorldSession.EntitySpawned` → `GameWindow.OnLiveEntitySpawned`. - **`src/AcDream.App/UI/Layout/ToolbarController.cs`** — calls `iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId)` per slot, re-runs `Populate()` on `ItemRepository.ItemAdded`/`ItemPropertiesUpdated` (so a late `CreateObject` re-binds the slot's icon). -- **(related, not icon-composite)** the **slot-number** system (`SetShortcutNum`, 3 digit arrays: occupied peace/war `0x10000042`/`0x10000043` from cell composite `0x10000346`, empty/background `0x1000005e` from composite `0x10000341`) is done — it's a separate `UIElement_UIItem` feature, not the icon composite, but lives on the same widget. +- **(related, not icon-composite)** the **slot-number** system (`SetShortcutNum`, 3 digit arrays: occupied regular/ghosted `0x10000042`/`0x10000043` from cell composite `0x10000346`, empty/background `0x1000005e` from composite `0x10000341`) is done — it's a separate `UIElement_UIItem` feature, not the icon composite, but lives on the same widget. The Boolean is `m_bShortcutGhosted`, true only in Magic mode; it is not peace/war. --- diff --git a/docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md b/docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md index 5c357be2..2b51e171 100644 --- a/docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md +++ b/docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md @@ -224,6 +224,45 @@ parse/store/write even though this toolbar ignores it. Do not add spell icons or spell casting to `gmToolbarUI`; implement the eight favorite-spell lists as a separate subsystem. +### 2.4 Physical-shortcut number ghosting + +The Boolean parameter of `UIElement_UIItem::SetShortcutNum @ 0x004E1590` is +not a peace/war selector. `ACCWeenieObject::SetShortcutNum @ 0x0058C100` +stores it verbatim as `m_bShortcutGhosted`, and `UIElement_UIItem::Update` +replays that named field into the widget. The digit-array selection is: + +```text +UIElement_UIItem.SetShortcutNum(slot, ghosted): // 0x004E1590 + if slot < 0: + hide shortcut-number element + return + + if item icon is in empty state 0x1000001C: + digits = property 0x1000005E // empty slot + else if ghosted: + digits = property 0x10000043 // gray mesh + else: + digits = property 0x10000042 // gold mesh + + set number image to digits[slot] + show shortcut-number element +``` + +`gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610` first stores +`mode != MAGIC_COMBAT_MODE`, then passes the inverse to every occupied +shortcut cell. In direct pseudocode: + +```text +shortcutsGhosted = (mode == MAGIC_COMBAT_MODE) +for each occupied shortcut slot i: + cell.SetShortcutNum(i, shortcutsGhosted) +``` + +Therefore NonCombat, Melee, and Missile all use property `0x10000042`; only +Magic uses the ghosted `0x10000043` sheet. Calling these arrays “peace” and +“war” reverses the meaning and incorrectly grays physical shortcut numbers in +melee and missile combat. + --- ## 3. `gmToolbarUI` input, use, and selection pseudocode diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index 92f11e82..1437f0f9 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -121,8 +121,8 @@ public static class FixtureProvider iconIds: MakeIconIds(stack), useItem: _ => { }, combatState: null, - peaceDigits: null, - warDigits: null, + regularDigits: null, + ghostedDigits: null, emptyDigits: null, sendAddShortcut: null, sendRemoveShortcut: null); diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 8d36f5b9..378ae05e 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -81,14 +81,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont // Digit sprite DID arrays for slot labels (top row, numbers 1-9). // 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. + // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps ghosting. // Occupancy branch (decomp 229481): - // occupied → peace 0x10000042 / war 0x10000043 (split by stance) + // occupied → regular 0x10000042 / ghosted 0x10000043 // empty → background digit 0x1000005e (stance-independent) - private uint[]? _peaceDigits; - private uint[]? _warDigits; + private uint[]? _regularDigits; + private uint[]? _ghostedDigits; private uint[]? _emptyDigits; - private bool _peace = true; // true = NonCombat (peace), false = any war stance + private bool _shortcutsGhosted; private ToolbarController( ImportedLayout layout, @@ -97,8 +97,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont Func iconIds, Action useItem, CombatState? combatState, - uint[]? peaceDigits, - uint[]? warDigits, + uint[]? regularDigits, + uint[]? ghostedDigits, uint[]? emptyDigits, ItemInteractionController? itemInteraction = null, Action? sendAddShortcut = null, @@ -117,8 +117,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont _iconIds = iconIds; _dragIconIds = dragIconIds; _useItem = useItem; - _peaceDigits = peaceDigits; - _warDigits = warDigits; + _regularDigits = regularDigits; + _ghostedDigits = ghostedDigits; _emptyDigits = emptyDigits; _itemInteraction = itemInteraction; _sendAddShortcut = sendAddShortcut; @@ -294,13 +294,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont /// combat-mode indicator elements accordingly. /// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator). /// - /// - /// Peace-mode digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element + /// + /// Regular 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). /// - /// War-mode digit DID array (property 0x10000043, same element). + /// Ghosted digit DID array (property 0x10000043, same element). /// /// Empty-slot background digit DID array (property 0x1000005e, stance-independent). /// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum @@ -314,8 +314,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont Func iconIds, Action useItem, CombatState? combatState = null, - uint[]? peaceDigits = null, - uint[]? warDigits = null, + uint[]? regularDigits = null, + uint[]? ghostedDigits = null, uint[]? emptyDigits = null, ItemInteractionController? itemInteraction = null, Action? sendAddShortcut = null, @@ -329,7 +329,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont Func? dragIconIds = null) { var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState, - peaceDigits, warDigits, emptyDigits, itemInteraction, + regularDigits, ghostedDigits, emptyDigits, itemInteraction, sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem, selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont, dragIconIds); @@ -413,7 +413,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont // Re-stamp slot number labels after any item change. // Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481): - // occupied → peace 0x10000042 / war 0x10000043; empty → background 0x1000005e. + // occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e. // The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511). RestampShortcutNumbers(); } @@ -444,21 +444,22 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont 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); + // The bool passed to SetShortcutNum is m_bShortcutGhosted, not a peace/war + // selector. Retail ghosts physical-item shortcuts only in Magic mode. + // Retail ref: gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610. + _shortcutsGhosted = mode == CombatMode.Magic; RestampShortcutNumbers(); } /// /// Push digit-array references and shortcut-number state into every slot cell. - /// Top row (indices 0–8): SetShortcutNum(i, _peace) — numbers 1–9 always shown + /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown /// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite /// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481). /// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there. /// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); /// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). - /// Occupancy → source: occupied → peace 0x10000042 / war 0x10000043; + /// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043; /// empty → background 0x1000005e (decomp 229481/229493). /// private void RestampShortcutNumbers() @@ -467,11 +468,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont { var cell = _slots[i]?.Cell; if (cell is null) continue; - cell.PeaceDigits = _peaceDigits; - cell.WarDigits = _warDigits; + cell.RegularDigits = _regularDigits; + cell.GhostedDigits = _ghostedDigits; cell.EmptyDigits = _emptyDigits; if (i < 9) - cell.SetShortcutNum(i, _peace); // top row: slot label digits 1–9 always shown + cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown else cell.ClearShortcutNum(); // bottom row: no slot labels } diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 43dfb6a9..cf03e1f4 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -403,11 +403,11 @@ public sealed class RetailUiRuntime : IDisposable return; } - var (peace, war, empty) = LoadToolbarDigits(); + var (regular, ghosted, empty) = LoadToolbarDigits(); ToolbarRuntimeBindings b = _bindings.Toolbar; ToolbarController = Layout.ToolbarController.Bind( layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat, - peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut, + regular, ghosted, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut, toggleCombat: b.ToggleCombat, selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar), selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u, @@ -586,9 +586,9 @@ public sealed class RetailUiRuntime : IDisposable Console.WriteLine("[D.6] retail jump bar from gmFloatyPowerBarUI LayoutDesc 0x21000072."); } - private (uint[] Peace, uint[] War, uint[]? Empty) LoadToolbarDigits() + private (uint[] Regular, uint[] Ghosted, uint[]? Empty) LoadToolbarDigits() { - uint[]? peace = null, war = null, empty = null; + uint[]? regular = null, ghosted = null, empty = null; lock (_bindings.Assets.DatLock) { var layout = _bindings.Assets.Dats.Get(0x21000037u); @@ -597,8 +597,8 @@ public sealed class RetailUiRuntime : IDisposable && composite.Children.TryGetValue(0x1000034Au, out var number) && number.StateDesc?.Properties is { } props) { - peace = ReadDataIds(props, 0x10000042u); - war = ReadDataIds(props, 0x10000043u); + regular = ReadDataIds(props, 0x10000042u); + ghosted = ReadDataIds(props, 0x10000043u); } if (layout is not null && layout.Elements.TryGetValue(0x10000341u, out var emptyComposite) @@ -607,18 +607,18 @@ public sealed class RetailUiRuntime : IDisposable empty = ReadDataIds(emptyProps, 0x1000005Eu); } - peace ??= + regular ??= [ 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u, 0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u, ]; - war ??= + ghosted ??= [ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u, 0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u, ]; - Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={peace.Length}, war={war.Length}, empty={empty?.Length ?? 0} entries."); - return (peace, war, empty); + Console.WriteLine($"[D.5.1] toolbar digit arrays ready: regular={regular.Length}, ghosted={ghosted.Length}, empty={empty?.Length ?? 0} entries."); + return (regular, ghosted, empty); } private static uint[]? ReadDataIds( diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index c7db762b..340eb372 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -166,7 +166,7 @@ public sealed class UiItemSlot : UiElement // ── Shortcut number (slot label) ───────────────────────────────────────── // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Retail draws the digit on the cell's ShortcutNum sub-element, picking the - // digit image from a DID-array property: 0x10000042 (peace) / 0x10000043 (war), + // digit image from a DID-array property: 0x10000042 (regular) / 0x10000043 (ghosted), // indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface // with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend. @@ -174,30 +174,30 @@ public sealed class UiItemSlot : UiElement /// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1. public int ShortcutNum { get; private set; } = -1; - /// True = draw peace digit set; false = war digit set. - public bool ShortcutPeace { get; private set; } = true; + /// True when retail marks the physical-item shortcut as ghosted. + public bool ShortcutGhosted { get; private set; } - /// Peace digit DID array. Index i → digit (i+1) sprite RenderSurface id. + /// Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id. /// 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. - public uint[]? PeaceDigits { get; set; } + /// property 0x10000042 when the shortcut is not ghosted. + public uint[]? RegularDigits { get; set; } - /// War digit DID array. Same layout as PeaceDigits. - /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229493) — war stance. - public uint[]? WarDigits { get; set; } + /// Ghosted digit DID array. Same layout as RegularDigits. + /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229485-229488). + public uint[]? GhostedDigits { get; set; } /// 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). public uint[]? EmptyDigits { get; set; } - /// 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. - public void SetShortcutNum(int index, bool peace) + /// Set the slot's shortcut position and ghosted state so the correct digit + /// is drawn. Call with index 0..8 for the top row. + public void SetShortcutNum(int index, bool ghosted) { - ShortcutNum = index; - ShortcutPeace = peace; + ShortcutNum = index; + ShortcutGhosted = ghosted; } /// Clear the shortcut number label (hides the digit). @@ -206,7 +206,7 @@ public sealed class UiItemSlot : UiElement /// /// 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) + /// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42) /// 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. @@ -214,7 +214,7 @@ public sealed class UiItemSlot : UiElement internal uint[]? ActiveDigitArray() { bool occupied = ItemId != 0; - return occupied ? (ShortcutPeace ? PeaceDigits : WarDigits) : EmptyDigits; + return occupied ? (ShortcutGhosted ? GhostedDigits : RegularDigits) : EmptyDigits; } // ── Events / draw ───────────────────────────────────────────────────────── @@ -292,7 +292,7 @@ public sealed class UiItemSlot : UiElement // Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). // Occupancy branch (decomp 229481): - // occupied (ItemId != 0) → peace/war digit set 0x10000042/43, split by stance + // occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43 // 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. diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index 6378af55..e4b49b64 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -681,15 +681,15 @@ public class ToolbarControllerTests // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). - // Fake digit arrays: 9 peace entries (0x10..0x18), 9 war entries (0x20..0x28), + // Fake digit arrays: 9 regular entries (0x10..0x18), 9 ghosted 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[] FakeWar = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u }; + private static readonly uint[] FakeRegular = { 0x10u,0x11u,0x12u,0x13u,0x14u,0x15u,0x16u,0x17u,0x18u }; + private static readonly uint[] FakeGhosted = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u }; private static readonly uint[] FakeEmpty = { 0x30u,0x31u,0x32u,0x33u,0x34u,0x35u,0x36u,0x37u,0x38u }; /// - /// After Bind with peace/war digit arrays, top-row cells (indices 0–8) have - /// ShortcutNum == i (the slot position) and ShortcutPeace == true (default NonCombat). + /// After Bind with regular/ghosted digit arrays, top-row cells (indices 0–8) have + /// ShortcutNum == i (the slot position) and ShortcutGhosted == false (default NonCombat). /// Bottom-row cells (indices 9–17) have ShortcutNum == -1 (no label). /// Retail: numbers are slot LABELS — shown on ALL top-row slots including empty ones. /// @@ -702,14 +702,14 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted); - // Top row: ShortcutNum == slot index, peace == true. + // Top row: ShortcutNum == slot index, ghosted == false. for (int i = 0; i < Row1.Length; i++) { var cell = slots[Row1[i]].Cell; Assert.Equal(i, cell.ShortcutNum); - Assert.True(cell.ShortcutPeace, $"top-row slot {i} should be peace at NonCombat"); + Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular at NonCombat"); } // Bottom row: no shortcut number. foreach (var id in Row2) @@ -717,26 +717,31 @@ public class ToolbarControllerTests } /// - /// After SetCombatMode(Melee), top-row cells switch to ShortcutPeace == false (war). + /// Retail keeps physical shortcut labels regular in NonCombat, Melee, and Missile. + /// gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610 passes ghosted=true only for Magic. /// - [Fact] - public void ShortcutNumbers_setCombatModeWar_topRowUsesWarDigits() + [Theory] + [InlineData(CombatMode.NonCombat)] + [InlineData(CombatMode.Melee)] + [InlineData(CombatMode.Missile)] + public void ShortcutNumbers_nonMagicModes_useRegularDigits(CombatMode mode) { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted); - ctrl.SetCombatMode(CombatMode.Melee); + ctrl.SetCombatMode(mode); - // Top row: still ShortcutNum == i, but now peace == false. for (int i = 0; i < Row1.Length; i++) { var cell = slots[Row1[i]].Cell; - Assert.Equal(i, cell.ShortcutNum); - Assert.False(cell.ShortcutPeace, $"top-row slot {i} should be war after Melee"); + Assert.Equal(i, cell.ShortcutNum); + Assert.False(cell.ShortcutGhosted, $"top-row slot {i} should be regular in {mode}"); + cell.SetItem((uint)(0x5000 + i), 0x99u); + Assert.Same(FakeRegular, cell.ActiveDigitArray()); } // Bottom row still has no number. foreach (var id in Row2) @@ -744,28 +749,38 @@ public class ToolbarControllerTests } /// - /// After SetCombatMode back to NonCombat, top-row switches back to peace (ShortcutPeace == true). + /// Magic mode ghosts physical shortcut labels, and returning to NonCombat restores them. /// [Fact] - public void ShortcutNumbers_backToNonCombat_restoresPeaceDigits() + public void ShortcutNumbers_magicGhosts_thenNonCombatRestoresRegularDigits() { var (layout, slots, _) = FakeToolbar(); var repo = new ClientObjectTable(); var ctrl = ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted); + + ctrl.SetCombatMode(CombatMode.Magic); + foreach (var id in Row1) + { + slots[id].Cell.SetItem(id, 0x99u); + Assert.True(slots[id].Cell.ShortcutGhosted); + Assert.Same(FakeGhosted, slots[id].Cell.ActiveDigitArray()); + } - ctrl.SetCombatMode(CombatMode.Melee); ctrl.SetCombatMode(CombatMode.NonCombat); for (int i = 0; i < Row1.Length; i++) - Assert.True(slots[Row1[i]].Cell.ShortcutPeace, - $"top-row slot {i} should be peace after returning to NonCombat"); + { + Assert.False(slots[Row1[i]].Cell.ShortcutGhosted, + $"top-row slot {i} should be regular after returning to NonCombat"); + Assert.Same(FakeRegular, slots[Row1[i]].Cell.ActiveDigitArray()); + } } /// - /// Digit arrays are correctly injected into each cell (PeaceDigits + WarDigits references). + /// Digit arrays are correctly injected into each cell. /// [Fact] public void ShortcutNumbers_digitArraysInjected() @@ -776,12 +791,12 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted); foreach (var id in Row1) { - Assert.Same(FakePeace, slots[id].Cell.PeaceDigits); - Assert.Same(FakeWar, slots[id].Cell.WarDigits); + Assert.Same(FakeRegular, slots[id].Cell.RegularDigits); + Assert.Same(FakeGhosted, slots[id].Cell.GhostedDigits); } } @@ -798,7 +813,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: FakeEmpty); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty); foreach (var id in Row1) Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits); @@ -819,7 +834,7 @@ public class ToolbarControllerTests ToolbarController.Bind(layout, repo, () => Array.Empty(), iconIds: (_,_,_,_,_) => 0u, useItem: _ => { }, - peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: null); + regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null); foreach (var id in Row1) Assert.Null(slots[id].Cell.EmptyDigits); diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs index 08da5cbe..1118b8c4 100644 --- a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs +++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs @@ -66,95 +66,95 @@ public class UiItemSlotTests } [Fact] - public void ShortcutPeace_defaultIsTrue() + public void ShortcutGhosted_defaultsFalse() { var s = new UiItemSlot(); - Assert.True(s.ShortcutPeace); + Assert.False(s.ShortcutGhosted); } [Fact] - public void SetShortcutNum_setsIndexAndPeace() + public void SetShortcutNum_setsIndexAndGhostedState() { var s = new UiItemSlot(); - s.SetShortcutNum(3, peace: false); + s.SetShortcutNum(3, ghosted: true); Assert.Equal(3, s.ShortcutNum); - Assert.False(s.ShortcutPeace); + Assert.True(s.ShortcutGhosted); } [Fact] - public void SetShortcutNum_peaceTrue() + public void SetShortcutNum_regularState() { var s = new UiItemSlot(); - s.SetShortcutNum(0, peace: true); + s.SetShortcutNum(0, ghosted: false); Assert.Equal(0, s.ShortcutNum); - Assert.True(s.ShortcutPeace); + Assert.False(s.ShortcutGhosted); } [Fact] public void ClearShortcutNum_setsMinusOne() { var s = new UiItemSlot(); - s.SetShortcutNum(5, peace: true); + s.SetShortcutNum(5, ghosted: false); s.ClearShortcutNum(); 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[] Regular = { 0x10u, 0x11u, 0x12u }; + private static readonly uint[] Ghosted = { 0x20u, 0x21u, 0x22u }; private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u }; /// /// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless - /// of ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — + /// of ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — /// else branch when m_elem_Icon->m_state == 0x1000001c (empty). /// [Fact] public void ActiveDigitArray_emptySlot_returnsEmptyDigits() { - var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty }; - s.SetShortcutNum(0, peace: true); + var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; + s.SetShortcutNum(0, ghosted: false); // ItemId == 0 → EmptyDigits Assert.Same(Empty, s.ActiveDigitArray()); } [Fact] - public void ActiveDigitArray_emptySlot_warStance_stillReturnsEmptyDigits() + public void ActiveDigitArray_emptySlot_ghosted_stillReturnsEmptyDigits() { - var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty }; - s.SetShortcutNum(0, peace: false); + var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; + s.SetShortcutNum(0, ghosted: true); // ItemId == 0 → EmptyDigits regardless of stance Assert.Same(Empty, s.ActiveDigitArray()); } /// - /// When ItemId != 0 (occupied), ActiveDigitArray returns PeaceDigits or WarDigits - /// depending on ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493). + /// When ItemId != 0 (occupied), ActiveDigitArray returns RegularDigits or GhostedDigits + /// depending on ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493). /// [Fact] - public void ActiveDigitArray_occupiedSlot_peaceStance_returnsPeaceDigits() + public void ActiveDigitArray_occupiedSlot_regular_returnsRegularDigits() { - var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty }; + var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; s.SetItem(0x5001u, 0x99u); - s.SetShortcutNum(0, peace: true); - Assert.Same(Peace, s.ActiveDigitArray()); + s.SetShortcutNum(0, ghosted: false); + Assert.Same(Regular, s.ActiveDigitArray()); } [Fact] - public void ActiveDigitArray_occupiedSlot_warStance_returnsWarDigits() + public void ActiveDigitArray_occupiedSlot_ghosted_returnsGhostedDigits() { - var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty }; + var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty }; s.SetItem(0x5001u, 0x99u); - s.SetShortcutNum(0, peace: false); - Assert.Same(War, s.ActiveDigitArray()); + s.SetShortcutNum(0, ghosted: true); + Assert.Same(Ghosted, s.ActiveDigitArray()); } [Fact] public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull() { - var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = null }; - s.SetShortcutNum(0, peace: true); + var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = null }; + s.SetShortcutNum(0, ghosted: false); Assert.Null(s.ActiveDigitArray()); }