Commit graph

790 commits

Author SHA1 Message Date
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
b47b21570a feat(slice2): TeleportArrivalRules.Decide — outdoor holds until landblock loaded (#145 §3.4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:12:37 +02:00
Erik
ad8c24ef8b feat(slice2): add PhysicsEngine.IsLandblockLoaded — readiness gate §3.4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:10:22 +02:00
Erik
8b5002791a feat(core/slice-1): Begin() sets EnterTunnel pending for portal/login/death entry; all sequencer tests pass
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>
2026-06-21 20:01:17 +02:00
Erik
c3d6eccf51 test(core/slice-1): full portal+logout event-sequence ordering + no-duplicate-fire coverage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:00:39 +02:00
Erik
f1b59f3a64 test(core/slice-1): FadeAlpha endpoint + monotonicity + ShowTunnel/ShowPleaseWait coverage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:59:51 +02:00
Erik
c2fc7ce1ef feat(core/slice-1): TeleportAnimSequencer — full 7-state Tick() with timed transitions and edge events
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>
2026-06-21 19:59:08 +02:00
Erik
0468df21f5 feat(core/slice-1): TeleportAnimSequencer — Begin(), IsActive, enter-sound edge event
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:56:56 +02:00
Erik
4f7e8ec30a feat(core/slice-1): TeleportAnimSequencer — define enums, record, event types
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:55:40 +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
Erik
79c0374d53 feat(net): D.2b-B B-Wire — InventoryRemoveObject (0x0024) parser
Top-level GameMessage (UIQueue), not a GameEvent. Server sends when an
object leaves the client's inventory view (sold / dropped / destroyed).
Layout: opcode(4) + guid(4) = 8 bytes — the smallest inventory wire.
3 tests: happy path, wrong opcode, truncated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:52:21 +02:00
Erik
87e354f583 feat(net): D.2b-B B-Wire — SetStackSize (0x0197) parser
Top-level GameMessage (UIQueue), not a GameEvent. Server sends after a
stack merge / split to update count + value. Layout: opcode(4) + seq(1)
+ guid(4) + stackSize(4) + value(4) = 17 bytes. 3 tests: happy path,
wrong opcode, truncated. Client consumer: ACCWeenieObject::ServerSaysSetStackSize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:51:42 +02:00
Erik
7b25d78506 feat(net): D.2b-B B-Wire — PrivateUpdatePropertyInt (0x02CD) parser
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>
2026-06-21 18:50:56 +02:00
Erik
3f190811be feat(core): D.2b-B B-Wire — ClientObjectTable.UpdateStackSize
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:48:54 +02:00
Erik
b56087b498 feat(core): D.2b-B B-Wire — ClientObjectTable.UpsertProperties (create-if-absent)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 18:48:04 +02:00
Erik
417b1375fd fix(ui): D.2b-B — inventory panels render over the backdrop (#145 continuation) + captions
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>
2026-06-21 17:58:44 +02:00
Erik
7605439efa test(physics #145): continuous-tracking test — refutes the review staleness claim
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>
2026-06-21 17:38:47 +02:00
Erik
6349ba49aa fix(physics #145): Slice 3 — carried-anchor membership; closes the far-town cascade
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>
2026-06-21 13:51:04 +02:00
Erik
7928b445ab fix(physics #145): Slice 2a — canonicalize outdoor seed + re-derive cell index every tick
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>
2026-06-21 10:55:27 +02:00
Erik
afe495b9e6 feat(physics #145): Slice 2a — PhysicsBody carries CellPosition; setter delta-syncs into the cell frame
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>
2026-06-21 10:49:39 +02:00
Erik
c980763322 refactor(physics #145): Slice 1 — rename Frame->CellFrame to avoid DatReaderWriter.Types.Frame collision
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>
2026-06-21 10:34:36 +02:00
Erik
438bb681a5 feat(physics #145): Slice 1 — Position/Frame types + LandDefs.GetBlockOffset (0x0043e630)
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>
2026-06-21 10:30:20 +02:00
Erik
1ccf07b705 fix(ui): D.2b-B — address phase-boundary review (burden % saturation + equipped filter)
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>
2026-06-21 09:28:29 +02:00
Erik
383e8b7b55 refactor(ui): D.2b-B — Populate uses GetContents only (drop test-driven fallback)
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.
2026-06-21 09:09:15 +02:00
Erik
89c640a54d feat(ui): D.2b-B — InventoryController bind + grid population (loose/side-bag partition)
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>
2026-06-21 09:04:48 +02:00
Erik
5e75d2ac76 feat(ui): D.2b-B — UiMeter vertical fill (burden bar, retail m_eDirection 2/4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:58:37 +02:00
Erik
fb050aed4d feat(core): D.2b-B — ClientObjectTable.SumCarriedBurden (carried-Burden fallback)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:55:35 +02:00
Erik
5875ac857d feat(core): D.2b-B — port EncumbranceSystem capacity/load + SetLoadLevel fill/percent
EncumbranceCapacity (decomp 0x004fcc00), Load (0x004fcc40), SetLoadLevel
fill=load/3 clamped + percent=floor(load*100) (0x004a6ea0). Golden tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:54:16 +02:00
Erik
0a5f91b6fe fix(streaming): #138 — rescue persistent entities from the pending bucket on unload
GpuWorldState.RemoveLandblock rescued persistent entities (the player)
only from the _loaded list, silently dropping one sitting in the
_pendingByLandblock bucket. The player is re-injected via AppendLiveEntity
every frame; right after a teleport its destination landblock has not
streamed in yet, so the player lands in the pending bucket — and if that
landblock is then unloaded during the streaming churn, the persistent
entry was dropped, violating the "persistent therefore survives unload"
contract. Leading candidate for the #138 "own avatar vanishes after a
couple round-trips" symptom (cumulative; needs user visual confirm).

Fix: scan the pending bucket for persistent guids and rescue them too,
so DrainRescued re-parks them at the next valid landblock. Provable
correctness fix with a deterministic test (rescue-from-pending plus a
negative for non-persistent). Core tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:07:28 +02:00
Erik
bf66fb4123 fix(streaming): #138 — re-hydrate server objects from the retained spawn table on reload
Doors/NPCs/portals vanished after a portal OUT of the 0x0007 dungeon back
to Holtburg. Root cause confirmed via ACE + holtburger cross-reference:
the dungeon collapse drops a landblock's render entities for FPS, and ACE
will NOT re-broadcast objects whose guid is still in its per-player
KnownObjects set (never cleared on a normal teleport — ACE relies on the
client retaining its object table and culling stale objects itself). So
nothing restored them on the way back.

Retail-faithful fix: a real client keeps its weenie_object_table and
re-renders the world from it (holtburger keeps the table across a
teleport; only suspends physics bodies). acdream's _lastSpawnByGuid (the
parsed CreateObject records — position + Setup + appearance) IS that
table and survives the collapse (the collapse path never calls
RemoveLiveEntityByServerGuid, the only thing that prunes it). On landblock
(re)load, replay OnLiveEntitySpawnedLocked for retained spawns whose
render entity is absent — independent of any ACE re-send.

- LandblockEntityRehydrator: pure selection (landblock match; skip
  already-present, the player, and mesh-less spawns), unit-tested (7).
- StreamingController: onLandblockLoaded callback after AddLandblock
  (Loaded = dungeon-exit expand) and AddEntitiesToExistingLandblock
  (Promoted = Far->Near).
- GameWindow.RehydrateServerEntitiesForLandblock: present-gate keys on
  GpuWorldState (NOT _entitiesByServerGuid, which holds collapse
  orphans), replay under _datLock; the replay's own
  RemoveLiveEntityByServerGuid de-dup scrubs the orphan state.

Corrects the handoff: ClientObjectTable is inventory-only (no world
position/Setup) and cannot rebuild a render entity; _lastSpawnByGuid is
the world-object table. Register row AP-48 (no retail 25s visibility
cull). dotnet build + 1518 Core tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:06:41 +02:00
Erik
45a5cc5bb6 fix(ui): #145 — importer honors ZLevel so the backdrop sits behind panels
The factory mapped ZOrder from ReadOrder only, so the gmInventoryUI
full-window backdrop (0x100001D0, ReadOrder 4) painted over the nested
panels (ReadOrder 1-3). Wire ElementDesc.ZLevel through ElementInfo /
ToInfo / Merge and fold it into ZOrder = ReadOrder - ZLevel*10000 (higher
ZLevel = further back, ReadOrder the within-layer tiebreaker). Vitals
(all ZLevel 0) are unchanged; chat (ZLevel 900) + toolbar (1,2) shift to
their dat layering — verify visually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:22:26 +02:00
Erik
85098f535d feat(ui): D.2b-B — sub-window mount (inheritor attaches base subtree)
LayoutImporter.Resolve now captures the resolved base element's children
and, for a pure-container leaf (no own children + no own media) inheriting
from a base WITH content, attaches that subtree. This pulls the nested
gmInventoryUI panels' content (paperdoll/backpack/3D-items) in through the
existing BaseElement+BaseLayoutId path. ShouldMountBaseChildren is a pure,
unit-tested predicate; it's inert for media-bearing inheritors and childless
style prototypes, so vitals/chat/toolbar are unaffected (existing importer
tests still green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:36:20 +02:00
Erik
4fd4b09f3f feat(ui): D.2b-B — UiItemList N-cell grid mode
Columns + CellWidth/CellHeight + a row-major CellOffset/LayoutCells pass.
CellWidth<=0 keeps the single-cell fill mode (toolbar slot sizes to the
list — unchanged); CellWidth>0 tiles cells in a grid. Layout runs on
AddItem and per-frame in OnDraw so a list resize reflows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:34:30 +02:00
Erik
a15bd3b56d fix(streaming): #145 — teleport re-use via server-authoritative placement
Portals only worked once per session: teleporting OUT of a dungeon
mis-rooted the player into the SOURCE dungeon's coordinate frame, so every
move was sent dungeon-framed and ACE rejected it ("failed transition") —
the player couldn't move, never reached a portal, and the world wouldn't
re-render (only skybox).

Root cause: acdream's streaming-relative frame recenters on teleport, but
resident physics landblocks keep their load-time world-offset. After
recentering onto the outdoor destination, the collapsed source dungeon
(offset 0,0 as the prior center) and the destination (offset 0,0 as the
new center) overlap, and the Z-agnostic outdoor cell-snap returns the
dungeon for both the arrival placement and every per-frame resolve.

Fix (server-authoritative teleport placement):
- Drop the stale source center landblock from physics at the teleport
  recenter (GameWindow.OnLivePositionUpdated) so the resolve falls through
  to the server position (Resolve NO-LANDBLOCK verbatim) until the
  destination streams in.
- Place outdoor teleports immediately (TeleportArrivalRules) — holding is
  futile because streaming does not progress during a PortalSpace hold.
- Clear a dangling CellGraph.CurrCell when its landblock is removed
  (PhysicsEngine.RemoveLandblock) — otherwise the dungeon-streaming gate
  keeps streaming collapsed onto the gone dungeon (only skybox renders).

Keeps DungeonStreamingGate (gate suppression during the hold). Indoor
(dungeon-entry) placement is unchanged (cell-keyed, IsSpawnCellReady).

User-verified: in->out->re-enter works repeatedly, no ACE errors, world
renders. Remaining facets (server objects + own avatar not rendering after
a teleport-out) are entity render/lifecycle — split to #138.

Registers AP-36 + AD-2 updated. New: DungeonStreamingGate (+4 tests),
TeleportArrivalRules (+4 tests). Build + 2727 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:38:00 +02:00
Erik
036db8b8ab feat(ui): D.2b-A — BringToFront + raise on show/click
BringToFront sets a window's ZOrder one past the max among its peers.
ShowWindow now raises on open; OnMouseDown raises any pressed top-level
window (retail-faithful stacking). Existing drag/resize tests unaffected
(raise only touches ZOrder, not geometry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:31:29 +02:00
Erik
6409038576 feat(ui): D.2b-A — UiRoot named-window registry (Show/Hide/Toggle)
A Dictionary<string,UiElement> registry on UiRoot with RegisterWindow +
Show/Hide/Toggle. Show/Hide flip UiElement.Visible (already gates
Draw/Tick/HitTest); Toggle returns the new visibility; unknown names are
no-ops. WindowNames.Inventory const shared by mount/registry/toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:30:22 +02:00
Erik
89a2a2857a test(ui): D.2b — cover ResizeRect maxH clamp (Bottom + Top edges)
Spec §6 test 3 — the maxH clamp is the load-bearing path for the toolbar collapse; the implementer updated the existing ResizeRect tests' signatures but didn't add the clamp-verification cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:37:33 +02:00
Erik
f74e017509 feat(ui): D.2b — toolbar collapse-to-one-row (bottom-edge snap resize hides/shows row 2)
UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:33:35 +02:00
Erik
ff3592ec25 refactor(core): D.5.3/B.2 — move ShortcutStore to Core.Items (decouple Load from the wire type)
Matches the ClientObjectTable model-placement convention; Load now takes (slot,objGuid) pairs so
the store has no Core.Net dependency. + self-drop wire-count assert + comment fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:50:33 +02:00