The FPS deep-dive landed (dense Arwic 75 -> ~165 fps via the cell-object
batching + cell-particle consolidation, both already committed). Remove the
throwaway diagnostic apparatus now that it has served its purpose:
- delete FrameProfiler.cs (whole-frame TimeElapsed + [PASS-GPU] glFinish +
[CPU-PHASE]/[GPU-PHASE] timers + the =1/=2 ACDREAM_FPS_PROF modes)
- GameWindow: _fpsProf/_frameProfiler/_msaaSamples fields, the BeginFrame/
EndFrame/MarkUpdateStart hooks, the terrain glFinish, and the landscape
sub-phase LsMark instrumentation
- RetailPViewRenderer: the DrawInside per-phase Phase()/MarkGpu markers
- ParticleRenderer / PortalDepthMaskRenderer / EnvCellRenderer: the per-pass
glFinish brackets
- delete DegradeCoverageProbeTests.cs (the dead distance-degrade probe)
KEPT (the real fixes): RetailPViewRenderer cell-object batching + consolidated
cell-particle pass; EnvCellRenderer.CellHasTransparent. Build + full test suite
green (468 App incl. pview replay tests; 1566 Core; 317 Net; 425 UI).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
Carries the parsed dat objects ApplyLoadedTerrainLocked needs so the worker
can pre-read them and the apply can run lock-free. Optional field (default
null) keeps existing LoadedLandblock construction back-compatible.
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>
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>
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-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>
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>
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>
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>
acdream accumulated every CreateObject from every town visited and never pruned by
distance/time (only on server DeleteObject / respawn de-dup), so the entity tables +
the O(N^2) TickAnimations scan grew with each hop and sank FPS (confirmed in Release).
Faithful port of holtburger liveness.rs (ACE_DESTRUCTION_TIMEOUT_SECS=25,
CONSERVATIVE_VISIBILITY_DISTANCE_M=384): a world entity is evicted only after being
>384m AND outside the 3x3 landblock neighborhood for 25s continuous (arm-on-leave /
clear-on-return). Logic in a pure, unit-tested EntityVisibilityCuller; GameWindow
wires a 1Hz tick that snapshots the world entities + player and tears each evicted
guid down through the existing pruner. Player + held/equipped/contained items are
excluded (player by guid; inventory items never carry a world position so they never
enter the culled map). A re-created object starts fresh (deadline cleared on remove).
Skipped during a teleport hold (frozen player position). AD-32 registered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After a teleport ACE floods a town's CreateObjects; WorldSession.Tick drained the
ENTIRE inbound queue every frame, each spawn hydrating mesh+textures under _datLock
on the render thread — monopolizing the update loop for ~a minute and starving the
streaming apply, so only the destination landblock loaded and neighbors trickled in
(visible at high render-FPS because update/render are separate Silk.NET callbacks).
Bound the per-frame drain to a ~4ms wall-clock budget once InWorld (handshake uses
the blocking PumpOnce path, never Tick). A time budget self-adapts to the highly
variable per-datagram cost; the tail stays queued (unbounded channel, FIFO) and
drains over the next few frames. Acks are queued per packet BEFORE the heavy handler
(WorldSession.cs:680), so deferring the tail only delays the tail's acks a few frames
— within ACE's tolerance (holtburger defers acks on a flush cadence; verified in
references/holtburger session/send.rs). Budget decision extracted as a pure testable
static (4 unit tests).
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>
Wires the dormant TeleportAnimSequencer as the transit driver: on PlayerTeleport
the player holds in PortalSpace behind a full-screen fade (FadeOverlay) until the
destination terrain is resident (TeleportWorldReady, gated on the priority-applied
landblock), then materializes (Place), and after the world fades back in regains
control + acks the server (FireLoginComplete). No movement resolves against the
empty world, so the outbound cell frame can't corrupt. Outdoor changes from
place-immediately back to hold-until-resident (now fast, not a band-aid).
- FadeOverlay: fullscreen NDC black quad, alpha = ShowTunnel ? 1 : FadeAlpha.
- Retires TeleportArrivalController + its 2 tests (TAS subsumes the driver role).
- Divergence register: AD-2 updated to the new mechanism; AD-31 (fade vs swirl).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BuildCellSetAndPickContaining discarded the bool from TryGetTerrainOrigin — when
the current landblock's terrain hadn't been applied yet (priority-apply in flight
after a teleport or dungeon exit), blockOrigin was silently set to (0,0,0). The
AdjustToOutside/GetOutsideLcoord math treated world-frame sphere coordinates as
block-local and marched the cell one landblock per tick in the direction of movement
until lbX or lbY underflowed to 0x00. ACE rejected every subsequent move as a
failed transition.
Fix: honor the bool return. When terrain is unregistered for an OUTDOOR seed
(low < 0x0100), return currentCellId verbatim — "no block-local frame →
preserve". This mirrors the NO-LANDBLOCK verbatim contract in PhysicsEngine.Resolve
and is correct: the cell stays last-known-correct until terrain registers.
Indoor seeds are explicitly excluded (blockOrigin is never consumed by the indoor
pick path; outdoorPickAllowed=false for indoor seeds).
Reproduce + verify via CellMarchLandblockPreservationTests (two new FAILING-before
tests: WestEdge and SouthEdge with empty cache, no anchor → lbX/lbY preserved).
TeleportFarTownRunawayTests updated: no-anchor path now also preserves (pre-fix it
marched south to 0x59; post-fix returns currentCell unchanged).
CellTransitFindCellSetTests, Issue112MembershipTests, PhysicsEngineTests: added
RegisterTerrain for the streaming-center block (in production it is always resident
before outdoor resolves run; tests that used blockOrigin=(0,0,0) as an implicit
fallback now register the block explicitly). All 1567 tests pass.
Divergence AD-30 added to retail-divergence-register.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a high-16-bit-prefix landblock residency check used as the teleport
worldReady gate — true once the destination landblock's terrain+cells
have been registered via AddLandblock, regardless of whether the caller
passes a canonical (0xFFFF), cell-resolved, or bare landblock id.
Two TDD tests confirm: false before registration, true after, and
that a cell-resolved id on the same landblock returns true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace RemoveAt(0)-in-a-loop drain idiom with RemoveRange(0, i) in both
Step-1 deferred drain and the post-found deferred drain inside DrainAndApply,
making each an O(N) single shift instead of O(N²) on the render-thread hot path.
Add PriorityNeverArrives_noThrow_noLoss_noDoubleApply test: sets a priority id
that never appears in the outbox, ticks several times, asserts no throw, no
loss of the non-priority completions, and no double-apply (applied count ==
completions enqueued). Comment above the priority-hunt explains the failure
mode: completions relocate to _deferredApply while the hunt is active and drain
at per-frame budget until the caller clears PriorityLandblockId.
Restyle test 2 to use named constructor arguments and break the compressed
lambda onto readable lines (matches test 1 style).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PriorityLandblockId (uint, default 0) + _deferredApply buffer.
DrainAndApply now: (1) applies up to budget from the deferred buffer,
(2) when PriorityLandblockId != 0, hunts the worker outbox in chunks
applying the priority LB immediately on match and buffering any
non-priority items drained past it for later frames,
(3) falls back to normal drain when no priority is set or not found.
Extracts ApplyResult(result) + ResultLandblockId(result) helpers so
both the priority and normal paths share identical side-effects.
No existing behaviour changes on the non-priority path.
21 streaming tests pass (19 existing + 2 new priority-apply tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>