fix(D.2b): PaperdollController review fixes — player-scope Concerns, loop clarity

Code-quality review on Task 5:
- I1: Concerns was unscoped (CurrentlyEquippedLocation != None) → an NPC's
  wielded item (which also carries that wire field) triggered spurious full
  repaints. Narrowed to (WielderId==p || ContainerId==p), matching
  InventoryController; OnObjectMoved's from/to-player backstop still catches
  unwield-into-a-side-bag. Populate's own scope already prevented wrong data;
  this kills the wasted repaints.
- I2: replaced the dual-`index++` (assign-vs-skip) with a for-i loop;
  SlotIndex = SlotMap position (= the drag payload's SourceSlot on unwield).
- M1/M2: comment that the cell's SpriteResolve + the discrete-slot accept/
  reject ring (0x060011F9/F8, not the grid insert-arrow) are factory-provided.
- Added two behavioral tests: a live player wield repaints the slot
  (ObjectMoved → Concerns → Populate); an NPC's wielded item never appears on
  the doll (player-scoping).
- Synced the stale spec §4b/§6c/§8 to the Task-3 Option-1 reality (the
  optimistic wield is ContainerId-based and does NOT write WielderId).

App suite 580 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 22:32:34 +02:00
parent 9f187c3e31
commit 3b8a39c49e
3 changed files with 54 additions and 15 deletions

View file

@ -155,20 +155,22 @@ This makes rollback faithful in **both** directions (today an unwield-reject los
```csharp
/// <summary>Optimistic (instant) wield: snapshot the pre-wield (container, slot, equip), then set
/// ContainerId = WielderId = wielderGuid and CurrentlyEquippedLocation = equipMask (matching the
/// ContainerId = wielderGuid and CurrentlyEquippedLocation = equipMask (matching the
/// server's WieldObject 0x0023 confirm), firing ObjectMoved for an immediate repaint. The caller
/// sends GetAndWieldItem; ConfirmMove (on the 0x0023 echo) / RollbackMove (on 0x00A0) reconcile.</summary>
public bool WieldItemOptimistic(uint itemId, uint wielderGuid, EquipMask equipMask)
{
if (!_objects.TryGetValue(itemId, out var item)) return false;
RecordPending(itemId, item);
item.WielderId = wielderGuid; // unambiguously the player's gear during the optimistic window
return MoveItem(itemId, wielderGuid, newSlot: -1, newEquipLocation: equipMask);
}
```
`MoveItem` already sets `ContainerId` + `CurrentlyEquippedLocation` + reindexes + fires `ObjectMoved`.
Setting `WielderId` here matches the steady state (the item's own CreateObject carries `Wielder`).
It does **not** write `WielderId` — acdream's `WieldObject 0x0023` confirm is also ContainerId-based (it
never sets `WielderId`), so the optimistic state equals the confirmed state and rollback fully restores
through `MoveItem` alone. (An equipped item is detected as the player's via `ContainerId == player`;
login-equipped items match via `WielderId` from their own CreateObject. Decided at the Task-3 code review.)
---
@ -239,9 +241,10 @@ jewelry — a single equip bit intersects exactly one slot mask). If found, `cel
iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects))`; else
`cell.Clear()` (transparent — nothing drawn).
`Concerns(o)`: repaint when `o.CurrentlyEquippedLocation != None || o.WielderId == player || o.ContainerId == player`;
`OnObjectMoved` also repaints when from/to is the player (an item being wielded/unwielded). Mirror
`InventoryController`'s debounce.
`Concerns(o)`: repaint when `o.WielderId == player || o.ContainerId == player` (player-scoped — an NPC's
wielded item carries `CurrentlyEquippedLocation` too and must NOT leak onto the doll; a player-equipped item
always has `WielderId==player` or `ContainerId==player`). `OnObjectMoved` also repaints when from/to is the
player (an item being wielded/unwielded into a side bag). Mirror `InventoryController`'s debounce.
### 6d. `IItemListDragHandler`
@ -295,8 +298,9 @@ _paperdollController = AcDream.App.UI.Layout.PaperdollController.Bind(
confidence (the contents grid proves the same `0x2100003D` base path), but load-bearing — if it fails,
fix the importer/factory before continuing.
- **Core — `EquipMaskTests`:** numeric pin of every member vs `acclient.h:3193`.
- **Core — `ClientObjectTableTests`:** `WieldItemOptimistic` sets equip + WielderId + container; `RollbackMove`
restores the pre-wield equip mask (the new snapshot field); outstanding-count across wield+move of the same item.
- **Core — `ClientObjectTableTests`:** `WieldItemOptimistic` sets equip + container (ContainerId-based; does
NOT write WielderId); `RollbackMove` restores the pre-wield equip mask (the new snapshot field);
outstanding-count across wield+move of the same item.
- **Core.Net — `GameEventWiringTests`:** `WieldObject 0x0023` now calls `ConfirmMove` (an optimistic wield's
snapshot clears on the echo).
- **App — `PaperdollControllerTests`:** element-id→mask map matches the dump; populate shows the equipped item