Captures the full teleport-flow journey (Slice 1 kept, Slice 2 hold built +
reverted), the root-cause findings (the IsLandblockLoaded key bug + the real
foundation problem: destination doesn't stream fast/complete during teleport,
likely _datLock starvation from the CreateObject flood), the open foundation
issues (#138 slow/incomplete streaming, lost-collision-after-teleport, FPS
leak Work-C, the PortalSpace freeze-vs-run-through question), and the clean
baseline (dd2eb8b). Written as evidence + open questions, not conclusions, so
a fresh session can re-examine the whole approach.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-tested: the Slice 2 'hold outdoor until landblock loaded' gate made
EVERY outdoor teleport a ~10 s freeze, because the destination landblock
does NOT load fast during the hold (lbs=0 the whole time — the #138
streaming gap + _datLock starvation from the CreateObject flood). The hold
was band-aiding a broken/slow foundation rather than fixing it, and it never
actually prevented the #145 edge cascade anyway (it force-snapped onto
NO-LANDBLOCK after the timeout regardless).
Reverts ad8c24e..c880973 to the pre-Slice-2 state (00ef47e): outdoor places
immediately again (fast teleports). The genuine bug found along the way —
IsLandblockLoaded queried the wrong key form (& 0xFFFF0000 vs the stored
| 0xFFFF) — is preserved in the history (c880973) and will be re-applied when
we re-introduce a proper hold ON A FIXED FOUNDATION.
Decision (user, 2026-06-21): fix the foundation FIRST — fast/complete
streaming during teleport (#138), the post-teleport lost-collision bug, and
the FPS leak (Work item C) — then revisit the teleport-flow animation. Slice 1
(the pure TeleportAnimSequencer) stays in (dormant, unwired, harmless).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Drag the bottom edge to expand the inventory and see more of the pack
(ResizeX=false + ResizableEdges=Bottom blocks horizontal resize). The contents
grid + its gm3DItemsUI sub-window + scrollbar + the backdrop stretch vertically
(Left|Top|Bottom); the paperdoll + side-bag column stay pinned at the top
(Left|Top). The grid's ViewHeight grows → more rows + the scrollbar thumb ratio
(view/content) reflects how much is left to scroll. Min = dat default (expand
only for now); max = 560. Visually confirmed: scroll + resize work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the gmInventoryUI content in the universal 8-piece beveled chrome (same
UiNineSlicePanel as vitals/chat/toolbar), so the inventory has a proper window
frame like every other window. The frame is the draggable window + the F12-
toggled registered window; the dat content sits inside it offset by the border.
Reused as the base for the vertical-resize step next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Slice 2 outdoor readiness gate queried IsLandblockLoaded(destCell &
0xFFFF0000) = e.g. 0x7D640000, but streaming stores landblocks under the
EncodeLandblockId form (low 16 = 0xFFFF), e.g. 0x7D64FFFF. The raw
ContainsKey never matched, so the outdoor teleport gate could NEVER flip
Ready and every outdoor arrival ran to the 600-frame (~10 s) timeout and
force-placed. The cascade was still prevented (the timeout force-place lands
cleanly), but the gate did no work — the 10 s freeze the apparatus showed
was this bug, NOT the #138 streaming stall I first suspected.
Root cause found via the apparatus re-test (3-agent investigation
wf_8b67a9d1-35c, all high-confidence) + verified against StreamingRegion.cs:99
(EncodeLandblockId | 0xFFFF), PhysicsEngine.cs:79 (stores as-is),
GameWindow.cs:5530 (queries & 0xFFFF0000).
Fix: IsLandblockLoaded normalizes its arg to the canonical 0xFFFF landblock
key, so the prefix form, any contained cell id, and the dat-id form all
resolve. Added the regression test the original Slice 2 test missed (it had
checked the same 0xFFFF form it added; the real caller passes the 0x..0000
form). Red on the prefix/cell forms before the fix, green after. 9/9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
6 tasks: (1) UiItemList clip+scroll via the shared UiScrollable + whole-row
clip; (2) mouse-wheel; (3) InventoryController binds the gutter scrollbar
0x100001C7 like ChatWindowController; (4) side-bag column 36px pitch + empty-
slot padding to capacity; (5) backdrop coverage — screenshot-gated, primary
fix is the clip; (6) verify + bookkeeping. TDD on the pure/controller logic
(internals test-visible); backdrop is the visual gate. Grounded in the dat
dumps + the existing scroll/scrollbar machinery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review follow-up: the exit-sound/login-complete events are emitted
inline at their transitions, so the _exitSoundPending/_loginCompletePending
fields were set-then-cleared dead code — removed. Added a comment explaining
why TunnelContinue's min-advance is gated on worldReady. No behavior change;
29/29 sequencer tests still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1 of the "full retail inventory" arc (Stage 2 = paperdoll, separate
spec). Grounded in the dat dumps: contents grid 0x100001C6 is 192x96 (6x3) +
scrollbar 0x100001C7 (16x96); backdrop 0x100001D0 is full-window 300x362
(so the "torn" look is the unclipped grid overflowing below the frame —
fixed by clipping); side-bag column 0x100001CA is 36x252 (7 slots).
Components: (A) UiItemList clip+scroll reusing UiScrollable + bind the gutter
scrollbar like ChatWindowController; (B) backdrop coverage — primary fix is A,
residual gated on the post-A screenshot; (C) side-bag column pads empty slots
up to capacity at 36px pitch. Paperdoll + B-Drag out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AP-49: TeleportAnimSequencer.ComputeFadeAlpha uses smoothstep in place of
retail's unrecovered 1024-entry GetAnimLevel lookup table
(gmSmartBoxUI::UseTime 0x004d6e30). Retire when the table contents are
extracted via cdb (spec §8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EnterTunnel fires on the first Tick after Begin for Portal/Login/Death kinds
(which enter directly at Tunnel). Already implemented in Task 1.2 via
_enterTunnelPending = _state == TeleportAnimState.Tunnel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TunnelContinue exit gate: minMet requires worldReady (min-continue hold);
maxForce fires unconditionally at MaxContinue (safety-net fallback when
world never loads). This matches spec §3.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bite-sized TDD plan for the retail teleport flow. Slice 1: pure
TeleportAnimSequencer (7-state TAS) + golden-timing tests. Slice 2: the
#145 readiness-gate fix (IsLandblockLoaded + Decide outdoorReady axis +
apparatus probe) — ships independently of the visuals. Slice 3:
TeleportFlowController (delegate-injected, unit-tested) + TeleportFadeOverlay
+ portal wiring (PlaceTeleportArrival split: place vs InWorld so the input
lock persists the whole animation). Slice 4: one yaw-freeze + portal sounds
via the EnumIDMap chain. Slice 5: de-dup login readiness onto Decide, route
login/death through the controller, logout (Shift+Esc) + 0xF653 + disconnect,
remove dead _teleportArrival plumbing. Slice 6 (literal 3D swirl) is a
follow-up plan gated on a cdb asset trace.
Drafted via two research + drafting workflows; slices 3-5 redrafted as one
cohesive unit against a pinned controller API after the first parallel pass
produced cross-slice inconsistencies (missing controller task, triplicated
yaw-freeze, a fabricated PlayerMovementController.Update signature). All
load-bearing signatures personally verified against the tree.
Plan: docs/superpowers/plans/2026-06-21-retail-teleport-flow.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword AP-48 (burden) and AP-49 (carry-aug) to fallback/default-only: B-Wire
ports the retail wire read (login PD-bundle UpsertProperties + live 0x02CD;
ObjectTableWiring applies all ints), so SumCarriedBurden / aug=0 are now
defensive only. Confirm the server sends EncumbranceVal/0xE6 at the visual
gate, then delete the rows. Add the D.2b-B B-Wire SHIPPED entry to ISSUES.
(Memory digest project_d2b_retail_ui.md + MEMORY.md updated out-of-tree.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
InventoryController.Concerns() now returns true when the updated object IS the
player object itself (o.ObjectId == playerGuid). Previously the method only
triggered a repopulate for objects that the player *contains* or *wields*, but
the player's own ClientObject is the carrier of EncumbranceVal — so a live
PrivateUpdatePropertyInt for burden would be silently ignored. Task 15 of the
B-Wire plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass () => _playerServerGuid to ObjectTableWiring.Wire and GameEventWiring.WireAll
so the new Batch 5/6 playerGuid parameters are populated. These were previously
left as null (default), meaning PD property upserts and PrivateUpdatePropertyInt
delivery would not route to the correct player object. Task 14 of the B-Wire plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- Wire signature gains optional 3rd param `Func<uint>? playerGuid` (existing
GameWindow caller `Wire(session, Objects)` still compiles — default null).
- ObjectIntPropertyUpdated gate loosened: was UiEffects-only, now applies ALL
PropertyInt updates on visible objects (server is the authority on object props).
- PlayerIntPropertyUpdated → UpdateIntProperty(playerGuid(), ...) so live
EncumbranceVal (0x02CD) updates the player's burden bar.
- StackSizeUpdated → UpdateStackSize(guid, stackSize, value).
- InventoryObjectRemoved → Remove(guid).
- Updated class doc-comment to list all 5 wired opcodes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add 3 new events (PlayerIntPropertyUpdated, StackSizeUpdated, InventoryObjectRemoved)
and their payload record types near the existing ObjectIntPropertyUpdated event.
Add 3 switch cases in the GameMessage dispatcher immediately after the
PublicUpdatePropertyInt (0x02CE) branch for:
- PrivateUpdatePropertyInt (0x02CD) — no-guid player-own property
- SetStackSize (0x0197) — stack count + value update
- InventoryRemoveObject (0x0024) — remove from inventory view
No test seam on the switch; verified by build + parser tests (Tasks 3/4/5)
+ live run per codebase convention (see ObjectTableWiringTests comment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
Mirror of PublicUpdatePropertyInt (0x02CE) but with no guid field —
targets the local player's own object. Burden (EncumbranceVal, PropertyInt 5)
changes ride this opcode on every pick-up / drop. Layout: opcode(4) + seq(1)
+ property(4) + value(4) = 13 bytes. 3 tests: happy path, wrong opcode, truncated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design/spec for B-Wire (follows B-Controller c38f098). Root cause found:
the burden binding already reads wire EncumbranceVal (PropertyInt 5) but
the value never arrives — login PD parses the player's int table then
drops it, the live 0x02CD (PrivateUpdatePropertyInt, no guid) is unparsed,
and ObjectTableWiring gates all non-UiEffects ints out.
Scope (user chose the full wire pass): player-property delivery (login PD
bundle upsert + 0x02CD parse + loosen the apply gate, retiring AP-48/AP-49),
latent-bug fixes (0x0022 4th field, 0x00A0 error), new C→S builders
(DropItem 0x001B / GetAndWieldItem 0x001A / NoLongerViewingContents 0x0195),
new S→C parsers (ViewContents 0x0196 GameEvent; SetStackSize 0x0197 +
InventoryRemoveObject 0x0024 GameMessages), and WireAll registration.
Opcodes pinned against ACE GameMessageOpcode.cs / GameActionType.cs; every
format requires grep-named → cross-ref → pseudocode → port + conformance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>