feat(D.5.1): persist PlayerDescription shortcuts (were parsed then discarded)

Add optional `onShortcuts` callback to `GameEventWiring.WireAll`; invoke
it with `parsed.Shortcuts` after the inventory/equipped loops in the
PlayerDescription handler.  `GameWindow` holds the list in a new
`Shortcuts` property (initialized to empty) so the toolbar (D.5.1 Task 5)
can read hotbar slots without keeping a parser reference.  Existing callers
compile unchanged — the parameter defaults to null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 22:01:40 +02:00
parent 5382d0a9d2
commit 6c485c2f06
3 changed files with 83 additions and 2 deletions

View file

@ -375,4 +375,73 @@ public sealed class GameEventWiringTests
Assert.NotNull(items.GetItem(0x50000A02u));
}
[Fact]
public void WireAll_PlayerDescription_invokesOnShortcuts()
{
// D.5.1 Task 4: WireAll must forward parsed.Shortcuts to the onShortcuts
// callback so the toolbar can read them without keeping a parser reference.
// Mirrors PlayerDescription_RegistersInventoryEntries_InItemRepository
// for the harness pattern; adds the Shortcut flag (0x1) + one 12-byte
// entry, followed by the legacy-hotbar count (0) + spellbook_filters (0)
// then empty inventory and equipped.
IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>? got = null;
var dispatcher = new GameEventDispatcher();
var items = new ItemRepository();
var combat = new CombatState();
var spellbook = new Spellbook();
var chat = new ChatLog();
GameEventWiring.WireAll(dispatcher, items, combat, spellbook, chat,
onShortcuts: list => got = list);
// PlayerDescription body — minimal: no property flags, ATTRIBUTE|ENCHANTMENT
// vectorFlags (so the parser sees has_health=1, attribute_flags=0,
// enchantment_mask=0 and advances past both vector blocks), then the trailer
// with option_flags=Shortcut (0x1).
//
// Trailer layout when option_flags=0x1 (Shortcut only, no SpellLists8):
// u32 option_flags = 0x1
// u32 options1 = 0
// u32 count = 1 ← shortcut block (Shortcut flag set)
// u32 idx = 0
// u32 guid = 0x5001
// u16 spellId = 0
// u16 layer = 0
// u32 legacyHotbar count = 0 ← SpellLists8 NOT set → legacy fallback
// u32 spellbook_filters = 0
// u32 inventory count = 0
// u32 equipped count = 0
var sb = new MemoryStream();
using var w = new BinaryWriter(sb);
w.Write(0u); // propertyFlags = 0
w.Write(0x52u); // weenieType
w.Write(0x201u); // vectorFlags = ATTRIBUTE | ENCHANTMENT
w.Write(1u); // has_health
w.Write(0u); // attribute_flags = 0 (no attrs)
w.Write(0u); // enchantment_mask = 0
// Trailer
w.Write(0x00000001u); // option_flags = Shortcut
w.Write(0u); // options1
// Shortcut block (option_flags & 0x1 set):
w.Write(1u); // count = 1
w.Write(0u); // idx = 0
w.Write(0x5001u); // guid = 0x5001
w.Write((ushort)0); // spellId = 0
w.Write((ushort)0); // layer = 0
// SpellLists8 NOT set → legacy single-list fallback:
w.Write(0u); // legacy hotbar list count = 0
// No DesiredComps, no CharacterOptions2, no GameplayOptions → strict path:
w.Write(0u); // spellbook_filters = 0
w.Write(0u); // inventory count = 0
w.Write(0u); // equipped count = 0
var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.PlayerDescription, sb.ToArray()));
dispatcher.Dispatch(env!.Value);
Assert.NotNull(got);
Assert.Single(got!);
Assert.Equal(0x5001u, got![0].ObjectGuid);
}
}