After Phase 1+2 (indoor cell rendering — missing floors fixed),
9 follow-up issues (#78-#86 in docs/ISSUES.md) need their own
phases. This handoff doc gives the next session everything it needs
to start cold: probe infrastructure status, issue cluster groupings,
suggested phase order, and the verification approach that worked
for Phase 1+2.
Companion prompt file is a self-contained kickoff that can be pasted
into a fresh Claude Code session to start work on the cluster.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps the references/WorldBuilder submodule pointer from 167788b
to 34460c4 — picks up the Setup-prefix guard at PrepareEnvCellMeshData
line 1223 that lands Phase 2 indoor cell rendering fix.
WB branch acdream-fix-floor-rendering pushed to
git@github.com:eriknihlen/WorldBuilder.git earlier in this session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Roadmap shipped-table: two new rows for Phase 1 (diagnostics) +
Phase 2 (fix). Header status block updated to 2026-05-19 with the
Phase 2 cause + fix one-liner and pointer to the 9 surfaced issues.
- ISSUES.md: filed nine new issues (#78-#86) covering the indoor
bugs the user observed once the floor rendered. Grouped under an
"Indoor walking issue cluster" header. Cross-references the Phase 1
+ Phase 2 work that surfaced them. Hypotheses + suspected root
causes documented for each.
The 9 issues split into two probable shared-cause groups:
- Cell BSP / portal cull (#78, #84, #85, #86) — likely fixable in
one phase.
- Indoor lighting plumbing (#79, #80, #81, #82) — needs separate
investigation per-symptom.
Plus #83 (stairs) which probably needs its own physics phase work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User visually confirmed floors render in Holtburg Inn after the WB
TryGet<Setup> guard. Probe re-capture: 0 [wb-error] lines (was 385),
0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123).
Documents the nine pre-existing indoor bugs the user observed during
verification (see-through floor, indoor collision, stairs, walls,
clicking, indoor lighting artifacts, stabs-don't-react-to-atmospheric-
lighting, slope terrain lighting). All pre-existing; filed for follow-up
phases via docs/ISSUES.md.
Phase 2 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 diagnostic chain identified the EXACT cause of 26/123 Holtburg
cells silently failing in WB's PrepareEnvCellMeshData:
ArgumentOutOfRangeException thrown from Setup.Unpack inside
DatReaderWriter when WB calls TryGet<Setup>(stab.Id, ...) on a stab id
whose prefix is GfxObj (0x01xxxxxx), not Setup (0x02xxxxxx).
DatReaderWriter finds the file in Portal's tree (GfxObjs and Setups
share tree-lookups), attempts to parse GfxObj bytes as Setup format,
throws OOR. Exception bubbles to PrepareMeshData's outer try/catch
which silently swallows + returns null. Entire cell fails to upload.
This commit lands the diagnostic infrastructure that surfaced the bug:
- WbMeshAdapter: replaced NullLogger<ObjectMeshManager> with a small
Console-backed ConsoleErrorLogger<T> private class. Filters to
LogLevel.Error+. WB's existing _logger.LogError(ex, ...) at the
swallow site now writes [wb-error] lines with type + message + top 5
stack frames. Bridges WB's intentional log point to acdream's console.
- WbMeshAdapter: extended [indoor-upload] NULL_RESULT probe with
reader-divergence diagnostic (ourCellDb.TryGet, wbResolveId.Count,
wbSelectedType, wbDbIsPortal, wbDbTryGet<EnvCell>, hadRenderData).
Made it possible to rule out cache-hits and reader-divergence as
causes before identifying the real one.
- Cause report at docs/research/2026-05-19-indoor-cell-rendering-cause.md
documents the full chain: 55 ArgumentOutOfRangeException stack traces
captured in one launch, all from PrepareEnvCellMeshData line 1223.
The fix itself (1-line guard at WB's TryGet<Setup> call site) is applied
to references/WorldBuilder/.../ObjectMeshManager.cs — which is a git
submodule. Will be committed separately to the WB submodule after
visual verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 Task 1's continuation logged [indoor-upload] NULL_RESULT
when WB's PrepareMeshDataAsync returned null. Extend the line to
include two cross-checks:
ourCellDb.TryGet=<bool> — acdream's DatCollection.Cell.TryGet<EnvCell>
wbResolveId.Count=<int> — WB's DefaultDatReaderWriter.ResolveId().Count
This narrows the cause among WB's null-return paths (ResolveId empty
vs TryGet<EnvCell> failure vs wrong type). Best-effort: both calls
wrapped in try/catch so diagnostic failures don't propagate.
Capture: 55 NULL_RESULTs across multiple landblocks ALL show
ourCellDb.TryGet=True + wbResolveId.Count=1. Both readers find the
cells in their indices, but WB's downstream PrepareMeshData logic
still returns null. Divergence is downstream of ResolveId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 confirmed 26/123 Holtburg cells silently fail in WB's
PrepareEnvCellMeshData / PrepareMeshData. WB's catch block at
ObjectMeshManager.cs:589 calls _logger.LogError(ex, ...) — but we
construct ObjectMeshManager with NullLogger, so the log is dropped.
Capture the Task from PrepareMeshDataAsync (previously fire-and-forget)
and attach a ContinueWith that, for EnvCell ids only when the probe
is on, logs:
[indoor-upload] FAILED cellId=0x... exception=<Type>: <Message>
stack=[<top 3 frames>]
[indoor-upload] NULL_RESULT cellId=0x...
Runs on ThreadPool — non-blocking. Zero cost when ProbeIndoorUploadEnabled
is off. AggregateException is unwrapped to InnerException for readability.
Stack truncated to top 3 frames.
Next: capture procedure, identify cause, target the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tasks:
1. Add exception-surfacing ContinueWith in WbMeshAdapter.IncrementRefCount
for EnvCell ids when ProbeIndoorUploadEnabled is on. Logs
[indoor-upload] FAILED + [indoor-upload] NULL_RESULT.
2. Capture procedure: user walks Holtburg with the probe on; analyze log.
3. Write cause report documenting the captured exception type(s).
4. Apply targeted fix (4a/4b/4c/4d sub-shapes for the 4 most-likely causes
— choice driven by Task 2's data). Or 4d: re-design if cause is none
of the above.
5. Verification: re-capture confirms completed lines, user visually
confirms floor in Holtburg Inn.
6. Roadmap update.
Tasks 2 and 5 are user-driven (must walk the client). Tasks 1, 3, 4, 6
can be subagent-dispatched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components:
1. WbMeshAdapter wraps the PrepareMeshDataAsync task with a continuation
that surfaces faulted-task exceptions + null-result cases for EnvCell
IDs only (gated by ProbeIndoorUploadEnabled). Two new log shapes:
[indoor-upload] FAILED cellId=0x... exception=<TypeName>: <Message>
stack=[<top 3 frames>]
[indoor-upload] NULL_RESULT cellId=0x...
2. Capture procedure: re-launch at Holtburg with the probe on, grep for
FAILED/NULL_RESULT lines, get definitive per-cell cause for the 26
missing-completion cells from Phase 1's capture.
3. Targeted fix: code change matching whichever exception type / null
pattern dominates. Fix shape is data-driven — see the contingency
table in the spec.
WB's catch at ObjectMeshManager.cs:589 already calls _logger.LogError,
but WbMeshAdapter constructs the manager with NullLogger.Instance, so
the log is dropped. Our continuation surfaces the same data scoped to
EnvCells only (avoids the thousands of GfxObj/Setup log lines a real
logger would emit during landblock streaming).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final code review of Phase 1 flagged that the three flag-mutating
tests leaked static state across test boundaries. Wrap each in
try/finally that snapshots IndoorAll on entry and restores it on
exit, matching the PhysicsDiagnosticsTests pattern at line 30-49.
Tests now safe under parallel test runs + future additions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captured at Holtburg landblock 0xA9B4 with ACDREAM_PROBE_INDOOR_ALL=1.
Result: 123 EnvCells in Holtburg get [indoor-upload] requested but
ONLY 97 get a matching [indoor-upload] completed. 26 cells silently
fail in WB's PrepareEnvCellMeshData / PrepareMeshData. The first
interior cell 0xA9B40100 — likely the inn entry or another major
building anchor — is among the failures, exactly matching the
user's "floor missing" symptom.
Other hypotheses ruled out:
- H2 (empty batches): completed cells have cellGeomVerts=14-86.
- H3 (cull bug): walk probe confirms cells pass all visibility filters.
- H4 (double-spawn): partCount values match expected SetupParts.
- H5 (transform double-apply): xform probe shows composedT==meshRefT;
no double-apply.
- H6 (MeshRefs structure): lookup probe shows isSetup=True and
partsHit≈partCount for uploaded cells.
Phase 2 plan: wrap PrepareMeshDataAsync with our own catch-and-log
in WbMeshAdapter so the swallowed exception (most likely cause of
the 26 silent failures, per WB ObjectMeshManager.cs:589) becomes
visible. Once we know the actual failure reason, target the fix.
Also flags IsEnvCellId false-positives on GfxObj IDs whose lower 24
bits ≥ 0x0100 — tightening recommended in Phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments the per-MeshRef draw loop in WbDrawDispatcher:
- [indoor-lookup]: per cell entity, dumps render-data hit/miss,
IsSetup, parts count, and a partsHit/partsMiss tally over the
SetupParts. Disambiguates hypothesis H2 (WB produces empty
ObjectRenderData with zero parts) and H6 (dispatcher fails to
traverse Setup).
- [indoor-xform]: only fires for the cell's synthetic geometry part
(the SetupPart whose GfxObjId has bit 32 set, per WB's
PrepareEnvCellMeshData cellGeomId convention). Logs the three
composed transform translations: entityWorld, meshRef.PartTransform,
partTransform, and the final composed matrix translation. Disambiguates
hypothesis H5 (transform double-apply — composedT lands at 2 ×
cellOrigin).
Rate-limited via the ShouldEmitIndoorProbe instance helper added in
Task 6 (now consumed — no longer dead code).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments WalkVisibleEntities to identify whether cell entities (first
MeshRef.GfxObjId low-16-bits >= 0x0100) pass all visibility filters or
get culled. Three emission paths:
- [indoor-cull] reason=visibleCellIds-miss -- when the ParentCellId
filter rejects the entity.
- [indoor-cull] reason=frustum -- when AABB frustum cull rejects.
- [indoor-walk] -- when the entity passes all filters and reaches the
draw list.
Rate-limited to once per cellId per ~1 sec (30 frames at 30 Hz) via
IndoorProbeState, a nested class wrapping _lastIndoorProbeFrame dictionary
and _indoorProbeFrameCounter (bumped at top of Draw()). WalkEntitiesInto
accepts a new optional IndoorProbeState? parameter (null = probes off,
default) so the test-friendly WalkEntities overload is unaffected. The
ShouldEmitIndoorProbe instance helper is also retained for Task 7 use.
Disambiguates hypothesis H3 (cull bug -- cell entity dropped before draw).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Instruments WbMeshAdapter at two sites:
- IncrementRefCount: on first call for an EnvCell id (low 16 bits >=
0x0100), tag the id in _pendingEnvCellRequests and log
[indoor-upload] requested.
- Tick: when WB's StagedMeshData drains an ObjectMeshData whose
ObjectId matches a pending EnvCell, log [indoor-upload] completed
with parts count, EnvCellGeometry vertex count, and upload result.
Missing "completed" lines after "requested" identify hypothesis H1
(WB silently returns null from PrepareEnvCellMeshData).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six checkboxes (ALL master + five individual probes) in the existing
DrawDiagnostics block. Toggling flips the corresponding
RenderingDiagnostics.Probe* flag live via DebugVM forwarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live-toggle wrappers for the five indoor-rendering probe flags plus the
ProbeIndoorAll master cascade. Pattern matches existing ProbeResolve /
ProbeCell / ProbeBuilding / ProbeAutoWalk mirrors so a checkbox flip in
the DebugPanel takes effect on the next frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the master IndoorAll cascade (both directions) and the IsEnvCellId
helper's 0x0100 boundary check across outdoor cells, indoor cells, and
landblock-prefixed forms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "add the probes you need. Better info, better code."
Original spec had a single ACDREAM_PROBE_INDOOR=1 with vague "log
lookup results" guidance. Replaced with five individually-toggleable
probes, each with:
- Specific env var name + DebugPanel checkbox name.
- Concrete log-line format.
- Exact code site to instrument.
- The hypothesis it disambiguates.
Probe set:
- ACDREAM_PROBE_INDOOR_WALK — dispatcher entity walk per cell
- ACDREAM_PROBE_INDOOR_LOOKUP — render-data lookup hit/miss + SetupParts
- ACDREAM_PROBE_INDOOR_UPLOAD — WB upload result (requested + completed)
- ACDREAM_PROBE_INDOOR_XFORM — composed world transform for cell geom
- ACDREAM_PROBE_INDOOR_CULL — visibility/frustum filter decisions
Plus ACDREAM_PROBE_INDOOR_ALL master toggle.
Implementation outline added: new RenderingDiagnostics static class
(mirrors L.2a's PhysicsDiagnostics pattern), DebugPanel subsection,
edits to WbDrawDispatcher + WbMeshAdapter.
Acceptance criteria refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial brainstorm assumed N.5 retirement broke EnvCell rendering by
leaving _pendingCellMeshes unconsumed. Pivoted mid-brainstorm:
- WB's PrepareMeshData routes EnvCell dat-record types to
PrepareEnvCellMeshData (ObjectMeshManager.cs:557) which produces an
IsSetup=true ObjectMeshData with the floor mesh as EnvCellGeometry.
- WbDrawDispatcher correctly handles IsSetup=true (line 607-621) by
iterating SetupParts and drawing each.
- DefaultDatReaderWriter loads region cell dats; ResolveId resolves
envCellId correctly.
- LandblockSpawnAdapter calls IncrementRefCount on every entity's
GfxObjId, including envCellId for cell entities. ServerGuid==0 passes
the atlas-tier filter.
Chain is structurally intact. The bug is somewhere subtler.
Spec pivots to a diagnostics-first phase: ACDREAM_PROBE_INDOOR=1
captures per-frame cell-entity walk + render-data lookup + SetupParts
traversal + composed-transform values. Six hypotheses (WB silently
returns null, empty batches, cull bug, double-spawn, transform
double-apply, dispatcher MeshRefs mismatch) match six concrete fix
shapes. Phase 2 design follows the probe data.
This is more honest than the original "build a new upload path"
design, which would have hidden the actual bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: third-person chase camera enters interiors before the
player body does, so the camera-based cameraInsideCell flag was
flipping the scene to indoor lighting prematurely (ambient drops to
0.2 white before the player has actually crossed the doorway).
Retail keys lighting off the PLAYER's cell. CellManager::ChangePosition
@ 0x004559B0 reads CObjCell::seen_outside on the player's current
cell — never on the camera. Match that semantics.
- CellVisibility.IsInsideAnyCell(Vector3): new non-caching brute-force
scan that's safe to call alongside ComputeVisibility(cameraPos)
without thrashing the camera cell cache.
- GameWindow render loop: derive playerInsideCell from the player's
Position when in player mode, otherwise fall back to cameraInsideCell
(orbit/fly debug camera).
- UpdateSunFromSky now takes playerInsideCell. The sky-render and
depth-buffer-clear decisions still use cameraInsideCell — those are
legitimately camera-POV concerns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Indoor cells rendered "almost black" because the hardcoded ambient at
GameWindow.cs:8342-8345 was an early-2026 guess (0.10, 0.09, 0.08 — half
retail brightness, warm-tinted) rather than the retail value. The named
retail decomp (acclient.pdb, Sept 2013 EoR build) shows
CellManager::ChangePosition @ 0x004559B0 calls
SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF) whenever the player's
CObjCell::seen_outside flag is 0 — a flat 0.20 white floor, not a
dungeon-tone warm color.
Investigation also confirmed:
- EnvCell.dat does NOT carry inline lights — CEnvCell::UnPack reads
numVisibleCells where Binary Ninja's heuristic decomp inferred
"num_lights". Retail's CObjCell.light_list is populated at runtime via
add_light() calls from neighbouring cell light registrations + per-cell
static-object Setup.Lights, NOT from the dat byte stream.
- Setup.Lights from indoor static objects (entity.SourceGfxObjOrSetupId
prefix 0x02xxxxxx) DO flow through LightInfoLoader.Load (line 5765)
and reach LightManager via LightingHookSink. The wire is intact; the
per-frame Tick + UBO upload chain (line 6865-6867) is intact.
- Retail's particle system does NOT emit lights from particles themselves.
The light comes from the owning Setup's LightInfo records.
Pre-existing failures in DispatcherToMovementIntegrationTests, BSPStepUpTests,
and MotionInterpreterTests are on the branch already and unrelated to this
change (verified by stashing + retesting).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After visual verification 2026-05-18 (turn lag, coast-and-settle,
slope-tilt, jump tracking with contact-plane projection all working),
make the retail chase camera the default. Legacy ChaseCamera stays
available via the DebugPanel toggle (ACDREAM_RETAIL_CHASE=0 or the
checkbox) pending a follow-up deletion commit.
Env var polarity now matches AlignToSlope: default-on if unset, off
only when explicitly "0".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original symptom: jumping made the camera swing around the player
vertically — the basis tilted up/down with the player's Z velocity.
Root cause: ComputeHeading used the raw 3D velocity vector as the
heading direction. During a jump, velocity has a substantial Z
component (vy ≈ jump speed), and `normalize((vx, vy, vz))` produced
a heading pointing up. The basis tilted accordingly and the camera
went under/over the player.
Retail's actual ALIGN_WITH_PLANE algorithm (decomp at
acclient_2013_pseudo_c.txt:95644-95795) is different:
1. Velocity is only used as a gate. If |vx| AND |vy| > epsilon
(player is moving in XY), proceed; otherwise fall back to the
LOOK_IN_DIRECTION path (player's facing direction unchanged).
2. The base heading is `localtoglobalvec(player, (0, 1, 0))` —
the player's local +Y axis in world space, which in our
convention is `(cos yaw, sin yaw, 0)`.
3. Pick a surface normal:
grounded: contact_plane.N
airborne: (0, 0, 1) [world up]
4. Project the base heading onto the plane perpendicular to that
normal: projected = forward - normal * dot(forward, normal).
5. Normalize. Fall back to the base if projection collapses.
Behaviorally:
* Standing jump (vx≈0, vy≈0): gate fails → base heading. Camera
doesn't move with the jump.
* Running jump (vx, vy, vz all nonzero, airborne): projects onto
world up → no-op since base is already horizontal. Camera basis
stays horizontal; player visibly rises in frame.
* Walking uphill (grounded, slope normal tilted): projection
adds a Z component matching the slope angle. Camera basis tilts
with the terrain.
* Walking on flat ground: projection is a no-op. Camera basis
horizontal.
Surface changes:
* RetailChaseCamera.ComputeHeading gains `isOnGround` and
`contactPlaneNormal` parameters.
* RetailChaseCamera.Update gains the same two parameters and
threads them through.
* GameWindow's two Update call sites pass `result.IsOnGround` and
`_playerController.ContactPlane.Normal` (already exposed on
PlayerMovementController — no plumbing change there).
* Tests: 2 existing heading tests reshaped (Moving* and Uphill);
2 new tests added (AirborneJumping straight-up + running-jump);
1 renamed (SlopeAlignDisabled). Net 25 → 27 tests in
RetailChaseCameraTests; full AcDream.App.Tests: 39 → 41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GameWindow now constructs both ChaseCamera + RetailChaseCamera at
player-mode entry, updates both per frame (legacy with isOnGround,
retail with BodyVelocity), and routes mouse/wheel/held-key input to
whichever the CameraDiagnostics flag selects. Mouse-Y goes through
RetailChaseCamera.FilterMouseDelta before AdjustPitch when retail is
active; legacy path is unchanged. Held-key bindings (CameraZoomIn/Out,
CameraRaise/Lower; default-unbound) integrate distance/pitch at
CameraDiagnostics.CameraAdjustmentSpeed per second.
Default behavior: ACDREAM_RETAIL_CHASE unset -> legacy camera as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New CollapsingHeader between Player Info and Performance: toggle +
slope-align checkbox + four sliders (translation stiffness, rotation
stiffness, mouse low-pass window, adjustment speed). All controls
write through DebugVM mirror properties to CameraDiagnostics statics;
changes take effect on the next frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four new InputAction entries for held-key offset integration
(CameraZoomIn/Out, CameraRaise/Lower; default unbound). Six new
DebugVM mirror properties forwarding to CameraDiagnostics so the
upcoming "Chase camera" DebugPanel section can drive them live.
Also folds in four small cleanups from the Task 4 code review:
- Both CameraDiagnostics-mutating tests in CameraControllerTests now
use try/finally save/restore (consistency with Task-3 follow-up B)
- Drop unused `using System.Numerics` from CameraControllerTests
- Reword the XML doc on CameraController.Active to explain WHY both
cameras are held simultaneously (flag flip takes effect on the
next Active access without re-entry) rather than restating the
getter logic
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EnterChaseMode now takes (ChaseCamera, RetailChaseCamera); Active
consults CameraDiagnostics.UseRetailChaseCamera to pick which to
expose. Flag flip at runtime swaps cameras instantly (both are kept
warm). GameWindow's two EnterChaseMode call sites get a temporary
stub RetailChaseCamera; Task 7 wires proper construction +
per-frame updates.
Also folds in two minor cleanups from the Task 3 code review:
- Update() discards the unused `right` axis from BuildBasis (no
caller in the chase-cam math; viewer_offset.X is always 0)
- The three CameraDiagnostics-mutating integration tests now
save and restore the static state in try/finally to avoid
ordering-dependent contamination
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the per-frame Update(playerPos, yaw, velocity, dt) entrypoint
that composes the math primitives into a renderable View matrix +
PlayerTranslucency. State: 5-frame velocity ring, damped eye + forward
unit vector, first-frame snap flag, mouse-filter shared state.
Public surface: Distance/Pitch/YawOffset/PivotHeight tunables,
AdjustDistance/Pitch (with clamps), FilterMouseDelta entry, View +
Position + PlayerTranslucency outputs. 5 new integration tests, all
pass; total RetailChaseCamera test count 25.
Also folds in two minor cleanups from the Task 2 code review:
- AverageVelocity uses ring.Length instead of hardcoded 5
- Basis_NearVerticalHeading test asserts orthogonality of right & up
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven pure-math helpers in the new RetailChaseCamera class:
ComputeHeading (slope-align with flat fallback), BuildBasis (heading
→ orthonormal frame, near-vertical fallback), PushVelocity +
AverageVelocity (5-entry FIFO ring), ComputeDampingAlpha (retail's
stiffness*dt*10), FilterMouseAxis (0.25s low-pass), ComputeTranslucency
(linear ramp 0.20..0.45 m). 20 tests, all pass. State machine + Update()
land in the next commit.
Per spec docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks: CameraDiagnostics static (Task 1) → RetailChaseCamera math
primitives (Task 2) → Update() integration (Task 3) → CameraController
dual-camera (Task 4) → InputAction + DebugVM mirrors (Task 5) →
DebugPanel section (Task 6) → GameWindow wiring (Task 7) → build +
test + visual handoff (Task 8). Each task is TDD-shaped with exact
code in every step. PlayerTranslucency is computed + tested but
applying to the player mesh is explicitly deferred (Q1 escape clause
in the spec).
For docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for porting retail's CameraManager + CameraSet behavior to a new
RetailChaseCamera class, controlled by a CameraDiagnostics toggle so the
user can A/B against legacy ChaseCamera. Covers six retail behaviors:
exponential damping (stiffness*dt*10), 5-frame velocity-averaged slope
alignment, mouse low-pass (0.25s window), held-key offset integration,
auto-fade <0.45m, and independent translation/rotation stiffness rates.
Brainstormed end-to-end before any code change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark #61 DONE inline with the resolution writeup, including the
widened scope note that the same link→cycle boundary bug also caused
the local-player run-stop twitch the user observed during the M2
anim-pass session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related AnimationSequencer fixes for visible animation glitches at
motion-cycle boundaries.
1. Link-tail blend hold (closes#61). BuildBlendedFrame was wrapping
nextIdx unconditionally to rangeLo at the high-frame boundary —
correct for looping cyclic nodes (idle/run/walk loops), wrong for
one-shot links and action overlays. During the ~30 ms fractional
tail before the sequencer transitions to the next queue node, the
blend mixed frame[end] with frame[0], producing a one-frame flash
through the anim's starting pose. Symptoms: door swing-open flap
(frame 0 = closed pose) and player run-stop twitch (frame 0 =
mid-stride). Fix: gate the wrap on curr.IsLooping; non-looping
nodes hold the boundary frame until AdvanceToNextAnimation fires.
2. Stop-anim direction fallback. Stopping from WalkBackward /
SideStepLeft / TurnLeft hit a null linkData from GetLink (the dat
authors a single forward/right stop link and reuses it for both
directions). SetCycle then enqueued only the Ready cycle, snapping
straight to idle with no leg-settle blend. Fix: when the primary
GetLink lookup is null, retry with the substate's low byte remapped
to its forward/right peer (0x06→0x05, 0x10→0x0F, 0x0E→0x0D).
Both fixes are pinned by new regression tests in
AnimationSequencerTests that fail against the prior code (Y=5.02 for
the link tail wrap → frame 0 blend; Y=0 for the backward stop snapping
to Ready cycle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves #77 from Active to Recently closed with the resolution writeup
(both halves — walk-vs-run misclassification + velocity-leak in
turn-in-place — plus the decomp anchors and verification record).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related close-range bugs reported in #77 share a root in
PlayerMovementController.DriveServerAutoWalk + BeginServerAutoWalk:
1. **Walk-vs-run misclassification.** BeginServerAutoWalk decided
`_autoWalkInitiallyRunning = (initialDist - distanceToObject) >= 1.0f`,
forcing run at any chase past ~1.6 m. ACE's wire-level walk-vs-run
answer is the MovementParameters CanCharge bit (0x10), which
Creature.SetWalkRunThreshold sets when server-side player→target
distance >= WalkRunThreshold/2 (= 7.5 m default). Retail's
MovementParameters::get_command (decomp 0x0052aa00) gates the run
path on CanCharge first; the inner walk_run_threshold check
practically always walks given ACE's 15 m default. The hardcoded
1.0 m threshold pushed run into the 3-5 m walk-range the user
reported should walk.
2. **Velocity leak in turn-in-place phase.** When the auto-walked body
crossed the destination, desiredYaw flipped ~180°, walkAligned
dropped to false, and the `if (!moveForward) return true;` branch
returned without zeroing body velocity. The body kept the prior
frame's running velocity (RunAnimSpeed × runRate ≈ 11 m/s) and
slid 4-5 m past the target before the turn-around rotation
completed — the "runs and slides away, runs back, picks up"
symptom in #77 bug B.
Changes:
- `CreateObject.ServerMotionState.CanCharge`: new bool prop reading
bit 0x10 of MoveToParameters. Cross-ref ACE
MovementParams.CanCharge = 0x10.
- `PlayerMovementController.BeginServerAutoWalk`: replaces the unused
`walkRunThreshold` parameter with `bool canCharge`; sets
`_autoWalkInitiallyRunning = canCharge`.
- `PlayerMovementController.DriveServerAutoWalk` turn-in-place branch:
calls `_motion.DoMotion(Ready, 1.0)` and zeros body horizontal
velocity (preserving Z for gravity). No-op for case (a) initial-turn
with stationary body; fixes (b) overshoot recovery and (c) settling
cases.
- `GameWindow.OnLiveMotionUpdated`: passes
`update.MotionState.CanCharge` through; [autowalk-begin] trace
shows `canCharge=` instead of `walkRunThresh=`.
- `GameWindow.InstallSpeculativeTurnToTarget`: predicts ACE's
CanCharge from local distance using ACE's exact 7.5 m rule, so the
speculative install agrees with the wire-triggered overwrite that
arrives moments later.
Visual-verified at Holtburg 2026-05-18: walk-range NPC click walks +
fires Use, walk-range F-key pickup walks + no overshoot, far-range
(8-10 m) pickup still runs. Test baseline unchanged (8 Core pre-existing
failures, 0 net-new failures across Core/Net/UI/App suites).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained handoff doc for a follow-up focused session that fixes#77.
Captures: the two bugs (NPC at walking range never auto-walks; pickup at
walking range overshoots and snaps back), the trace evidence from Step 2's
verification run (cmd=0x0005 speed=-1.84 from ACE), four ranked
root-cause hypotheses (H1 missing BeginServerAutoWalk fire / H2
walk-run-threshold misclassification / H3 negative ForwardSpeed
sign-interpretation / H4 arrival predicate firing too early), the
reproduction recipe with ACDREAM_PROBE_AUTOWALK=1, acceptance criteria,
and the "don't workaround, fix root cause" guardrail.
The auto-walk diagnostic infrastructure already exists from Phase B.6
work — the next session just turns it on and reads the trace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#77 captures two close-range visual bugs observed during Step 2 verification
that are pre-existing — not caused by 0b25df5. Both live in
PlayerMovementController.DriveServerAutoWalk / threshold logic from
Phase B.6 work (#75). Trace evidence captured from launch.log of the
verification run.
#76 closed with the diag-trace evidence: identity invariants matched
across session / _liveSession / _liveSessionController.Session
(hashcode=1034657 throughout), so the original suspected bug-class was
wrong. The first Step 2 attempt's apparent regression was almost
certainly a stale ACE session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 2 of the extraction sequence in docs/architecture/code-structure.md
§4. Lifts DNS resolution + WorldSession instantiation + wireEvents
callback + per-frame Tick + Dispose out of GameWindow.TryStartLiveSession
into a dedicated AcDream.App.Net.LiveSessionController.
What moves:
- DNS resolution (IPAddress.TryParse + Dns.GetHostAddresses fallback,
IPv4 preferred) → LiveSessionController.ResolveEndpoint
- WorldSession instantiation → LiveSessionController.CreateAndWire
- "live: connecting to ..." console line → CreateAndWire
- Try/catch around the setup phase → CreateAndWire (separate from the
Connect/EnterWorld try block that stays in GameWindow)
- Per-frame _liveSession?.Tick() → _liveSessionController?.Tick()
- OnClosing _liveSession?.Dispose() → _liveSessionController?.Dispose()
What stays in GameWindow:
- The 25+ event subscriptions (extracted into a new private
WireLiveSessionEvents method that the controller invokes via callback)
- The Connect → CharacterList → EnterWorld → post-EnterWorld setup dance
(touches Chat, _playerServerGuid, _vitalsVm, _worldState, _settingsStore,
_settingsVm, _playerModeAutoEntry; moving these would balloon Step 2's
scope and risk surface)
- All 60+ outbound _liveSession.Send* call sites (touch the field by
name; LiveSessionController.Session is the controller-side mirror)
The _liveSession field remains as a convenience handle synced with
_liveSessionController.Session; it tracks the same WorldSession instance.
Behavior preservation:
- Same DNS-resolution sequence, same "live: connecting to ..." line,
same wiring-vs-Connect ordering as pre-refactor.
- Same 25+ event subscriptions in the same order, byte-for-byte.
- Same Connect/EnterWorld error handling (the catch block stays in
GameWindow because it disposes _combatChatTranslator which is also
a GameWindow field).
Closes#76.
One subtle nullable-flow fix the compiler required: the chat-bus
lambda's `var liveSession = _liveSession;` capture became
`var liveSession = session;` (the non-null parameter) so the compiler
can prove non-null inside the lambda body. Both pointed to the same
WorldSession instance; only the static analysis changed.
Verification:
- dotnet build green
- dotnet test: AcDream.App.Tests 10/10, Core.Net.Tests 294/294,
UI.Abstractions.Tests 419/419 — all green. Core.Tests 1073/1081
(same 8 pre-existing physics failures as baseline; unrelated).
- Live ACE session against +Acdream verified end-to-end:
* Connection + handshake + EnterWorld
* Door double-click → OnLiveMotionUpdated round-trip
(cmd=0x000B open / cmd=0x000C close)
* NPC double-click → outbound Use
* Ground item F-key pickup × 4 successful (Amaranth, Comfrey,
Damiana, Dragonsblood)
* Spawn stream + chat channels + remote-entity motion all healthy
* Clean OnClosing → controller.Dispose
Walking-range auto-walk + pickup-overshoot bugs observed during
verification are pre-existing (filed as #77); they live in
PlayerMovementController.DriveServerAutoWalk / threshold logic
which this refactor did not touch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A first attempt at extracting LiveSessionController out of GameWindow.cs
(Step 2 of docs/architecture/code-structure.md §4) was implemented and
reverted in the same session after visual verification at Holtburg
exposed three regressions: chat input field accepted text but nothing
was sent; door/NPC double-click fired the Use packet outbound but had
no visible client-side effect; R + click-target auto-walk no longer
fired the deferred Use on arrival (re-opens issue #63 / #75 territory).
Filing as a tracked open issue so the next refactor attempt has a
clear root-cause-investigation starting point with four hypotheses to
test. Step 1 (eda936d) and the Rule 5 follow-up (32423c2) remain
clean and verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First application of CLAUDE.md's new Code Structure Rules §5
("Runtime probes belong in diagnostic owner classes"). Migrates the
four ACDREAM_DUMP_STEEP_ROOF call-site env reads into a single
PhysicsDiagnostics.DumpSteepRoofEnabled property initialized from the
env var at type init, with a runtime setter that follows the existing
ProbeResolveEnabled / ProbeCellEnabled / ProbeBuildingEnabled pattern.
Sites migrated:
- AcDream.Core/Physics/PhysicsEngine.cs:637 (KILL-VELOCITY-APPLIED log)
- AcDream.Core/Physics/TransitionTypes.cs:718 (PHASE3-RESET log)
- AcDream.App/Input/PlayerMovementController.cs:1117 (FRAME log)
- AcDream.App/Input/PlayerMovementController.cs:1199 (per-frame bounce log)
Behavior-preservation only. ACDREAM_DUMP_STEEP_ROOF=1 still produces
identical [steep-roof] log output. The class-comment in
PhysicsDiagnostics already anticipated this migration
("Future slices may fold the older ACDREAM_DUMP_* env vars into this
class for unified runtime toggling").
Not yet wired to a DebugVM checkbox — runtime toggling is available
via the property setter for future debugging sessions, but exposing
it on the panel is a 30-second future cut, not in scope here.
Build: green.
Tests: same pass/fail profile as before this commit (8 pre-existing
Core failures unrelated to physics-diagnostics; App.Tests / Core.Net.Tests
/ UI.Abstractions.Tests all green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts 13 startup-time environment variables out of GameWindow.cs into a
single typed AcDream.App.RuntimeOptions record read once in Program.cs.
Behavior-preservation only — no live behavior change, no visual change.
Verified end-to-end against ACE on 127.0.0.1:9000: full M1 demo loop
(walk Holtburg, click door, click NPC, portal entry) plus DEVTOOLS
ImGui panels load cleanly.
Why: GameWindow.cs is 10,304 LOC and scattered Environment.GetEnvironmentVariable
calls were one of the structural smells called out in the new
"Code Structure Rules" doc. Typed options is the safest cut to make
first because the substitution is mechanical and parsing semantics
get pinned by unit tests.
What lands:
- CLAUDE.md: removed stale R1→R8 execution-phases line, replaced with
pointers to the milestones doc + strategic roadmap (the actual
source of truth). Tightened the "check ALL FOUR references"
section to describe WB as the production rendering base, not
just a reference. New "Code Structure Rules" section (6 rules)
captures the discipline we're committing to.
- docs/architecture/acdream-architecture.md: removed dangling link
to the deleted memory/project_ui_architecture.md.
- docs/architecture/code-structure.md (NEW, 376 LOC): rationale for
the 6 rules + 6-step extraction sequence
(RuntimeOptions → LiveSessionController → LiveEntityRuntime →
SelectionInteractionController → RenderFrameOrchestrator →
GameEntity aggregation). This PR is Step 1.
- src/AcDream.App/RuntimeOptions.cs (NEW, 100 LOC): typed record
with FromEnvironment(string) factory and Parse(datDir, env)
overload for testability. Covers ACDREAM_LIVE, _TEST_HOST/PORT/
USER/PASS, _DEVTOOLS, _DUMP_MOVE_TRUTH, _NO_AUDIO,
_ENABLE_SKY_PES, _HIDE_PART, _RETAIL_CLOSE_DEGRADES,
_DUMP_SCENERY_Z, _STREAM_RADIUS.
- src/AcDream.App/Program.cs: builds RuntimeOptions once, passes
to GameWindow.
- src/AcDream.App/Rendering/GameWindow.cs: ctor takes RuntimeOptions;
7 startup-cached env-var fields become expression-bodied
properties or direct _options.X reads; TryStartLiveSession,
audio init, legacy stream-radius branch all route through
_options.
- tests/AcDream.App.Tests/ (NEW project, 10 unit tests + csproj):
pins parser semantics — default-off bools, the literal "0"
gate for RETAIL_CLOSE_DEGRADES, the >=0 guard for
STREAM_RADIUS, null-vs-empty for user/pass, exact-"1" check
for diagnostic flags. Registered in AcDream.slnx.
Out of scope (per code-structure.md §4):
- Per-call-site ACDREAM_DUMP_* / _REMOTE_VEL_DIAG diagnostic reads
sprinkled through GameWindow (~40 sites). Rule 5 in CLAUDE.md
commits us to migrating these opportunistically as larger
extractions land, not in a bulk pass.
- AcDream.Core's project-reference to Chorizite.OpenGLSDLBackend.
Only the stateless .Lib namespace is used; tightening the project
reference is documented as future work in code-structure.md §2.
Build: green.
Tests: AcDream.App.Tests 10/10 ✓, Core.Net.Tests 294/294 ✓,
UI.Abstractions.Tests 419/419 ✓,
AcDream.Core.Tests 1073/1081 (8 pre-existing failures verified
against pre-refactor baseline by stash-and-rerun).
Visual verification: full M1 demo loop against ACE +Acdream login
including DEVTOOLS panel host load.
Next: Step 2 — extract LiveSessionController per code-structure.md §4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User chose fundamentals-first after M1 landed: indoor walking is
untested (M1 only verified outdoor + doorway), indoor lighting is
broken, camera clips into walls indoors. Better to fix indoor
foundations before stacking combat on top.
Three sub-phases proposed for the new M2:
1. Camera correctness (~1 day)
2. Indoor collision audit (~1 week)
3. Indoor lighting basics (~1-2 weeks)
Combat ("Kill a drudge") slides to M3.
Next session opens with superpowers:brainstorming to scope the
demo scenario + agree on sub-phase boundaries + update the
milestones doc + CLAUDE.md to formalize the reorder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures M1 landing, Phase B.6 architectural details, new rules
(no-workarounds, no-demo-videos, graceful-shutdown), M2 next steps,
and test baselines for fresh-session pickup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 "Walkable + clickable world" landed 2026-05-16 with Phase B.6
(d640ed7). All four demo targets work end-to-end retail-faithfully:
walk Holtburg, open inn door, click NPC, pick up item.
Freeze list applied: L.2 (collision), B.4 (interaction outbound),
B.5 (pickup) — these phases are off-limits until M7 polish unless
something is actively broken.
CLAUDE.md "currently working toward" advanced to M2 — "Kill a drudge."
Phases to ship: F.2 (Inventory panel), F.3 (Combat math + damage),
F.5a (dev panels Attributes/Skills/Equipped/Inventory), L.1c
(combat animation wiring), L.1b (command router prereq).
Also removes the "record a demo video" requirement from milestone
discipline (CLAUDE.md rule #3 + milestones doc operating rule #3) —
user finds video recording pointless. Milestones are now textual
events: writeup + freeze flip + CLAUDE.md "currently working toward"
update. Saved as feedback memory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#63, #69, #74, #75. Replaces the chain of Commit-B workarounds
that compensated for ACE's MoveToChain getting cancelled by a leaked
user-MoveToState packet during inbound auto-walk. The fix is
architectural — auto-walk drives the body directly from the
server-supplied path data, no player-input synthesis, no spurious
wire-packet transitions, no grace-period band-aid.
Architectural change (closes#75):
PlayerMovementController.ApplyAutoWalkOverlay → DriveServerAutoWalk.
- Steps Yaw toward target at retail-faithful turn rates.
- Computes desired forward velocity from path runRate.
- Calls _motion.DoMotion(WalkForward, speed) directly for the
motion-interpreter state (drives animation cycle).
- Sets _body.set_local_velocity directly when grounded.
- Returns true to gate the user-input motion + velocity section
in Update so user-input flow doesn't overwrite auto-walk
velocity or motion state.
Mirrors retail's MovementManager::PerformMovement case 6 (decomp
0x00524440) which never touches the user-input pipeline during
server-controlled auto-walk.
Wire-layer guard at GameWindow.cs:6419 retained as a SEMANTIC
statement (`if (result.MotionStateChanged && !IsServerAutoWalking)`):
user-MoveToState packets are for user-driven motion intent. During
server-controlled auto-walk, the motion-state transitions caused by
the animation override (RunForward / WalkForward / TurnLeft /
TurnRight cycles) must not leak as user-cancellation packets. This
is NOT the deleted 500ms grace-period band-aid; it's the wire-layer
expressing the user-vs-server motion split.
Animation plumbed for auto-walk phases (closes#69):
- Moving forward → WalkForward (speed=1.0) / RunForward (speed=runRate)
- Turn-first phase → TurnLeft / TurnRight (sign of yawStep)
- Aligned-but-pre-step / arrival → no override (idle)
Driven via _autoWalkMovingForwardThisFrame + _autoWalkTurnDirectionThisFrame
fields set in DriveServerAutoWalk and read in the MovementResult
construction at the bottom of Update. UpdatePlayerAnimation picks up
the localAnimCmd as the highest-priority animation source.
Walk/run threshold = 1.0m, retail-observed. ACE's wire-default of
15.0f is too generous; ACE's own physics layer uses 1.0f at
MovementParameters.cs:50 (with the 15.0f line commented out) and
Creature.cs:312 notes "default 15 distance seems too far". The
formula matches retail's MovementParameters::get_command at decomp
0x0052aa00: running = (initialDist - distance_to_object) >=
threshold, evaluated ONCE at chain start and held for the rest of
the auto-walk (matches retail "runs all the way / walks all the way"
behaviour). Wire-supplied threshold is ignored.
Pickup gate (IsPickupableTarget) now uses BF_STUCK
(acclient.h:6435, bit 0x4) to discriminate immovable scenery from
real pickup items that share a Misc ItemType. Sign (pwd=0x14 with
BF_STUCK) → blocked; spell component (pwd=0x10, no BF_STUCK) →
allowed. ACE's PutItemInContainer (Player_Inventory.cs:831-836)
responds with WeenieError.Stuck (0x29) on stuck items so the gate
prevents wasted wire packets + a UX dead-end.
R-key dispatch by target type. UseCurrentSelection's top-level
IsUseableTarget gate was wrong (blocked USEABLE_NO=1 items that
ARE pickupable). Reordered:
1. Creature → SendUse
2. Pickupable → SendPickUp
3. Useable → SendUse
4. Otherwise → "cannot be used" toast
Each handler keeps its own gate. Matches retail's per-action
server-side validation.
AP cadence revert (closes#74). With the MoveToChain race fixed,
the per-frame "send while moving" cadence is no longer load-bearing.
Reverted to retail's two-branch ShouldSendPositionEvent gate
(acclient_2013_pseudo_c.txt:700233-700285):
Interval NOT elapsed (< 1 sec): send if cell or contact-plane changed.
Interval elapsed (>= 1 sec): send if cell or position frame changed.
Adds _lastSentContactPlane field + ApproxPlaneEqual helper +
PlayerMovementController.ContactPlane public accessor. Extended
NotePositionSent(Vector3, uint, Plane, float) — both outbound sites
(MoveToState + AP) pass _playerController.ContactPlane.
Effective rates: 0 Hz idle, ~1 Hz smooth motion, per-event on
cell/plane changes, 0 Hz airborne.
CLAUDE.md updated with no-workarounds rule (commit `da126f9` on
the worktree branch). Saved as feedback memory at
memory/feedback_no_workarounds.md.
Tests: build green; Core.Net 294/294; Core 1073/1081 (baseline,
8 pre-existing Physics failures unchanged). Visual-verified
end-to-end on 2026-05-16 for far/near Use + PickUp on NPCs,
doors, items, spell components, signs (correctly blocked), corpses,
turn-first animation, run/walk thresholds, idle quiet, smooth-
motion 1Hz.
Spec: docs/superpowers/specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md
Plan: docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retires divergences flagged in the 2026-05-16 faithfulness audit:
1. AP cadence. Replaces the 1 Hz idle / 10 Hz active flat heartbeat
with a diff-driven model gated on `Contact && OnWalkable`
(acclient_2013_pseudo_c.txt:700327 SendPositionEvent). Sends on
position or cell change while grounded on walkable, plus a 1 sec
heartbeat; suppressed entirely airborne. PlayerMovementController
exposes `NotePositionSent(pos, cellId, now)` which GameWindow stamps
after each AutonomousPosition / MoveToState send — mirrors retail's
shared `last_sent_position_time` between SendPositionEvent
(0x006b4770) and SendMovementEvent (0x006b4680). Known divergence
from retail: ours is per-frame-while-moving, retail's effective rate
is ~1 Hz during smooth motion (cell/plane checks). Filed as #74,
blocked by #63 — when #63 lands we revert to retail's narrower gate.
2. Workaround retirement. Removes TinyMargin (0.05 m inside arrival)
and the AP-flush before re-send (`SendAutonomousPositionNow`). The
diff-driven cadence makes both obsolete. Close-range turn-first
deferred Use is kept (it IS retail — ACE Player_Move.cs:66-87
mirrors retail's CreateMoveToChain pre-callback rotation), renamed
`OnAutoWalkArrivedSendDeferredAction` to clarify it's a FIRST send.
`isRetryAfterArrival` parameter dropped.
3. Far-range Use/PickUp retry. Restored — was load-bearing, not the
"redundant cleanup" the Group 2 audit thought. Issue #63 means ACE
drops the first Use as too-far without re-polling on subsequent APs;
the arrival re-send is what makes far-range Use complete. Logs
include `(queued for arrival re-send pending #63)` to make this
explicit. Removes when #63 closes.
4. Screen-rect picker. New `AcDream.Core.Selection.ScreenProjection`
helper shared by `WorldPicker` and `TargetIndicatorPanel`. The
`Setup.SelectionSphere` projects to a screen-space square (retail
anchor `SmartBox::GetObjectBoundingBox` 0x00452e20); picker
hit-tests the mouse pixel against the same rect the indicator draws,
inflated by 8 px (`TriangleSize`). Guarantees what-you-see is
what-you-click — including rect corners that were dead zones under
the old ray-sphere picker. Per-type radius (1.0/1.6/2.0 m) and
vertical-offset (0.2/0.9/1.0/1.5 m) heuristic lambdas retired;
`IsTallSceneryGuid` deleted; `EntityHeightFor` trimmed to 1.5 m × scale
defensive default. No defensive sphere synth — entities without a
baked `SelectionSphere` are skipped, matching retail's
`GfxObjUnderSelectionRay` (0x0054c740).
5. Rotation rate run multiplier (Commit A precursor). `TurnRateFor(running)`
helper applies retail's `run_turn_factor = 1.5f` (PDB-named
0x007c8914) under HoldKey.Run, matching `apply_run_to_command` at
0x00527be0 (line 305098). Effective: walking ≈ 90°/s, running ≈ 135°/s.
Keyboard A/D + ApplyAutoWalkOverlay both use it.
6. Useability gate (Commit A precursor). `IsUseableTarget` corrected to
`useability != 0` per `ItemUses::IsUseable` at 256455 — ANY non-zero
passes (USEABLE_NO=1, USEABLE_CONTAINED=8, etc.), not just the
USEABLE_REMOTE bit. Cross-checked against 4 call sites in retail
(ItemHolder::UseObject 0x00588a80, DetermineUseResult 0x402697,
UsingItem 0x367638, disable-button-state 0x198826). Added
`ProbeUseabilityFallbackEnabled` diagnostic
(`ACDREAM_PROBE_USEABILITY_FALLBACK=1`) to measure how often the
creature/BF_DOOR fallback fires for ACE-seed-DB entities with
null useability.
CLAUDE.md updated with the graceful-shutdown rule for relaunch:
Stop-Process bypasses the logout packet, leaving ACE's session marked
logged-in for ~3+ min. CloseMainWindow() sends WM_CLOSE so the
shutdown hook runs and the logout packet reaches ACE.
Tests: +3 ScreenProjectionTests + 6 WorldPickerRectOverloadTests = +9.
Core.Net 294/294 pass; Core 1073/1081 (8 pre-existing Physics failures
unchanged). Visual-verified 2026-05-16: rotation rate, useability,
screen-rect click area, double-click + R-key + F-key Use/PickUp at
short and long range — dialogue/door/pickup fire on arrival.
Filed follow-ups #70 (triangle apex/size DAT sprite), #71 (picker
Stage B polygon refine), #72 (cdb omega.z probe), #73 (retail-message
sweep pattern), #74 (per-frame AP chattier than retail — blocked by
#63). Old ray-sphere `WorldPicker.Pick(origin, direction, ...)`
overload kept for back-compat; no callers in acdream proper.
Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>