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>
Drives the doll 3-D pass in a pre-UI hook (after the world passes, before
_uiHost.Draw), gated on inventory-open AND doll-view (the viewport widget's
Visible, set by the Slots toggle). RefreshPaperdollDoll clones the live
player entity (_entitiesByServerGuid[player]) into a DollEntityBuilder doll
with COPIED MeshRefs (frozen pose); re-dress is triggered by a dirty flag
set in OnLiveAppearanceUpdated (0xF625) — the C# analog of RedressCreature.
Correctness fix: the doll is passed in animatedEntityIds so the dispatcher
BYPASSES the Tier-1 classification cache (WbDrawDispatcher.cs:1142). Without
it, a re-dress (a new WorldEntity with the same fixed DollRenderId) would
serve the previous doll's cached batches and the new gear wouldn't appear.
Renderer disposed in the GameWindow teardown. Build + full App suite green
(594). Static doll (no idle animation yet — next slice). Visual gate next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The C# analog of CreatureMode::Render: draws one re-dressed player clone
into a private FBO (RGBA8 + depth24-stencil8) with the fixed DollCamera +
one distant light (retail 0.3,1.9,0.65 @ 2.0), sealed in a GLStateScope so
it can't disturb world/UI GL state. frustum:null ⇒ the doll's synthetic
landblock is always visible ⇒ walked from entry.Entities and drawn from its
current MeshRefs (static pose; idle animation is a later slice).
Manages the FBO directly via GL (ManagedGLFramebuffer is unused/bitrotted
WB infra). UiViewport blit flips V (FBO bottom-left origin vs UI top-left)
so the doll isn't upside-down. Not yet wired — GameWindow hook is next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Brainstorm-approved design for Sub-phase C Slice 2: the 3-D doll
UiViewport (dat Type 0xD) + the "Slots" toggle, extending the shipped
PaperdollController.
Key decisions settled in the brainstorm:
- Compositing = render-to-texture (reuse ManagedGLFramebuffer + GLStateScope):
the doll renders to an off-screen buffer in a pre-UI hook, then the
UiViewport widget blits it as a normal sprite -> correct painter order
for free, fully sealed 3-D pass.
- The armor/non-armor partition is the decomp-exact 9-slot set that
ListenToElementMessage (idMessage==1, 0x100005be) flips, not an
EquipMask heuristic.
- Doll = a dedicated WorldEntity cloned from the local player's Setup +
current ObjDesc (the player IS a WorldEntity -- corrects the handoff),
re-dressed on ObjDescEvent 0xF625; reuses EntitySpawnAdapter/
AnimatedEntityState.
- Seam IUiViewportRenderer lives in AcDream.App.UI (intra-App decoupling),
not Core -- user-approved divergence from the handoff's "Core interface".
Recovered the corrupted light immediate ("ff&?" = 0x3f266666 ~= 0.65).
AP-66 reworded: empty-slot frame stays (slots ring the doll, no overlay).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carries the corrected paperdoll model forward (figure = live doll, NOT
silhouettes; the Slots button toggles doll-view vs armor-slot-view — REPLACE,
not overlay). Scopes Slice 2 = (A) the toggle (read ListenToElementMessage
idMessage==1) + (B) the UiViewport Type 0xD doll via a Core->App
IUiViewportRenderer seam reusing EntitySpawnAdapter. Includes the decomp
anchors, camera/light immediates to decode, open questions, and the
new-session prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Divergence AP-66: Slice-1 empty-slot frame vs the Slice-2 doll-backed
transparent look (retired when the doll viewport lands).
- Paperdoll handoff: prominent top note correcting the WRONG "transparent /
per-slot silhouettes" framing — the figure IS the live 3D doll; the
Slots toggle + doll = Slice 2. Points to the project memory's Slice 1
entry for the full DO-NOT-RETRY.
(The detailed shipped-log + corrected model live in the auto-loaded
claude-memory/project_d2b_retail_ui.md, updated this session.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Adds the _paperdollController field and PaperdollController.Bind(...)
call in the inventory-frame block alongside InventoryController, wiring
up the paperdoll equip slots with their icon composer and wield action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Code-review minor: prevents a future WielderId-only 'is this wielded?' check
from silently missing an optimistically-wielded item (WielderId==0 until the
server confirm; ContainerId==wielder is the live signal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Code-quality review minors on Task 2: fix the missing space in the
FingerWearRight= alignment and document why bit 31 (0x80000000, present
only in the header's CLOTHING_LOC composite) has no named member.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Sub-phase C, Slice 1 — bind the ~25 paperdoll equip slots to live
equipped-item data + make them drag-drop wield/unwield targets. No 3D
doll (that's Slice 2).
Brainstorm findings baked in:
- The handoff's "build the wire gap" premise is STALE: BuildGetAndWieldItem
+ SendGetAndWieldItem already shipped in B-Wire. The whole optimistic-move
machine + InventoryController:IItemListDragHandler already exist — Slice 1
mirrors them. Unwield is free (the inventory grid handler already does it).
- Found a latent bug: acdream's EquipMask enum diverges from canonical AC
(acclient.h:3193 INVENTORY_LOC) from bit 0x2000 up (phantom HandArmor/
FootArmor). Correct it to the verbatim retail values + a numeric-pin test.
Blast radius is safe (4 round-trip test refs).
- Empty equip slots are TRANSPARENT (EmptySprite=0), per the user — faithful,
zero Slice-2 rework.
Real scope: correct EquipMask (Core) + WieldItemOptimistic & equip-aware
rollback snapshot (Core) + ConfirmMove on the WieldObject 0x0023 handler
(Core.Net) + PaperdollController (App) + GameWindow wiring. Element-id→mask
map verified dump ↔ deep-dive §3a ↔ acclient.h.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Detailed handoff for the next session. Key correction from the deep-dive:
retail empty equip slots are TRANSPARENT (the doll shows through), NOT
silhouettes — the current blue border is wrong. Decomposes into Slice 1
(equip slots: bind to CurrentlyEquippedLocation + drag-to-wield via the
GetAndWieldItem 0x001A wire gap) and Slice 2 (the heavy 3D doll UiViewport
Type 0xD + Core->App IUiViewportRenderer seam). Maps the reuse surface
(UiItemSlot, drag spine, optimistic-move, EntitySpawnAdapter) + open
questions + a paste-ready new-session prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
Brainstorm output for B-Drag. Drop an inventory item: empty grid slot ->
first empty; on an item -> insert before; on a side-bag cell -> into that
container. Green insert-arrow (valid) / red circle (full). Movement is
OPTIMISTIC/instant per the user — local MoveItem on drop + repaint, server
reconciles via 0x0022 echo, rolls back via 0x00A0 (the rollback the B-Wire
note reserved). InventoryController : IItemListDragHandler; pending-move
tracking in ClientObjectTable (Core, reachable from the Core.Net handlers);
SendPutItemInContainer wraps BuildPickUp 0x0019. Retail anchors: InqDropIconInfo
0x004e26f0 / ItemList_InsertItem / HandleDropRelease / ServerSaysMoveItem.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
Comment-only. (1) the InventoryController summary said "Read-only: no
container switching" — now live. (2) note the _openContainer 0-vs-playerGuid
sentinel equivalence so the dual main-pack path isn't mistaken for dead code.
No behavior change. Code-quality review verdict: APPROVED (ship to gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>