Commit graph

709 commits

Author SHA1 Message Date
Erik
9ed9d8dbd9 fix(studio): Task 4 — bind PaperdollController + wire empty-slot sprites
FixtureProvider.Populate for 0x21000023 was binding only
InventoryController and omitting PaperdollController, so the
~21 equip slots rendered empty even though sample equipped items
existed in the table.  Also, the three per-list empty-slot
sprites were not being resolved from the dat (contentsEmpty /
sideBagEmpty / mainPackEmpty all defaulted to 0), so the slot cells
had no background art.

Fixes:
- FixtureProvider.Populate gains a DatCollection dats param
  (mirrors the GameWindow.OnLoad lookup at line 2233-2235).
  The 0x21000023 case now resolves all three empty sprites via
  ItemListCellTemplate.ResolveEmptySprite and passes them to
  InventoryController.Bind.
- PaperdollController.Bind is now called after InventoryController
  in the same 0x21000023 case, matching GameWindow:2257-2265
  (same layout subtree, contentsEmpty as the equip-slot placeholder,
  sendWield: null since there is no live session in the studio).
- StudioWindow.OnLoad passes _dats! to the updated Populate signature.

SampleData.AddEquipped was already correct: MoveItem(guid, PlayerGuid,
-1, equipMask) at ClientObjectTable.cs:134 sets
CurrentlyEquippedLocation = newEquipLocation and calls Reindex which
places the item in _containerIndex[PlayerGuid].  No SampleData change
needed.

Tests: 2 new assertions in FixtureProviderTests —
  sideBags_matchInventoryControllerFilter (Type.HasFlag(Container)
  || ItemsCapacity > 0 filter must match both bags) and
  equippedItems_retainLocationAndAreInContents (equipped items have
  CurrentlyEquippedLocation != None AND appear in GetContents so the
  paperdoll controller can find them).  604 pass / 2 skip / 0 fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:59:13 +02:00
Erik
0bdc866c15 feat(studio): FixtureProvider — sample data populates the 2-D panels
Task 4 of the UI Studio plan: adds SampleData (static ClientObjectTable
builder with a synthetic player, 6 loose items, 2 side bags, and 3 equipped
pieces) and FixtureProvider (switches on layoutId and calls the production
controller Bind methods — VitalsController, ToolbarController,
InventoryController — so the studio previews panels with plausible data
instead of empty widgets).

Icon ids approach: raw-resolve stub — resolves the base iconId via
RenderStack.ResolveChrome and returns the GL handle directly. This is v1
(single-layer icon); the full 5-layer IconComposer composite is a live-game
concern, not a layout-preview concern.

StudioWindow wires FixtureProvider.Populate after _source.Load; the
ClientObjectTable is stored on _objects so controller event subscriptions
(ObjectAdded/ObjectMoved) remain live for the window's lifetime.

4 new FixtureProviderTests (SampleTable_hasPackContents,
SampleTable_hasWeaponAndArmor, SampleTable_hasEquippedItems,
SampleTable_hasSideBags) — all pass. Full App suite: 602 passed / 2
skipped (pre-existing) / 0 failed. Build green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:45:11 +02:00
Erik
df6b5b3b39 feat(studio): StudioWindow + LayoutSource — render a dat panel standalone
Task 2 of the acdream UI Studio plan. Adds three new files under
src/AcDream.App/Studio/ and one new test file:

- StudioOptions.cs: record + Parse() for the ui-studio CLI args (positional
  dat dir or ACDREAM_DAT_DIR, --layout 0xNNNN, --markup <path>; defaults to
  vitals 0x2100006C when neither layout nor markup given).
- LayoutSource.cs: wraps LayoutImporter.Import for dat-backed layouts; markup
  path sets LastError "markup unsupported (Task 6)" and returns null for now.
  Exposes Kind / LayoutId / MarkupPath / LastError / CurrentLayout / Reload().
- StudioWindow.cs: Silk.NET 1280×720 GL 4.3 window; boots RenderBootstrap,
  wires input to UiHost, loads the panel via LayoutSource, adds the root to
  UiHost.Root.AddChild. QualitySettings resolved the same way GameWindow.Run()
  does (SettingsStore → QualitySettings.From → WithEnvOverrides).
- Program.cs: ui-studio dispatch at the top of top-level statements (before
  the dat-dir parse) so `dotnet run -- ui-studio` routes to StudioWindow.
