fix #212: preserve shortcut tint in physical combat
Port the retail m_bShortcutGhosted meaning instead of treating SetShortcutNum's boolean as peace versus war. Keep occupied shortcut numbers on the gold sheet in NonCombat, Melee, and Missile, reserve the gray sheet for Magic, and lock the four-mode matrix with conformance tests and corrected research. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
ab98cda26b
commit
5090aa7217
9 changed files with 197 additions and 112 deletions
|
|
@ -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
|
**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
|
**Severity:** HIGH
|
||||||
**Component:** inventory projection / combat mode
|
**Component:** inventory projection / combat mode
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/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.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).
|
- **`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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
spell casting to `gmToolbarUI`; implement the eight favorite-spell lists as a
|
||||||
separate subsystem.
|
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
|
## 3. `gmToolbarUI` input, use, and selection pseudocode
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,8 @@ public static class FixtureProvider
|
||||||
iconIds: MakeIconIds(stack),
|
iconIds: MakeIconIds(stack),
|
||||||
useItem: _ => { },
|
useItem: _ => { },
|
||||||
combatState: null,
|
combatState: null,
|
||||||
peaceDigits: null,
|
regularDigits: null,
|
||||||
warDigits: null,
|
ghostedDigits: null,
|
||||||
emptyDigits: null,
|
emptyDigits: null,
|
||||||
sendAddShortcut: null,
|
sendAddShortcut: null,
|
||||||
sendRemoveShortcut: null);
|
sendRemoveShortcut: null);
|
||||||
|
|
|
||||||
|
|
@ -81,14 +81,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||||
// 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 ghosting.
|
||||||
// Occupancy branch (decomp 229481):
|
// Occupancy branch (decomp 229481):
|
||||||
// occupied → peace 0x10000042 / war 0x10000043 (split by stance)
|
// occupied → regular 0x10000042 / ghosted 0x10000043
|
||||||
// empty → background digit 0x1000005e (stance-independent)
|
// empty → background digit 0x1000005e (stance-independent)
|
||||||
private uint[]? _peaceDigits;
|
private uint[]? _regularDigits;
|
||||||
private uint[]? _warDigits;
|
private uint[]? _ghostedDigits;
|
||||||
private uint[]? _emptyDigits;
|
private uint[]? _emptyDigits;
|
||||||
private bool _peace = true; // true = NonCombat (peace), false = any war stance
|
private bool _shortcutsGhosted;
|
||||||
|
|
||||||
private ToolbarController(
|
private ToolbarController(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
|
|
@ -97,8 +97,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState,
|
CombatState? combatState,
|
||||||
uint[]? peaceDigits,
|
uint[]? regularDigits,
|
||||||
uint[]? warDigits,
|
uint[]? ghostedDigits,
|
||||||
uint[]? emptyDigits,
|
uint[]? emptyDigits,
|
||||||
ItemInteractionController? itemInteraction = null,
|
ItemInteractionController? itemInteraction = null,
|
||||||
Action<ShortcutEntry>? sendAddShortcut = null,
|
Action<ShortcutEntry>? sendAddShortcut = null,
|
||||||
|
|
@ -117,8 +117,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
_iconIds = iconIds;
|
_iconIds = iconIds;
|
||||||
_dragIconIds = dragIconIds;
|
_dragIconIds = dragIconIds;
|
||||||
_useItem = useItem;
|
_useItem = useItem;
|
||||||
_peaceDigits = peaceDigits;
|
_regularDigits = regularDigits;
|
||||||
_warDigits = warDigits;
|
_ghostedDigits = ghostedDigits;
|
||||||
_emptyDigits = emptyDigits;
|
_emptyDigits = emptyDigits;
|
||||||
_itemInteraction = itemInteraction;
|
_itemInteraction = itemInteraction;
|
||||||
_sendAddShortcut = sendAddShortcut;
|
_sendAddShortcut = sendAddShortcut;
|
||||||
|
|
@ -294,13 +294,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
/// combat-mode indicator elements accordingly.
|
/// combat-mode indicator elements accordingly.
|
||||||
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
|
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="peaceDigits">
|
/// <param name="regularDigits">
|
||||||
/// 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.
|
/// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id.
|
||||||
/// Null if the dat lookup failed (no digits drawn). Retail reference:
|
/// Null if the dat lookup failed (no digits drawn). Retail reference:
|
||||||
/// 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="ghostedDigits">Ghosted digit DID array (property 0x10000043, same element).</param>
|
||||||
/// <param name="emptyDigits">
|
/// <param name="emptyDigits">
|
||||||
/// Empty-slot background digit DID array (property 0x1000005e, stance-independent).
|
/// Empty-slot background digit DID array (property 0x1000005e, stance-independent).
|
||||||
/// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
/// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||||||
|
|
@ -314,8 +314,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState = null,
|
CombatState? combatState = null,
|
||||||
uint[]? peaceDigits = null,
|
uint[]? regularDigits = null,
|
||||||
uint[]? warDigits = null,
|
uint[]? ghostedDigits = null,
|
||||||
uint[]? emptyDigits = null,
|
uint[]? emptyDigits = null,
|
||||||
ItemInteractionController? itemInteraction = null,
|
ItemInteractionController? itemInteraction = null,
|
||||||
Action<ShortcutEntry>? sendAddShortcut = null,
|
Action<ShortcutEntry>? sendAddShortcut = null,
|
||||||
|
|
@ -329,7 +329,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||||
{
|
{
|
||||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||||
peaceDigits, warDigits, emptyDigits, itemInteraction,
|
regularDigits, ghostedDigits, emptyDigits, itemInteraction,
|
||||||
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
|
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
|
||||||
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont,
|
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont,
|
||||||
dragIconIds);
|
dragIconIds);
|
||||||
|
|
@ -413,7 +413,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
|
|
||||||
// Re-stamp slot number labels after any item change.
|
// Re-stamp slot number labels after any item change.
|
||||||
// Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481):
|
// 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).
|
// The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511).
|
||||||
RestampShortcutNumbers();
|
RestampShortcutNumbers();
|
||||||
}
|
}
|
||||||
|
|
@ -444,21 +444,22 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
e.Visible = show[i];
|
e.Visible = show[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-stamp digit set: peace glyphs in NonCombat, war glyphs in any combat stance.
|
// The bool passed to SetShortcutNum is m_bShortcutGhosted, not a peace/war
|
||||||
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196610-196621).
|
// selector. Retail ghosts physical-item shortcuts only in Magic mode.
|
||||||
_peace = (mode == CombatMode.NonCombat);
|
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610.
|
||||||
|
_shortcutsGhosted = mode == CombatMode.Magic;
|
||||||
RestampShortcutNumbers();
|
RestampShortcutNumbers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 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
|
/// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite
|
||||||
/// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481).
|
/// 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;
|
/// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043;
|
||||||
/// empty → background 0x1000005e (decomp 229481/229493).
|
/// empty → background 0x1000005e (decomp 229481/229493).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void RestampShortcutNumbers()
|
private void RestampShortcutNumbers()
|
||||||
|
|
@ -467,11 +468,11 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
||||||
{
|
{
|
||||||
var cell = _slots[i]?.Cell;
|
var cell = _slots[i]?.Cell;
|
||||||
if (cell is null) continue;
|
if (cell is null) continue;
|
||||||
cell.PeaceDigits = _peaceDigits;
|
cell.RegularDigits = _regularDigits;
|
||||||
cell.WarDigits = _warDigits;
|
cell.GhostedDigits = _ghostedDigits;
|
||||||
cell.EmptyDigits = _emptyDigits;
|
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, _shortcutsGhosted); // top row: slot labels 1–9 always shown
|
||||||
else
|
else
|
||||||
cell.ClearShortcutNum(); // bottom row: no slot labels
|
cell.ClearShortcutNum(); // bottom row: no slot labels
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -403,11 +403,11 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (peace, war, empty) = LoadToolbarDigits();
|
var (regular, ghosted, empty) = LoadToolbarDigits();
|
||||||
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
||||||
ToolbarController = Layout.ToolbarController.Bind(
|
ToolbarController = Layout.ToolbarController.Bind(
|
||||||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
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,
|
toggleCombat: b.ToggleCombat,
|
||||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
||||||
selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u,
|
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.");
|
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)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
||||||
|
|
@ -597,8 +597,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
&& composite.Children.TryGetValue(0x1000034Au, out var number)
|
&& composite.Children.TryGetValue(0x1000034Au, out var number)
|
||||||
&& number.StateDesc?.Properties is { } props)
|
&& number.StateDesc?.Properties is { } props)
|
||||||
{
|
{
|
||||||
peace = ReadDataIds(props, 0x10000042u);
|
regular = ReadDataIds(props, 0x10000042u);
|
||||||
war = ReadDataIds(props, 0x10000043u);
|
ghosted = ReadDataIds(props, 0x10000043u);
|
||||||
}
|
}
|
||||||
if (layout is not null
|
if (layout is not null
|
||||||
&& layout.Elements.TryGetValue(0x10000341u, out var emptyComposite)
|
&& layout.Elements.TryGetValue(0x10000341u, out var emptyComposite)
|
||||||
|
|
@ -607,18 +607,18 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
empty = ReadDataIds(emptyProps, 0x1000005Eu);
|
empty = ReadDataIds(emptyProps, 0x1000005Eu);
|
||||||
}
|
}
|
||||||
|
|
||||||
peace ??=
|
regular ??=
|
||||||
[
|
[
|
||||||
0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u,
|
0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u,
|
||||||
0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u,
|
0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u,
|
||||||
];
|
];
|
||||||
war ??=
|
ghosted ??=
|
||||||
[
|
[
|
||||||
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
||||||
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
|
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
|
||||||
];
|
];
|
||||||
Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={peace.Length}, war={war.Length}, empty={empty?.Length ?? 0} entries.");
|
Console.WriteLine($"[D.5.1] toolbar digit arrays ready: regular={regular.Length}, ghosted={ghosted.Length}, empty={empty?.Length ?? 0} entries.");
|
||||||
return (peace, war, empty);
|
return (regular, ghosted, empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint[]? ReadDataIds(
|
private static uint[]? ReadDataIds(
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ public sealed class UiItemSlot : UiElement
|
||||||
// ── Shortcut number (slot label) ─────────────────────────────────────────
|
// ── Shortcut number (slot label) ─────────────────────────────────────────
|
||||||
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||||
// Retail draws the digit on the cell's ShortcutNum sub-element, picking the
|
// 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
|
// 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.
|
// 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.</summary>
|
/// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1.</summary>
|
||||||
public int ShortcutNum { get; private set; } = -1;
|
public int ShortcutNum { get; private set; } = -1;
|
||||||
|
|
||||||
/// <summary>True = draw peace digit set; false = war digit set.</summary>
|
/// <summary>True when retail marks the physical-item shortcut as ghosted.</summary>
|
||||||
public bool ShortcutPeace { get; private set; } = true;
|
public bool ShortcutGhosted { get; private set; }
|
||||||
|
|
||||||
/// <summary>Peace digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
/// <summary>Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
||||||
/// Injected by the controller after reading LayoutDesc 0x21000037.
|
/// Injected by the controller after reading LayoutDesc 0x21000037.
|
||||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
||||||
/// property 0x10000042 (peace) or 0x10000043 (war) by stance.</summary>
|
/// property 0x10000042 when the shortcut is not ghosted.</summary>
|
||||||
public uint[]? PeaceDigits { get; set; }
|
public uint[]? RegularDigits { get; set; }
|
||||||
|
|
||||||
/// <summary>War digit DID array. Same layout as PeaceDigits.
|
/// <summary>Ghosted digit DID array. Same layout as RegularDigits.
|
||||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229493) — war stance.</summary>
|
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229485-229488).</summary>
|
||||||
public uint[]? WarDigits { get; set; }
|
public uint[]? GhostedDigits { get; set; }
|
||||||
|
|
||||||
/// <summary>Empty-slot digit DID array (property 0x1000005e, stance-independent).
|
/// <summary>Empty-slot digit DID array (property 0x1000005e, stance-independent).
|
||||||
/// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
/// 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>
|
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).</summary>
|
||||||
public uint[]? EmptyDigits { get; set; }
|
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 ghosted state 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.</summary>
|
||||||
public void SetShortcutNum(int index, bool peace)
|
public void SetShortcutNum(int index, bool ghosted)
|
||||||
{
|
{
|
||||||
ShortcutNum = index;
|
ShortcutNum = index;
|
||||||
ShortcutPeace = peace;
|
ShortcutGhosted = ghosted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clear the shortcut number label (hides the digit).</summary>
|
/// <summary>Clear the shortcut number label (hides the digit).</summary>
|
||||||
|
|
@ -206,7 +206,7 @@ public sealed class UiItemSlot : UiElement
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
|
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
|
||||||
/// branch in UIElement_UIItem::SetShortcutNum (decomp 229481):
|
/// 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)
|
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
|
||||||
/// Exposed as an internal method so unit tests can assert array selection without
|
/// Exposed as an internal method so unit tests can assert array selection without
|
||||||
/// needing a real render context.
|
/// needing a real render context.
|
||||||
|
|
@ -214,7 +214,7 @@ public sealed class UiItemSlot : UiElement
|
||||||
internal uint[]? ActiveDigitArray()
|
internal uint[]? ActiveDigitArray()
|
||||||
{
|
{
|
||||||
bool occupied = ItemId != 0;
|
bool occupied = ItemId != 0;
|
||||||
return occupied ? (ShortcutPeace ? PeaceDigits : WarDigits) : EmptyDigits;
|
return occupied ? (ShortcutGhosted ? GhostedDigits : RegularDigits) : EmptyDigits;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Events / draw ─────────────────────────────────────────────────────────
|
// ── Events / draw ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -292,7 +292,7 @@ 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).
|
||||||
// Occupancy branch (decomp 229481):
|
// 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
|
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
|
||||||
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
|
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
|
||||||
// full-cell Alphablend so the transparent region is invisible.
|
// full-cell Alphablend so the transparent region is invisible.
|
||||||
|
|
|
||||||
|
|
@ -681,15 +681,15 @@ 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 regular entries (0x10..0x18), 9 ghosted entries (0x20..0x28),
|
||||||
// 9 empty (background) entries (0x30..0x38).
|
// 9 empty (background) entries (0x30..0x38).
|
||||||
private static readonly uint[] FakePeace = { 0x10u,0x11u,0x12u,0x13u,0x14u,0x15u,0x16u,0x17u,0x18u };
|
private static readonly uint[] FakeRegular = { 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[] FakeGhosted = { 0x20u,0x21u,0x22u,0x23u,0x24u,0x25u,0x26u,0x27u,0x28u };
|
||||||
private static readonly uint[] FakeEmpty = { 0x30u,0x31u,0x32u,0x33u,0x34u,0x35u,0x36u,0x37u,0x38u };
|
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 regular/ghosted digit arrays, top-row cells (indices 0–8) have
|
||||||
/// ShortcutNum == i (the slot position) and ShortcutPeace == true (default NonCombat).
|
/// ShortcutNum == i (the slot position) and ShortcutGhosted == false (default NonCombat).
|
||||||
/// Bottom-row cells (indices 9–17) have ShortcutNum == -1 (no label).
|
/// 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.
|
/// Retail: numbers are slot LABELS — shown on ALL top-row slots including empty ones.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -702,14 +702,14 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
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++)
|
for (int i = 0; i < Row1.Length; i++)
|
||||||
{
|
{
|
||||||
var cell = slots[Row1[i]].Cell;
|
var cell = slots[Row1[i]].Cell;
|
||||||
Assert.Equal(i, cell.ShortcutNum);
|
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.
|
// Bottom row: no shortcut number.
|
||||||
foreach (var id in Row2)
|
foreach (var id in Row2)
|
||||||
|
|
@ -717,26 +717,31 @@ public class ToolbarControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Theory]
|
||||||
public void ShortcutNumbers_setCombatModeWar_topRowUsesWarDigits()
|
[InlineData(CombatMode.NonCombat)]
|
||||||
|
[InlineData(CombatMode.Melee)]
|
||||||
|
[InlineData(CombatMode.Missile)]
|
||||||
|
public void ShortcutNumbers_nonMagicModes_useRegularDigits(CombatMode mode)
|
||||||
{
|
{
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
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++)
|
for (int i = 0; i < Row1.Length; i++)
|
||||||
{
|
{
|
||||||
var cell = slots[Row1[i]].Cell;
|
var cell = slots[Row1[i]].Cell;
|
||||||
Assert.Equal(i, cell.ShortcutNum);
|
Assert.Equal(i, cell.ShortcutNum);
|
||||||
Assert.False(cell.ShortcutPeace, $"top-row slot {i} should be war after Melee");
|
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.
|
// Bottom row still has no number.
|
||||||
foreach (var id in Row2)
|
foreach (var id in Row2)
|
||||||
|
|
@ -744,28 +749,38 @@ public class ToolbarControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShortcutNumbers_backToNonCombat_restoresPeaceDigits()
|
public void ShortcutNumbers_magicGhosts_thenNonCombatRestoresRegularDigits()
|
||||||
{
|
{
|
||||||
var (layout, slots, _) = FakeToolbar();
|
var (layout, slots, _) = FakeToolbar();
|
||||||
var repo = new ClientObjectTable();
|
var repo = new ClientObjectTable();
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
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);
|
ctrl.SetCombatMode(CombatMode.NonCombat);
|
||||||
|
|
||||||
for (int i = 0; i < Row1.Length; i++)
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Digit arrays are correctly injected into each cell (PeaceDigits + WarDigits references).
|
/// Digit arrays are correctly injected into each cell.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShortcutNumbers_digitArraysInjected()
|
public void ShortcutNumbers_digitArraysInjected()
|
||||||
|
|
@ -776,12 +791,12 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
peaceDigits: FakePeace, warDigits: FakeWar);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted);
|
||||||
|
|
||||||
foreach (var id in Row1)
|
foreach (var id in Row1)
|
||||||
{
|
{
|
||||||
Assert.Same(FakePeace, slots[id].Cell.PeaceDigits);
|
Assert.Same(FakeRegular, slots[id].Cell.RegularDigits);
|
||||||
Assert.Same(FakeWar, slots[id].Cell.WarDigits);
|
Assert.Same(FakeGhosted, slots[id].Cell.GhostedDigits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -798,7 +813,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: FakeEmpty);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: FakeEmpty);
|
||||||
|
|
||||||
foreach (var id in Row1)
|
foreach (var id in Row1)
|
||||||
Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits);
|
Assert.Same(FakeEmpty, slots[id].Cell.EmptyDigits);
|
||||||
|
|
@ -819,7 +834,7 @@ public class ToolbarControllerTests
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<ShortcutEntry>(),
|
() => Array.Empty<ShortcutEntry>(),
|
||||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||||
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: null);
|
regularDigits: FakeRegular, ghostedDigits: FakeGhosted, emptyDigits: null);
|
||||||
|
|
||||||
foreach (var id in Row1)
|
foreach (var id in Row1)
|
||||||
Assert.Null(slots[id].Cell.EmptyDigits);
|
Assert.Null(slots[id].Cell.EmptyDigits);
|
||||||
|
|
|
||||||
|
|
@ -66,95 +66,95 @@ public class UiItemSlotTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShortcutPeace_defaultIsTrue()
|
public void ShortcutGhosted_defaultsFalse()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot();
|
var s = new UiItemSlot();
|
||||||
Assert.True(s.ShortcutPeace);
|
Assert.False(s.ShortcutGhosted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetShortcutNum_setsIndexAndPeace()
|
public void SetShortcutNum_setsIndexAndGhostedState()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot();
|
var s = new UiItemSlot();
|
||||||
s.SetShortcutNum(3, peace: false);
|
s.SetShortcutNum(3, ghosted: true);
|
||||||
Assert.Equal(3, s.ShortcutNum);
|
Assert.Equal(3, s.ShortcutNum);
|
||||||
Assert.False(s.ShortcutPeace);
|
Assert.True(s.ShortcutGhosted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetShortcutNum_peaceTrue()
|
public void SetShortcutNum_regularState()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot();
|
var s = new UiItemSlot();
|
||||||
s.SetShortcutNum(0, peace: true);
|
s.SetShortcutNum(0, ghosted: false);
|
||||||
Assert.Equal(0, s.ShortcutNum);
|
Assert.Equal(0, s.ShortcutNum);
|
||||||
Assert.True(s.ShortcutPeace);
|
Assert.False(s.ShortcutGhosted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ClearShortcutNum_setsMinusOne()
|
public void ClearShortcutNum_setsMinusOne()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot();
|
var s = new UiItemSlot();
|
||||||
s.SetShortcutNum(5, peace: true);
|
s.SetShortcutNum(5, ghosted: false);
|
||||||
s.ClearShortcutNum();
|
s.ClearShortcutNum();
|
||||||
Assert.Equal(-1, s.ShortcutNum);
|
Assert.Equal(-1, s.ShortcutNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
|
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
|
||||||
|
|
||||||
private static readonly uint[] Peace = { 0x10u, 0x11u, 0x12u };
|
private static readonly uint[] Regular = { 0x10u, 0x11u, 0x12u };
|
||||||
private static readonly uint[] War = { 0x20u, 0x21u, 0x22u };
|
private static readonly uint[] Ghosted = { 0x20u, 0x21u, 0x22u };
|
||||||
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
|
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless
|
/// 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).
|
/// else branch when m_elem_Icon->m_state == 0x1000001c (empty).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
|
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
|
||||||
s.SetShortcutNum(0, peace: true);
|
s.SetShortcutNum(0, ghosted: false);
|
||||||
// ItemId == 0 → EmptyDigits
|
// ItemId == 0 → EmptyDigits
|
||||||
Assert.Same(Empty, s.ActiveDigitArray());
|
Assert.Same(Empty, s.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActiveDigitArray_emptySlot_warStance_stillReturnsEmptyDigits()
|
public void ActiveDigitArray_emptySlot_ghosted_stillReturnsEmptyDigits()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
|
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
|
||||||
s.SetShortcutNum(0, peace: false);
|
s.SetShortcutNum(0, ghosted: true);
|
||||||
// ItemId == 0 → EmptyDigits regardless of stance
|
// ItemId == 0 → EmptyDigits regardless of stance
|
||||||
Assert.Same(Empty, s.ActiveDigitArray());
|
Assert.Same(Empty, s.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When ItemId != 0 (occupied), ActiveDigitArray returns PeaceDigits or WarDigits
|
/// When ItemId != 0 (occupied), ActiveDigitArray returns RegularDigits or GhostedDigits
|
||||||
/// depending on ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
|
/// depending on ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[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.SetItem(0x5001u, 0x99u);
|
||||||
s.SetShortcutNum(0, peace: true);
|
s.SetShortcutNum(0, ghosted: false);
|
||||||
Assert.Same(Peace, s.ActiveDigitArray());
|
Assert.Same(Regular, s.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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.SetItem(0x5001u, 0x99u);
|
||||||
s.SetShortcutNum(0, peace: false);
|
s.SetShortcutNum(0, ghosted: true);
|
||||||
Assert.Same(War, s.ActiveDigitArray());
|
Assert.Same(Ghosted, s.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
|
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
|
||||||
{
|
{
|
||||||
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = null };
|
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = null };
|
||||||
s.SetShortcutNum(0, peace: true);
|
s.SetShortcutNum(0, ghosted: false);
|
||||||
Assert.Null(s.ActiveDigitArray());
|
Assert.Null(s.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue