From 97c17c5bc3e7bd9fcc65e198532bd4618c0fd614 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 14:58:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(app):=20Phase=20B.2=20=E2=80=94=20use=20ser?= =?UTF-8?q?ver=20position=20directly,=20fix=20yaw=20wrap=20+=20turn=20spam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Input/PlayerMovementController.cs | 13 ++++++-- src/AcDream.App/Rendering/GameWindow.cs | 31 +++++++++++++++---- src/AcDream.Core/Physics/PhysicsEngine.cs | 3 ++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 6db677c..e151197 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -79,6 +79,10 @@ public sealed class PlayerMovementController if (input.TurnLeft) Yaw += TurnSpeed * dt; Yaw -= input.MouseDeltaX * MouseTurnSensitivity; + // Wrap yaw to [-PI, PI] so it doesn't grow unbounded and cause + // NaN/overflow in sin/cos and confuse the chase camera offset. + while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI; + while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI; // 2. Compute movement delta in the player's facing direction. float speed = input.Run ? RunSpeed : WalkSpeed; @@ -130,12 +134,17 @@ public sealed class PlayerMovementController sidestepSpeed = speed * 0.5f / WalkSpeed; } - if (input.TurnRight || input.MouseDeltaX > 0.5f) + // Turn commands from KEYBOARD only (A/D). Mouse turning is applied + // directly to Yaw above and doesn't generate a turn command — if it + // did, mouse jitter would flip turnCmd between TurnRight/TurnLeft + // every frame, causing stateChanged=True on every frame and flooding + // the server with MoveToState spam. + if (input.TurnRight) { turnCmd = 0x6500000Du; // TurnRight turnSpeed = TurnSpeed; } - else if (input.TurnLeft || input.MouseDeltaX < -0.5f) + else if (input.TurnLeft) { turnCmd = 0x6500000Eu; // TurnLeft turnSpeed = TurnSpeed; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 897c26c..af7aedc 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -203,12 +203,15 @@ public sealed class GameWindow : IDisposable float plocalX = playerEntity.Position.X - (plbX - _liveCenterX) * 192f; float plocalY = playerEntity.Position.Y - (plbY - _liveCenterY) * 192f; uint pinitCellId = ((uint)plbX << 24) | ((uint)plbY << 16) | 0x0001u; - // Let the physics engine resolve the correct Z for the initial position - // so we don't start underground. - var initResult = _physicsEngine.Resolve( - playerEntity.Position, pinitCellId, - System.Numerics.Vector3.Zero, 10f); - _playerController.SetPosition(initResult.Position, initResult.CellId); + // Use the server-sent position directly — the server already + // gave us a valid position at CreateObject time. Running it + // through PhysicsEngine.Resolve was mapping the player into + // an indoor cell (e.g., the foundry at Z=66) when they're + // actually standing on outdoor terrain at Z=93+. The physics + // engine's cell-containment check is too aggressive for the + // initial placement. Once the player starts moving, Resolve + // will keep them on the correct surface. + _playerController.SetPosition(playerEntity.Position, pinitCellId & 0xFFFFu); // Derive initial yaw from the entity's server-sent rotation // rather than hardcoding. Extract yaw from the quaternion. var q = playerEntity.Rotation; @@ -216,6 +219,12 @@ public sealed class GameWindow : IDisposable 2f * (q.W * q.Z + q.X * q.Y), 1f - 2f * (q.Y * q.Y + q.Z * q.Z)); _playerController.Yaw = yaw; + + Console.WriteLine($"[PLAYER-INIT] entityPos=({playerEntity.Position.X:F1},{playerEntity.Position.Y:F1},{playerEntity.Position.Z:F1}) " + + $"entityRot=({q.X:F3},{q.Y:F3},{q.Z:F3},{q.W:F3}) " + + $"initCellId=0x{(pinitCellId & 0xFFFFu):X4} " + + $"yaw={yaw:F3} " + + $"physics landblocks={_physicsEngine.LandblockCount}"); _chaseCamera = new AcDream.App.Rendering.ChaseCamera { Aspect = _window!.Size.X / (float)_window.Size.Y, @@ -1489,6 +1498,16 @@ public sealed class GameWindow : IDisposable var result = _playerController.Update((float)dt, input); + // DIAG: dump player state every ~60 frames to see what's happening. + if (_perfFrameCount % 60 == 0) + { + Console.WriteLine($"[PLAYER] pos=({result.Position.X:F1},{result.Position.Y:F1},{result.Position.Z:F1}) " + + $"cell=0x{result.CellId:X8} ground={result.IsOnGround} " + + $"yaw={_playerController.Yaw:F2} " + + $"fwdCmd={result.ForwardCommand?.ToString("X8") ?? "idle"} " + + $"stateChanged={result.MotionStateChanged}"); + } + // Update the player entity's position + rotation so it renders at // the physics-resolved location each frame. if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 3ddf887..984d5a8 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -21,6 +21,9 @@ public sealed class PhysicsEngine { private readonly Dictionary _landblocks = new(); + /// Number of registered landblocks (diagnostic). + public int LandblockCount => _landblocks.Count; + private sealed record LandblockPhysics( TerrainSurface Terrain, IReadOnlyList Cells,