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:
Erik 2026-07-13 11:21:12 +02:00
parent ab98cda26b
commit 5090aa7217
9 changed files with 197 additions and 112 deletions

View file

@ -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<ItemType, uint, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState,
uint[]? peaceDigits,
uint[]? warDigits,
uint[]? regularDigits,
uint[]? ghostedDigits,
uint[]? emptyDigits,
ItemInteractionController? itemInteraction = null,
Action<ShortcutEntry>? 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).
/// </param>
/// <param name="peaceDigits">
/// Peace-mode digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element
/// <param name="regularDigits">
/// 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).
/// </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">
/// 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<ItemType, uint, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState = null,
uint[]? peaceDigits = null,
uint[]? warDigits = null,
uint[]? regularDigits = null,
uint[]? ghostedDigits = null,
uint[]? emptyDigits = null,
ItemInteractionController? itemInteraction = null,
Action<ShortcutEntry>? sendAddShortcut = null,
@ -329,7 +329,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Func<ItemType, uint, uint, uint, uint, uint>? 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();
}
/// <summary>
/// Push digit-array references and shortcut-number state into every slot cell.
/// Top row (indices 08): SetShortcutNum(i, _peace) — numbers 19 always shown
/// Top row (indices 08): SetShortcutNum(i, _shortcutsGhosted) — numbers 19 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 917): 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).
/// </summary>
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 19 always shown
cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 19 always shown
else
cell.ClearShortcutNum(); // bottom row: no slot labels
}