From c7fa1d36fb285fb8a5ebe25ba80da4d90825a615 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 13 Apr 2026 23:11:49 +0200 Subject: [PATCH] feat(movement): wire server RunRate into player MotionInterpreter 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 --- .../Input/PlayerMovementController.cs | 12 +++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 12 +++++++++++ src/AcDream.Core.Net/Messages/CreateObject.cs | 21 +++++++++++++------ src/AcDream.Core.Net/Messages/UpdateMotion.cs | 15 ++++++++++++- .../Physics/MotionInterpreterTests.cs | 18 ++++++++++++++++ 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index f97159b..457760d 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -115,6 +115,18 @@ public sealed class PlayerMovementController _motion = new MotionInterpreter(_body); } + /// + /// Apply a server-echoed run rate (ForwardSpeed from UpdateMotion) to the + /// player's MotionInterpreter. The server broadcasts the real RunRate + /// derived from the character's Run skill; wiring it here ensures + /// get_state_velocity produces the correct speed instead of the default 1.0. + /// + public void ApplyServerRunRate(float forwardSpeed) + { + _motion.InterpretedState.ForwardSpeed = forwardSpeed; + _motion.apply_current_movement(cancelMoveTo: false, allowJump: false); + } + public void SetPosition(Vector3 pos, uint cellId) { _body.Position = pos; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 7d729aa..a634772 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -952,6 +952,18 @@ public sealed class GameWindow : IDisposable if (!newCycleIsGood) return; + // Wire server-echoed RunRate into the player's MotionInterpreter. + // The server broadcasts the character's real Run-skill-derived ForwardSpeed + // in UpdateMotion; without this the player would always move at 4.0 m/s + // (ForwardSpeed = 1.0 hardcoded in MotionInterpreter defaults). + if (_playerController is not null + && update.Guid == _playerServerGuid + && update.MotionState.ForwardSpeed.HasValue + && update.MotionState.ForwardSpeed.Value > 0f) + { + _playerController.ApplyServerRunRate(update.MotionState.ForwardSpeed.Value); + } + // Sequencer path if (ae.Sequencer is not null) { diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index aa891f6..d08008d 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -109,7 +109,7 @@ public static class CreateObject /// Nullified Statue of a Drudge, which is rendered in the wrong pose /// if you only consult the MotionTable's default style. /// - public readonly record struct ServerMotionState(ushort Stance, ushort? ForwardCommand); + public readonly record struct ServerMotionState(ushort Stance, ushort? ForwardCommand, float? ForwardSpeed = null); /// /// Server instruction to replace the surface texture at @@ -479,6 +479,7 @@ public static class CreateObject p += 2; ushort? forwardCommand = null; + float? forwardSpeed = null; // 0 = Invalid is the only union variant we care about for static // entities. Walking/turning entities use the other variants but @@ -513,13 +514,21 @@ public static class CreateObject forwardCommand = BinaryPrimitives.ReadUInt16LittleEndian(mv.Slice(p)); p += 2; } - // Remaining fields (SideStep, Turn, speeds, commands list, - // align) are deliberately not parsed — we already have what - // the resolver needs and the outer length tells the caller - // where MovementData ends. + // SidestepCommand (0x4) — skip + if ((flags & 0x4u) != 0) { if (mv.Length - p < 2) goto done; p += 2; } + // TurnCommand (0x8) — skip + if ((flags & 0x8u) != 0) { if (mv.Length - p < 2) goto done; p += 2; } + // ForwardSpeed (0x10) + if ((flags & 0x10u) != 0) + { + if (mv.Length - p < 4) goto done; + forwardSpeed = BinaryPrimitives.ReadSingleLittleEndian(mv.Slice(p)); + p += 4; + } + done:; } - return new ServerMotionState(currentStyle, forwardCommand); + return new ServerMotionState(currentStyle, forwardCommand, forwardSpeed); } catch { diff --git a/src/AcDream.Core.Net/Messages/UpdateMotion.cs b/src/AcDream.Core.Net/Messages/UpdateMotion.cs index 7e6a51b..edc9717 100644 --- a/src/AcDream.Core.Net/Messages/UpdateMotion.cs +++ b/src/AcDream.Core.Net/Messages/UpdateMotion.cs @@ -102,6 +102,7 @@ public static class UpdateMotion pos += 2; ushort? forwardCommand = null; + float? forwardSpeed = null; if (movementType == 0) { @@ -130,9 +131,21 @@ public static class UpdateMotion forwardCommand = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } + // SidestepCommand (0x4) — skip + if ((flags & 0x4u) != 0) { if (body.Length - pos < 2) goto done; pos += 2; } + // TurnCommand (0x8) — skip + if ((flags & 0x8u) != 0) { if (body.Length - pos < 2) goto done; pos += 2; } + // ForwardSpeed (0x10) + if ((flags & 0x10u) != 0) + { + if (body.Length - pos < 4) goto done; + forwardSpeed = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); + pos += 4; + } + done:; } - return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, forwardCommand)); + return new Parsed(guid, new CreateObject.ServerMotionState(currentStyle, forwardCommand, forwardSpeed)); } catch { diff --git a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs index 7e8a278..bc08af7 100644 --- a/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs +++ b/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs @@ -651,4 +651,22 @@ public sealed class MotionInterpreterTests Assert.Equal(2.0f, interp.MyRunRate, precision: 5); } + + [Fact] + public void ApplyCurrentMovement_RunForward_SetsMyRunRate() + { + // Verifies that wiring a server-echoed ForwardSpeed (from UpdateMotion + // 0xF74C InterpretedMotionState flag 0x10) into the MotionInterpreter + // produces the correct MyRunRate and get_state_velocity result. + var body = new PhysicsBody(); + var mi = new MotionInterpreter(body); + mi.InterpretedState.ForwardCommand = MotionCommand.RunForward; + mi.InterpretedState.ForwardSpeed = 2.375f; + + mi.apply_current_movement(cancelMoveTo: false, allowJump: true); + + Assert.Equal(2.375f, mi.MyRunRate, precision: 3); + var vel = mi.get_state_velocity(); + Assert.Equal(4.0f * 2.375f, vel.Y, precision: 2); + } }