- LayoutSourceTests.cs: dat-gated test (skips when dats absent); verifies that
  the vitals LayoutDesc (0x2100006C) loads, root is non-null, byId contains the
  vitals root element (0x100005F9), and Kind == DatLayout. Passes (1/1 with dats
  present; silently skips on CI).

Note: the spec asserts FindElement(0x2100006Cu) — that id is the LayoutDesc
dat id, not a widget element id. The actual vitals root element id is 0x100005F9
(confirmed from the vitals_2100006C.json fixture). The test uses the correct
element id and documents the discrepancy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:26:47 +02:00
Erik
8fa66c23d5 fix(D.2b): Slice 2 — retail-exact doll pose, camera + heading (visual gate)
The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.

Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).

Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.

Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.

Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:16:27 +02:00
Erik
8ee3d89feb fix(D.2b): Slice 2 — buttons inside a whole-window-Draggable frame get their Click
Visual gate 2 (user): the "Slots" toggle caption was visible but unclickable.

Root cause (UiRoot.OnMouseDown/OnMouseUp): a left-press on a non-drag-source
widget inside a whole-window-Draggable frame (the inventory window's IA-12
drag) set _windowDragTarget; OnMouseUp then early-returned before emitting the
Click. So the paperdoll Slots button (the first plain button inside the
draggable inventory frame) never received its click. Chat/toolbar buttons
escape this — their frames aren't whole-window-draggable.

