feat(ui): preserve exact retail shortcut records
Carry signed index, object id, and raw spell word losslessly through PlayerDescription, session storage, drag mutation, and AddShortcut wire serialization while keeping gmToolbarUI object-only. Retire AP-103 and record the live ACE relog persistence gate. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
e65119f0c6
commit
b5b230c860
21 changed files with 271 additions and 181 deletions
|
|
@ -71,8 +71,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
||||
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { });
|
||||
|
|
@ -87,8 +87,8 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable(); // item NOT present yet
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
||||
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
||||
|
|
@ -104,8 +104,8 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
|
||||
var controller = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
|
||||
|
||||
|
|
@ -127,8 +127,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
||||
uint used = 0;
|
||||
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
|
|
@ -148,7 +148,7 @@ public class ToolbarControllerTests
|
|||
int characterClicks = 0;
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
ctrl.BindWindowToggles(
|
||||
toggleInventory: () => inventoryClicks++,
|
||||
|
|
@ -191,7 +191,7 @@ public class ToolbarControllerTests
|
|||
int inventoryClicks = 0;
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction);
|
||||
|
|
@ -213,7 +213,7 @@ public class ToolbarControllerTests
|
|||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
|
||||
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
|
||||
|
|
@ -243,7 +243,7 @@ public class ToolbarControllerTests
|
|||
ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_, _, _, _, _) => 0u,
|
||||
useItem: _ => { },
|
||||
toggleCombat: () => toggles++);
|
||||
|
|
@ -263,7 +263,7 @@ public class ToolbarControllerTests
|
|||
repo.AddOrUpdate(new ClientObject { ObjectId = itemId, Type = ItemType.Misc });
|
||||
var shortcuts = new[]
|
||||
{
|
||||
new PlayerDescriptionParser.ShortcutEntry(0, itemId, 0, 0),
|
||||
new ShortcutEntry(0, itemId, 0),
|
||||
};
|
||||
uint used = 0;
|
||||
uint selected = 0;
|
||||
|
|
@ -316,7 +316,7 @@ public class ToolbarControllerTests
|
|||
nowMs: () => 1_000);
|
||||
var shortcuts = new[]
|
||||
{
|
||||
new PlayerDescriptionParser.ShortcutEntry(0, target, 0, 0),
|
||||
new ShortcutEntry(0, target, 0),
|
||||
};
|
||||
var controller = ToolbarController.Bind(
|
||||
layout,
|
||||
|
|
@ -358,11 +358,11 @@ public class ToolbarControllerTests
|
|||
var controller = ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_, _, _, _, _) => 1u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction,
|
||||
sendAddShortcut: (slot, id) => sends.Add((slot, id)));
|
||||
sendAddShortcut: entry => sends.Add(((uint)entry.Index, entry.ObjectId)));
|
||||
|
||||
Assert.True(controller.CreateShortcutToItem(item));
|
||||
Assert.Equal(item, slots[Row1[0]].Cell.ItemId);
|
||||
|
|
@ -384,7 +384,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
||||
|
||||
// Only peace indicator (index 0 = 0x10000192) is visible.
|
||||
|
|
@ -404,7 +404,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
|
||||
|
||||
ctrl.SetCombatMode(CombatMode.Melee);
|
||||
|
|
@ -426,7 +426,7 @@ public class ToolbarControllerTests
|
|||
var combat = new CombatState();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { },
|
||||
combatState: combat);
|
||||
|
||||
|
|
@ -465,7 +465,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar);
|
||||
|
||||
|
|
@ -490,7 +490,7 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar);
|
||||
|
||||
|
|
@ -517,7 +517,7 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar);
|
||||
|
||||
|
|
@ -539,7 +539,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar);
|
||||
|
||||
|
|
@ -561,7 +561,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: FakeEmpty);
|
||||
|
||||
|
|
@ -582,7 +582,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
|
||||
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: null);
|
||||
|
||||
|
|
@ -604,8 +604,8 @@ public class ToolbarControllerTests
|
|||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
|
||||
|
||||
int iconCallCount = 0;
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
|
|
@ -629,8 +629,8 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable(); // item NOT present yet
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 1, ObjectGuid: 0x5003u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 1, ObjectId: 0x5003u, SpellId: 0) };
|
||||
|
||||
int iconCallCount = 0;
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
|
|
@ -656,8 +656,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5004u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5004u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 3, ObjectId: 0x5004u, SpellId: 0) };
|
||||
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { });
|
||||
|
|
@ -682,8 +682,8 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5005u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0xCAFEBABEu, WeenieClassId = 99u, IconId = 0u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 4, ObjectGuid: 0x5005u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new List<ShortcutEntry>
|
||||
{ new(Index: 4, ObjectId: 0x5005u, SpellId: 0) };
|
||||
|
||||
int iconCallCount = 0;
|
||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
|
|
@ -708,7 +708,7 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
|
||||
for (int i = 0; i < Row1.Length; i++)
|
||||
|
|
@ -733,7 +733,7 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
|
||||
var list = slots[Row1[0]];
|
||||
|
|
@ -748,7 +748,7 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
var list = slots[Row1[0]];
|
||||
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
|
||||
|
|
@ -756,12 +756,12 @@ public class ToolbarControllerTests
|
|||
}
|
||||
|
||||
// ── B.2: live drag handler (store + reorder/remove + wire) ───────────────
|
||||
private static (System.Collections.Generic.List<(uint i,uint g)> adds,
|
||||
System.Collections.Generic.List<uint> removes) NewSpies(out System.Action<uint,uint> add, out System.Action<uint> rem)
|
||||
private static (System.Collections.Generic.List<ShortcutEntry> adds,
|
||||
System.Collections.Generic.List<uint> removes) NewSpies(out System.Action<ShortcutEntry> add, out System.Action<uint> rem)
|
||||
{
|
||||
var adds = new System.Collections.Generic.List<(uint,uint)>();
|
||||
var adds = new System.Collections.Generic.List<ShortcutEntry>();
|
||||
var removes = new System.Collections.Generic.List<uint>();
|
||||
add = (i, g) => adds.Add((i, g));
|
||||
add = entry => adds.Add(entry);
|
||||
rem = i => removes.Add(i);
|
||||
return (adds, removes);
|
||||
}
|
||||
|
|
@ -772,8 +772,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
|
|
@ -795,9 +795,9 @@ public class ToolbarControllerTests
|
|||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); // A
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); // B
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0),
|
||||
new(Index: 5, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0),
|
||||
new(Index: 5, ObjectId: 0x5002u, SpellId: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
|
|
@ -810,8 +810,36 @@ public class ToolbarControllerTests
|
|||
Assert.Equal(0x5001u, slots[Row1[5]].Cell.ItemId);
|
||||
Assert.Equal(0x5002u, slots[Row1[3]].Cell.ItemId);
|
||||
Assert.Equal(new[] { 3u, 5u }, removes.ToArray());
|
||||
Assert.Contains((5u, 0x5001u), adds);
|
||||
Assert.Contains((3u, 0x5002u), adds);
|
||||
Assert.Contains(new ShortcutEntry(5, 0x5001u, 0u), adds);
|
||||
Assert.Contains(new ShortcutEntry(3, 0x5002u, 0u), adds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleDropRelease_reindexesWithoutLosingRawShortcutFields()
|
||||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, IconId = 1u });
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, IconId = 2u });
|
||||
var shortcuts = new[]
|
||||
{
|
||||
new ShortcutEntry(3, 0x5001u, 0x11223344u),
|
||||
new ShortcutEntry(5, 0x5002u, 0xA5C31234u),
|
||||
};
|
||||
var (adds, _) = NewSpies(out var add, out var remove);
|
||||
var controller = ToolbarController.Bind(
|
||||
layout, repo, () => shortcuts,
|
||||
iconIds: (_, _, _, _, _) => 1u,
|
||||
useItem: _ => { },
|
||||
sendAddShortcut: add,
|
||||
sendRemoveShortcut: remove);
|
||||
|
||||
var payload = Assert.IsType<ItemDragPayload>(slots[Row1[3]].Cell.GetDragPayload());
|
||||
controller.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload);
|
||||
controller.HandleDropRelease(slots[Row1[5]], slots[Row1[5]].Cell, payload);
|
||||
|
||||
Assert.Contains(new ShortcutEntry(5, 0x5001u, 0x11223344u), adds);
|
||||
Assert.Contains(new ShortcutEntry(3, 0x5002u, 0xA5C31234u), adds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -820,8 +848,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
|
|
@ -833,8 +861,8 @@ public class ToolbarControllerTests
|
|||
|
||||
Assert.Equal(0x5001u, slots[Row1[7]].Cell.ItemId);
|
||||
Assert.Equal(0u, slots[Row1[3]].Cell.ItemId);
|
||||
Assert.Contains((7u, 0x5001u), adds);
|
||||
Assert.DoesNotContain(adds, a => a.i == 3u);
|
||||
Assert.Contains(new ShortcutEntry(7, 0x5001u, 0u), adds);
|
||||
Assert.DoesNotContain(adds, a => a.Index == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -843,8 +871,8 @@ public class ToolbarControllerTests
|
|||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new System.Collections.Generic.List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||
var shortcuts = new System.Collections.Generic.List<ShortcutEntry>
|
||||
{ new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
|
||||
var (adds, removes) = NewSpies(out var add, out var rem);
|
||||
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
|
||||
|
|
@ -855,7 +883,7 @@ public class ToolbarControllerTests
|
|||
ctrl.HandleDropRelease(slots[Row1[3]], slots[Row1[3]].Cell, payload); // drop back on slot 3
|
||||
|
||||
Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); // re-added to source (net no-op)
|
||||
Assert.Contains((3u, 0x5001u), adds); // AddShortcut(3, A) sent
|
||||
Assert.Contains(new ShortcutEntry(3, 0x5001u, 0u), adds); // AddShortcut(3, A) sent
|
||||
Assert.Single(removes); // exactly RemoveShortcut(3) [from the lift]
|
||||
Assert.Single(adds); // exactly AddShortcut(3, A) [the re-place]
|
||||
}
|
||||
|
|
@ -865,7 +893,7 @@ public class ToolbarControllerTests
|
|||
{
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
ToolbarController.Bind(layout, new ClientObjectTable(),
|
||||
() => System.Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
() => System.Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9
|
||||
}
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ public sealed class GameEventWiringTests
|
|||
// 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;
|
||||
IReadOnlyList<ShortcutEntry>? got = null;
|
||||
|
||||
var dispatcher = new GameEventDispatcher();
|
||||
var items = new ClientObjectTable();
|
||||
|
|
@ -629,7 +629,7 @@ public sealed class GameEventWiringTests
|
|||
|
||||
Assert.NotNull(got);
|
||||
Assert.Single(got!);
|
||||
Assert.Equal(0x5001u, got![0].ObjectGuid);
|
||||
Assert.Equal(0x5001u, got![0].ObjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -76,23 +77,23 @@ public sealed class InventoryActionsTests
|
|||
[Fact]
|
||||
public void BuildAddShortcut_ItemShortcut_FieldLayout()
|
||||
{
|
||||
// ShortCutData = Index(u32), ObjectId(u32), SpellId(u16), Layer(u16). Item → spell/layer 0.
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 0, objectGuid: 0x3E1, spellId: 0, layer: 0);
|
||||
var entry = new ShortcutEntry(Index: 0, ObjectId: 0x3E1u, SpellId: 0u);
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, entry);
|
||||
Assert.Equal(24, body.Length);
|
||||
Assert.Equal(InventoryActions.AddShortcutOpcode,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12))); // index
|
||||
Assert.Equal(0x3E1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16))); // objectGuid
|
||||
Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20))); // spellId
|
||||
Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22))); // layer
|
||||
Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(12)));
|
||||
Assert.Equal(0x3E1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAddShortcut_SpellShortcut_PacksSpellAndLayerAsU16s()
|
||||
public void BuildAddShortcut_PreservesSignedIndexAndRawSpellWord()
|
||||
{
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 2, objectGuid: 0, spellId: 0x1234, layer: 3);
|
||||
Assert.Equal(0x1234, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20)));
|
||||
Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22)));
|
||||
var entry = new ShortcutEntry(Index: -1, ObjectId: 0u, SpellId: 0xA5C31234u);
|
||||
byte[] body = InventoryActions.BuildAddShortcut(seq: 1, entry);
|
||||
Assert.Equal(-1, BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(12)));
|
||||
Assert.Equal(0xA5C31234u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -390,21 +390,20 @@ public sealed class PlayerDescriptionParserTests
|
|||
writer.Write(0x01u); // option_flags = SHORTCUT
|
||||
writer.Write(0xCAFEu); // options1 sentinel
|
||||
|
||||
// Shortcut count + 2 entries (16 B each).
|
||||
// Shortcut count + 2 packed ShortCutData entries (12 B each).
|
||||
writer.Write(2u);
|
||||
writer.Write(0u); writer.Write(0xAABBCCDDu); writer.Write((ushort)0); writer.Write((ushort)0);
|
||||
writer.Write(7u); writer.Write(0u); writer.Write((ushort)1234); writer.Write((ushort)5);
|
||||
writer.Write(0); writer.Write(0xAABBCCDDu); writer.Write(0u);
|
||||
writer.Write(7); writer.Write(0u); writer.Write(0x000504D2u);
|
||||
|
||||
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(2, parsed!.Value.Shortcuts.Count);
|
||||
Assert.Equal(0u, parsed.Value.Shortcuts[0].Index);
|
||||
Assert.Equal(0xAABBCCDDu, parsed.Value.Shortcuts[0].ObjectGuid);
|
||||
Assert.Equal((ushort)0, parsed.Value.Shortcuts[0].SpellId);
|
||||
Assert.Equal(7u, parsed.Value.Shortcuts[1].Index);
|
||||
Assert.Equal((ushort)1234, parsed.Value.Shortcuts[1].SpellId);
|
||||
Assert.Equal((ushort)5, parsed.Value.Shortcuts[1].Layer);
|
||||
Assert.Equal(0, parsed.Value.Shortcuts[0].Index);
|
||||
Assert.Equal(0xAABBCCDDu, parsed.Value.Shortcuts[0].ObjectId);
|
||||
Assert.Equal(0u, parsed.Value.Shortcuts[0].SpellId);
|
||||
Assert.Equal(7, parsed.Value.Shortcuts[1].Index);
|
||||
Assert.Equal(0x000504D2u, parsed.Value.Shortcuts[1].SpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -424,9 +423,9 @@ public sealed class PlayerDescriptionParserTests
|
|||
writer.Write(3u); // claimed shortcut count = 3
|
||||
// First entry complete (16 B).
|
||||
writer.Write(1u); writer.Write(0xAAAAu); writer.Write((ushort)10); writer.Write((ushort)1);
|
||||
// Second entry truncated to 8 bytes — ReadU16 will throw FormatException.
|
||||
// Second entry truncated to 8 bytes — the raw spell-word read throws.
|
||||
writer.Write(2u); writer.Write(0xBBBBu);
|
||||
// (no SpellId/Layer — payload ends here)
|
||||
// (no raw SpellId word — payload ends here)
|
||||
|
||||
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
|
||||
|
||||
|
|
@ -435,7 +434,7 @@ public sealed class PlayerDescriptionParserTests
|
|||
Assert.True(parsed!.Value.TrailerTruncated);
|
||||
// First entry survives in the partial list.
|
||||
Assert.Single(parsed.Value.Shortcuts);
|
||||
Assert.Equal(1u, parsed.Value.Shortcuts[0].Index);
|
||||
Assert.Equal(1, parsed.Value.Shortcuts[0].Index);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -725,7 +724,7 @@ public sealed class PlayerDescriptionParserTests
|
|||
|
||||
// Shortcuts: count=1
|
||||
writer.Write(1u);
|
||||
writer.Write(3u); writer.Write(0xCAFEFACEu); writer.Write((ushort)100); writer.Write((ushort)2);
|
||||
writer.Write(3); writer.Write(0xCAFEFACEu); writer.Write(0x00020064u);
|
||||
|
||||
// 8 hotbars, all empty for brevity.
|
||||
for (int i = 0; i < 8; i++) writer.Write(0u);
|
||||
|
|
@ -754,7 +753,8 @@ public sealed class PlayerDescriptionParserTests
|
|||
Assert.Equal(0xBB000002u, v.Options2);
|
||||
Assert.Equal(0xF11Du, v.SpellbookFilters);
|
||||
Assert.Single(v.Shortcuts);
|
||||
Assert.Equal(0xCAFEFACEu, v.Shortcuts[0].ObjectGuid);
|
||||
Assert.Equal(0xCAFEFACEu, v.Shortcuts[0].ObjectId);
|
||||
Assert.Equal(0x00020064u, v.Shortcuts[0].SpellId);
|
||||
Assert.Equal(8, v.HotbarSpells.Count);
|
||||
Assert.All(v.HotbarSpells, l => Assert.Empty(l));
|
||||
Assert.Single(v.DesiredComps);
|
||||
|
|
|
|||
|
|
@ -6,34 +6,39 @@ namespace AcDream.Core.Tests.Items;
|
|||
public class ShortcutStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Load_mapsSlotToObjId_skipsEmptyAndOutOfRange()
|
||||
public void Load_preservesAllRawFieldsAndRejectsOutOfRangeIndex()
|
||||
{
|
||||
var store = new ShortcutStore();
|
||||
store.Load(new (int, uint)[]
|
||||
store.Load(new[]
|
||||
{
|
||||
(0, 0x5001u),
|
||||
(3, 0x5002u),
|
||||
(5, 0u), // empty/spell → skipped
|
||||
(99, 0x5003u), // out of range → skipped
|
||||
new ShortcutEntry(0, 0x5001u, 0x11223344u),
|
||||
new ShortcutEntry(3, 0x5002u, 0u),
|
||||
new ShortcutEntry(5, 0u, 0xA5C31234u),
|
||||
new ShortcutEntry(-1, 0x5003u, 1u),
|
||||
new ShortcutEntry(18, 0x5004u, 2u),
|
||||
});
|
||||
Assert.Equal(0x5001u, store.Get(0));
|
||||
Assert.Equal(0x5002u, store.Get(3));
|
||||
Assert.Equal(0u, store.Get(5));
|
||||
Assert.Equal(new ShortcutEntry(5, 0u, 0xA5C31234u), store.GetEntry(5));
|
||||
Assert.Equal(0x11223344u, store.GetEntry(0)!.Value.SpellId);
|
||||
Assert.True(store.IsEmpty(1));
|
||||
Assert.Null(store.GetEntry(-1));
|
||||
Assert.Null(store.GetEntry(18));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetRemoveGet_roundtrip_andBoundsSafe()
|
||||
{
|
||||
var store = new ShortcutStore();
|
||||
store.Set(4, 0xABCDu);
|
||||
store.SetItem(4, 0xABCDu);
|
||||
Assert.Equal(0xABCDu, store.Get(4));
|
||||
Assert.False(store.IsEmpty(4));
|
||||
store.Remove(4);
|
||||
Assert.True(store.IsEmpty(4));
|
||||
Assert.Equal(0u, store.Get(-1));
|
||||
Assert.Equal(0u, store.Get(18));
|
||||
store.Set(18, 1u); // no-op, no throw
|
||||
store.SetItem(18, 1u); // no-op, no throw
|
||||
store.Remove(-1); // no-op, no throw
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue