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>
CellId fix:
L.3 M2 introduced OnLivePositionUpdated player-remote routing that
returned without setting `rmState.CellId = p.LandblockId`. The legacy
path always set this (formerly at line 3601). Airborne player remotes
fall through to the legacy TickAnimations path which gates
ResolveWithTransition on `rm.CellId != 0`; without the cell-id update
the sphere sweep was skipped, K-fix15 landing detection never fired,
and the body fell through the floor on jumps.
Fix: set `rmState.CellId = p.LandblockId` early in the M2 player-remote
branch (after orientation snap, before any return).
User-verified 2026-05-05: jumps now land cleanly with sequencer
leaving Falling on landing.
#42 filed:
Visual verification of M4 also exposed a ~1 m horizontal drift on
stationary jumps (body arcs through the air offset from actor's actual
position; lands at offset; snaps back on next UM). User confirms this
is pre-existing, masked by the legacy path's hard-snap-on-every-UP
behavior that M2 explicitly removed per retail spec
(03-up-routing.md § 3 "AIRBORNE NO-OP"). Filed as #42 with three
candidate fix paths (pragmatic legacy-restore, root-cause investigation,
or hybrid soft-correction).
M5 NPCs verified clean (legacy path unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleans up dead code revealed by L.3 M2/M3:
GameWindow.cs:
- RemoteMotion.LastServerZ field deleted (only consumed by the M2-
removed Step 5 landing fallback in TickAnimations; never read).
- RemoteMotion.TargetOrientation field deleted (audit § 1 flagged as
DEAD; only ever written, never read).
- Stale ACDREAM_INTERP_MANAGER comments removed from RemoteMotion.Interp
and OnLivePositionUpdated (the env-var no longer gates anything as
of M2).
- Doc-comments on Interp + Position rewritten to describe the M2/M3
production semantics (queue catch-up + REPLACE-style combiner).
CLAUDE.md:
- ACDREAM_INTERP_MANAGER env-var entry rewritten as a retirement note
pointing at commit 40d88b9 (M2). The path it gated is now the
default for player remotes.
Build green, dotnet test green (8 pre-existing failures unchanged on
this baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores PositionManager.ComputeOffset call in TickAnimations player-
remote branch. M2 was queue-only (body chases server but stops between
UPs after head reached); M3 adds the retail REPLACE behavior:
- Queue active and not reached → catch-up vector (REPLACES anim).
- Queue empty or head reached → anim root motion (seqVel × dt rotated
by body.Orientation) drives translation between UPs.
- Blip-to-tail still fires on fail_count > 3.
Mirrors retail UpdatePositionInternal @ 0x00512c30 per
docs/research/2026-05-04-l3-port/05-position-manager-and-partarray.md
§ 6: PositionManager::adjust_offset OVERWRITES local frame's origin
with catch-up when active; otherwise no-op (anim root motion stands).
User-verified 2026-05-05: "Best implementation we have had so far.
Running works, walking works, strafing works."
Closes#40 (env-var path regression — replaced wholesale).
Files #41 for residual sub-decimeter blips: velocity-synthesis magnitude
(RunAnimSpeed × adjustedSpeed) overshoots server pace slightly, queue
walks it back every UP. Within retail's DesiredDistance / MinDistance
tolerances; not a correctness bug. Fix path requires porting
add_motion @ 0x005224b0 and cdb-tracing retail's actual
CSequence::velocity magnitude.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the M1 InterpolationManager into the per-tick + UP-receipt paths
in GameWindow for player remote entities. Visual-verified against a
retail-controlled remote: smooth body chase, no per-UP rubber-band, no
staircase on slopes.
OnLivePositionUpdated:
- Gate changed from `ACDREAM_INTERP_MANAGER == "1"` to
`IsPlayerGuid(update.Guid)`. NPCs continue through the legacy
synth-velocity branch (ServerVelocity / ServerMoveTo) below — their
motion model is correct as-is.
- Within-bubble enqueue passes `currentBodyPosition` so the M1 far-
branch detection (>100 m from body) can pre-arm an immediate blip.
- Three branches (airborne no-op, near-enqueue, far-snap) now sync
`entity.Position = rmState.Body.Position` before returning. This
overrides the unconditional `entity.Position = worldPos` snap at
the top of the function. Without this sync the entity teleports
forward to server truth on UP receipt and TickAnimations yanks it
back to the queue-driven body next frame — visible 0.5–1 m rubber-
band per UP.
TickAnimations:
- Gate changed from `ACDREAM_INTERP_MANAGER == "1"` to
`IsPlayerGuid(serverGuid) && !rm.Airborne`. Airborne player remotes
fall through to the legacy path so K-fix15 landing + gravity sweep
still fire on the jump arc.
- Step 2 (per-frame translation) replaced. Was
`rm.Position.ComputeOffset(...)` (mixed queue catch-up + animation
root motion); now direct `rm.Interp.AdjustOffset(...)` (queue-only,
no anim contribution). M3 will layer anim root motion on top so
legs match body pace; for M2 the body chases server position
smoothly without any anim-driven translation.
- Step 4b (ResolveWithTransition collision sweep) REMOVED for player
remotes. Server already collision-resolved the broadcast position;
running the sweep on tiny per-frame queue catch-up deltas amplified
micro-bounces into the ISSUES.md #40 staircase + flat-ground blips.
- Step 5 (LastServerZ landing fallback) REMOVED — unreachable in the
`!rm.Airborne` branch.
Per retail spec (docs/research/2026-05-04-l3-port/01-per-tick.md +
04-interp-manager.md): m_velocityVector stays 0 for grounded remotes,
apply_current_movement is local-player-only, and per-tick translation
comes entirely from InterpolationManager queue catch-up.
Behavior for player remotes:
| Scenario | Path | Translation source |
|-----------------------|--------|------------------------------|
| Grounded near (≤96m) | M2 | Queue catch-up (2× max-speed)|
| Grounded far (>96m) | M2 | Hard-snap to worldPos |
| Far enqueue (>100m) | M2 | Pre-armed blip-to-tail |
| Airborne (mid-jump) | Legacy | Gravity arc + sweep |
| Landing | M2 | Hard-snap, queue cleared |
NPCs: legacy path unchanged (synth velocity, ServerMoveTo, etc.).
Closes the regression observed in 9b0f4f2 ("modern, not retail-faithful")
and the L.3 attempts on 91bf1e0 / e94e791. Replaces the env-var path
(ACDREAM_INTERP_MANAGER=1) which was marked DO-NOT-ENABLE in
ISSUES.md #40 — the env-var no longer toggles anything for player
remotes; this IS the path now.
Build green, dotnet test green (8 pre-existing failures unchanged on
this baseline; verified via stash on a3f53c2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites src/AcDream.Core/Physics/InterpolationManager.cs from the spec
in docs/research/2026-05-04-l3-port/04-interp-manager.md. Public API
preserved (Vector3-returning AdjustOffset, Enqueue, Clear, IsActive,
Count) so PositionManager + GameWindow callers continue to compile;
internals are full retail spec.
Bug fixes vs prior port (audit 04-interp-manager.md § 7):
#1 progress_quantum accumulates dt (sum of frame deltas), not step
magnitude. Retail line 353140; the prior port's `+= step` made
the secondary stall ratio meaningless.
#3 Far-branch Enqueue (dist > AutonomyBlipDistance = 100m) sets
_failCount = StallFailCountThreshold + 1 = 4, so the next
AdjustOffset call's post-stall check fires an immediate blip-to-
tail snap. Retail line 352944. Prior port silently drifted
toward far targets at catch-up speed instead of teleporting.
#4 Secondary stall test ports the retail formula verbatim:
cumulative / progress_quantum / dt < CREATURE_FAILED_INTERPOLATION_PERCENTAGE.
Audit notes the units are 1/sec (likely Turbine bug or x87 FPU
misread by Binary Ninja) — mirrored byte-for-byte regardless.
#5 Tail-prune is a tail-walking loop, not a single-tail compare.
Multiple consecutive stale tail entries within DesiredDistance
(0.05 m) of the new target collapse together. Retail line 352977.
#6 Cap-eviction at the HEAD when count reaches 20 (already correct
in the prior port; verified).
New API: Enqueue gains an optional `currentBodyPosition` parameter so
the far-branch detection can reference the body when the queue is
empty. Backward-compatible (default null = pre-far-branch behavior).
UseTime collapsed into AdjustOffset's tail (post-stall blip check)
since acdream has no per-tick UseTime call separate from
adjust_offset; identical semantic outcome.
State fields renamed to retail names with sentinel values:
_frameCounter, _progressQuantum, _originalDistance (init = 999999f
sentinel per retail line 0x00555D30 ctor), _failCount.
Tests:
- 17/17 InterpolationManagerTests green.
- New test Enqueue_FarBranch_PrearmsImmediateBlipOnNextAdjustOffset
pins the bug #3 fix: enqueueing 150 m away triggers a same-tick
blip (delta length ≈ 150 m), and the queue clears.
Spec tree: 17 research docs (00–14) under docs/research/2026-05-04-l3-port/.
00-master-plan + 00-port-plan describe the 8-phase rollout. 01-per-tick,
03-up-routing, 04-interp-manager, 05-position-manager-and-partarray,
06-acdream-audit, 14-local-player-audit are the L.3 spec used by this
commit and the M2 follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five parallel agents + dat probes ruled out:
- byte-level decode primitive (matches ACViewer)
- polygon emission (no ST_DOUBLE / Surface.Type & 6 issues)
- per-PART texture-override scoping (correctly per-MeshRef'd)
- SubPalette indexing convention (full-size 2048 palettes, *8 wire un-pack
is single-applied)
Smoking gun: for +Acdream the server sends 10 SubPaletteSwap ranges that
overlay palette indices [0..320), [576..1024), [1392..1488), [1728..1920).
The complement — [320..576), [1024..1392), [1488..1728), [1920..2048) —
is NOT overlaid. Base palette 0x0400007E at those indices has
red/skin tones. Coat texture UVs sampling those non-overlaid indices
render as visible "skin stub at top of coat".
Either ACE sends incomplete SubPaletteSwap data, or retail does extra
client-side ClothingTable computation we (and ACE) don't.
Diagnostic harness now lives at tools/InspectCoatTex/Program.cs;
GameWindow's DUMP_CLOTHING also probes runtime SubPalette dat sizes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-of-session cleanup of the 2026-05-03 remote-motion debug session.
Documentation:
- CLAUDE.md: add ⚠️ DO-NOT-ENABLE warning for ACDREAM_INTERP_MANAGER=1
in the diagnostic env-vars list. Add an "Outbound motion wire format"
section documenting acdream's WalkForward+HoldKey.Run encoding (which
ACE auto-upgrades to RunForward on relay) so future sessions don't
re-derive it.
- docs/ISSUES.md: file two new issues:
* #39 — Run↔Walk cycle transition not visible on observed
retail-driven player remotes when watched from acdream. Root cause
located: ApplyServerControlledVelocityCycle is gated by
IsPlayerGuid, excluding the exact case where ACE doesn't broadcast
a UM (shift toggle while direction key held). Fix sketch ~10
lines, separate commit. Cross-references the four-agent
investigation prompt.
* #40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed. Documents
why (e94e791 conflated MoveOrTeleport with update_object), the
visible symptoms (staircase Z, position blips), and why
Commit B (039149a)'s ResolveWithTransition port was insufficient
(env-var path also clears body.Velocity → no horizontal Euler
motion → sweep input is queue catch-up only, which stair-steps).
Fix path = separate L.3 follow-up to re-integrate PositionManager
additively on top of the legacy chain.
Code:
- GameWindow.cs:6042: prepend a ⚠️ REGRESSED warning block at the top
of the env-var per-frame branch so anyone reading the code is
immediately aware not to enable it. Cross-references ISSUES.md #40.
- AnimationSequencer.cs: re-throttle [SCFAST]/[SCFULL] diagnostics to
0.5s per instance (rolled back from A.1's unthrottled experiment).
Already served its purpose; throttled is enough for steady-state
diagnostics. Restore _lastSetCycleDiagTime field.
No behavior change for any current launch (env-var unset = legacy
path unchanged). Build green; baseline test failures (8) unchanged
from prior commit, none introduced by this session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restores per-frame collision/terrain sweep that was DROPPED by e94e791
(L.3.1+L.3.2 Task 3) when the ACDREAM_INTERP_MANAGER=1 path replaced
the per-tick logic with a stripped-down version intended to mirror
retail's CPhysicsObj::MoveOrTeleport.
That was a category error: MoveOrTeleport (acclient @ 0x00516330) is
the *network packet handler* entry point — minimal work. The per-frame
physics tick is retail's update_object (FUN_00515020) — full chain
including FUN_005148A0 Transition::FindTransitionalPosition (the
collision sweep). The legacy (env-var off) path mirrors update_object
correctly; the env-var path was missing this single step.
Symptoms that map directly to the missing sweep:
- "Staircase" Z drift on slopes (horizontal Euler motion sinks into
rising ground until the next UP pops it up). User-confirmed for
BOTH retail-driven AND acdream-driven remotes when observed from
acdream.
- Position blips during steady-state motion (predicted XY drifts
unconstrained between UPs, then UP hard-snaps).
Fix: copy the legacy path's "Step 4: collision sweep" block (lines
~6483-6569) into the env-var per-frame branch, between
UpdatePhysicsInternal and the existing landing fallback. Includes
post-resolve landing detection (K-fix15 + K-fix17 mirror) so airborne
remotes correctly transition back to grounded after the sweep clamps
them to a walkable surface.
Sphere dims match the legacy path verbatim (0.48m radius, 1.2m height,
0.4m step-up/down, EdgeSlide moverFlags) — retail human-scale, already
proven via the legacy path before the e94e791 regression.
Does NOT address the separate Run↔Walk cycle bug (different root
cause: missing velocity-derived cycle inference for player remotes).
That's a follow-up commit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Commit A's log refuted H2 (UPCYCLE never fires for player guids — gated
by IsPlayerGuid), H4 (SCNULLFALLBACK count = 0), H5 (PartTemplate
counts always match anim PartFrames). The remaining puzzle:
SCFULL Ready=23 dominates (all motions: 41 total)
SETCYCLE picker logged: only 9 transitions to Ready
Difference (≥14 extra Ready full-rebuilds) suggests a non-picker source,
OR many UMs arriving with no ForwardCommand bit being routed through
the picker's `else if (!command.HasValue) { fullMotion = Ready; }` at
GameWindow.cs:2671-2673, knocking the cycle back to Ready mid-Walk/Run.
This commit removes the 0.5s throttle on SCFAST and SCFULL (every call
now logs) and adds [UM_RAW] at OnLiveMotionUpdated entry to print:
- stance / fwd / fwdSpd / side / turn / movementType / isMoveTo
- sequencer.CurrentMotion at call time
per UM, gated on ACDREAM_REMOTE_VEL_DIAG=1.
Combined: one repro pass tells us (a) UM arrival rate per remote, (b)
which UMs lack ForwardCommand, (c) whether the picker is the source of
the 14+ extra Ready calls. Commit B is then a one-line fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds five diagnostics, no behavior changes. All gated on existing
ACDREAM_REMOTE_VEL_DIAG=1 env var. Plan at
~/.claude/plans/yes-make-a-plan-parsed-axolotl.md.
Five hypotheses surviving from the four-agent investigation
(docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md):
H1 SEQSTATE silently swallowed by OMEGA_DIAG sharing throttle clock
H2 ApplyServerControlledVelocityCycle races UM-driven SetCycle per UP
H3 SetCycle fast-path returns without updating _currNode
H4 GetLink/GetCycle null → defensive fallback lands on stale head
H5 PartTemplate.Count diverges from anim PartFrames.Count → silent
identity-quat freeze
Diagnostics added (all log lines are grep-prefixed):
D1 Split LastSeqStateLogTime field for SEQSTATE — own throttle.
Foundational: every other diag depends on SEQSTATE telling truth.
D2 [UPCYCLE] inside ApplyServerControlledVelocityCycle, +
[UPCYCLE_SRC] at the call site (wire vs synth velocity).
D3 [SCFAST] in fast-path return, [SCFULL] at full-rebuild end.
D4 [SCNULLFALLBACK] in the null-data defensive fallback.
D5 [PARTSDIAG] with pt.Count / seqFrames.Count / setup.Parts.Count /
anim.PartFrames[0].Frames.Count + sum-of-components hash.
Repro recipe:
$env:ACDREAM_INTERP_MANAGER = "1"
$env:ACDREAM_REMOTE_VEL_DIAG = "1"
dotnet run … 2>&1 | Tee-Object tools/diag-logs/walkrun-<ts>.log
Then watch a retail-driven character through acdream and exercise:
idle → W run → release → shift+W walk → release → demote → promote →
run+turn (this last one is the H1 trap).
Decision matrix in the plan file maps each [TAG] signature to a
specific Commit B fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hand-off briefing for the remaining "observed retail char's leg cycle
doesn't visibly switch in acdream" bug. Captures everything we learned
today including:
- All 8 commits shipped today (turn-sign, observed-velocity revert,
retail-faithful tick, Commands-list SubState skip, currNode reset)
- Confirmed wins: body translation, run-in-circles, jump landing
position + animation, turn-left direction
- Confirmed remaining bug: walk/run/idle leg cycle on observed remotes
+ residual steady-state blippiness
- Diagnostic infrastructure (FWD_WIRE, CMD_LIST, HASCYCLE, SETCYCLE,
SEQSTATE, TURN_WIRE, OMEGA_DIAG, VEL_DIAG) and how to reproduce
- cdb live trace findings (retail uses additive add_to_queue with no
truncate; we have ClearCyclicTail + rebuild)
- Six concrete next-step hypotheses
- A self-contained prompt for the next research agent
- Notes on rejected approaches (link-skip, full-reset, scaling hack)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skip SubState commands in UM Commands list iteration
Two related fixes for the "remote-driven character animation cycle
does not visibly switch" bug:
1. AnimationSequencer.SetCycle now snapshots _queue.Last BEFORE
appending the new link/cycle nodes, then forces _currNode onto
preEnqueueTail.Next (= first newly-added node). Without this,
_currNode could stay pointing into stale non-cyclic head frames
left over from the previous cycle (typically a Walk_link or
Ready_link's tail), and the visible animation continues playing
those stale frames before the queue advances naturally to the
new cycle. Local player avoided the bug because
PlayerMovementController fires SetCycle in a tight per-input loop
that keeps the queue clean; remote player accumulates stale
link drains across many bundled UMs.
2. OnLiveMotionUpdated's UM Commands list iteration now skips
SubState class commands (high byte 0x40-0x4F like Ready
0x41000003). The router's SetCycle call for those would silently
override the animCycle picker's own SetCycle a few lines above
in the same UM packet — verified via SETCYCLE diag captures
showing run/walk being immediately re-cycled to Ready. Only
Action / Modifier / ChatEmote class commands (overlays that
interleave with the cycle) belong in this list iteration.
This fixed the landing-from-jump animation issue (user-confirmed:
"landing now works"). Walk↔run direct transitions still don't
visibly switch the leg cycle for observed retail-driven characters
even though ae.Sequencer.CurrentMotion correctly transitions
(per-tick SEQSTATE diag added — proves the sequencer's logical
state holds the right motion). Bug is somewhere downstream of
SetCycle's queue/state setup, possibly in Advance/BuildBlendedFrame
or in how seqFrames are applied to MeshRefs for remote entities.
Filed for next investigation.
Adds env-var-gated diagnostics (ACDREAM_REMOTE_VEL_DIAG=1):
CMD_LIST — what's in the UM's Commands list at receive time
HASCYCLE — whether the requested cycle exists in the dat
SEQSTATE — per-tick sequencer.CurrentMotion + CurrentSpeedMod
for the observed retail char (1Hz throttled)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both changes were too aggressive:
1. Full queue reset on locomotion-locomotion transitions (c06b6c5)
— turned out the user's tests went through Ready (no direct
walk↔run transitions in the wire), so the fix never fired
and didn't address the actual bug.
2. Unconditional skip of every transition link
— killed ALL transition animations across the board (jump
landing, run-to-stop, sit-down, lie-down, etc.) for every
entity, not just the locomotion-locomotion case. User
correctly identified this as a much bigger regression.
Sequencer is back to pre-c06b6c5 baseline: ClearCyclicTail-only
on motion change, transition link enqueued normally. The
walk↔run-direct-transition issue (and the broader
remote-only-doesn't-update issue) remains open and requires a
different approach.
Confirmed regression isolation: local +Acdream's transitions in
acdream client work (visible legs switch correctly), and acdream
chars observed from a parallel retail client also have working
transitions. The bug is specifically when acdream observes a
RETAIL-driven character — somewhere in the inbound
UpdateMotion → animCycle picker → SetCycle path, the visible
cycle update is being lost. Filed for separate investigation.
Adds an env-var-gated HASCYCLE diagnostic in OnLiveMotionUpdated
that confirmed cycle resolution succeeds (HasCycle=True for both
RunForward 0x44000007 and WalkForward 0x45000005 on style
0x8000003D), so the bug isn't in MotionTable cycle lookup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When AnimationSequencer.SetCycle transitions between forward-locomotion
cycles (Walk↔Run, Walk↔WalkBackward, etc.) — i.e. when both old and new
motion's low byte is in {0x05 WalkForward, 0x06 WalkBackward, 0x07
RunForward} — do a full queue drain + _currNode/_firstCyclic reset
(matching the existing skipTransitionLink branch) instead of just
ClearCyclicTail. Without this, _currNode is left pointing into the
previous cycle's non-cyclic head (link frames from the prior Ready→walk
transition), and the visible legs continue playing those head frames
before reaching the new run cycle.
Investigation findings (cdb live trace of retail at
tools/cdb-scripts/walk_run_motion_trace.log):
Retail's actual approach is "additive add_to_queue with no truncate" —
MotionTableManager handles the natural progression via per-tick
CheckForCompletedMotions / remove_redundant_links cleanup. Acdream
doesn't have that machinery, so this fix is the closest viable
emulation: force the queue back to a clean state and rebuild from
scratch on the locomotion-cycle transition.
User-reported symptom this addresses (walk→run direct transition,
release shift while W held): visible animation cycle did not switch
until next motion event. Verified via FWD_WIRE + SETCYCLE diags that
both ACE and our SetCycle are firing correctly on the transition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live cdb trace of retail acclient.exe (v11.4186, PDB-matched) capturing
the exact function call sequence for a direct walk-to-run motion
transition where the user holds shift+W (walk) then releases SHIFT
while still holding W (transition to run).
Trace bps on:
- CPhysicsObj::DoInterpretedMotion (0x0050EA70)
- CPartArray::DoInterpretedMotion (0x00518750)
- MotionTableManager::PerformMovement (0x0051C0B0)
- MotionTableManager::add_to_queue (0x0051BFE0)
- MotionTableManager::truncate_animation_list (0x0051BCA0)
- CMotionTable::DoObjectMotion (0x00523E90)
- CMotionTable::StopObjectMotion (0x00523EC0)
Captured trace at tools/cdb-scripts/walk_run_motion_trace.log shows
the precise walk-to-run sequence:
[79] CPhysicsObj::DoInterpretedMotion: motion=45000005 walk start
[82] CMotionTable::DoObjectMotion: motion=45000005
[83] MotionTableManager::add_to_queue: arg1=45000005 arg2=00000001
[89] CPhysicsObj::DoInterpretedMotion: motion=44000007 run start
[92] CMotionTable::DoObjectMotion: motion=44000007
[93] MotionTableManager::add_to_queue: arg1=44000007 arg2=00000001
[104] CMotionTable::StopObjectMotion: motion=44000007 run end
Critical structural finding for #L.4-walk-run:
Retail does NOT call truncate_animation_list during the walk→run
transition. truncate_animation_list never fires in the entire 200-hit
trace. Retail also does NOT call StopObjectMotion(WalkForward) before
add_to_queue(RunForward). Retail just appends the new motion to the
queue and lets MotionTableManager (and its CheckForCompletedMotions /
remove_redundant_links per-tick cleanup, not yet traced) handle the
natural progression.
acdream's AnimationSequencer.SetCycle aggressively calls
ClearCyclicTail() at line 430 BEFORE enqueuing the new cycle, which
destroys the in-flight walk cycle's frames. The new run cycle is
enqueued but _currNode is left in a state that doesn't smoothly
continue — visible to the user as "it just blips forward walking,
AS SOON as press another key like turning, its starts running"
(the next motion event re-fires SetCycle which finally aligns state).
Fix is a structural refactor of SetCycle to mirror retail's
"additive queue with auto-cleanup" semantics. Out of scope for this
research commit; filed as #L.4 in the next ISSUES.md entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the hybrid that double-counted forward translation:
predicted body.Velocity (set per-tick by apply_current_movement) +
the seqVel-derived offset both pushed the remote body forward at
~11.7 m/s × dt for run, summing to ~23.4 m/s × dt — the user's
"way too fast" + 1-Hz blip.
Per the named-retail decomp investigation 2026-05-03 (research agent
report dispatched against acclient_2013_pseudo_c.txt for
CSequence::update + UpdatePositionInternal + UpdateObjectInternal +
adjust_offset, line citations in the env-var path comments):
CPhysicsObj::UpdateObjectInternal (0x005156b0)
→ UpdatePositionInternal (0x00512c30)
→ CPartArray::Update (writes anim root motion into the offset frame)
→ PositionManager::adjust_offset (REPLACES the offset with catch-up
when the body is far from the queue head; otherwise leaves the
anim root motion alone — Frame::operator=(arg2, &__return)
semantics, NOT additive)
→ Frame::combine (out = m_position + offset)
→ UpdatePhysicsInternal (out += body.Velocity × dt + 0.5·accel·dt²)
For a remote in steady-state RunForward where the server hasn't pushed
an explicit velocity, m_velocityVector ≈ 0 and ALL per-tick translation
comes from the animation root motion (CSequence::update_internal +
Frame::combine of crossed pos_frames keyframes). Our port doesn't
extract per-keyframe pos_frames from the .anm assets; instead
AnimationSequencer.CurrentVelocity is the synthesized equivalent
(RunAnimSpeed × ForwardSpeed averaged), passed through
PositionManager.ComputeOffset.
Concrete changes in the env-var (ACDREAM_INTERP_MANAGER=1) path:
* Pass seqVel = ae.Sequencer.CurrentVelocity to ComputeOffset (was
Vector3.Zero — that disabled the animation-root-motion source and
left only the queue catch-up to drive translation, which lagged
server pace).
* Clear rm.Body.Velocity to Vector3.Zero for grounded remotes each
tick. Mirrors retail's m_velocityVector ≈ 0 for remotes; prevents
UpdatePhysicsInternal from adding a second 11.7 m/s × dt on top of
the seqVel-driven translation.
* Stop calling apply_current_movement per tick. Retail only calls it
on motion-state changes (per cdb traces from the L.5 investigation),
not per physics tick. body.Velocity-based translation is now the
AIRBORNE-only path (gravity integration during jumps).
Also reverts an unacceptable "scaling hack" (per-tick body.Velocity
scaled by observed serverSpeed/predictedSpeed) the user explicitly
rejected as patching over an unsolved structural problem.
GetMaxSpeed reverted to RunAnimSpeed × rate (matches ACE
MotionInterp.cs:670-678; the earlier "return bare rate" change came
from a misread of an x87-decompiled get_max_speed where Binary Ninja
showed the return type as void).
AnimationSequencer.SetCycle now ALWAYS overwrites CurrentVelocity for
known locomotion cycles (Walk/WalkBackward/Run/SideStepRight/
SideStepLeft) instead of gating on `CurrentVelocity.LengthSquared() <
1e-9f`. The gate was correct for non-locomotion entities with
dat-baked HasVelocity, but for Humanoid where the dat is silent and
the only thing that could set CurrentVelocity before synthesis was a
transition link's HasVelocity flag, the gate would silently leave the
body advancing at the link's velocity instead of the cycle's intended
steady-state.
Adds wire-arrival diagnostics gated on ACDREAM_REMOTE_VEL_DIAG=1
(SETCYCLE, FWD_WIRE) used to trace the bug to ground truth.
User-confirmed improvements vs prior state:
- Steady-state run no longer "way too fast"
- Run-in-circles smoother (rectangle effect gone)
- Jump landing in correct location
- Turn-left visibly turns left
Outstanding (not addressed by this commit, deferred for next
investigation): walk↔run direct transitions don't visibly switch the
animation cycle until the next motion event fires. Both legacy and
new paths exhibit the same behavior, so the bug lives in the
SetCycle queue manipulation pipeline shared by both — not in the
per-tick translation path that this commit revises. Wire trace
confirms ACE delivers the WalkForward → RunForward transition
correctly and SetCycle does fire for it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Multi-bug fix for the env-var-gated retail-faithful remote tick path
(ACDREAM_INTERP_MANAGER=1). Combines four previously-stacked defects
into one coherent rewrite:
1. PositionManager.ComputeOffset was additive (rootMotion + correction).
Retail's PositionManager::adjust_offset (acclient @ 0x00555190 →
InterpolationManager::adjust_offset @ 0x00555d30) REPLACES the
offset frame via Frame::operator=(arg2, &__return) when catch-up
engages — it does NOT add to the rootOffset that CPartArray::Update
wrote. Switched to "correction overrides root motion" semantics.
2. MotionInterpreter.GetMaxSpeed was returning RunAnimSpeed × rate
(~11.7 m/s for run skill 200). The retail decomp at
acclient_2013_pseudo_c.txt:305127 shows get_max_speed returns the
bare run rate (~2.94) — the function's float return rides the x87
FPU stack, which Binary Ninja shows as void. Caller multiplies by
2.0 to get the catch-up speed. With the wrong return our catch-up
was 23.5 m/s instead of retail's 5.88 m/s — the queue would walk
the body 4× too aggressively.
3. The env-var TickAnimations branch was DOUBLE-COUNTING forward
translation: it applied seqVel × dt via PositionManager.ComputeOffset
AND let UpdatePhysicsInternal advance body.Position += body.Velocity
× dt. Both were ~11.7 m/s for run, so body raced at 23.4 m/s —
"way too fast" per the user. Pass seqVel=Vector3.Zero to
ComputeOffset; let body.Velocity (refreshed per tick by
apply_current_movement) drive the bulk translation alone.
4. Body orientation only applied sequencer.CurrentOmega per tick. For
the running-in-circles case ACE broadcasts ForwardCommand=RunForward
AND TurnCommand=TurnLeft on the same UpdateMotion; the sequencer
picks the RunForward cycle whose synthesized CurrentOmega is zero,
so body never rotated between UPs and body.Velocity stayed in an
out-of-date world direction — the visible "rectangle when running
circles" effect. Prefer ObservedOmega (set explicitly in
OnLiveMotionUpdated from the wire's TurnCommand + signed TurnSpeed)
when present; fall back to seqOmega for standalone turn cycles.
Also adds:
- Sequencer-reset call in the env-var landing-fallback so the legs
un-fold from Falling on land (mirrors the legacy K-fix17 path).
- LastServerZ now only updates on IsGrounded UPs, so the per-tick
landing-fallback floor doesn't drift up to the player's airborne
peak Z and force-land mid-arc — fixes the user-reported "small
landing in the air before landing on the ground" when jumping
while moving.
- VEL_DIAG now samples at UP arrival with overlapping windows, plus
TURN_WIRE / OMEGA_DIAG / FWD_WIRE diagnostics gated on
ACDREAM_REMOTE_VEL_DIAG=1 used to trace these bugs to ground truth.
Verified via live retail-driven character observation 2026-05-03:
turn-left now rotates left (was animating right with snap), running
in circles is much smoother, jumping lands on ground (no mid-air
pause). Residual ~20% steady-state overshoot for walk remains —
WalkAnimSpeed=3.12 (decompiled retail constant) doesn't match ACE's
actual broadcast walk pace (~2.6 m/s). Tracked separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The wire-arrival animCycle picker in OnLiveMotionUpdated was passing
MathF.Abs(turnSpeed) to the sequencer, stripping the sign that ACE uses
to encode TurnLeft. Confirmed via live wire trace 2026-05-03: TurnLeft
input from a retail-driven character arrives as
turnCmd16=0x000D (TurnRight), TurnSpeed=-1.500 — mirroring retail's
adjust_motion convention on the wire. With Abs, both directions
collapsed onto motion=TurnRight + speedMod=+1.5, and the synthesize-
omega path computed -2.25 (CW = right) for both. Visible symptom:
TurnLeft animated as TurnRight then blipped to the correct facing on
the next UpdatePosition.
Pass the signed speed through unchanged. The sequencer's negative-
speed path (EnqueueMotionData multiplies MotionData.Omega by speedMod;
the synthesize-omega fallback uses -(pi/2)*adjustedSpeed) produces the
correct CCW omega for TurnLeft now that the sign survives.
Also adds a TURN_WIRE diagnostic gated on ACDREAM_REMOTE_VEL_DIAG=1
that prints every wire-arrived TurnCommand with reconstructed enum
and signed speed, plus splits the OMEGA_DIAG throttle off
LastVelDiagLogTime onto its own LastOmegaDiagLogTime so the two
diagnostics don't starve each other.
Verified with the same trace: TURN_WIRE speed=-1.500 -> OMEGA_DIAG
Z=+2.250 (CCW = TurnLeft), TURN_WIRE speed=+1.500 -> OMEGA_DIAG
Z=-2.250 (CW = TurnRight). Both directions now have correct sign.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Option-A patches addressing visual issues from the L.3.1+L.3.2
remote-entity motion path (gated by ACDREAM_INTERP_MANAGER=1):
1. Landing fallback. ACE doesn't always send IsGrounded=true on the
landing frame, so airborne remotes kept falling under gravity and
visually "disappeared into the ground" until the next non-stop UP
forced a re-snap. Track the most recent server-broadcast Z on every
UP (including mid-arc airborne ones) and, in TickAnimations, snap
the body back up + clear airborne when its predicted Z drops more
than 0.5 m below that floor.
2. TurnLeft omega sign. The synthesize-omega fallback in
AnimationSequencer (used when MotionData ships without HasOmega)
had case 0x0E using zomega = +(pi/2) * adjustedSpeed, but
adjust_motion above already remapped 0x0E to 0x0D with
adjustedSpeed = -speedMod. The double-negate produced -Z (clockwise
= right) for both turn directions, matching the reported "turning
left animates as turning right". Use the same -(pi/2) * adjustedSpeed
formula as case 0x0D so the negation lands the result on +Z (CCW).
3. Velocity diagnostic. New env var ACDREAM_REMOTE_VEL_DIAG=1 prints
one line per moving remote per ~2 seconds comparing the sequencer's
CurrentVelocity to the server's effective broadcast pace
((LastServerPos - PrevServerPos) / dt). Lets us measure the
speed-overshoot ratio that produces the residual 1-Hz blippiness
before tuning a fix.
Refs Phase L.3.1+L.3.2 spec at
docs/superpowers/specs/2026-05-02-l3-remote-entity-motion-design.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Visual verification (Task 4) revealed two missing pieces from the
retail per-frame tick port (acclient!CPhysicsObj::update_object
@ 0x00513730):
1. body.calc_acceleration() must run BEFORE UpdatePhysicsInternal so
gravity (set via PhysicsStateFlags.Gravity in OnLiveVectorUpdated)
actually decays jump velocity. Without it body.Acceleration stays
stale or zero → endless rise on jumps.
2. sequencer.CurrentOmega must be applied to body.Orientation per frame.
Retail's TurnRight/TurnLeft cycles bake angular velocity that drives
smooth rotation between UPs; we were only snapping orientation on
UP receipt (~1 Hz), producing visible chop on turning remotes.
Both fixes are part of the retail tick we already started porting in
PositionManager — just missing pieces.
Speed-overshoot bug (sequencer.CurrentVelocity > server's actual
broadcast pace) is still being investigated in a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PositionFlags.IsGrounded (0x04) was already parsed by UpdatePosition
but not exposed through the Parsed record or EntityPositionUpdate.
Adds the bool field to both records so OnLivePositionUpdated can
consume it for retail-faithful MoveOrTeleport routing
(acclient @ 0x00516330: has_contact=false → no-op during airborne arc).
Consumed in subsequent task (L.3.1+L.3.2 Task 3).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Visual verification of L.3.1-as-originally-scoped (commit ae79e34
through e08accf) revealed that InterpolationManager corrections alone
cannot produce smooth motion — retail also relies on animation root
motion (the L.3.2 PositionManager work, originally deferred). The two
halves are functionally inseparable.
Spec changes:
- L.3.1 sub-lane absorbs L.3.2's PositionManager
- New section: PositionManager architecture (pure-function ComputeOffset
returning Vector3 delta; combines body-local seqVel * dt rotated to
world + InterpolationManager.AdjustOffset correction)
- New section: IsGrounded plumbing through EntityPositionUpdate (the
PositionFlags.IsGrounded=0x04 is already parsed; just expose it)
- New section: retail-faithful jump pipeline (airborne → no-op per
MoveOrTeleport's has_contact=0 semantics; landing detected via first
IsGrounded=true UP after airborne)
- Acceptance criteria updated for combined scope
- Implementation order: 6 commits remaining (after the revert at 1641d6e)
- Stall-blip TAIL annotation (Task 0 resolution) folded in
L.3.3 (MoveToManager) stays a separate sub-lane.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Round 1 (5154a3e) tried to fix:
- heading locked → orientation snap-on-receipt (good idea)
- endless jump → landing detector via UP-with-zero-velocity (didn't work; ACE sends non-zero velocity through arc)
Round 2 (f199a6a) tried to fix:
- chop at 1 Hz → seed body.Velocity from update.Velocity for between-UP extrapolation (didn't help)
- endless jump → reported-Z-near-body-Z + falling-velocity heuristic (didn't catch reliably)
The actual problem was scoping: L.3.1's "InterpolationManager only" cannot
produce smooth motion. Retail combines animation root motion (L.3.2 /
PositionManager) + InterpolationManager corrections. Both halves are
required for "remotes look smooth".
Reverting to e08accf (Task 6 — VectorUpdate.Omega). The next commits
will properly port PositionManager + plumb IsGrounded through the wire
parser, replacing L.3.1-only with L.3.1+L.3.2 combined per the
revised spec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Round 2 fix for two visual bugs that survived commit 5154a3e:
Bug 1 (chop at 1 Hz UP cadence): Round 1 zeroed body.Velocity each
tick on grounded remotes, leaving AdjustOffset as the sole motion
source. AdjustOffset catches up in ~150 ms then sits idle until the
next UP at 1 Hz, producing visible "updates every 1 second" stepping.
Root cause: retail achieves smoothness via animation root motion +
AdjustOffset *corrections*; we only ported corrections (root motion
is Phase L.3.2 / PositionManager). Workaround for L.3.1: seed
body.Velocity from update.Velocity on every grounded UP so
UpdatePhysicsInternal integrates position += vel*dt between UPs,
with the queue providing corrective patches via AdjustOffset.
Bug 2 (endless jump): Round 1 tried to detect landing via "UP arrives
during airborne with no velocity" but ACE keeps sending non-zero
velocity through the arc, so the detector never fired. Fix: stop
maintaining a local "predicted arc". Server is authoritative for
airborne position too -- hard-snap from each UP during airborne;
body.Velocity (set by OnLiveVectorUpdated) integrates between UPs
for smoothing. Landing detected via reported-Z-near-body-Z + falling/
settled velocity heuristic (more reliable than the velocity-zero
test).
Per-frame tick: removed the !rm.Airborne velocity clamp from Round 1.
OnLivePositionUpdated now owns velocity policy; per-tick just
integrates whatever is set.
Both deviations from retail decomp are documented in source comments
and slated for L.3.2 (PositionManager) cleanup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Visual verification (Task 7) revealed two bugs in the new env-var
gated path:
1. Heading locked at login direction. Cause: AdjustOffset returns
position delta only; the dist≤96 enqueue branch never updated
body.Orientation. Fix: apply orientation unconditionally on every
UpdatePosition (snap-on-receipt). Position lerps via queue.
2. Endless jumping. Cause: (a) body.Velocity persisted forever
after arc landed because apply_current_movement no longer ran;
(b) UpdatePositions during the arc were enqueued, fighting the
gravity sim. Fix: skip enqueue when rm.Airborne (mirrors retail
MoveOrTeleport has_contact=false → no-op); zero non-airborne
body.Velocity each tick (mirrors legacy apply_current_movement);
detect landed when receiving UpdatePosition while airborne with
no/zero velocity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>