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:
parent
5382d0a9d2
commit
6c485c2f06
3 changed files with 83 additions and 2 deletions
|
|
@ -596,6 +596,9 @@ public sealed class GameWindow : IDisposable
|
||||||
public readonly AcDream.Core.Spells.SpellTable SpellTable = LoadSpellTable();
|
public readonly AcDream.Core.Spells.SpellTable SpellTable = LoadSpellTable();
|
||||||
public readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
|
public readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
|
||||||
public readonly AcDream.Core.Items.ItemRepository Items = new();
|
public readonly AcDream.Core.Items.ItemRepository Items = new();
|
||||||
|
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
|
||||||
|
public IReadOnlyList<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry> Shortcuts { get; private set; }
|
||||||
|
= System.Array.Empty<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry>();
|
||||||
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
|
||||||
// PlayerDescription so the Vitals HUD can render those bars.
|
// PlayerDescription so the Vitals HUD can render those bars.
|
||||||
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
|
||||||
|
|
@ -2309,7 +2312,8 @@ public sealed class GameWindow : IDisposable
|
||||||
_lastSeenRunSkill, _lastSeenJumpSkill);
|
_lastSeenRunSkill, _lastSeenJumpSkill);
|
||||||
Console.WriteLine($"player: applied server skills run={_lastSeenRunSkill} jump={_lastSeenJumpSkill}");
|
Console.WriteLine($"player: applied server skills run={_lastSeenRunSkill} jump={_lastSeenJumpSkill}");
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
onShortcuts: list => Shortcuts = list);
|
||||||
|
|
||||||
// Phase I.7: subscribe to CombatState events and emit
|
// Phase I.7: subscribe to CombatState events and emit
|
||||||
// retail-faithful "You hit X for Y damage" chat lines into
|
// retail-faithful "You hit X for Y damage" chat lines into
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,11 @@ public static class GameEventWiring
|
||||||
// (matching ACE's CreatureSkill.Current minus
|
// (matching ACE's CreatureSkill.Current minus
|
||||||
// augs/multipliers/vitae which we still don't model).
|
// augs/multipliers/vitae which we still don't model).
|
||||||
Action<int /*runSkill*/, int /*jumpSkill*/>? onSkillsUpdated = null,
|
Action<int /*runSkill*/, int /*jumpSkill*/>? onSkillsUpdated = null,
|
||||||
Func<uint /*skillId*/, IReadOnlyDictionary<uint, uint> /*attrCurrents*/, uint /*formulaBonus*/>? resolveSkillFormulaBonus = null)
|
Func<uint /*skillId*/, IReadOnlyDictionary<uint, uint> /*attrCurrents*/, uint /*formulaBonus*/>? resolveSkillFormulaBonus = null,
|
||||||
|
// D.5.1 Task 4: persists Shortcuts from each PlayerDescription so the
|
||||||
|
// toolbar can populate itself at login without keeping a parser reference.
|
||||||
|
// Optional so all existing callers and tests compile unchanged.
|
||||||
|
Action<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>>? onShortcuts = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||||
ArgumentNullException.ThrowIfNull(items);
|
ArgumentNullException.ThrowIfNull(items);
|
||||||
|
|
@ -430,6 +434,10 @@ public static class GameEventWiring
|
||||||
newSlot: -1,
|
newSlot: -1,
|
||||||
newEquipLocation: (EquipMask)eq.EquipLocation);
|
newEquipLocation: (EquipMask)eq.EquipLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D.5.1 Task 4: forward shortcut bar entries to the caller so the
|
||||||
|
// toolbar can read them without holding a parser reference.
|
||||||
|
onShortcuts?.Invoke(p.Value.Shortcuts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -375,4 +375,73 @@ public sealed class GameEventWiringTests
|
||||||
Assert.NotNull(items.GetItem(0x50000A02u));
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue