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);
+ }
}