perf(D.5.4): toolbar re-binds only on shortcut-guid object changes; clear on remove

Now that the object table holds ALL entities (creatures, NPCs, world objects),
filtering ObjectAdded/Updated/Removed to the 18 shortcut guids prevents the bar
from thrashing on every creature spawn in a busy zone.

Also subscribes to ObjectRemoved so a despawned/traded-away item clears its slot
(matching retail gmToolbarUI::SetDelayedShortcutNum's deferred-bind contract).

Four new unit tests (iconIds spy pattern) verify: non-shortcut ObjectAdded/Removed
do NOT invoke Populate; shortcut ObjectAdded deferred-binds; shortcut ObjectRemoved
clears the slot. 2671 tests, 4 skipped, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 16:57:04 +02:00
parent a9d40addac
commit a33e897400
2 changed files with 124 additions and 3 deletions

View file

@ -314,4 +314,111 @@ public class ToolbarControllerTests
foreach (var id in Row1)
Assert.Null(slots[id].Cell.EmptyDigits);
}
// ── E1: Guid filter + ObjectRemoved tests (D.5.4) ───────────────────────
/// <summary>
/// ObjectAdded for a guid NOT in the shortcut list does NOT call iconIds again
/// (no spurious Populate on creature/NPC spawns in a busy zone).
/// D.5.4: ToolbarController filters to shortcut guids only.
/// The iconIds spy lets us count how many times Populate actually ran.
/// </summary>
[Fact]
public void ObjectAdded_nonShortcutGuid_doesNotCallIconIds()
{
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) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x77u; }, useItem: _ => { });
int callsAfterBind = iconCallCount; // 1 call from initial Populate
// Fire ObjectAdded with a completely unrelated guid (a creature, NOT a shortcut).
repo.AddOrUpdate(new ClientObject { ObjectId = 0xDEADBEEFu, WeenieClassId = 42u, IconId = 0u });
// iconIds must NOT have been called again — the filter blocked Populate.
Assert.Equal(callsAfterBind, iconCallCount);
}
/// <summary>
/// ObjectAdded for a guid that IS in the shortcut list calls iconIds again (deferred bind).
/// This is the filtered-path counterpart of DeferredRebind_whenItemArrivesLate.
/// </summary>
[Fact]
public void ObjectAdded_shortcutGuid_callsIconIds()
{
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) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0x99u; }, useItem: _ => { });
Assert.Equal(0, iconCallCount); // not called — item absent during initial Populate
Assert.Equal(0u, slots[Row1[1]].Cell.ItemId);
// Now the shortcut item arrives — filter must PASS and Populate re-run.
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5003u, WeenieClassId = 1u, IconId = 0x06005678u });
Assert.Equal(1, iconCallCount); // iconIds called exactly once for the deferred bind
Assert.Equal(0x5003u, slots[Row1[1]].Cell.ItemId);
}
/// <summary>
/// ObjectRemoved for a guid that IS in the shortcut list clears the slot.
/// D.5.4: subscribes to ObjectRemoved so a removed item evicts its icon.
/// </summary>
[Fact]
public void ObjectRemoved_shortcutGuid_clearsSlot()
{
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) };
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { });
Assert.Equal(0x5004u, slots[Row1[3]].Cell.ItemId); // bound
// Remove the item from the session (server despawn / trade away).
// Populate re-runs: item is gone from repo → slot clears.
repo.Remove(0x5004u);
Assert.Equal(0u, slots[Row1[3]].Cell.ItemId);
}
/// <summary>
/// ObjectRemoved for a guid NOT in the shortcut list does NOT call iconIds again.
/// D.5.4: the ObjectRemoved subscription also filters to shortcut guids.
/// </summary>
[Fact]
public void ObjectRemoved_nonShortcutGuid_doesNotCallIconIds()
{
var (layout, _, _) = FakeToolbar();
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) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => { iconCallCount++; return 0xBBu; }, useItem: _ => { });
int callsAfterBind = iconCallCount; // 1 call for the shortcut item
// Remove an unrelated object — filter must block Populate.
repo.Remove(0xCAFEBABEu);
Assert.Equal(callsAfterBind, iconCallCount); // unchanged
}
}