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>
3rd live session found the carried-anchor fix is incomplete: the cascade recurs
when a teleport arrives onto a NOT-YET-STREAMED landblock near an edge (0xC98C
arrival at local Y=190.3, NO-LANDBLOCK -> marches 0x8C->0xFE, wire localY=-21684,
ACE rejects). Streamed-arrival case IS fixed (verified ~10 landblocks). Same root
as the Z free-fall (#135/#138 placed-but-unstreamed gap). Prior 'gate passed' was
premature. Needs apparatus (anchor/guard diagnostic at the crossing) before a fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #145: the original fix was incomplete (mounted panels inherited ZLevel 1000 →
behind the backdrop → washed out); continuation 417b137 closes it. Updated status.
- D.2b-B SHIPPED entry: visually confirmed; the two render bugs (backdrop wash-out
+ captions host-direct) documented.
- New OPEN issue: contents grid overflows (needs the gutter scrollbar wired; scroll
was out-of-scope for B-Controller per the spec).
- Roadmap ledger: B-Controller visually confirmed + the render fix noted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual verification surfaced two render bugs (controller LOGIC was already correct):
1. BACKDROP WASH-OUT (the big one — #145 continuation). The mounted backpack/
3D-items panels inherited their sub-window root's ZLevel 1000 via the merge's
zero-wins-base rule. The #145 ZOrder fold (ReadOrder − ZLevel·10000) turned 1000
into ZOrder ≈ −10,000,000 — sinking the panels BEHIND the frame's Alphablend
backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpainted the panels'
captions/burden-meter/cells (the paperdoll root is ZLevel 0 so it escaped, which
is why the previous session thought #145 was done). Fix: the sub-window mount now
keeps each slot's OWN frame ZLevel, so panels sit in front of the backdrop.
Root-caused via a one-shot sprite-segment-order dump (backdrop was painting after
the panel content) + a live ZLevel probe.
2. CAPTIONS. The caption elements resolve to UiText; driving a nested child UiText
didn't paint. AttachCaption now drives the host UiText directly.
Locked by InventoryFrameImportProbe (real-dat smoke: asserts each mounted panel's
ZOrder > the backdrop's). Visually confirmed by the user: dark backdrop behind,
Burden 17% + vertical bar + Contents-of-Backpack + full item grid all visible.
Build + App(532)/Core(1526) tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review flagged a possible CellPosition staleness on indoor->outdoor
transition. Verified against source: false positive (SnapToCell isn't called on
building entry; the body is world-space so the delta is frame-invariant; the anchor
disengages indoors). Added a test proving CellPosition tracks the outdoor cell under
the world position across a multi-cell, cross-landblock walk and stays canonical.
Core cell-sync 6/6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SetPosition zeros the body velocity but the motion interpreter kept the
PRE-teleport ForwardCommand (RunForward), so the next Update() rebuilt that
run vector via get_state_velocity and the player sprinted off in the old
direction on arrival. DoMotion(Ready) makes the player arrive at rest. Not a
cascade fix anymore (Slice 3 closed that) — the last user-visible bit of #145.
All suites green: Core 1529 / App 480 / UI 425 / Net 313.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The outdoor membership pick derived the landblock origin from the terrain
registry, which returns (0,0) for an UNSTREAMED neighbour — so a fresh far-town
teleport at a landblock edge marched the cell id one block per physics tick
(the cascade; the 17410 ACE rejects is its wire artifact).
Fix: thread the CARRIED cell-relative frame anchor (body.Position -
body.CellPosition.Frame.Origin) into the pick via SpherePath.CarriedBlockOrigin.
That anchor IS the true landblock world origin, correct even for an unstreamed
neighbour, so the pick re-derives the SAME (consistent) cell and never marches.
- CellTransit.FindCellSet/BuildCellSetAndPickContaining: Vector3? carriedBlockOrigin
(null default = legacy TryGetTerrainOrigin → every existing caller/test untouched).
- PhysicsEngine.ResolveWithTransition: set the anchor from a SEEDED OUTDOOR body
whose carried landblock matches the resolve cell (else null → legacy).
- PlayerMovementController.SetPosition: 3-arg overload seeds CellPosition from the
wire's (cell, local) via SnapToCell; 2-arg delegates with cellLocal=pos (anchor
(0,0,0) == legacy → zero test churn).
- GameWindow.CellLocalForSeed: the placement seam (_liveCenter used ONCE here to
derive the cell-local; physics carries it forward without _liveCenter).
Regression: TeleportFarTownRunawayTests (south + east edge, unstreamed neighbour).
Core 1529 / App 480, zero regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wire local is LANDBLOCK-relative [0,192); the cell low word = floor(local/24),
so a consistent (cell,local) pair must keep them in lockstep. Two retail-faithful
corrections vs the first pass:
- SnapToCell canonicalizes the OUTDOOR seed via AdjustToOutside (retail
SetPositionInternal/adjust_to_outside @0x00504A40 — the #107 'never trust a
server (cell,pos) pair' protection). Indoor seeds stay verbatim (BSP-validated).
- SyncCellPositionDelta calls AdjustToOutside on EVERY delta, not just on 192 m
crossings, so intra-landblock 24 m cell-index changes track (needed by Slice 3
membership). Idempotent within a cell.
Tests rewritten to verify both (the earlier test paired an inconsistent cell+local).
Core 1527 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CellPosition (retail Position type) alongside the world Vector3 Position.
The Position setter mirrors each world delta into the cell-local origin and
calls AdjustToOutside only when the local coord crosses a landblock boundary
([0,192) on X or Y), so the within-block cell id is preserved from the wire
seed. SnapToCell seeds both positions from the wire's (cell, local) pair
verbatim — no streaming center involved. Unseeded bodies (ObjCellId==0) and
indoor cells are no-ops in the delta path. UpdatePhysicsInternal's existing
`Position +=` desugars through the new setter automatically; no call sites
changed. 4 new unit tests; full Core suite 1526 passed / 0 failed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new value type collided with DatReaderWriter.Types.Frame (used in
physics-adjacent code like ShadowShapeBuilder), which the structural fix
(per-file using-aliases across 6 files) would have re-incurred in every
later physics slice. Renamed the TYPE to CellFrame; the Position.Frame
MEMBER keeps retail's name. Restored the 5 alias-only files to their
pre-Slice-1 state; synced spec + plan. Core 1522 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces the two value types (Frame, Position) that represent retail's
cell-relative position pair (acclient.h:30647/30658). Types are unused
by consumers yet — zero behavior change. Also ports LandDefs::get_block_offset
(pc:69189, @0x0043e630): world-meter offset between two named landblock ids,
the ONLY cross-cell translation primitive in retail physics. Conformance tests:
same-landblock→Zero, south-neighbour→(0,-192,0) (the exact #145 cascade cell),
east-neighbour→(+192,0,0), diagonal→(+192,+192,0). 4/4 pass; full Core suite
1522 passed / 0 failed. DatFrame alias added to 4 files that had using
DatReaderWriter.Types + using AcDream.Core.Physics in scope simultaneously.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approved design for the cell-relative physics frame port: retire
_liveCenter from physics (render-only), carry Position{ObjCellId,
Frame{local in [0,192), quat}} as the source of truth, port
get_block_offset (verified 0x0043e630 nets to delta-landblock * 192 m),
translate inter-tick collision state per-cell like retail (0x0050a592).
7-slice parallel-frame migration; conformance via transform-on-read (no
fixture re-capture).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workflow wf_87607d15-c43 (4 research streams + synthesis + 3 adversarial verifiers, HIGH confidence) confirmed the far-town runaway is a cell-membership label cascade from a discarded TryGetTerrainOrigin bool (CellTransit.cs:736 -> (0,0) origin for unstreamed neighbors), not a free-fall; 17410 is a wire artifact. User chose the architectural fix: port retail's cell-relative Position + retire _liveCenter from physics. Handoff doc carries the verified mechanism, the retail port table (decomp addresses), acdream divergence sites, apparatus (desync-capture.jsonl + probes + harness template), and the brainstorming-gate requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opus phase-boundary review findings:
- I1 (faithfulness): LoadToPercent now computes from the CLAMPED fill
(floor(LoadToFill(load)*300)), so the burden % SATURATES at 300% like retail
(decomp 176544-176576 clamps arg2 to [0,1] BEFORE the *300). The old
floor(load*100) over-read to 400% at 4x capacity. Golden test corrected.
- I2 (partition): exclude equipped items (CurrentlyEquippedLocation != None) from
the contents grid + selector — a mid-session self-wield routes them through
MoveItem(item, WielderGuid=player) into GetContents(player); retail's gm3DItemsUI
shows pack contents only. New conformance test.
- N1: drop dead 'using System.Collections.Generic' (left after the 383e8b7 cleanup).
- N3: AP-48 risk wording (drift can be low OR high vs server EncumbranceVal).
Build green; BurdenMath 17, InventoryController/UiMeter 10 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds _inventoryController field (after _selectedObjectController) and calls
InventoryController.Bind in the existing inventory-init block, between
RegisterWindow and the Console.WriteLine. Uses Objects/iconComposer/
vitalsDatFont already in scope; strength lambda pattern-matches AttributeSnapshot
to int? (uint? has no implicit cast to int?).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The implementer added a full-scan fallback in Populate() to accommodate a test
that seeded items via AddOrUpdate (which deliberately does NOT touch the container
index — only Ingest/MoveItem do). That was a production workaround for a faulty
test, and inconsistent (it only triggered when the index was wholly empty).
Root-fix: Populate reads GetContents(player) only — the index IS retail's
per-container item list. The test now seeds via the faithful indexed path
(AddOrUpdate + MoveItem → Reindex) through a SeedContained helper. 530 App tests green.
Task 4: InventoryController.Bind + Populate: find-by-id bind for the 7 inventory
element ids, configure grid (6 cols x 32px) + container list, partition
GetContents(player) into loose items (contentsGrid) vs side bags (containerList),
populate main-pack cell in topContainer, subscribe ObjectAdded/Moved/Removed/Updated
for live rebuilds. Handles both Ingest-indexed (production) and AddOrUpdate-direct
(test) paths via fallback scan.
Task 5: burden meter (vertical UiMeter, FillFromBottom=true) + RefreshBurden port
of CACQualities::InqLoad (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel
(0x004a6ea0): EncumbranceCapacity/LoadRatio/LoadToFill/LoadToPercent. Three
AttachCaption overlays (Burden, Contents of Backpack, %text). 5 tests all green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captured: teleport to far town (201,91) places correctly via the #145 verbatim path, then the per-frame resolve marches membership one landblock south/frame (un-rebased local position) until ACE rejects the inconsistent (cell, local) pair. #138 re-hydrate exonerated. Root cause under multi-agent research (acdream code + retail decomp oracle + capture).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>