Next-session brief: build retail's collision-inclusion model (PhysicsState/
ETHEREAL/HAS_PHYSICS_BSP predicate + per-cell shadow-list registration + the
building-shell/terrain/EnvCell channels), map acdream's per-channel ad-hoc
filters against it, enumerate deviations, and design ONE unified retail-faithful
mechanism to replace them. Motivated by this session's #146/#147 — both were the
same shape (a per-channel filter diverging from retail). Brainstorm-gated,
report-first, no guess-patches. Includes the apparatus inventory + a paste-ready
prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "#145 far-town frame" premise was wrong: #146's bldOrigin probe proved
Arwic buildings are correctly framed. The walls were portal-less buildings
skipped by the collision cache; the terrain-3%-grounded sub-question is
re-scoped LOW (no fall-through observed; likely a contactPlaneValid nuance).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A town's outer/perimeter walls have NO collision even on a fresh login: the
player walks straight through them while houses block fine. Confirmed NOT a
frame issue — #146's bldOrigin probe showed Arwic buildings are correctly
framed (~12 m from the player, not km off), so the "#145 far-town frame"
premise was wrong here.
Root cause (dat-confirmed, Issue147ArwicBuildingsDumpTests): a perimeter wall
is stored in LandBlockInfo.Buildings as a doorless shell — 16 of Arwic's 30
buildings are PORTAL-LESS, ringing the town at 24 m intervals (x=12/132,
y=12/108). The building-collision cache loop skipped them via
`if (building.Portals.Count == 0) continue;` — a filter meant only for the
transit/entry feature (CellTransit.CheckBuildingTransit) that also dropped the
collision shell. Retail's find_building_collisions (0x006b5300) tests the shell
BSP independent of the portal list, so a doorless wall still collides.
Fix: don't skip portal-less buildings — cache them with an empty portal list
(no transit) but their collision shell (ModelId) intact. User-verified: Arwic
perimeter walls now block (Collided/Slid). Adds the dat-dump fixture test.
Suites green: Core 1569(+2 skip), App 468(+2 skip), UI 425, Net 317.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Static building/house-wall collision worked on a fresh login to a town but was
lost after logging in elsewhere and PORTALING in: the foot-sphere clipped
straight through walls while server-spawned doors (own collision) still blocked.
Root (capture-confirmed, ACDREAM_PROBE_BUILDING bldOrigin): the building shell
BSP's WorldTransform is computed from the streaming-relative origin
(origin = (lb − _liveCenter)·192) AT CACHE TIME, and CacheBuilding is idempotent
(first-wins per cell). A teleport recenters _liveCenter, but the idempotent guard
never re-bases the cached transform — so the shell sat at a stale world offset
(login Arwic → portal Holtburg: bldOrigin=(-5488,2149), ~5.5 km from the player
at (83,24)), and FindBuildingCollisions never penetrated → result=OK everywhere →
no block. Terrain doesn't suffer this because AddLandblock overwrites its
WorldOffset on every apply.
Fix: clear a landblock's cached buildings at the start of each ApplyLoadedTerrain
(PhysicsDataCache.RemoveBuildingsForLandblock), then let the existing loop
re-populate fresh with the CURRENT origin — the per-apply re-base terrain already
gets, while keeping CacheBuilding's per-cell first-wins within each fresh pass
(retail CSortCell::add_building). Also adds bldOrigin to the [bldg-channel] probe.
Verified on the exact repro (Arwic login → Holtburg portal → walk into wall):
bldOrigin now (107.5,36.0) at the wall (was -5488,2149); channel returns
Collided → Slid (block + wall-slide). Suites green: Core 1568(+2 skip),
App 468(+2 skip), UI 425, Net 317.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue A ("no collision after death/portal") investigated capture-first and
confirmed to be TWO distinct bugs (user-corroborated + ACDREAM_CAPTURE_RESOLVE
data, 255,832 player resolves):
- #147 (HIGH) far-town (Arwic): grounded only 3% of resolves vs 100% at
Holtburg/dungeon; city/perimeter walls never block even on a fresh login —
the #145 streaming-relative-frame family. Scope as a brainstormed #145
sub-phase, not a one-commit fix.
- #146 (MEDIUM) Holtburg: building/house-wall collision works on fresh login
but is lost after portaling in. The [bldg-channel] probe fires post-portal
(building is cached + reached) yet result=OK as the foot-sphere walks into
the wall — the building WorldTransform is baked from _liveCenter at cache
time and CacheBuilding is idempotent, so the recenter leaves a stale offset.
Also recorded against #138 that symptom B (avatar vanish) was root-caused and
fixed in afd5f2a (RelocateEntity-during-PortalSpace), not just the pending-
bucket rescue candidate.
No guess-patches applied to the collision code (DO-NOT-RETRY area).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After a teleport-OUT the per-frame avatar-sync (GameWindow ~:8018) called
GpuWorldState.RelocateEntity with the player controller's cell — which stays the
FROZEN SOURCE cell until PlaceTeleportArrival materializes the destination. So
mid-transit it dragged the avatar (which the teleport's rescue/re-inject had
correctly placed at the destination center) back into the now-UNLOADED source
landblock's pending bucket, where nothing recovers it (RelocateEntity only scans
_loaded). Net: the avatar vanished after teleporting out and stayed gone.
Fix: skip the per-frame relocate while the player is in PortalSpace. The teleport
machinery (DrainRescued + PlaceTeleportArrival) owns the avatar's landblock during
transit; per-frame relocation resumes at FireLoginComplete (InWorld).
Live-verified with a new env-gated avatar-lifecycle probe (ACDREAM_PROBE_ENT /
EntityVanishProbe; [ent] draw-set transitions + [dyn] cull check): pre-fix the
trace showed `[ent] APPEND lb=0x0007FFFF(source) -> PENDING -> DRAWSET ABSENT`
never recovering; post-fix every teleport goes RESCUE -> ABSENT ->
APPEND(destination) -> PRESENT and stays drawn (4 teleports incl. to Holtburg,
session ended PRESENT).
Suites green: Core 1568(+2 skip), App 468(+2 skip), UI 425, Net 317.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both observed 2026-06-24 after the FPS work; both are the known #135/#138 placed-
but-unstreamed streaming gap (NOT FPS-work regressions — those commits are render-
only). Handoff maps the symptoms to ISSUES, flags the delicate-area lessons (no
guess-patches, the reverted hold, fix-the-foundation, capture-first), and points
at the physics digest + streaming refs + capture apparatus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Report: OUTCOME section (the two shipped fixes, the glFinish-artifact correction,
the deliberately-unpursued scenery-CPU/terrain-GPU headroom). ISSUES: recently-
closed entry with SHAs + pointers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
DrawCellObjectLists drew cell particles PER visible cell, and each call
(DrawRetailPViewCellParticles -> ParticleRenderer.Draw) re-walked the ENTIRE
live particle set to filter by owner id — O(cells x particles). Measured via
[CPU-PHASE] at dense Arwic this was the cellobjects sink (~5.4 ms CPU; the
phase's GPU share is 0.01 ms — pure CPU). gpu is 0.5 ms; the dense town is
~96% CPU-bound (the earlier "6.7 ms GPU" was a moving/streaming transient).
Static owners are disjoint per cell (InteriorEntityPartition.ByCell partitions
by ParentCellId), so the UNION of survivors (= _allCellStatics, already
accumulated in loop 1 for the batched entity draw) draws EXACTLY the same
emitters in ONE pass: the callback gates on owner id, the renderer sorts
globally back-to-front, and the per-cell slice was never used for clipping
(scissor gate deleted in T3; DisableClipDistances). Runs after the batched
static draw so emitters still depth-test against same-cell statics. Deletes the
loop-2 re-cull and collapses the per-cell BuildDrawList allocations N -> 1
(also eases the GC spikes).
Adversarial review (3 angles) confirmed emitter-set equivalence, no double-
draw, equal-or-better compositing, and no lost per-cell side effect — and found
the OLD code DOUBLE-DREW additive particles for multi-view-polygon cells (one
DrawCellParticles per slice, same owner set each time; the #121 over-bright
class). Consolidation draws each emitter once, fixing that latent bug.
Pixels identical-or-better; draw-mechanism speed only. Build + full suite green
(pview replay tests incl. HouseExitWalkReplay/TowerAscent/Issue130 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DrawCellObjectLists called WbDrawDispatcher.Draw once per visible cell (each
orphaning 6 SSBOs + full state setup) — the top CPU-submission sink at dense
Arwic (cellobjects ~3.5ms/frame, measured via [CPU-PHASE]; the frame is
~96% CPU-bound, GPU only 0.5ms). Apply the shipped cells-shell batching
pattern to cell OBJECTS: collapse N per-cell draws into ONE cross-cell draw.
Two-loop structure preserves the statics-before-particles depth order:
loop 1 — per-cell viewcone cull, accumulate all survivors + the union of
cell ids;
one batched DrawEntityBucket for every cell's statics;
loop 2 — per-cell DrawCellParticles, after the statics own the depth buffer.
Correctness: same survivor set (union visibleCellIds gate is equivalent to the
per-cell {cellId} filter); transparency composites equal-or-better (the
dispatcher sorts opaque front-to-back + transparent back-to-front globally,
WbDrawDispatcher.cs:1469-1470); particles still occlude against same-cell
statics (loop 2 runs after the batched draw); dynamics-last + shells + seals
unchanged. Also drops the per-cell new[]{entry} alloc (~50-100/frame) to one.
Pixels identical — draw-mechanism speed only (render-perf is not faithfulness-
gated per the project steer). Spec: docs/superpowers/specs/2026-06-23-cellobject-
draw-batching-design.md. Build + full test suite green (App pview replay tests
incl. HouseExitWalkReplay/Issue130DoorwayStrip pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decouple the whole-frame TimeElapsed query from the per-pass glFinish so the
CPU-vs-GPU split is honest: ACDREAM_FPS_PROF=2 runs the frame query with NO
per-pass glFinish (PassGpuEnabled stays "1"-gated). Plus [CPU-PHASE] timers
around each DrawInside phase (flood/assemble/prepare/partition/landscape/
portalmask/shells/cellobjects/dynamics) — the CPU analog of [PASS-GPU].
This is what proved the dense town is ~96% CPU-bound (GPU=0.5ms) and that the
cost is per-cell draw submission (cellobjects ~3.5ms), not the portal floods /
punch-seal / clip allocs the static analysis had guessed. Throwaway; strip with
the rest of the FPS apparatus (plan Task 5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cells batching shipped (29->75fps, 2.6x); remaining ~12ms is diffuse (particles
~3, punch/seal ~3, ~5.5 unattributed) + frame spikes. Next session: deep-dive
with RenderDoc to attribute + push higher. Distance-degrade theory is dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-pass GPU attribution for the dense-town deep-dive: ParticleRenderer.Draw and
PortalDepthMaskRenderer.DrawDepthFan report into FrameProfiler [PASS-GPU]. Joins
the cells/terrain timers. glFinish serializes -> inflates absolutes; use for
relative attribution. STRIP with the rest of the apparatus when FPS work lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DrawBuildingLookIns had the same per-cell heavy Render pattern. Lift the opaque
shell Render out into one per-building batch (after that building's aperture
punches); keep the per-cell loop for transparent (skip-empty) + per-cell statics
/dynamics/particles. User-verified: no missing walls, look-in interiors correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dense-town FPS root cause: DrawEnvCellShells called the heavy per-frame
EnvCellRenderer.Render once PER cell x opaque+transparent (~94 calls/frame,
24.75ms = 75% of GPU at Arwic). Batch opaque into one Render(Opaque, allCells)
(z-buffer order; per-instance CellId-keyed lighting => safe) + skip the
transparent Render for opaque-only cells, keep far->near for the rest.
Measured (Arwic, same facing, profiler on): cells 94 calls/24.75ms -> 1 call/
0.37ms; frame 34ms/29fps -> ~10ms/100fps p50.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live profiling found the dense-town (Arwic 29fps) bottleneck: EnvCellRenderer
.Render called ~94x/frame (per-cell x opaque+transparent) = 24.75ms = 75% of
the GPU frame. Render is a heavy per-frame method (state reset + SSBO upload +
MDI) invoked per-cell for far->near transparency order. Eliminated, with
evidence, every other suspect incl. the handoff's distance-degrade theory
(entities 0.22ms; resolution-independent => not fill; update 0.1ms).
Spec: batch the shell draws into one Render per pass. Opaque needs no order
(z-buffer) + lighting is per-instance (CellId-keyed SSBO) => safe to batch.
Transparent: skip opaque-only cells, preserve order for the rest.
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>
Session shipped the _datLock-contention fix (lockwait 88ms->0, kept). Remaining
FPS pain (sustained ~30 in Fort Tethana, view-direction-dependent) root-caused to
the absence of distance-based LOD/degrade: we draw every frustum-visible object at
full detail. Next: port retail UpdateViewerDistance/get_degrade. Full mechanism,
file:line map, apparatus state, and DO-NOT-RETRY lessons captured.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeferredApplyBacklog / ForceReloadCount / LastForceReloadDropCount, read by
GameWindow's [FRAME-DIAG] rollup. Diagnostic scaffolding (ACDREAM_WB_DIAG=1);
strip with the rest of [FRAME-DIAG] when the FPS work fully lands.
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>
ApplyLoadedTerrainLocked makes zero DatCollection calls (all dats pre-read by
the worker into lb.PhysicsDats), and its other mutations are update-thread-only
or ConcurrentDictionary-safe, so the dat lock is unnecessary around it.
Removing it eliminates the measured 24ms-median / 88ms-p95 lockwait stall that
was the 30↔200 FPS swing. The worker still serializes its own dat reads on
_datLock; only the apply stops contending. Build + 1568 Core tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ApplyLoadedTerrainLocked's six Get<T> sites now read from lb.PhysicsDats via
TryGetValue (loud-fail on a gather/apply id mismatch). Zero _dats.Get calls
remain in the apply. Behavior identical: same ids -> same cached dat objects
-> same surfaces/BSP/ShadowObjects. Lock removal is the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BuildPhysicsDatBundle mirrors the apply's six Get<T> sites (LandBlockInfo,
EnvCell, Environment, building Setup, entity GfxObj, entity Setup) under the
worker's existing _datLock and attaches them to LoadedLandblock. Far tier
gets PhysicsDatBundle.Empty.
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>
5-task TDD plan: PhysicsDatBundle on LoadedLandblock; worker pre-reads the
apply's six Get<T> sites into it; ApplyLoadedTerrainLocked reads from the
bundle; drop the apply's lock(_datLock); verify lockwait->0; strip probes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives the doll 3-D pass in a pre-UI hook (after the world passes, before
_uiHost.Draw), gated on inventory-open AND doll-view (the viewport widget's
Visible, set by the Slots toggle). RefreshPaperdollDoll clones the live
player entity (_entitiesByServerGuid[player]) into a DollEntityBuilder doll
with COPIED MeshRefs (frozen pose); re-dress is triggered by a dirty flag
set in OnLiveAppearanceUpdated (0xF625) — the C# analog of RedressCreature.
Correctness fix: the doll is passed in animatedEntityIds so the dispatcher
BYPASSES the Tier-1 classification cache (WbDrawDispatcher.cs:1142). Without
it, a re-dress (a new WorldEntity with the same fixed DollRenderId) would
serve the previous doll's cached batches and the new gear wouldn't appear.
Renderer disposed in the GameWindow teardown. Build + full App suite green
(594). Static doll (no idle animation yet — next slice). Visual gate next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 30↔200 FPS swing is _datLock contention: the streaming worker holds
the global dat lock for the full per-landblock build (lockwait measured
24ms median / 88ms p95), stalling the update thread's ApplyLoadedTerrain.
Fix (A1): the worker pre-reads the apply's six Get<T> sites into a physics
dat bundle so ApplyLoadedTerrainLocked makes zero DatCollection calls and
its lock(_datLock) is removed. Approved design; implementation next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The C# analog of CreatureMode::Render: draws one re-dressed player clone
into a private FBO (RGBA8 + depth24-stencil8) with the fixed DollCamera +
one distant light (retail 0.3,1.9,0.65 @ 2.0), sealed in a GLStateScope so
it can't disturb world/UI GL state. frustum:null ⇒ the doll's synthetic
landblock is always visible ⇒ walked from entry.Entities and drawn from its
current MeshRefs (static pose; idle animation is a later slice).
Manages the FBO directly via GL (ManagedGLFramebuffer is unused/bitrotted
WB infra). UiViewport blit flips V (FBO bottom-left origin vs UI top-left)
so the doll isn't upside-down. Not yet wired — GameWindow hook is next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the palette/part-override mapping at GameWindow.cs:3390-3431 in a
testable static helper. Build() accepts plain (SubPaletteId, Offset, Length)
and (PartIndex, GfxObjId) tuples, builds PaletteOverride only when
subPalettes.Count > 0 (same gate as GameWindow), and poses the entity at
origin facing the viewer (191.367905° / +Z). Reserved synthetic guid
0xDA11D011 keeps the doll distinct from the live player and satisfies
EntitySpawnAdapter's ServerGuid != 0 guard. 7 new tests green; suite 594/0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds ArmorSlotElementIds (the exact 9 ids gmPaperDollUI::ListenToElementMessage
flips per decomp 175674-175706) and PaperdollViewState (SlotView/DollVisible/
ArmorSlotsVisible/Toggle) as a nested public class on PaperdollController.
Wires ApplyView() into the constructor so the Slots button (0x100005BE) drives
armor-slot Visible and the doll viewport Visible on every click. Initial state
is doll-view (armor slots hidden). The doll viewport element (0x100001D5) is
bound as UiElement? so this slice stays independent of Slice 3's IUiViewportRenderer
seam; _armorSlots collects whichever of the 9 ids were found in the live layout.
Three new unit tests verify the id set, the default state, and the round-trip
toggle against the public PaperdollViewState surface only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brainstorm-approved design for Sub-phase C Slice 2: the 3-D doll
UiViewport (dat Type 0xD) + the "Slots" toggle, extending the shipped
PaperdollController.
Key decisions settled in the brainstorm:
- Compositing = render-to-texture (reuse ManagedGLFramebuffer + GLStateScope):
the doll renders to an off-screen buffer in a pre-UI hook, then the
UiViewport widget blits it as a normal sprite -> correct painter order
for free, fully sealed 3-D pass.
- The armor/non-armor partition is the decomp-exact 9-slot set that
ListenToElementMessage (idMessage==1, 0x100005be) flips, not an
EquipMask heuristic.
- Doll = a dedicated WorldEntity cloned from the local player's Setup +
current ObjDesc (the player IS a WorldEntity -- corrects the handoff),
re-dressed on ObjDescEvent 0xF625; reuses EntitySpawnAdapter/
AnimatedEntityState.
- Seam IUiViewportRenderer lives in AcDream.App.UI (intra-App decoupling),
not Core -- user-approved divergence from the handoff's "Core interface".
Recovered the corrupted light immediate ("ff&?" = 0x3f266666 ~= 0.65).
AP-66 reworded: empty-slot frame stays (slots ring the doll, no overlay).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carries the corrected paperdoll model forward (figure = live doll, NOT
silhouettes; the Slots button toggles doll-view vs armor-slot-view — REPLACE,
not overlay). Scopes Slice 2 = (A) the toggle (read ListenToElementMessage
idMessage==1) + (B) the UiViewport Type 0xD doll via a Core->App
IUiViewportRenderer seam reusing EntitySpawnAdapter. Includes the decomp
anchors, camera/light immediates to decode, open questions, and the
new-session prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the scenery-frame, building-reach, and stream-resid probes added during the
world-load/FPS deep-dive (all root-caused + fixed). Gated-off diagnostics only; no
behavior change. The fixes they found remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terrain vertices are baked into the GPU relative to the render origin (_liveCenter),
which only moves on a teleport. A FAR jump unloads/reloads everything → fine. But a
NEARBY outdoor jump (e.g. (170,168)->(169,180), 12 landblocks) leaves the old and new
streaming windows overlapping, so StreamingRegion.RecenterTo KEEPS the ~330 overlapping
blocks (correct — they're in the new window) — yet they still hold vertices baked at the
OLD origin. The instant _liveCenter moves they render shifted by the jump distance: a
band of terrain hanging in the sky ("terrain in the sky" arcs). Confirmed by probe: every
stale slot was offset by EXACTLY deltaLB*192 ((-1,12)*192 = (-192,2304)), 330 of them
persisting after the hop.
Fix: StreamingController.ForceReloadWindow() — on an OUTDOOR teleport, SYNCHRONOUSLY drop
every resident landblock (render slot + physics + state) so none survives the frame stale,
then null the region so NormalTick re-bootstraps the whole window fresh at the new origin
(the near ring is priority-applied behind the fade; the rest streams). Called from
OnLivePositionUpdated's outdoor recenter branch; sealed dungeons keep PreCollapseToDungeon.
Verified live: the exact nearby hop that produced 330 stale slots now produces 0 across
the whole session, and the horizon is clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Divergence AP-66: Slice-1 empty-slot frame vs the Slice-2 doll-backed
transparent look (retired when the doll viewport lands).
- Paperdoll handoff: prominent top note correcting the WRONG "transparent /
per-slot silhouettes" framing — the figure IS the live 3D doll; the
Slots toggle + doll = Slice 2. Points to the project memory's Slice 1
entry for the full DO-NOT-RETRY.
(The detailed shipped-log + corrected model live in the auto-loaded
claude-memory/project_d2b_retail_ui.md, updated this session.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User correction at the visual gate: the green "figure" in the paperdoll is
the LIVE 3D character (the doll — can be naked), NOT per-slot silhouettes.
The default view (Slots button OFF) = the doll + non-armor slots; pressing
Slots hides the doll and shows the armor slots. So the doll + the Slots
toggle are Slice 2 (the UiViewport); there are no per-slot silhouette sprites
to chase.
For Slice 1 (no doll yet) the right empty-slot look is simply a VISIBLE FRAME
so every slot position can be seen + used — which fixes the "I see only slots
with equipment, no empty slots" report. The earlier transparent (EmptySprite=0)
came from the stale silhouette assumption. PaperdollController now takes an
emptySlotSprite; GameWindow passes the inventory grid's empty square
(0x06004D20) for a consistent visible frame.
App suite 580 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PickupEvent (0xF74A) and DeleteObject (0xF747) are semantically distinct:
- 0xF747 = weenie DESTROYED → evict from ClientObjectTable (weenie_object_table)
- 0xF74A = object LEFT THE 3D WORLD VIEW (moved into a container) → remove
the 3D WorldEntity, but the weenie persists in ClientObjectTable
Before this fix, both paths fired EntityDeleted identically, causing
ObjectTableWiring to evict the weenie from ClientObjectTable. The follow-up
InventoryPutObjInContainer (0x0022) then tried MoveItem on an unknown guid
and no-op'd, so the unwielded item simply vanished.
Fix: add `bool FromPickup` (default false) to DeleteObject.Parsed. WorldSession
sets it true on the PickupEvent path and false on the DeleteObject path.
ObjectTableWiring.Wire's EntityDeleted handler skips table.Remove when
FromPickup is true, preserving the weenie for the container-move echo.
GameWindow.OnLiveEntityDeleted (3D entity removal) is untouched — it fires
for both pickups and destroys, as intended.
Divergence register: AP-65 added (data ghosts for other-player pickups until
teleport/relog clear; harmless — no UI queries ContainerId 0).
Tests: +5 (DeleteObject FromPickup parser regression; wiring retain/evict
semantics; Parsed default/explicit FromPickup). 343/343 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three apparatus-confirmed fixes from the world-load/FPS deep-dive (all live-verified).
1. trees-in-sky — scenery ground-Z now samples THIS landblock's OWN heightmap
(TerrainSurface.SampleZFromHeightmap, lock-step with the physics terrain) instead
of the global PhysicsEngine.SampleTerrainZ query. At build time the landblock isn't
registered in physics yet, so that query could only return null OR a STALE
neighbour's height — the previous location's terrain, still registered after a
teleport recenter — planting scenery at the old altitude (+250..500m, confirmed via
the [scenery-z-stale] probe). Own-heightmap is correct in every case; the query is
removed. (GameWindow.BuildSceneryEntitiesForStreaming)
2. FPS per-hop — TickAnimations recovered each animated entity's server guid via an
O(N) ReferenceEquals reverse scan over ALL _entitiesByServerGuid (which never
evicts, so N climbs every teleport — the drops-with-each-hop sink). Replaced with
ae.Entity.ServerGuid: O(1), exact-equivalent (the dict key IS entity.ServerGuid).
(GameWindow.TickAnimations)
3. teleport arrival + bulk floating terrain — two streaming fixes:
- Near-ring eager-apply: a teleport applies the destination's 3x3 surroundings
(StreamingController.PriorityRadius) and holds the fade until they're resident
(PhysicsEngine.IsNeighborhoodTerrainResident), so the player arrives in a loaded,
collidable world instead of one landblock in the void.
- Immediate unloads: DrainAndApply no longer throttles UNLOADS at the per-frame
load budget — they're cheap (free GPU buffers, no upload). A teleport produced
~600 unloads draining at 4/frame, leaving the previous region resident for
seconds (floating terrain) and accumulating across rapid hops (951 resident vs a
625 window). Only GPU-upload LOADS are metered now. Cut out-of-window resident
650 -> 63 and resident 951 -> 688 (live-verified via [resid-audit]).
Includes gated-off diagnostic probes (ACDREAM_PROBE_SCENERY_FRAME / _BLDG_REACH /
_STREAM_RESID) used to root-cause the above — zero-cost when unset, same pattern as
the committed tp-probe.
The pre-existing teleport-induced "terrain arcs in the sky" (present in the dd2eb8b
baseline too, with NONE of this work) are a SEPARATE bug — investigated next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the _paperdollController field and PaperdollController.Bind(...)
call in the inventory-frame block alongside InventoryController, wiring
up the paperdoll equip slots with their icon composer and wield action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-quality review on Task 5:
- I1: Concerns was unscoped (CurrentlyEquippedLocation != None) → an NPC's
wielded item (which also carries that wire field) triggered spurious full
repaints. Narrowed to (WielderId==p || ContainerId==p), matching
InventoryController; OnObjectMoved's from/to-player backstop still catches
unwield-into-a-side-bag. Populate's own scope already prevented wrong data;
this kills the wasted repaints.
- I2: replaced the dual-`index++` (assign-vs-skip) with a for-i loop;
SlotIndex = SlotMap position (= the drag payload's SourceSlot on unwield).
- M1/M2: comment that the cell's SpriteResolve + the discrete-slot accept/
reject ring (0x060011F9/F8, not the grid insert-arrow) are factory-provided.
- Added two behavioral tests: a live player wield repaints the slot
(ObjectMoved → Concerns → Populate); an NPC's wielded item never appears on
the doll (player-scoping).
- Synced the stale spec §4b/§6c/§8 to the Task-3 Option-1 reality (the
optimistic wield is ContainerId-based and does NOT write WielderId).
App suite 580 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>