Two linked issues both rooted in skipping parts of the retail physics chain.
## 1. Remote staircase on slopes — Euler never integrated between UPs
TickAnimations called rm.Body.update_object(now) for remote integration, but
PhysicsBody.update_object gates on MinQuantum = 1/30s (retail FUN_00515020
early-return). At our 60fps render tick (~16 ms), deltaTime < MinQuantum on
almost every frame → early return AND LastUpdateTime never advances → position
effectively never integrates. Remote Position changed only on UP hard-snap,
producing visible teleport strides uphill (the "staircase" the user reported).
Fix: call UpdatePhysicsInternal(dt) directly for the remote tick — the same
pattern PlayerMovementController.cs:358 uses for the local player. Wire
ResolveWithTransition in afterwards so the remote's Euler-advanced position
gets swept through the same retail collision chain (find_env_collisions +
find_obj_collisions + step_down + 6-path BSP dispatcher) that the local
player already goes through.
New field RemoteMotion.CellId tracks the remote's cell across frames; set
from UpdatePosition.p.LandblockId and updated from transition output.
## 2. Local player floating on downhill slopes — ContactPlane not persisted
Running a character down a slope faster than ~0.5 m/s vertical: per-frame
Euler moves feet horizontally (no Z component since velocity is world-XY).
After Euler, feet are above the new-XY terrain. ValidateWalkable takes the
"above surface" branch without setting a contact plane, DoStepDown probes
~4 cm down (the retail StepDownHeight default), fails to find the surface
8-10 cm below, and the character stays at the old Z. Over a sustained
descent this accumulates into a visible hover.
Retail's PhysicsObj carries ContactPlane + ContactPlaneCellID as persistent
fields (ACE PhysicsObj.cs:2598-2604 get_object_info → InitContactPlane).
Each transition call seeds CollisionInfo.ContactPlane from the previous
frame's plane. That seed is what lets AdjustOffset project horizontal
velocity onto the slope surface — so the Euler offset acquires a Z
component matching the slope and the sphere tracks terrain without needing
step-down to do the catch-up every frame.
Fix: add PhysicsBody.ContactPlane* fields mirroring PhysicsObj's. Extend
ResolveWithTransition with an optional `body` parameter; when provided, seed
the transition's CollisionInfo from body.ContactPlane at the start, copy
back (preferring current, falling back to LastKnown) at the end. Both local
(PlayerMovementController) and remote (TickAnimations) pass their body.
Verified live: DIAG samples showed pre/post/resolved Z all exactly equal
before the MinQuantum bypass (Euler frozen). After bypass, deltas dropped
to floating-point noise on slopes for remotes. Local hover on downhill
resolved in separate visual pass.
All 717 tests green. No API breaks (ResolveWithTransition's body param is
optional, backwards-compatible).
Cross-refs:
- decompile: FUN_00515020 update_object, FUN_005111D0 UpdatePhysicsInternal,
FUN_005148A0 transition init
- ACE: PhysicsObj.cs:2586-2621 get_object_info, Transition.cs:613-620 InitContactPlane
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the motion/animation pipeline:
1. Player's local animation was getting reset to speedMod=1.0 every ~100ms.
ACE's BroadcastMovement echoes the player's own motion back via
UpdateMotion. When ACE's ForwardSpeed == 1.0, the ForwardSpeed flag is
omitted (InterpretedMotionState.BuildMovementFlags), so our wire parser
returns null and we default to speedMod=1.0 — clobbering the
locally-authoritative 2.375 × runRate that UpdatePlayerAnimation just
set. Legs would crank up to full cadence for one frame then get slammed
back to walking rate.
Fix: for the player's own guid, skip the wire-echo SetCycle entirely.
UpdatePlayerAnimation is the authoritative driver for the local
player's animation; the server echo is only useful for observers of
other characters. User-confirmed: legs now hold their full cadence.
2. Remote entities teleported between UpdatePositions because the
sequencer's CurrentVelocity was always zero (Humanoid dat ships every
locomotion MotionData with Flags=0x00, so EnqueueMotionData leaves
CurrentVelocity at Vector3.Zero). Dead-reckoning's Priority 1
(sequencer velocity) never triggered, falling through to EMA which
has bootstrap lag + gets polluted by teleport-class server snaps.
Fix: synthesize CurrentVelocity in SetCycle from the retail locomotion
constants (WalkAnimSpeed=3.12, RunAnimSpeed=4.0, SidestepAnimSpeed=1.25)
× speedMod, matching the decompiled get_state_velocity (FUN_00528960)
which uses these same constants directly instead of MotionData.Velocity.
The dat's HasVelocity field is reserved for non-locomotion motions
(kick-off velocities, flying creatures, etc).
Diag confirmed synthesis fires and DR picks it up with src=seq and
correct magnitude. More visual polish may still be needed for the
"lagging remote" symptom — see follow-up.
Also adds `PlayerMovementController.BodyVelocity` utility getter for HUD/
debug use, and `ACDREAM_ANIM_SPEED_SCALE` env var as a tunable knob for
visual pacing overrides.
All 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The decompiled get_state_velocity (FUN_00528960) literally computes
`RunAnimSpeed * ForwardSpeed` — a 4.0 × runRate world velocity. That
matches retail only when the character's MotionTable happens to bake
MotionData.Velocity.Y = 4.0 on RunForward (true for Humanoid, not
necessarily for other creatures or swapped weapon-style cycles).
When MotionData.Velocity ≠ RunAnimSpeed, the body's world velocity
drifts away from the animation's baked-in root-motion velocity, and
you see the classic "legs cycle too slowly for how fast the body is
sliding" visual bug. User reports ~30% discrepancy ("running animation
is too slow"), consistent with Humanoid RunForward's actual dat
Velocity being ~3.0 rather than the 4.0 constant.
The fix per r03 §1.3: physics body velocity = MotionData.Velocity ×
speedMod. That's exactly what AnimationSequencer.CurrentVelocity
already exposes. Route it into MotionInterpreter via an opt-in
Func<Vector3> accessor. When wired, get_state_velocity uses the
sequencer's cycle velocity as the primary forward-axis drive; when
unwired (tests, physics bodies without a sequencer), falls back to
the decompiled constant path — byte-compatible with retail on the
shapes where it actually matters.
The RunAnimSpeed × rate max-speed clamp at the bottom of
FUN_00528960 stays intact — Option B only replaces the *drive*, not
the clamp. 20 m/s phantom MotionData can't teleport the player.
Wiring: GameWindow attaches `playerAE.Sequencer.CurrentVelocity` to
`_playerController` on Tab-player-mode entry. The sequencer is always
built before the player enters chase mode, so timing is safe.
Sidestep continues to use SidestepAnimSpeed — the sequencer only
tracks the current forward cycle, so strafe is a separate axis.
6 new MotionInterpreterTests verify: accessor overrides constant path,
zero Y falls back to constant (link transitions), clamp still applies,
Ready state doesn't leak accessor value, sidestep axis is untouched.
All 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug 1: remote chars never animate, just teleport.
Root cause: when OnLiveMotionUpdated transitions a remote entity from
Ready to a locomotion cycle (cmd=0x0007 RunForward), the
_remoteLastMove timestamp is still pegged to the last position update
from BEFORE the motion change (often >300ms old). On the very next
TickAnimations, stop-detection signal 1 immediately fires
(now - last.Time > 300ms), and the sequencer is flipped straight back
to Ready. Result: the run cycle flashes for one frame and is gone.
Fix: when we enter a locomotion cycle from a non-locomotion one, stamp
_remoteLastMove[guid].Time = now and drState.LastServerPosTime = now
so the stop-timer starts a fresh 300ms window from the transition.
Bug 2 + 3: Our own player's walk/run toggle not broadcast when only
Shift toggles mid-move.
Root cause: PlayerMovementController's motion-state-change detection
compared only (ForwardCommand, SidestepCommand, TurnCommand). When
the user walks (W) then adds Shift mid-stride, ForwardCommand stays
WalkForward but outForwardSpeed jumps 1.0 -> runRate and localAnimCmd
swaps Walk -> Run. 'changed' stayed false, no MoveToState broadcast,
server still thought we were walking. Retail observers saw walking.
Fix: extend the diff to include outForwardSpeed, input.Run (hold-key),
and localAnimCmd. Any of them flipping forces a new MoveToState.
Bug 4: Wrong MotionCommand class byte reconstruction.
Root cause: OnLiveMotionUpdated's heuristic OR'd the sequencer's
current-motion class byte with the wire-received low 16 bits, producing
values like 0x41000007 for RunForward (actual retail value is
0x44000007). Cycle key lookup uses only low 24 bits so the animation
mostly-worked, but the wrong class byte broke stance-aware code paths
and any downstream consumer that keys off the class.
Fix: route ForwardCommand through MotionCommandResolver.ReconstructFullCommand
(same path already used for Commands[] items) — retail-exact class
byte recovery via a reflection-built enum lookup table.
Build + 711 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three separate fixes landed today, each addressing a specific bug the
user observed during live play:
1. NPC clothing changes by camera angle (InstancedMeshRenderer)
- Group key was (GfxObjId) only, so every humanoid NPC using the
same body mesh piled into one instance group; only the first
instance's texture was used for the entire DrawInstanced batch,
so which NPC's palette "won" changed as frustum culling and
iteration order shuffled entries.
- Now keyed by (GfxObjId, PaletteHash ^ SurfaceOverridesHash)
so only compatible instances batch; each unique appearance gets
its own draw call. Perf hit is small (humanoid NPCs each emit
one more draw call); visually every NPC is now stable.
2. GpuWorldState dedup on respawn
- Server re-sends CreateObject for the same guid on visibility
refresh / landblock crossing / appearance update. AppendLiveEntity
was blindly appending each time, so GpuWorldState accumulated
multiple copies of the same entity, each with its own
PaletteOverride / MeshRefs. That alone wasn't the clothing bug
(that was #1) but it would have caused other overlap problems
downstream.
- Added RemoveEntityByServerGuid + WorldGameState.RemoveById;
OnLiveEntitySpawnedLocked calls both before creating the new
entity so respawns replace cleanly.
3. Motion wire format — run animation sync with retail observers
- ACE's MovementData constructor only computes interpState.ForwardSpeed
on the WalkForward/WalkBackwards branch; every other ForwardCommand
falls into `else` and passes through WITHOUT speed set, giving
observers speed=0. Sending RunForward directly meant retail
clients saw us "run in place" while position drifted forward.
- Wire: always WalkForward + HoldKey.Run for running. ACE
auto-upgrades to RunForward with creature.GetRunRate() for
broadcast — correct command + correct speed at observers.
- Added per-axis FORWARD_HOLD_KEY / SIDE_STEP_HOLD_KEY /
TURN_HOLD_KEY so every active axis carries HoldKey.Run when
running (matches holtburger's build_motion_state_raw_motion_state).
- Added LocalAnimationCommand to MovementResult so our own
client still plays the RunForward cycle locally while the wire
stays WalkForward. Wire vs. local animation command are now
decoupled.
- Walk-backward wire command changed from WalkForward@-0.65 to
WalkBackward@1.0 (holtburger pattern).
- Strafe speed changed from 0.5 to 1.0 on wire AND local physics
(matches retail sidestep pace).
4. Jump height default + env-var tuning
- Default jumpSkill bumped from 100 → 200 (jump ≈ 3m at full
charge, closer to retail feel for a mid-level character).
- ACDREAM_RUN_SKILL and ACDREAM_JUMP_SKILL env vars now override
the defaults so the user can tune per-character until we parse
PlayerDescription and plumb real skill values through.
5. JustLanded signal on MovementResult
- Tracks airborne→grounded transition so future animation code
can fire the landing cycle when we land. Just a bool flag for
now — no consumer yet (the proper action-queue path will use it).
Not in this commit: jump animation itself. An earlier attempt to
SetCycle(Jump=0x2500003b) fed an Action-type motion into the SubState
cycle resolver, which produced a "torso" mis-render. Reverted. The
proper fix is porting the retail motion action-queue semantics into
AnimationSequencer — see docs/research/deepdives/r03-motion-animation.md
for the spec. That's the next session's work.
470 tests pass, build clean.
Replace simple Z-snap PhysicsEngine.Resolve with ResolveWithTransition
that uses the ported CTransition sphere-sweep pipeline. Movement is
subdivided into sphere-radius steps, terrain collision tested at each
step with step-down for ground contact maintenance.
Falls back to simple Resolve if transition fails. Player controller
now passes pre/post integration positions to the transition system.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build and send GameAction(Jump) with extent + world-space launch
velocity + sequence counters. Wire format from holtburger
JumpActionData::pack. Server can now validate and replicate jumps
to nearby clients.
Also compute RunRate locally via PlayerWeenie.InqRunRate when
running (server doesn't echo UpdateMotion ForwardSpeed to sender).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for jump physics:
- Skip ground-snap when velocity Z > 0 (prevents immediate re-landing
at high framerates where per-frame Z delta < 0.05 snap threshold)
- Guard apply_current_movement velocity write behind OnWalkable check
(prevents MotionInterpreter.DoMotion from zeroing jump velocity on
every frame while airborne)
- Guard PlayerMovementController velocity replacement behind OnWalkable
(preserves momentum during airborne flight)
Also fix run speed: compute RunRate locally via PlayerWeenie.InqRunRate
instead of waiting for server UpdateMotion echo (server doesn't echo
to sender). With Run skill 200, run speed is now 9.5 m/s instead of
4.0 m/s.
Strip all diagnostic logging from previous debug sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SmallVelocity threshold (0.25 m/s) in UpdatePhysicsInternal was zeroing
velocity every frame while airborne at the jump apex. With vel~0.01 m/s
and gravity adding only 0.012/frame, the zeroing won every frame and
the character got stuck at peak height forever.
Fix: only apply small-velocity zeroing when OnWalkable (grounded).
While airborne, gravity must accumulate freely through the zero-crossing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for jump physics:
- Skip ground-snap when velocity Z > 0 (prevents immediate re-landing
at high framerates where per-frame Z delta < 0.05 snap threshold)
- Guard apply_current_movement velocity write behind OnWalkable check
(prevents MotionInterpreter.DoMotion from zeroing jump velocity on
every frame while airborne)
- Guard PlayerMovementController velocity replacement behind OnWalkable
(preserves momentum during airborne flight)
Jump works locally but server packet not yet sent (BUG-002).
Facing direction mismatch logged as BUG-003.
RunRate not verified as BUG-004.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hold spacebar to charge (0→1 over 1s), release to jump. Height from
GetJumpHeight formula using Jump skill via PlayerWeenie. Jump physics
use MotionInterpreter.jump() → LeaveGround() → get_leave_ground_velocity().
JumpExtent is returned in MovementResult (non-null when jump fires this
frame) so GameWindow can log and eventually send the server jump packet.
Double-jump is prevented by jump_is_allowed() checking Contact+OnWalkable
flags before allowing another jump. Tests updated to use charge-then-release
pattern matching the new input model.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements IWeenieObject with GetRunRate and GetJumpHeight from
decompiled client, cross-referenced against ACE MovementSystem.
Default skills (Run=200, Jump=100) used until skill parsing ships.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse ForwardSpeed from UpdateMotion (0xF74C) InterpretedMotionState.
Feed server-echoed RunRate into the player's MotionInterpreter so
get_state_velocity produces the correct speed. Previously hardcoded
at 1.0 (4.0 m/s), now matches character's Run skill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the ad-hoc movement simulation with the ported retail physics:
- PlayerMovementController now owns a PhysicsBody (gravity, friction, Euler
integration with sub-stepping) and a MotionInterpreter (motion state machine,
speed constants from retail dat).
- Orientation quaternion is synced from Yaw each frame (Yaw=0 → +X, matching
the cos/sin convention the camera and outbound messages expect).
- Horizontal velocity is composed from MotionInterpreter.get_state_velocity()
speeds (WalkAnimSpeed=3.12, RunAnimSpeed=4.0, SidestepAnimSpeed=1.25 from
decompiled globals) then pushed via PhysicsBody.set_local_velocity so the
orientation quaternion rotates them into world space correctly.
- Vertical velocity (gravity / jump / fall) is snapshot before DoMotion calls
so apply_current_movement's set_local_velocity(0,0,0) can't clobber it.
- Jump delegates to MotionInterpreter.jump() + LeaveGround() which calls
get_leave_ground_velocity() → DefaultJumpVz=10.0 (retail value).
- PhysicsEngine.Resolve is still called each frame with zero delta to sample
terrain/cell Z under the body and set Contact+OnWalkable accordingly.
- Drive UpdatePhysicsInternal(dt) directly instead of update_object(wallClock)
to avoid the MinQuantum (~33ms) guard that would silently drop 60fps frames.
Test update: jump loop extended from 30→50 frames to cover the longer flight
time from retail DefaultJumpVz=10 (≈2.04s) vs old JumpImpulse=5 (≈1.02s).
303 tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ROOT CAUSE FIX for persistent slope Z clipping.
The SWtoNE/SEtoNW triangle boundary tests were swapped. AC's naming is
counter-intuitive: "SWtoNE cut" means BL and TR are the ISOLATED vertices
— the shared hypotenuse runs TL(0,1)→BR(1,0), so the dividing test is
tx+ty=1, NOT ty=tx. We had them backwards, causing every cell to sample
from the wrong triangle — up to 7.5 unit Z errors on steep terrain.
Fixed by cross-referencing WorldBuilder-ACME-Edition which has:
- ClientReference.cs: faithful C# port of decompiled AC client code
- TerrainConformanceTests.cs: verified against 25,600 cells
- TerrainGeometryGenerator.GetHeight(): matches the mesh index buffer
Also removes the slope gradient hack from PlayerMovementController —
no longer needed since SampleZ now returns exact triangle-surface Z.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The triangle-aware Z sampling from the previous commit produces exact
terrain-surface Z values, but a point-sampled Z on a tilted surface
places the character's center on the surface while their feet (which
extend horizontally) clip into the rising terrain ahead/behind.
Fix: in PlayerMovementController, sample Z 1 unit ahead in the walk
direction and add 40% of the gradient as an upward bias. This
compensates for the character's collision cylinder radius on slopes
while producing zero bias on flat ground. The bias is applied in the
movement controller (gameplay concern) not in TerrainSurface.SampleZ
(which stays exact for physics/tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fundamental terrain fixes based on the AC2D + holtburger deep dive:
1. Terrain split formula: replaced WorldBuilder's physics-path formula
(214614067/1813693831) with AC2D's render-path formula (0x0CCAC033,
0x421BE3BD, 0x6C1AC587, 0x519B8F25). The two produce different splits
for some cells. Since the render mesh uses this formula, the physics
Z sampler must match it to avoid misalignment on slopes.
2. Triangle-aware Z: replaced bilinear interpolation in TerrainSurface
with per-triangle barycentric interpolation. Each cell is split into
two triangles (using the same AC2D formula). SampleZ determines which
triangle the query point falls in, then interpolates within that
triangle. This produces Z values that exactly match the visual terrain
mesh — no more slope clipping.
Removes the multi-point Z sampling hack from PlayerMovementController
(no longer needed with exact triangle Z).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Slope clipping: replaced single foot-forward Z sample with 4-point
sampling (forward, back, left, right at 0.7 units). Takes the max Z
across all samples so both uphill and downhill slopes keep feet above
the terrain mesh surface. Removed the +0.1 Z bias entirely.
2. Player culling: replaced per-entity scan (alwaysVisibleEntityId) with
per-landblock skip (neverCullLandblockId). The player's current
landblock is computed from _playerController.Position and passed to
the renderer. Simpler, faster, and more reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for user-reported movement bugs:
1. Character disappears far from spawn: streaming observer now computed
from _playerController.Position when player mode is active, instead
of _lastLivePlayerLandblockId which only updates from server echoes
(never for autonomous moves). The 5x5 streaming window now follows
the player as they walk.
2. Jump physics from ACE: JumpImpulse=5.0 and GravityAccel=9.8
matching AC's formula: velocity_z = sqrt(height * 19.6) where
height = BurdenMod * (JumpSkill / (JumpSkill + 1300) * 22.2 + 0.05)
For a new char (skill=100, burden=50%): height≈1.31, vz≈5.07.
3. Gravity reduced from 20 to 9.8 (AC's F_GRAVITY constant).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three user-reported movement fixes:
1. Player disappears when facing away: StaticMeshRenderer now accepts
an alwaysVisibleEntityId. When a culled landblock contains the
player entity, it is still drawn. Prevents the frustum culler from
hiding the player character when they walk far from their spawn
landblock.
2. Jump too high: JumpImpulse reduced from 10.0 to 3.5 (placeholder;
retail scales by Jump skill value from the server).
3. Slope Z alignment: replaced the frame-delta slope bias with a
foot-forward sampling approach — sample terrain Z at 1 unit ahead
in the walk direction and use max(center, foot) as the ground Z.
Handles multi-grade slopes where the terrain rises faster than a
single-point sample tracks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two targeted fixes for user-reported movement bugs:
1. Wall bounce: PortalPlane.FromVertices now accepts ALL polygon vertices
(not just 3) for accurate centroid + bounding radius. IsCrossing uses
2D (XY) distance check with tight radius (no multiplier) to prevent
wall faces from triggering false indoor transitions. Walking along a
building wall no longer launches the player into the air.
2. Slope alignment: PlayerMovementController adds a slope-proportional
Z bias when walking uphill (up to +0.8 on steep slopes, grounded
only). Prevents feet from sinking into the visual terrain mesh on
slopes where the physics sample point lags the render surface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four targeted fixes for user-reported movement/visual bugs:
1. Player entity disappearing: GpuWorldState now supports persistent
entities (MarkPersistent/DrainRescued). The player character survives
landblock unloads and gets re-injected into the streaming window at
the current center landblock.
2. Feet sinking into terrain: +0.15 Z bias in PlayerMovementController
keeps the character model above terrain z-fighting edge cases.
3. Camera after portal teleport: ChaseCamera.Update now called
immediately after teleport snap so the camera recenters on the new
position instead of lingering at the pre-teleport location.
4. Scenery on roads: SceneryGenerator now checks road status at the
final displaced position (not just the origin vertex), catching
objects that drift from non-road vertices onto road cells.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PlayerTeleport (0xF751) is a standalone GameMessage (u16 sequence,
align-4). When received, WorldSession fires TeleportStarted(uint sequence).
GameWindow subscribes: OnTeleportStarted sets PlayerMovementController.State
= PortalSpace, freezing all WASD/physics input. OnLivePositionUpdated
detects arrival (different landblock or >100 unit jump on our character guid),
recenters the streaming origin, resolves physics for ground Z, snaps the
player entity + controller, returns State to InWorld, and sends
GameActionLoginComplete directly (matching holtburger's PlayerTeleport
handler: send_login_complete on every portal transition).
PlayerMovementController gains PlayerState enum + early-return guard: if
State == PortalSpace, Update() returns a zero-movement result immediately
so no MoveToState / AutonomousPosition messages are emitted during transit.
WorldSession gains ResetLoginComplete() for callers that need to re-arm
the latch (documented; not called by the teleport path since we send
LoginComplete directly rather than through the PlayerCreate latch).
Opcode source: holtburger/crates/holtburger-protocol/src/opcodes.rs:84
Wire layout: holtburger/crates/.../movement/messages/teleport.rs
Build: 0 errors. Tests: 283 passed, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes for the "position never changes when walking" bug:
1. StepUpHeight was 1.0 units — too tight. The player started at
Z=92.2 (ACE relocation from previous session) but terrain Z was
~94, so every movement attempt had a Z delta of 1.8 which
exceeded the limit. Increased to 5.0 (forgiving for MVP; AC
default for humans is ~2 from Setup.StepUpHeight).
2. Initial position now resolves through PhysicsEngine with a huge
step height (100) to snap to the correct terrain Z regardless
of where the server-sent Z currently is. With indoor transitions
disabled, this always produces the outdoor terrain height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three more fixes from the diagnostic dump:
1. Initial position: PhysicsEngine.Resolve was mapping the player
into an indoor EnvCell (foundry at Z=66) when they're standing
on outdoor terrain at Z=93+. The cell-containment check was too
aggressive for initial placement. Now uses the server-sent
position directly — the server already gave us a valid position.
2. Yaw unbounded: mouse delta accumulated without wrapping, growing
to 24+ radians. Now wraps to [-PI, PI] after every turn.
3. Turn command spam: MouseDeltaX > 0.5 threshold was too low for
raw pixel deltas. Any mouse jitter triggered turnCmd flips every
frame → stateChanged=True → MoveToState flood to the server.
Mouse turning now only affects yaw directly; turn COMMANDS only
come from A/D keyboard (matching retail client behavior where
mouse-look doesn't generate a TurnRight/TurnLeft command).
265 tests still green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-frame controller that reads MovementInput (WASD/ZX/Shift/mouse),
drives PhysicsEngine.Resolve for collision, and tracks motion state
changes for outbound server messages + animation switching. Walk
(~4 u/s) and run (~7 u/s) speeds match AC retail. Heartbeat timer
triggers AutonomousPosition every ~200ms while moving.
5 new tests covering idle, forward, run, turn, and state-change
detection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>