Fix (toolkit, root cause not band-aid): add UiElement.HandlesClick (a virtual
opt-out parallel to IsDragSource); UiButton overrides it true; OnMouseDown
routes a HandlesClick press to the widget (like CapturesPointerDrag) instead of
the window-drag, so OnMouseUp emits the Click. 2 regression tests lock it
(HandlesClick widget in a Draggable frame emits Click; a plain one doesn't).

Build + full App suite green (596, +2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:08:59 +02:00
Erik
fe319bd2aa fix(D.2b): Slice 2 visual gate — frame the whole doll + caption the Slots toggle
Visual gate 1 (user): the doll rendered but only the legs showed (camera
aimed at the model origin = the feet) and the Slots button was invisible.

- DollCamera: aim the look-at at mid-body (~0.95 m) and stand back ~3.7 m
  so the whole ~1.9 m figure fits. (Size is a later retail-comparison
  polish per the user.)
- PaperdollController: the Slots button (0x100005BE) is found + wired
  (diagnostic confirmed armorSlots=9/9, viewport=UiViewport,
  slotsButton=UiButton) but its dat element has no face sprite, so it drew
  nothing. Give it a gold "Slots" caption (UiButton.Label, like chat Send)
  via a new datFont param on Bind. Temporary [Slice2-paperdoll] diagnostic
  logs the button rect + the found widgets (stripped at wrap-up).

Idle animation deferred to a focused follow-up (faithful idle needs a full
AnimatedEntity + Sequencer through the TickAnimations multi-branch path +
re-dress coordination — real integration risk vs the verified static doll).

Build + full App suite green (594).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:58:22 +02:00
Erik
362d41aacf feat(D.2b): Slice 2 — DollEntityBuilder (player Setup+ObjDesc -> doll WorldEntity)
Mirrors the palette/part-override mapping at GameWindow.cs:3390-3431 in a
testable static helper. Build() accepts plain (SubPaletteId, Offset, Length)
and (PartIndex, GfxObjId) tuples, builds PaletteOverride only when
subPalettes.Count > 0 (same gate as GameWindow), and poses the entity at
origin facing the viewer (191.367905° / +Z). Reserved synthetic guid
0xDA11D011 keeps the doll distinct from the live player and satisfies
EntitySpawnAdapter's ServerGuid != 0 guard. 7 new tests green; suite 594/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:07:02 +02:00
Erik
ebcdf44c0c feat(D.2b): Slice 2 — UiViewport widget (dat Type 0xD) + IUiViewportRenderer seam
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:03:42 +02:00
Erik
10cb31223f feat(D.2b): Slice 2 — DollCamera (fixed paperdoll ICamera)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:00:27 +02:00
Erik
c5604ff6ad feat(D.2b): Slice 2 — paperdoll armor/non-armor partition + Slots toggle state
Adds ArmorSlotElementIds (the exact 9 ids gmPaperDollUI::ListenToElementMessage
flips per decomp 175674-175706) and PaperdollViewState (SlotView/DollVisible/
ArmorSlotsVisible/Toggle) as a nested public class on PaperdollController.

Wires ApplyView() into the constructor so the Slots button (0x100005BE) drives
armor-slot Visible and the doll viewport Visible on every click. Initial state
is doll-view (armor slots hidden). The doll viewport element (0x100001D5) is
bound as UiElement? so this slice stays independent of Slice 3's IUiViewportRenderer
seam; _armorSlots collects whichever of the 9 ids were found in the live layout.

Three new unit tests verify the id set, the default state, and the round-trip
toggle against the public PaperdollViewState surface only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 08:57:50 +02:00
Erik
a4c68520ea fix(D.2b): paperdoll empty slots show a visible frame (not transparent)
User correction at the visual gate: the green "figure" in the paperdoll is
the LIVE 3D character (the doll — can be naked), NOT per-slot silhouettes.
The default view (Slots button OFF) = the doll + non-armor slots; pressing
Slots hides the doll and shows the armor slots. So the doll + the Slots
toggle are Slice 2 (the UiViewport); there are no per-slot silhouette sprites
to chase.

For Slice 1 (no doll yet) the right empty-slot look is simply a VISIBLE FRAME
so every slot position can be seen + used — which fixes the "I see only slots
with equipment, no empty slots" report. The earlier transparent (EmptySprite=0)
came from the stale silhouette assumption. PaperdollController now takes an
emptySlotSprite; GameWindow passes the inventory grid's empty square
(0x06004D20) for a consistent visible frame.

App suite 580 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:02:11 +02:00
Erik
db0cac03c4 fix(D.2b): PickupEvent removes the 3D render, not the weenie — unwield lands in the pack
PickupEvent (0xF74A) and DeleteObject (0xF747) are semantically distinct:
- 0xF747 = weenie DESTROYED → evict from ClientObjectTable (weenie_object_table)
- 0xF74A = object LEFT THE 3D WORLD VIEW (moved into a container) → remove
  the 3D WorldEntity, but the weenie persists in ClientObjectTable

Before this fix, both paths fired EntityDeleted identically, causing
ObjectTableWiring to evict the weenie from ClientObjectTable. The follow-up
InventoryPutObjInContainer (0x0022) then tried MoveItem on an unknown guid
and no-op'd, so the unwielded item simply vanished.

Fix: add `bool FromPickup` (default false) to DeleteObject.Parsed. WorldSession
sets it true on the PickupEvent path and false on the DeleteObject path.
ObjectTableWiring.Wire's EntityDeleted handler skips table.Remove when
FromPickup is true, preserving the weenie for the container-move echo.

GameWindow.OnLiveEntityDeleted (3D entity removal) is untouched — it fires
for both pickups and destroys, as intended.

Divergence register: AP-65 added (data ghosts for other-player pickups until
teleport/relog clear; harmless — no UI queries ContainerId 0).

Tests: +5 (DeleteObject FromPickup parser regression; wiring retain/evict
semantics; Parsed default/explicit FromPickup). 343/343 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:39:27 +02:00
Erik
3b8a39c49e 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>
2026-06-22 22:32:34 +02:00
Erik
9f187c3e31 feat(D.2b): PaperdollController — equip slots bind + wield drag handler (Slice 1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:24:52 +02:00
Erik
f8799489c2 fix(D.2b): ConfirmMove on the WieldObject 0x0023 echo (clears optimistic wield)
Mirrors the InventoryPutObjInContainer 0x0022 handler which already does
MoveItem + ConfirmMove. Without this, WieldItemOptimistic's pending snapshot
would linger until the session ended (or incorrectly roll back on 0x00A0).
ConfirmMove is a no-op when nothing is pending, so safe for server-initiated
login wields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:19:39 +02:00
Erik
c1a84cbe0c fix(D.2b): wield is ContainerId-based — drop the WielderId write (code review)
Code-quality review (needs-changes) on Task 3: WieldItemOptimistic wrote
item.WielderId directly, but RollbackMove (via MoveItem) never cleared it
→ a wield-rollback left a pack item with a stale WielderId=player.

Root-cause fix (vs the reviewer's snapshot-WielderId suggestion): acdream's
existing WieldObject 0x0023 confirm models a wielded item as ContainerId=
wielder + equip=mask and does NOT touch WielderId. So the optimistic path
must match — drop the WielderId write entirely. Optimistic state now equals
the confirmed state, rollback fully restores through MoveItem alone, and the
stale-state class is structurally eliminated. The paperdoll's
(WielderId==p || ContainerId==p) filter still matches optimistic wields via
ContainerId (login-equipped items match via WielderId from their CreateObject).

Also: + the wield+move outstanding-count combo test (spec §8), MoveItem doc
note that it doesn't manage WielderId, and the test pins WielderId==0 post-
optimistic-wield to document the model. Core 74/74 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:15:18 +02:00
Erik
0c353185c3 feat(D.2b): WieldItemOptimistic + equip-aware rollback snapshot (Core)
Add WieldItemOptimistic (instant optimistic wield — sets ContainerId=WielderId=player,
CurrentlyEquippedLocation=equipMask, snapshots pre-wield position) and extend the
_pendingMoves tuple to carry the pre-move EquipMask so RollbackMove restores EQUIPPED
state faithfully on server rejection. Shared RecordPending helper replaces the inline
snapshot block in MoveItemOptimistic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:03:47 +02:00
Erik
1810c71f5c fix(D.2b): correct EquipMask to canonical retail INVENTORY_LOC + numeric-pin test
The old enum invented two phantom bits (HandArmor=0x2000, FootArmor=0x10000) and
used non-retail names (Necklace, LeftBracelet, RightBracelet, LeftRing, RightRing,
AetheriaRed/Yellow/Blue), shifting every slot above 0x1000 out of alignment with
retail INVENTORY_LOC (acclient.h:3193) and ACE EquipMask.

Replace the enum body with verbatim retail values. Add EquipMaskTests to
numeric-pin every member so future renumbering breaks at compile/test time.

Existing consumers (ClientObjectTable, InventoryController, GameEventWiringTests,
ClientObjectTableTests) only reference EquipMask.MeleeWeapon and EquipMask.None --
both present at their correct retail values -- so no call sites needed updating.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:56:06 +02:00
Erik
bd4b49f810 test(D.2b): probe — paperdoll equip slots resolve to UiItemList
De-risk gate for Sub-phase C: verifies that the gmPaperDollUI subtree
imported under the inventory frame (0x21000023) materialises a
representative set of 6 equip-slot element ids as UiItemList widgets,
confirming the controller-binding plan can proceed without an importer
pre-fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 21:52:54 +02:00
Erik
82ab0e045a fix(D.2b): gapless insert on optimistic move — stop the inventory reshuffle
Visual gate: moving an item within a bag reshuffled every item ("the order is
not set"). Root cause: insert-before set ONLY the dragged item's ContainerSlot
to N, colliding with the item already at N; the sort-by-slot tie then reordered
the whole grid on every repaint. Fix: MoveItemOptimistic now does a proper
index-based INSERT (shift the others, renumber 0..N-1 gapless) like retail's
ItemList_InsertItem, and HandleDropRelease uses the target's GRID INDEX
(SlotIndex) as the placement rather than its raw ContainerSlot. Regression test
pins the shifted, gapless order. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:29:22 +02:00
Erik
975f89c870 fix(D.2b): harden optimistic-move pending map (Opus review I1+I2)
I2 (real bug): Clear() (teleport/logoff) and Remove() (item destroyed) now drop
the item's _pendingMoves entry — a stale snapshot on a recycled guid could
otherwise mis-rollback a different future item.
I1 (race): track an OUTSTANDING count per item so an early ConfirmMove (0x0022)
for the first of several in-flight moves of the SAME item can't clear the
snapshot while a later move is unconfirmed — a later reject can still roll back
to the original instead of stranding the item at the rejected position.
Both with regression tests; full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:12:54 +02:00
Erik
8df395894e test(D.2b): close drag-drop spec-review coverage gaps
Add the 3 tests the spec-compliance review flagged: 0x0022 ConfirmMove clears
the pending move (no rollback after a confirm); OnDragOver advisory-accepts a
closed bag (cap>0, contents not indexed = AP-61); App-level drop -> RollbackMove
reverts the optimistic move to the original container+slot. Production code was
already correct; these exercise the spec's test plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:03:24 +02:00
Erik
81d9b3b37a feat(D.2b): InventoryController drag-drop handler (optimistic move + green-arrow/red-circle)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:54:53 +02:00
Erik
4aebf444d9 feat(D.2b): wire ConfirmMove (0x0022 echo) + RollbackMove (0x00A0) for optimistic inventory moves
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:51:20 +02:00
Erik
f7f9ae052e feat(D.2b): ClientObjectTable optimistic move + confirm/rollback for inventory drag
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:49:25 +02:00
Erik
a45c421bd1 feat(D.2b): per-container capacity bar on inventory cells
Faithful port of retail UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0):
each container cell (side bags + main pack) shows a vertical UIElement_Meter
(element 0x10000347, back 0x06004D22 / fill 0x06004D23) filled to
GetNumContainedItems / ItemsCapacity, clamped [0,1]; hidden for non-containers
(CapacityFill=-1). Drawn procedurally on UiItemSlot like the triangle/square
overlays (back full + front clipped bottom-up). Right-anchored flush to the cell
edge (visual gate: the dat X=26 sat ~5px off the right edge). Visually confirmed
2026-06-22. Divergence AP-59; polish deferred to ISSUES #146 (exact rect/anchor,
fill direction vs m_eDirection 0x6f, closed-bag lazy-load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:54:54 +02:00
Erik
077586a0f0 fix(D.2b): main-pack backpack icon is 0x0600127E, not 0x060011F4
The prior commit pinned 0x060011F4 from a research dat-dump of
GetDIDByEnum(0x10000004,7) — it rendered as a GREEN TILE (green slot, no
pack) at the visual gate. Dat-exported the candidates (AcDream.Cli
dump-sprite-sheet / export-ui-sprite): 0x0600127E is the 32x32 brown
backpack (user-hinted, PNG-confirmed). Swap the pinned literal; the
test + AP-51 register row updated to the visually-verified id. Container
type-underlay (green) + backpack base still composited via _iconIds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:33:58 +02:00
Erik
c71b32f73d feat(D.2b): main-pack cell draws the constant backpack icon (AP-51)
The m_topContainer cell (0x100001C9) rendered blank (tex=0). Retail's
IconData::RenderIcons (0x0058d1ee) has an IsThePlayer() branch that draws a
CONSTANT backpack — m_idIcon = GetDIDByEnum(0x10000004, 7) = 0x060011F4,
m_itemType = TYPE_CONTAINER — NOT the player's body icon (the original AP-51
"equipped-pack weenie icon" premise was wrong). Compose that base over the
Container type-underlay via the existing _iconIds delegate. Verified vs decomp
(407546-407549) + IconComposer.GetIcon (base=arg2, type drives underlay) + a
live dat dump (map 0x25000008 index 7 = 0x060011F4). Test locks type+literal.
AP-51 reworded to the residual hardcoded-vs-runtime-resolve nuance (cf. AP-55).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:04:08 +02:00
Erik
7407a71d68 feat(D.2b): InventoryController container-switching + open/selected indicators
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:32:31 +02:00
Erik
842462e375 feat(D.2b): UiItemSlot open-container triangle + selected-item square overlays
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:29:39 +02:00
Erik
ad9d2651c1 feat(D.2b): ViewContents wiring is a full REPLACE (consumes the GameEventWiring TODO)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:28:08 +02:00
Erik
48bf752cf1 feat(D.2b): ClientObjectTable.ReplaceContents — authoritative full-replace for ViewContents
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:27:13 +02:00
Erik
22d92315e5 fix(ui): D.2b empty-slot art — container cells use inner slot bg, not the selected-container triangle
Visual gate (2026-06-22) showed the side-bag/main-pack empty cells drawing yellow
triangles. Root cause: the frame-first heuristic grabbed the 36x36 container
prototype's DirectState child 0x06005D9C — which is the open/SELECTED-container
triangle indicator, NOT a background frame — and stamped it onto every empty cell.

Fix: drop frame-first; FindIconEmpty resolves the inner m_elem_Icon ItemSlot_Empty
THROUGH BaseElement inheritance (0x1000033F -> 0x10000340 -> base 0x1000033E ->
0x1000033B -> 0x06000F6E), so containers get the dark slot background matching the
inventory. Pin test now asserts exact ids (0x06004D20 contents / 0x06000F6E
containers) — the old structural-only assertion let the wrong-but-valid 0x06005D9C
pass (Opus review Minor 2, now vindicated). The triangle + green/yellow selection
square is deferred to container-switching (AP-56 reworded). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:45:36 +02:00
Erik
8c719cd3e9 feat(ui): D.2b empty-slot art — InventoryController applies per-list empty sprites
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:05:12 +02:00
Erik
4785232523 feat(ui): D.2b empty-slot art — UiItemList.CellEmptySprite stamps cells
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:03:19 +02:00
Erik
b8626401ad feat(ui): D.2b empty-slot art — ItemListCellTemplate resolver (0x1000000e -> 0x21000037)
Ports UIElement_ItemList::InternalCreateItem (0x004e3570) attribute lookup to a
testable static helper. The 0x1000000e property is an EnumBaseProperty (not
DataIdBaseProperty as the spec anticipated) — discovered by runtime reflection and
confirmed against the live dat. Pinned sprite ids from real-dat test:
  contents  (0x100001C6): 0x06004D20
  side-bag  (0x100001CA): 0x06005D9C
  main-pack (0x100001C9): 0x06005D9C

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:00:36 +02:00
Erik
1be7e65fad feat(ui): D.2b inventory finish — contents grid shows full main-pack capacity
The contents grid now pads empty slot frames up to the main-pack capacity
(player ItemsCapacity, default 102 per retail "up to 102 items"), so the pack
reads like retail's fixed 102-slot grid you scroll through — not just the loose
items. Mirrors the side-bag column padding. Three existing grid tests updated
for the now-padded count; new test covers the 102-pad.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:21:20 +02:00
Erik
14ea938c7f fix(ui): D.2b inventory finish — grid cells exempt from the anchor pass
Root cause of the "grid escapes the window when scrolled" bug: UiElement.
DrawSelfAndChildren runs ApplyAnchor on every child AFTER OnDraw, which captured
each cell's scroll-0 position and reset Top to it every frame, fighting
LayoutCells' scroll offset. Cells are laid out procedurally by the list, so they
must be exempt — AddItem now sets cell.Anchors = None, making LayoutCells the
sole authority. Regression test reproduces the anchor-pass interaction (the unit
tests missed it by calling LayoutCells in isolation, never the draw traversal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:51:08 +02:00
Erik
4112a53683 feat(ui): D.2b inventory finish — side-bag column slots (36px pitch + empties)
The side-bag column (0x100001CA, 36x252 = 7 slots) pads empty slot frames up to
the player's ContainersCapacity (clamped to 7), at the correct 36px pitch
(split from the contents grid's 32px). Reads like retail's bag column. Two
existing tests updated for the now-padded count (divergence AP-52: 7-slot
fallback when capacity is absent — register row added in the wrap-up commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:33:03 +02:00
Erik
fad807587d feat(ui): D.2b inventory finish — bind contents-grid gutter scrollbar
InventoryController binds 0x100001C7 (factory Type-11 UiScrollbar) to the
contents grid's UiScrollable + the shared 0x2100003E scrollbar sprites,
mirroring ChatWindowController. The grid now scrolls instead of overflowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:29:53 +02:00
Erik
0d3117596c feat(ui): D.2b inventory finish — UiItemList mouse-wheel scroll
OnEvent handles the Scroll wheel in grid mode (mirrors UiText's sign), driving
the shared UiScrollable. The bound gutter scrollbar (next task) is the primary
scroll affordance; the wheel is the convenience path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:26:57 +02:00
Erik
366af0c34f feat(ui): D.2b inventory finish — UiItemList clip+scroll via UiScrollable
Grid mode now drives a shared UiScrollable from its content + clips whole rows
to the panel (cells offset by -ScrollY, Visible toggled by whole-row clip,
mirroring UiText). Fill mode (toolbar single cell) unchanged. LayoutCells made
internal for tests; RowCount helper added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:25:11 +02:00
Erik
7badecf387 test(app): D.2b-B B-Wire — burden wire-first + live-refresh tests + review notes
Final-review response. Adds the spec §5 / Task-16 burden end-to-end coverage
the per-task plan missed: (1) burden reads wire EncumbranceVal over the carried
sum (asserts 50% from wire 7500, not 20% from sum 3000 — retires AP-48's
fallback as the primary), (2) a live player-int update repaints the bar (60%),
which only happens via the C1d Concerns `o.ObjectId == p` branch. Both are
discriminating (fail if the respective branch is reverted).

Plus two clarifying comments from the review: the 0x02CD player route depends on
the login PD upsert having created the player object (no-ops, no phantom, if
not); and a TODO that the container-open phase must treat ViewContents as a full
replace, not the additive merge used here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:19:13 +02:00
Erik
a69c733e6e fix(test): update AppraiseTests.ParsePutObjInContainer_RoundTrip for 4-field parser
Task 7 extended ParsePutObjInContainer to require 16 bytes (added ContainerType
as the 4th field). The pre-existing AppraiseTests round-trip test built only 12
bytes (old 3-field layout), so it returned null and failed. Update the test to
supply 16 bytes and also assert ContainerType.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:06:05 +02:00
Erik
3a9c188233 feat(net): D.2b-B B-Wire — register ViewContents + InventoryPutObjectIn3D/SaveFailed/CloseGroundContainer
Register four previously-unwired GameEvent handlers after InventoryPutObjInContainer:

- ViewContents (0x0196): iterates ParseViewContents entries and calls
  items.RecordMembership(entry.Guid, containerId) for each — so the
  object table is correct before the container-open UI mounts.
- InventoryPutObjectIn3D (0x019A): calls items.MoveItem(guid, 0u) to
  unparent a dropped item from its container (it's now a ground object).
- InventoryServerSaveFailed (0x00A0): parse + log; rollback behavior
  deferred to B-Drag (acdream has no speculative moves yet).
- CloseGroundContainer (0x0052): parse + log; no table change needed
  (the container-open view is UI-only).

Two dispatcher tests cover ViewContents membership and Put3D unparenting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:01:53 +02:00
Erik
d97d84d7ca feat(net): D.2b-B B-Wire — PlayerDescription delivers player properties to ClientObject
Add optional Func<uint>? playerGuid parameter (last in WireAll signature so all
existing callers compile unchanged). When provided, the PD handler calls
items.UpsertProperties(playerGuid(), p.Value.Properties) immediately after the
null-guard, landing EncumbranceVal (PropertyInt 5) and other player stats into the
player ClientObject. Upsert (create-if-absent) handles PD arriving before the
player's CreateObject. Retires AP-48/AP-49 divergence rows (wired in Task 16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:00:59 +02:00
Erik
de8baa1aa1 feat(net): D.2b-B B-Wire — DropItem/GetAndWieldItem/NoLongerViewingContents builders + Send wrappers
Task 9 of the B-Wire inventory-wire plan. Adds three outbound C→S GameAction
builders to InventoryActions.cs (DropItem 0x001B, GetAndWieldItem 0x001A,
NoLongerViewingContents 0x0195) plus matching opcode constants after TeleToPoiOpcode.
Adds SendDropItem/SendGetAndWieldItem/SendNoLongerViewingContents wrappers to
WorldSession.cs (after SendRemoveShortcut, ~line 1157), following the exact same
NextGameActionSequence() + SendGameAction() pattern as the existing Send* family.
Three byte-layout tests added to InventoryActionsTests.cs (12 total, all green).
Core.Net build clean. Unblocks B-Drag (drop) and container-open (NoLongerViewingContents
on close) call sites in the inventory controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:58:30 +02:00
Erik
7013651a90 fix(net): D.2b-B B-Wire — ParseInventoryServerSaveFailed reads weenieError
0x00A0 InventoryServerSaveFailed carries (itemGuid, weenieError) per ACE
GameEventInventoryServerSaveFailed.cs and holtburger events.rs:147. The
old parser returned only the itemGuid as uint?, silently dropping the
error code. Replaced with a typed InventoryServerSaveFailed record that
reads both u32s (8-byte guard). Parser was unwired (no callers in
GameEvents.cs or GameEventWiring.cs) so the signature change is safe.
1 new test in GameEventsInventoryTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:55:56 +02:00
Erik
01910e3fab fix(net): D.2b-B B-Wire — ParsePutObjInContainer reads containerType (4th field)
0x0022 InventoryPutObjInContainer carries 4 u32s per ACE
GameEventItemServerSaysContainId.cs: itemGuid, containerGuid, placement,
containerType. The parser was reading only 3 (12 bytes) and silently
dropping containerType. Fixed the record struct to add ContainerType and
raised the length guard to 16. GameEventWiring caller uses only
.ItemGuid/.ContainerGuid/.Placement — adding a positional field is
source-compatible. 1 new test in GameEventsInventoryTests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:55:14 +02:00
Erik
43afcc2adb feat(net): D.2b-B B-Wire — ParseViewContents (0x0196)
GameEvents.ParseViewContents parses the ViewContents GameEvent payload:
containerGuid + count + [guid, containerType]×count. Records
ViewContentsEntry and ViewContents added. 3 unit tests added in
GameEventsInventoryTests.cs (two-entry, zero-count, truncated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:54:25 +02:00