Phase N.1 step 5: when the flag is set, Generate() delegates to
GenerateViaWb. Default off; flag flips to default-on in step 7
after visual verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 4: parallel implementation of Generate() that calls
WB's SceneryHelpers (Displace/CheckSlope/RotateObj/ObjAlign/ScaleObj)
and TerrainUtils (OnRoad/GetNormal) instead of the inline ports.
Not yet wired in — Generate() still runs the legacy path. Step 5
adds the dispatch.
Per-helper conformance tests in step 3 prove the Displace/OnRoad/
GetNormalZ/ScaleObj substitutions are behavior-equivalent. Rotation
is intentionally NOT conformance-tested because our existing port
diverges from retail by ~180°; WB's RotateObj fixes that bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 3: prove our inline algorithms match WorldBuilder's
helpers for representative inputs including the 0xA9B1 edge-vertex case.
Four conformance tests pass: Displace, OnRoad, GetNormalZ, ScaleObj.
Our hand-ported algorithms match WB's helpers exactly for these.
Rotation is intentionally NOT conformance-tested. Investigation against
retail's Frame::set_heading (named-retail 0x00535e40) and
Frame::set_vector_heading (0x00535db0) showed our acdream port uses a
shortcut formula `yawDeg = -(450-degrees)%360` that diverges from
retail's atan2 round-trip by ~180°. WorldBuilder's SetHeading ports
the round-trip faithfully and matches retail. Our existing port is
wrong — undetectable visually because per-tree rotation noise masks
the offset. The migration to WB.SceneryHelpers.RotateObj fixes this
bug; adding a conformance test would lock in the wrong behavior.
Bumps IsOnRoad to internal so the OnRoad conformance test can call it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 2: read the env var into a static bool. No behavior
change yet — the flag is consumed in step 5 when GenerateViaWb is
wired in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses code-review feedback on commit 26cf2b8. The dropped
ArgumentException length guards were correct to drop because
DatReaderWriter.LandBlock self-initializes Terrain[] and Height[]
to length 81 in its constructor — but that invariant was not
documented anywhere visible to future readers. Adds an XML doc
<remarks> block explaining the guarantee so callers constructing
synthetic LandBlocks know what to expect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.1 step 1: WbSceneryAdapter.BuildTerrainEntries converts our
LandBlock dat type into the TerrainEntry[81] shape WorldBuilder's
TerrainUtils / SceneryRenderManager consume.
Field mapping (TerrainInfo → TerrainEntry):
TerrainInfo.Road (bits 0-1) → TerrainEntry.Road
TerrainInfo.Type (bits 2-6) → TerrainEntry.Type
TerrainInfo.Scenery (bits 11-15) → TerrainEntry.Scenery
LandBlock.Height[i] → TerrainEntry.Height
The spec listed the texture property as 'Texture' but TerrainEntry's
actual property is named 'Type' (confirmed from source). The spec also
described LandBlock.Terrain as ushort[81] but it is TerrainInfo[81] —
DatReaderWriter already decodes the bit fields so the adapter uses
TerrainInfo's named properties rather than raw bit-shift expressions.
Spec: docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase N.0 setup for the WorldBuilder migration. Replaces the local
read-only clone of Chorizite/WorldBuilder at references/WorldBuilder/
with a git submodule pointing at our fork
(github.com/eriknihlen/WorldBuilder.git, branch acdream).
Changes:
- .gitignore: exempt references/WorldBuilder from the references/
ignore rule so the submodule can be tracked.
- .gitmodules (new): submodule entry tracking acdream branch on fork.
- src/AcDream.Core/AcDream.Core.csproj: add ProjectReference to
WorldBuilder.Shared and Chorizite.OpenGLSDLBackend so we can call
TerrainUtils, SceneryHelpers, etc. from our Core code.
Build green, all 93 scenery/terrain tests pass. The 8 pre-existing
DispatcherToMovement test failures are unrelated and exist on main.
Notes for users picking up this branch on main:
- After merge, the existing local clone at references/WorldBuilder
may need to be removed before `git submodule update --init` will
populate the submodule.
- Working on the fork happens via `cd references/WorldBuilder && git
checkout acdream && <changes> && git push`. To pull upstream
Chorizite/WorldBuilder fixes: `git remote add upstream
https://github.com/Chorizite/WorldBuilder.git && git fetch upstream
&& git merge upstream/master`.
Next: Phase N.1 — replace SceneryGenerator algorithm calls with
WB's SceneryHelpers + TerrainUtils.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two design docs and a roadmap entry for the strategic shift
from "port retail rendering algorithms ourselves" to "depend on a
fork of Chorizite/WorldBuilder for rendering + dat-handling."
- docs/superpowers/specs/2026-05-08-phase-n-worldbuilder-migration-design.md
— parent design: integration model (fork + submodule), 10 sub-phases
(N.0 setup through N.10 GL consolidation), strangler-fig phasing
with per-phase feature flags, retail-decomp boundary clarified for
what WB does NOT cover (network, physics, animation, motion, UI,
plugin, audio, chat).
- docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md
— N.1 detailed design: replace IsOnRoad / DisplaceObject /
slope-normal calc / rotation / scale inside SceneryGenerator.Generate()
with calls to WB's SceneryHelpers + TerrainUtils. Keep data flow,
ScenerySpawn shape, and renderer integration. Add small LandBlock →
TerrainEntry[] adapter. Feature flag ACDREAM_USE_WB_SCENERY=1.
- docs/plans/2026-04-11-roadmap.md — Phase N entry added between
Phase M and Phase J. Lists all 10 sub-phases with effort estimates.
Fork already created at https://github.com/eriknihlen/WorldBuilder.
N.0 setup (replace references/WorldBuilder/ snapshot with submodule,
add project references, build green) is the next implementation step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saves the comprehensive inventory of what WorldBuilder provides
(terrain, scenery, static objects, EnvCells, portals, sky, particles,
texture decode, mesh extraction, visibility) vs what acdream still
ports from retail decomp (network, physics, animation, movement, UI,
plugin, audio, chat).
This is the load-bearing reference for the strategic shift from
"port retail algorithms ourselves" to "rely on WorldBuilder for
rendering + dat-handling, port only what WB doesn't cover."
Updates CLAUDE.md:
- Adds top-level instruction: read the inventory FIRST before
re-porting anything in the 🟢 list
- Reframes references/WorldBuilder/ as acdream's rendering BASE,
not just a reference repo
- Updates the "Reference hierarchy by domain" table to point
rendering/dat questions at WorldBuilder, with retail decomp as
cross-check
Subsequent commits will fork WorldBuilder and replace our terrain/
scenery/object rendering with calls into the fork.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The obj_within_block check using Setup.SortingSphere.Radius rejects
far too many spawns. Sorting spheres for trees are 5-10m, creating
a wide exclusion zone around every landblock edge. WorldBuilder
produces correct scenery with just bounds+road+building+slope checks
and no bounding sphere check. Revert to match WorldBuilder's approach.
The single extra tree near the road at vtx=(4,8) in 0xA9B1 remains
as a known minor discrepancy from retail — root cause TBD.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Retail's CLandBlock::get_land_scenes creates a PhysicsObj for each
scenery spawn, then calls CPhysicsObj::obj_within_block (0x00461c30)
which verifies the model's sorting sphere stays within [r, 192-r] on
both axes. Edge-vertex spawns displaced close to the boundary (e.g.,
a tree at Y=190.97 from vertex y=8) get rejected because their sorting
sphere extends past the landblock edge.
We were missing this check, which caused a tree near a road at
~(85, 191) in landblock 0xA9B1 to appear in acdream but not retail.
The tree legitimately passed all other filters (road, building, slope)
but its Setup sorting sphere radius (~2-5m) meant it overflowed the
boundary.
Fix: look up each unique Setup's SortingSphere.Radius from the dat
(cached per objectId) and apply the within-block bounds check after
the slope filter, matching retail's order. GfxObj objects (0x01) use
radius 0 (permissive) since they're typically small single-mesh items.
Also: remove temporary ACDREAM_SCENERY_DIAG logging, fix duplicate
xmldoc on IsOnRoad, update road check reference to named-retail PDB
symbol (CLandBlock::on_road at 0x0052FFF0).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes to match retail CLandBlock::get_land_scenes (0x00530460):
1. Loop bound: iterate 9×9 vertices (side_vertex_count=9), not 8×8
cells. Edge vertices (x=8 or y=8) produce valid spawns when the
per-object displacement shifts the position back into [0, 192).
Confirmed by named retail decomp do-while condition, WorldBuilder
vertLength=9, ACViewer Terrain.Count=81, AC2D wTopo[9][9].
2. Building suppression: check at the DISPLACED position's cell
(CSortCell::has_building per spawn), not at the loop vertex index.
Matches WorldBuilder buildingsGrid[gx2, gy2] pattern.
3. Slope filter: replace finite-difference gradient approximation
with triangle-aware normal sampling via new static method
TerrainSurface.SampleNormalZFromHeightmap. Picks the correct
triangle via IsSplitSWtoNE, matching retail find_terrain_poly →
polygon->plane.N.z and WorldBuilder's GetNormal().
Tests: 5 new tests for SampleNormalZFromHeightmap (flat=1.0, sloped<1,
cross-validates with SampleSurface instance method) and DisplaceObject
edge-vertex validity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-contained brief for a fresh agent: bug description with the
2026-05-06 screenshot evidence, what's confirmed (Z fix from #48 is
in, Stab placement is correct, dat content matches), five competing
hypotheses (LCG noise drift / cell-coord transposition / Align
reference / slope filter / SceneInfo lookup), and the cdb retail
trace as the gold-standard diagnostic to disambiguate. Same pattern
as the #48 handoff — paste the doc into a new session as the prompt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#48 (a few specific scenery trees hover above terrain). Bilinear
fallback in GameWindow.SampleTerrainZ had its diagonal triangle-pair
arms swapped relative to the AC2D split-direction physics path. Both
sampler paths now share TerrainSurface.InterpolateZInTriangle as one
source of truth, pinned by a 1500-point conformance test.
Visual verified at Holtburg landblock 0xA9B30001 — formerly floating
32 m pines (setups 0x020002D3 / 0x020002D9) now sit flush.
Files #49 (separate scenery X/Y placement drift, identified in the
same session via screenshot pair).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#48. Trees on sloped cells visibly hovered above the visible
terrain because GameWindow.SampleTerrainZ (the bilinear fallback used
during scenery hydration before physics registers a landblock) had
its diagonal arms swapped — used the SEtoNW triangle test on SWtoNE
cells and vice versa. The ACDREAM_DUMP_SCENERY_Z=1 diagnostic showed
every scenery line ran through the bilinear path (streaming race),
so on hilly terrain scenery was placed at a Z up to ~1.5 m off from
the visible mesh.
Latent since ff325ab (2026-04-17 "feat(ui): debug overlay + refined
input controls" carrying along the upgrade). That commit reached for
WorldBuilder TerrainUtils.GetHeight as the secondary oracle and
re-derived the triangle-pair tests; the named-retail / ACE algorithm
in TerrainSurface.SampleZ (used by the physics path / player Z) was
always correct, so player feet stayed flush — the two paths just
disagreed and only scenery noticed.
Fix:
- TerrainSurface.InterpolateZInTriangle (private static) — single
source of truth for the triangle pick + barycentric Z, sourced
from FUN_00532a50 / ACE LandblockStruct.ConstructPolygons.
- TerrainSurface.SampleZFromHeightmap (public static) — heightmap-
byte-array variant for the scenery hydration fallback. Both this
and TerrainSurface.SampleZ (instance) now delegate to the same
InterpolateZInTriangle.
- GameWindow.SampleTerrainZ — thin wrapper over the new static.
- TerrainSurfaceTests.SampleZFromHeightmap_AgreesWithInstance_AcrossWholeLandblock
asserts both sampler paths agree at 1500 sample points across both
diagonals, so future drift gets caught.
The ACDREAM_DUMP_SCENERY_Z=1 diagnostic in BuildSceneryEntitiesForStreaming
is kept committed (env-var gated, zero cost when off) — useful for
the related #49 scenery (X, Y) placement investigation filed in the
same commit.
Visual verified at Holtburg landblock 0xA9B30001 2026-05-06: the
formerly floating 32 m pines (setups 0x020002D3 / 0x020002D9) now
sit flush on the visible terrain mesh.
Test baseline: dotnet test reports the same 8 pre-existing motion /
BSP step-up failures as the handoff doc warned about — no new
failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in #38 render-interpolation camera work before testing #48
diagnostic dump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-spawn / per-rendered-mesh log line at scenery hydration: rendered
gfx id, sample source (physics vs bilinear), groundZ, BaseLoc.Z,
finalZ, mesh vertex Z range, and DIDDegrade slot 0 metadata. One log
line lets the user identify a floating tree by world coords and the
data picks the hypothesis (BaseLoc.Z addition / sampler drift /
DIDDegrade selection). Diagnostic-first per CLAUDE.md before the fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep local physics authoritative at the retail 30 Hz MinQuantum, but expose a render-only position that lerps between completed physics ticks for the player mesh and chase-camera target. Network outbound continues to use the discrete physics position.
Also make the visually confirmed #47 humanoid close-detail DIDDegrade path default-on, with ACDREAM_RETAIL_CLOSE_DEGRADES=0 left as a diagnostic opt-out.
Verification: dotnet build AcDream.slnx -c Debug; focused #38 interpolation tests passed; visual confirmed smooth 2026-05-06. Full dotnet test AcDream.slnx -c Debug --no-build still has the known 8 AcDream.Core.Tests baseline failures.
Co-authored-by: Codex <codex@openai.com>
Files Issue #48 (tree-Z hover on subset of species) and lands the
agent handoff prompt for the next pickup session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A few specific scenery GfxObjs render with their trunk base ~0.5-1.5m
above the terrain mesh while the vast majority sit flush. Per-GfxObj-
id ⇒ deterministic across instances. Player Z snap is unaffected.
Side-by-side with retail confirmed the same species place flush there.
Filed with three competing hypotheses: per-GfxObj origin convention
(some tree meshes authored with origin at bbox-center vs trunk-base),
physics-vs-bilinear terrain Z mismatch on NE↔SW-cut cells, or the
same DIDDegrade close-detail story as #47 applied to scenery.
Detailed cut-and-paste handoff for the next agent at
docs/research/2026-05-06-issue-48-handoff.md — covers the diagnostic
dump that disambiguates the hypotheses with one log line per
offending tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the agent handoff prompt for Issue #38 (chase-camera 30 fps
regression from L.5's physics-tick gate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings GfxObjDegradeResolver and the ACDREAM_RETAIL_CLOSE_DEGRADES wiring
that resolves humanoid body parts to their retail close-detail meshes via
DIDDegrade slot 0. User-confirmed visual fix for the bulky/flat humanoid
body bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Humanoid bodies (Setup 0x02000001 + heritage variants) rendered visibly
flat / bulky vs retail because we drew the base GfxObj id from Setup /
AnimPartChange directly. Retail's CPhysicsPart::LoadGfxObjArray
(0x0050DCF0) treats that base id as the entry point to a DIDDegrade
table; close/player rendering uses Degrades[0].Id, which is the
higher-detail mesh that carries bicep / deltoid / shoulder geometry.
ACViewer also has this bug — it was the key signal it isn't acdream-
specific. Both clients drew the LOD-3 base mesh (e.g. 14 verts / 17
polys for Aluvian Male upper arm 0x01000055), missing the close-
detail variant (0x01001795: 32 verts / 60 polys).
Adds GfxObjDegradeResolver that walks the table with safe fallbacks
at every step. Wired in GameWindow after AnimPartChange application
and before texture-change resolution so texture overrides match the
resolved mesh's surfaces. Gated by ACDREAM_RETAIL_CLOSE_DEGRADES=1
and scoped to humanoid setups (34 parts with >=8 null-sentinel
attachment slots) while the fix bakes — the change is harmless on
non-humanoid setups (resolver falls back to base when no degrade
table) but we hold the broader sweep until LOD distance plumbing
lands.
User confirmed visually 2026-05-06: bicep, deltoid, and back-muscle
definition match retail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained pickup brief for the bulky-humanoid bug. Has:
- the bug + acceptance criterion
- everything ruled out this session (with evidence)
- starting facts confirmed via diagnostics
- 4 ranked hypotheses (per-vertex normals → ambient → MSAA → frame
composition) with concrete tests for each
- diagnostic env vars + their output shapes
- the CLAUDE.md grep-named-first workflow
- files most likely to need edits
- live test workflow (env vars, expected entities in Holtburg)
- constraints (don't break drudges / scenery / +Acdream local view)
Designed to drop straight into a fresh agent's prompt window.
Filed #47 in docs/ISSUES.md — humanoid characters using Setup 0x02000001
(players + Woodsman + other Aluvian NPCs) render visibly bulkier and less
shape-defined than retail's view. Drudges and other monster setups render
identically. Independent of equipment (naked +Je still shows it).
Investigation this session ruled out 0xF625 ObjDescEvent drops (real bug,
fixed in e471527, but doesn't explain shape), HiddenParts overlap,
ParentIndex walking (animation frames are setup-root coords already),
and player-specific data flow (NPCs using same setup affected too).
Diagnostic infrastructure landed alongside the issue (env-var-gated, no
runtime cost when off):
- ACDREAM_DUMP_CLOTHING=1 now also prints:
- setup.Parts.Count, flatten.Count, APC count on header
- ParentIndex[] and DefaultScale[] arrays
- IdleFrame per-part Origin + Orientation (first 17 parts)
- per-part EMIT line: gfx, subMeshes count, triangle count
- TOTAL triangle / meshRef counts per entity
This is what nailed down "all 34 parts emit" + "animation frames are
setup-root not parent-local" + "humans get setup-wide 180°-Z rotation
that drudges don't" — saved hours next session.
Open hypotheses for #47 next session: per-face vs smoothed vertex
normals (per-vertex normals from dat may be face-style for human
GfxObjs but smooth for monsters), low cell ambient leaving back faces
flat-shadowed, missing MSAA on the GL window.
Retail-driven players observed from acdream rendered with stale
appearance — wrong skin/hair palettes, missing clothing — because
ACE's mid-session appearance broadcasts (equip/unequip/tailoring/
recipe/option-toggle) ride opcode 0xF625 ObjDescEvent and acdream
silently dropped them. Initial CreateObject carries the appearance
at spawn time, but every later equip change only updates via 0xF625
(per Skunkwors protocol docs in ACE/.../GameMessageObjDescEvent.cs).
Retail handles via SmartBox::HandleObjDescEvent (named-retail 0x453340).
Why: the retail observer sees the *server-relayed* view of remotes,
not retail's local build, so dropping ObjDescEvent freezes appearance
at the partial state in the first CreateObject.
How:
- Extract CreateObject's ModelData parsing into reusable
CreateObject.ReadModelData(span, ref pos) returning
(BasePaletteId, SubPalettes, TextureChanges, AnimPartChanges).
- Add ObjDescEvent.cs (parser for 0xF625):
body = u32 opcode | u32 guid | ModelData | u32 instanceSeq | u32 visualDescSeq.
- WorldSession.AppearanceUpdated event + dispatcher branch.
- GameWindow.OnLiveAppearanceUpdated splices new ModelData onto the
cached spawn and replays via OnLiveEntitySpawned. The dedup at the
start of OnLiveEntitySpawnedLocked tears down the old GPU/animated/
collision state cleanly before rebuild.
- _lastSpawnByGuid cache populated at spawn-end and tracked through
UpdatePosition so re-applies use current position (no pop-back to
login spot on equip toggle).
- ACDREAM_DUMP_APPEARANCE=1 env var prints structured SP/TC/APC
decode for every 0xF625 — replaces the earlier raw-hex preview.
- ACDREAM_DUMP_CLOTHING extended with setup.Parts.Count, flatten.Count,
and per-part triangle counts for offline polygon-budget audit.
Tests: 4 new ObjDescEvent tests (round-trip + parser drift guard);
269 net tests green. User-verified live: skin/hair colors match
retail's character data; equip/unequip no longer pops position.
Note: a separate "puffy arms / bulky body" geometry issue remains
where base body parts visibly overlap clothing meshes — different
root cause, tracked separately.
#45 — closed by commit e9e080d. PlayerMovementController hands a raw
localAnimSpeed (1.0 slow / runRate fast); UpdatePlayerAnimation now
scales sidestep cycles by WalkAnimSpeed/SidestepAnimSpeed × 0.5 to
match ACE's BroadcastMovement formula. User-verified.
#46 — filed. Retail clients observing acdream's local +Acdream
character see visibly blippy / laggy movement. Local acdream view of
the same character is fine; acdream observing retail-driven
characters is also fine (after #39 / #45). The degradation is
specifically on the OUTBOUND path. Likely culprits ranked: AutoPos
heartbeat cadence (acdream's fixed 200 ms is suspect per
project_retail_motion_outbound memory), MoveToState send conditions,
sequence counters, or absent HasVelocity on UPs. Verification approach
documented (two retail clients + one acdream side-by-side; cdb
breakpoint count of MovementManager::unpack_movement on retail
observer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User observed (during fix#5 visual verify of #39): "our own Acdream
client renders sidestep walking too slow". Filed as #45.
Root cause: PlayerMovementController.cs:871 computes localAnimSpeed as
the raw `runRate || 1.0`, while ACE's BroadcastMovement converts
inbound MoveToState SidestepSpeed via
speed × 3.12 / 1.25 × 0.5
(Network/Motion/MovementData.cs:124-131). Observer-side cycles play at
the ACE-scaled value (~1.248 slow / ~3.0 fast clamped); the local
cycle was playing at the raw 1.0 / runRate — about 80% of retail
cadence for slow strafe.
Fix: in UpdatePlayerAnimation, when animCommand is SideStepLeft / Right
(low byte 0x0F or 0x10), multiply animSpeed by
WalkAnimSpeed / SidestepAnimSpeed × 0.5 = 3.12 / 1.25 × 0.5 = 1.248
before calling SetCycle. Same factor as ACE; no clamp on the local
side (sequencer handles MultiplyCyclicFramerate naturally).
Forward / backward / turn cycles unchanged — those use WalkAnimSpeed
or RunAnimSpeed as base, where localAnimSpeed = wire ForwardSpeed
already produces the right cadence.
Build clean. Visual verify pending: user reports slow-strafe cadence
should match retail / our own observed-remote rendering after this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User observed the Shift-toggle cycle transition was "not as fast as
retail" after fixes #3-#5 landed the velocity-fallback path. Worst-case
added latency was the full 500 ms grace window before the first UP
could refine the cycle.
200 ms covers the actual UM/UP race — UMs arrive on direction-key
events, UPs at 5-10 Hz, so the first UP after a fresh UM lands
~100-200 ms behind it. Below that, fallback could prematurely
overwrite a UM's cycle decision; above that adds latency for no
correctness benefit. Direction flips (W↔S, A↔D, Forward↔Strafe)
update via UM directly so they're unaffected by this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#45 — local +Acdream slow-strafe walking renders too slow. User
observed during fix#5 visual verify of #39: the observer-side fix
landed, then the user noticed the matching animation on the local
player was also playing at sub-walk cadence. Likely the same
SidestepAnimSpeed (1.25) vs WalkAnimSpeed (3.12) mismatch as fix#5
but on the local UpdatePlayerAnimation path. Filed for separate
investigation.
#39 — added "Progress 2026-05-06" section listing the five-commit
fix sequence (8fa04af → 863d96b → bb026b7 → 2653b30 → cc62e1c →
349ba65), the wire-level finding that retail genuinely does NOT
broadcast on Shift toggle (refuting the earlier confused trace
analysis), the user-verified working cases (1/2/4/5) and the
residual items (latency from 500ms UM grace, direction-flip cases
3/6/7 not yet explicitly verified).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix#4 (commit cc62e1c) divided the observed horizSpeed by WalkAnimSpeed
(3.12 m/s) when computing the sidestep speedMod. That made slow strafe
come out 2.5× too slow because retail's sidestep cycle uses
SidestepAnimSpeed (1.25 m/s) — a smaller base — per
MotionInterpreter.cs:592 `velocity.X = SidestepAnimSpeed * SideStepSpeed`.
User report: "Strafe left and right slowly now is SUPER slow :)".
Replace MotionInterpreter.WalkAnimSpeed with MotionInterpreter.SidestepAnimSpeed
in the sidestep branch only. Forward / backward branches continue using
WalkAnimSpeed (correct for those motions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report from fix#3 visual verify (commit 2653b30):
- Forward Run↔Walk Shift toggle: WORKS now
- Strafe Shift toggle: no transition (was out of scope)
- "When I shift walk backwards, the retail char gets animated walking
slow forward but blipping backwards" — REGRESSION
Root cause of the backward regression: ACE encodes WalkBackward as
`WalkForward` motion with NEGATIVE speedMod (MovementData.cs:115
`interpState.ForwardSpeed *= -0.65f`). My fix#1's hysteresis branches
treated lowByte 0x05 / 0x07 as "forward" and computed positive speedMod
from horizSpeed, overwriting the negative sign. Result: animation
played forward-walk while body kept moving backward (the rubber-band).
Strafe gap: sidestep (low byte 0x0F / 0x10) wasn't in fix#1's scope,
so ApplyPlayerLocomotionRefinement returned early for sidestep cycles.
Retail does the same wire-silence on Shift toggle for sidestep, so
observer-side cycle refinement must also fire for it.
Fix:
- Probe `currentSign = sign(CurrentSpeedMod)` to detect backward direction
- For sidestep (lowByte 0x0F or 0x10): keep motion ID, refine
speedMod magnitude = horizSpeed / WalkAnimSpeed, preserve sign
- For backward (forward-class lowByte AND currentSign < 0): keep
WalkForward motion (per ACE encoding), refine magnitude, preserve
negative sign — no "RunBackward" motion exists, only |speedMod|
changes between Walk-back (~0.65) and Run-back (~1.91 = runRate × 0.65)
- Forward (currentSign >= 0): existing Walk↔Run hysteresis unchanged
Build clean. Diagnostics: [UPCYCLE_PLAYER] line still prints; the
new sidestep / backward branches use the same SetCycle call so
their decisions appear in [SCFULL] / [CURRNODE] for inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual verify with the proper Shift-toggle scenario revealed that fix#1's
ApplyPlayerLocomotionRefinement was UNREACHABLE for player remotes — the
L.3 M2 routing at line 3626+ returns at line 3755, BEFORE the call site
at line 3879. The legacy NPC-only block that compute server velocity +
calls ApplyServerControlledVelocityCycle never runs for players.
[UPCYCLE_PLAYER] count = 0 in launch-39-fix2.log and launch-39-diag2.log
proved the velocity-fallback path was completely dead code for players.
Wire-level evidence (launch-39-diag2.log):
- [FWD_WIRE] for retail actor 0x50000001 over a clean Hold-W-press-Shift-
release-Shift-release-W test shows ONLY Ready→Run and Run→Ready
transitions. NO Walk wire transitions for the Shift toggle. So retail's
outbound MoveToState logic does NOT emit a fresh packet on HoldKey-only
changes (refutes the launch-39-fix2 hypothesis that both directions
emit; the earlier fix2 log's many Walk↔Run transitions came from
W press/release cycles WITH Shift held continuously, not from Shift
toggling alone).
- [VEL_DIAG] over the same test shows clear walk-pace (~2.5 m/s) and
run-pace (~11.5 m/s) periods, so the actor's actual physical speed
IS changing despite the wire silence.
Fix: in OnLivePositionUpdated's L.3 M2 player-remote block, after the
near-enqueue / far-snap routing and before the early `return`, compute
synth velocity from PrevServerPos / LastServerPos and call into
ApplyServerControlledVelocityCycle. The function's internal routing
(commit 8fa04af) sends player remotes through ApplyPlayerLocomotionRefinement
which has the 500 ms UM grace + forward-direction + hysteresis logic
to flip Run↔Walk only when no fresh UM is authoritative.
Build clean. Diagnostics: [UPCYCLE_SRC] now prints `src=synth-player`
when the player-remote path fires (distinct from `src=synth`/`src=wire`
in the legacy NPC path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual-verify of fix#2 (commit 863d96b) showed [SCFULL] correctly reports
currNodeIsCyclic=True after each direct Walk↔Run SetCycle (the link is
removed and _currNode is set to _firstCyclic). User report still:
- Animation continues running visually after Shift toggle to Walk
- Body slows ("speed decreases"), causing rubber-banding
- Adding a turn motion in that state makes the cycle finally transition
to walking
So either:
- _currNode is reset to a stale node BETWEEN SetCycle and Advance
- _currNode is correctly on the new cycle but its AnimRef is wrong
(e.g., the same Animation as the previous cycle, dat-side issue)
- BuildBlendedFrame reads from somewhere other than _currNode
Adds CurrentNodeDiag + FirstCyclicAnimRefHash properties on
AnimationSequencer that expose the active node's Animation
identity-hash, IsLooping, Framerate, frame range, and FramePosition.
TickAnimations logs them on every SEQSTATE tick (1 Hz throttle, gated
on ACDREAM_REMOTE_VEL_DIAG=1).
The [CURRNODE] line with animRef vs firstCyclicAnimRef proves whether
_currNode is actually on the new cycle's anim or has drifted to
something else. Compared across SetCycle SCFULL log lines + the
following CURRNODE ticks, we can see the exact moment the cycle
diverges from what SetCycle set.
No code-behavior changes. Pure read-only instrumentation. Per
Phase 4.5 of systematic-debugging — STOP attempting fixes; gather
evidence first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause identified by research-agent read of AnimationSequencer.SetCycle
+ Advance + the per-tick TickAnimations call site:
- SetCycle enqueues transition link + new cycle, then forces _currNode
onto firstNew (the LINK), per the 357dcc0 fix that pinned _currNode
to the most-recently-enqueued node.
- Advance plays the link to completion (~100–300 ms at Framerate 30 ×
link runSpeed) before AdvanceToNextAnimation moves _currNode forward
to the cycle.
- For Walk↔Run direct toggles faster than the link's drain time, the
next UM arrives, SetCycle restarts _currNode on a fresh link, and
the cycle node at the queue tail is never reached.
- BuildBlendedFrame returns frames from the link the entire time —
user observes the link's interpolation pose ("blips forward in
walking animation"), never the new Walk or Run cycle.
Confirmed by [SCFULL] currNodeIsCyclic=False after every direct
Walk↔Run transition in launch-39-candidate.log.
Fix: when prev motion AND new motion are both locomotion cycles
(WalkForward, WalkBackward, RunForward, SideStep L/R), land
_currNode on _firstCyclic (the new cycle node) instead of firstNew
(the link), and remove the just-enqueued link from the queue.
Conditional on BOTH being locomotion to avoid regressing cases that
DO need the link to play:
- Idle→Run (link is the wind-up pose)
- Falling→Ready (landing animation)
- Ready→Sitting/Crouching/Sleeping
- Combat substates (attack/parry/ready transitions)
Reverted commit c06b6c5 demonstrated that unconditional link skip
breaks all of those — this fix is narrower.
Retail reference: cdb live trace 2026-05-03 of a Walk→Run direct
transition logged add_to_queue(45000005) followed by
add_to_queue(44000007) with truncate_animation_list never firing —
matching the observed semantics this fix implements.
42/42 AnimationSequencer tests pass. The 8 pre-existing test
failures elsewhere on the branch (BSPStepUp, MotionInterpreter
WalkBackward, etc.) are unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual-verify of commit 8fa04af in launch-39-candidate.log refutes the
static-analysis hypothesis that retail does not broadcast UMs on
HoldKey-only changes. The log shows:
- [FWD_WIRE] for retail actor 0x50000001 contains many direct Walk↔Run
transitions (0x44000007 ↔ 0x45000005). ACE IS sending UMs on Shift
toggle.
- [SETCYCLE] fires correctly per UM; Sequencer.CurrentMotion cycles
through Walk / Run / Turn / Sidestep correctly per [UM_RAW].
- [UPCYCLE_PLAYER] never fired — UM grace correctly suppressed it
(UMs at >2 Hz, well within 500 ms grace).
- User reports legs visually stuck in walking animation despite the
wire/sequencer saying Run.
Conclusion: bug is downstream of Sequencer.CurrentMotion — same as
2026-05-03 hypothesis F. Most likely _currNode lands on the walk-to-run
transition link after SetCycle (`currNodeIsCyclic=False` confirmed in
[SCFULL]) and Advance does not progress past it to the cycle.
The candidate fix code (LastUMTime, ApplyPlayerLocomotionRefinement,
hysteresis constants, un-gated call site) is left in place — harmless
because UM grace blocks the velocity-fallback path while UMs arrive,
and the infrastructure may be useful for cases #2–#7 if those need
velocity fallback. But it does not close case #1.
Updates ISSUES.md #39 with refuted hypothesis + new evidence + next
step pointer. findings-static.md gains "Visual-verify result" §
documenting the diagnostic dump and recommending the next investigation
target be AnimationSequencer.Advance queue-handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a player-remote velocity-fallback path to ApplyServerControlledVelocityCycle
so that when retail (the actor) toggles Shift while holding W and acdream is
the observer, the visible leg cycle switches Run↔Walk within ~200–500 ms even
though no fresh UM arrives. Static analysis (ACE GameActionMoveToState +
MovementData.cs auto-upgrade + acdream's prior diag traces) suggests retail
does NOT broadcast a fresh MoveToState on HoldKey-only changes — acdream's
UMs handle direction-key changes and our local +Acdream's transitions, but
retail-driven actors leave the cycle stuck.
Changes (all in src/AcDream.App/Rendering/GameWindow.cs):
- New RemoteMotion.LastUMTime field, stamped in OnLiveMotionUpdated
- ApplyServerControlledVelocityCycle: removed inner IsPlayerGuid gate;
routes player remotes to new ApplyPlayerLocomotionRefinement
- ApplyPlayerLocomotionRefinement (forward-direction only):
- 500 ms UM grace window (UMs win when fresh)
- Forward-direction-only (low byte 0x05 / 0x07)
- Hysteresis: Run → Walk demote at < 4.5 m/s; Walk → Run promote > 5.5 m/s
- Skip SetCycle when neither motion ID nor speedMod changed meaningfully
- [UPCYCLE_PLAYER] diag gated on ACDREAM_REMOTE_VEL_DIAG=1
- Outer call site in OnLivePositionUpdated un-gated (!IsPlayerGuid removed);
per-remote routing now lives inside the function
Scope: case #1 (Run↔Walk forward) only. Cases #2–#7 (backward, sidestep
speed-buckets, direction-flips) remain deferred — PlanFromVelocity is
forward-only and its NPC-tuned thresholds (RunThreshold=1.25) do not
separate player Walk (~2.5 m/s) from player Run (~9 m/s); a TTD trace
of retail's per-direction algorithm should ground the wider fix.
ISSUES.md #39 updated with progress; investigation-prompt.md and a new
findings-static.md committed under
docs/research/2026-05-06-locomotion-cycle-transitions/ (the prompt was
authored on a parallel branch in commit 7a38da3 and is brought into this
worktree here so the next session can find it without branch-hopping).
Build clean. The 8 pre-existing test failures on this branch
(BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope, MotionInterpreter
WalkBackward GetMaxSpeed, etc.) are unrelated to this change — verified
by running them with the diff stashed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#42 — moved from OPEN to DONE in place (rich investigation log preserved
below the new Resolution block). The originally-listed mechanisms (H1
slope-driven AdjustOffset projection, H2 step-down probe, H3 EdgeSlide)
were all RULED OUT by the first evidence run; root cause was self-
collision in FindObjCollisions, not in-sweep mechanism choice. Added
forward-pointer to retail's CObjCell::find_obj_collisions self-skip
(named-retail acclient_2013_pseudo_c.txt:308931).
#43 — new entry in Recently closed for the slope staircase on grounded
player remotes. Diagnosis: PositionManager.ComputeOffset's seqVel-only
fallback returned flat-Z motion because anim cycles bake Z=0 body-local,
producing visible 5 Hz Z stepping at the server-UP cadence. Fix: project
the fallback onto the local terrain plane (mirrors retail's
CTransition::adjust_offset contact-plane projection at the queue-empty
boundary). Verified via 9193 queue-empty-with-non-zero-offset.Z ticks
across a 34m vertical traversal.
Both diagnostic env-vars kept in tree for future regression hunts:
ACDREAM_AIRBORNE_DIAG=1 and ACDREAM_SLOPE_DIAG=1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grounded player remotes were showing a ~5 Hz Z staircase when running
up/down slopes — the rate of server UpdatePositions. Body Z stayed flat
between UPs, then ramped over ~100ms during the queue-active chase to
each new server position, then went flat again until the next UP.
Diagnosis (no diagnostic needed — the math is unambiguous):
PositionManager.ComputeOffset has two modes via
InterpolationManager.AdjustOffset:
- Queue active (body chasing a waypoint): returns
`(head − body) / dist × min(catchUpSpeed × dt, dist)`. 3D direction,
Z follows server's reported Z naturally.
- Queue empty / head-reached (within DESIRED_DISTANCE = 0.05m of the
most recent UP): returns Vector3.Zero. ComputeOffset falls back to
`seqVel × dt rotated into world` — pure animation root motion. Every
locomotion cycle bakes Z=0 in body-local, so the world result has
Z=0 too. XY advances at the running pace; Z stays at the last UP.
For a runner at maxSpeed ≈ 4 m/s with catchUpSpeed = 2× = 8 m/s and
server UPs at ~5 Hz, body covers ~0.8m per UP, chases for ~100ms
(queue-active 3D path, Z ramps), then sits in seqVel-only mode for
~100ms (Z flat) until the next UP. Visible as a 5 Hz Z staircase.
Fix mirrors retail's CTransition::adjust_offset contact-plane projection
(named-retail acclient_2013_pseudo_c.txt:272296-272346) for grounded
motion, applied at the queue-empty boundary instead of inside the sweep:
PositionManager.ComputeOffset gains an optional Vector3? terrainNormal.
When the seqVel-only fallback runs AND a non-trivial terrain normal is
supplied, project rootMotionWorld onto the plane:
result = rootMotionWorld − N × dot(rootMotionWorld, N)
Anim XY motion gains a corresponding Z component proportional to slope
angle × forward speed, so body Z follows the terrain mesh between UPs.
No-op on flat ground (N ≈ +Z, dot ≈ 0); cannot regress L.3 M2's
flat-ground verification.
GameWindow.TickAnimations grounded-remote path samples
PhysicsEngine.SampleTerrainNormal at the body's current XY each tick
and passes it to ComputeOffset. SampleTerrainNormal is a thin public
wrapper over the existing internal SampleTerrainWalkable that returns
just the plane normal (no need to expose the internal sample shape).
Diagnostic: ACDREAM_SLOPE_DIAG=1 prints a per-tick [SLOPE] line with
guid, body Z before/after, offset, queue active flag, and the sampled
plane Nz so we can grep before/after the fix and confirm Z changes
continuously between UPs on slopes.
Tests: PositionManagerTests gains two cases:
- slope projection: 30° east-tilted plane, body running due east at
4 m/s for 1s → expect (3.0, 0, −1.732) (descends along slope, not
flat). Math: dot(seqVel, N) = 2.0 → result = (4,0,0) − (0.5,0,0.866)
× 2.0 = (3.0, 0, −1.732).
- flat-ground no-op: N = +Z, expect identical Y-only motion as the
pre-fix behavior.
Build green. 357 pass / 6 pre-existing fail (same set as ec59a08;
verified by stashing this change). The pre-existing
`ComputeOffset_BothActive_Combined` failure reflects an outdated
additive-design test docstring; the M2 commit (40d88b9) deliberately
changed the implementation to REPLACE semantics to fix the prior
3×-server-pace overshoot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause confirmed via two-run diagnostic and the named-retail decomp:
the airborne sweep was colliding with the moving entity's OWN ShadowEntry
because FindObjCollisions had no self-skip filter. Live entities (local
player, remotes) register a Cylinder in ShadowObjectRegistry on spawn
(GameWindow.cs:2545) and UpdatePosition tracks its world position each
tick, so the moving sphere's own cylinder is always at the body's
position. Without a gate, CylinderCollision sees the sphere overlapping
its own cylinder volume and slides the sphere ~1m horizontally on every
frame the path produces non-zero motion.
Why grounded mostly hides it and airborne exposes it:
- Stationary grounded → numSteps=0, TransitionalInsert never runs.
- Walking grounded → push fires but motion escapes the cyl radius and
the deflection blends into normal motion.
- Stationary airborne (jump) → pure +Z motion; the cyl push is the
only horizontal contribution and manifests as a clean ~1m drift.
Run-2 evidence (launch-42-r2.log) — 152 [SWEEP-OBJ] events, every one
with type=Cylinder, gfxObj=0x02000001 (humanoid setup), R=0.679,
H=1.835, at obj.Position EXACTLY matching the body's pre.Position. Run
1 had already ruled out H1 (cpN=(0,0,1) flat, no slope projection).
Retail does the same skip — CObjCell::find_obj_collisions at
named-retail acclient_2013_pseudo_c.txt:308931:
if ((physobj->parent == 0 && physobj != arg2->object_info.object))
`arg2->object_info.object` is the OBJECTINFO::object self-pointer set
by OBJECTINFO::init at acclient_2013_pseudo_c.txt:274435. Our port
mirrors this with an EntityId-based filter:
- ObjectInfo gains a SelfEntityId field (default 0 = no filter).
- ResolveWithTransition gains an optional `uint movingEntityId = 0`
parameter that sets it.
- FindObjCollisions skips entries whose EntityId matches
SelfEntityId when the id is non-zero.
- PlayerMovementController gains a LocalEntityId property; GameWindow
refreshes it per-tick from `_entitiesByServerGuid[_playerServerGuid]`.
- GameWindow's airborne-remote ResolveWithTransition call site passes
`movingEntityId: kv.Key` (kv.Key is the local entity id keying
`_animatedEntities`, same id used at the spawn-time
ShadowObjects.Register).
Default 0 keeps tests and one-shot callers (no registered ShadowEntry)
working unchanged.
Lock-the-fix unit test:
`PhysicsEngineTests.ResolveWithTransition_SelfShadowEntry_NotPushedWhenIdMatches`
registers a humanoid Cylinder at the body's exact position (matching
GameWindow's spawn pattern), then asserts that:
- movingEntityId=0 (control) → unfiltered XY drift > 0.5m
- movingEntityId=registered id (fix) → XY drift ≈ 0
Diagnostic wiring (a36369d + this commit's [SWEEP-OBJ] addition) stays
in tree, env-var gated (ACDREAM_AIRBORNE_DIAG=1) so it produces no
output in normal use but lets us verify the fix on the live client and
debug future regressions.
Build: green. Tests: 355 pass, 6 fail (all pre-existing per the handoff
prompt — verified by stashing this change; the BSPStepUp C3 failure is
on the prior commit too).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of #42 root-cause investigation per the handoff doc. We
A/B confirmed (commit b37b713) that the ~1m XY drift on retail-
observed stationary jumps comes from inside ResolveWithTransition
when the per-tick airborne sweep runs (CellId fix at GameWindow.cs
3467). What we don't yet know: whether the drift originates in
H1 (initial-overlap depenetration along a tilted-terrain normal),
H2 (step-down probe firing despite isOnGround=false), or H3
(EdgeSlide on near-vertical motion grazing a wall).
This diagnostic gates a one-line Console trace on
ACDREAM_AIRBORNE_DIAG=1 AND !isOnGround so it doesn't pollute
grounded movement, and prints:
[SWEEP] airborne pre=(...) target=(...) post=(...)
cell=PRE->POST ok=BOOL deltaXY=(dx,dy)
cp=valid|none cpN=(nx,ny,nz)
deltaXY = post - target — for a clean stationary +Z jump we
expect (0,0). Non-zero with cp=valid and a tilted cpN confirms
H1; non-zero direction tracking actor facing instead of terrain
orientation points to H2/H3.
Code-walk findings recorded for the next investigation pass:
- K-fix7 already prevents seeding ContactPlane on entry for
airborne (PhysicsEngine.cs:493-519), so step 0's AdjustOffset
cannot consume a stale plane.
- BUT ValidateWalkable can still SET ContactPlane during step 0's
collision pass via the "below plane" branch (TransitionTypes.cs
1320-1352) when sphere lowPoint dips below the tilted terrain
triangle. Step 1's AdjustOffset would then consume that fresh
plane and the "moving away from contact plane" branch
(TransitionTypes.cs:1749-1754) projects the +Z offset along the
slope normal, redirecting Z motion into XY.
- Step-down branch is correctly gated on oi.Contact (matches
retail CTransition::transitional_insert at named-retail
acclient_2013_pseudo_c.txt:273249, "(state & 1) == 0" returns
OK without firing step-down).
- Retail's IS_VIEWER_OI=0x4 branch in OBJECTINFO::validate_walkable
(acclient.h:6185) is never set anywhere in the named decomp,
so the airborne path runs the same code in retail as in acdream.
User repros at flat plaza / east hillside / north hillside; the
direction-correlation of deltaXY with local terrain orientation
identifies which hypothesis is firing.
Build green; 13 PhysicsEngine tests green. No behavior change
when ACDREAM_AIRBORNE_DIAG is unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the original 96-line note with a detailed self-contained
brief targeted at someone picking up #42 cold in a new session.
Adds:
- Explicit ruled-out list (wire data, Euler error, stale velocity,
diagnostic instrumentation) — saves rediscovering dead ends.
- The user's "buildings + jumping puzzles" constraint that rules out
blanket sweep-disable.
- Specific file/line targets in PhysicsEngine.cs (470, 478-491,
493-519, 521-530, 532, 534-558) and TransitionTypes.cs (786-846,
1305-1311) with a brief reading order.
- Phase 1 / Phase 2 / Phase 3 investigation plan with concrete
diagnostic harness (`ACDREAM_AIRBORNE_DIAG=1` + `[SWEEP]` log) and
direction-correlation test.
- Per-hypothesis fix paths so the agent doesn't re-derive them from
the diagnosis.
- Full acceptance criteria including build/test gates and visual
test sequence (flat / hillside / running / doorway / puzzle / land).
- Hard rules (don't blame ACE, don't disable sweep, don't touch L.3
motion code, don't reduce sphere dims, etc.).
- cdb breakpoint recipe for retail-vs-acdream A/B comparison.
- Pre-session reading list with line numbers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the elated-aryabhata-208d5e branch into main. 7 commits implementing
the L.3 retail-faithful remote-entity motion port:
de129bc M1 Fresh InterpolationManager port + retail spec
40d88b9 M2 Queue routing for player-remote UPs + entity-position sync
2365c8c M3 Animation root motion fallback for idle queue
d57ace0 M6 Cleanup — dead fields + stale env-var references
c26bbbb M4 Jump-CellId fix + #42 filed
b37b713#42 root cause confirmed via A/B test
5cc2812 Handoff prompt for #42 PhysicsEngine investigation
User-verified visual checks: smooth body chase on running/walking/strafing,
no per-UP rubber-band, no slope staircase, NPCs pathing correctly, jumps
land cleanly. Two follow-up issues filed:
#41 sub-decimeter steady-state blips (velocity-synthesis residual; LOW)
#42 airborne XY drift on jumps (PhysicsEngine.ResolveWithTransition
depenetration; root cause confirmed; deep-dive prompt at
docs/research/2026-05-05-issue-42-handoff.md)
Replaces the env-var-gated experimental path (ACDREAM_INTERP_MANAGER=1)
which was marked DO-NOT-ENABLE — the env-var no longer toggles anything.
NPCs and airborne player remotes still use the legacy path; only grounded
player remotes route through the new retail-faithful queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Self-contained next-session brief for the airborne XY drift
follow-up. Captures: confirmed root cause (ResolveWithTransition,
verified A/B), three ranked hypotheses for the in-sweep mechanism
(initial-overlap depenetration on non-+Z terrain normal is leading),
three fix paths in preference order, repro steps with terrain-slope
direction-correlation test, and the acceptance criteria.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A/B-tested 2026-05-05 with user observing retail-controlled remote:
- With CellId fix removed: jumps render with geometrically-correct
XY (no drift) but body falls through the floor.
- With CellId fix applied: jumps land cleanly but arc shows ~1 m
horizontal offset; snaps back on next UM.
Confirms the drift originates inside ResolveWithTransition, not from
wire data, local Euler error, or stale velocity. CellId fix kept in
place because falling through the floor is more disruptive than
~1 m visual jitter that resolves on next input.
#42 updated with the verified diagnosis, three ranked-by-probability
hypotheses for the in-sweep mechanism (initial-overlap depenetration
along non-+Z terrain normal is the leading candidate), three matching
fix paths, and a deterministic repro recipe for the next session.
The right next step is investigating PhysicsEngine.ResolveWithTransition
and comparing against retail's CTransition::find_valid_position
(docs/research/named-retail/) — out of scope for the L.3 motion port,
files as a follow-up PhysicsEngine bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>