diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
index a506bdd9c..4d9bcfb80 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPacketTests.cs
@@ -67,17 +67,5 @@ namespace Server.Tests.Network
var result = ns.SendPipe.Reader.TryRead();
AssertThat.Equal(result.Buffer[0].AsSpan(0), expected);
}
-
- [Fact]
- public void TestNullFastwalkStack()
- {
- var expected = new NullFastwalkStack().Compile();
-
- var ns = PacketTestUtilities.CreateTestNetState();
- ns.SendInitialFastwalkStack(new uint[]{ 0, 0, 0, 0, 0, 0 });
-
- var result = ns.SendPipe.Reader.TryRead();
- AssertThat.Equal(result.Buffer[0].AsSpan(0), expected);
- }
}
}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPackets.cs
index 68fa0a6f6..57d1e9dd5 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPackets.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MovementPackets.cs
@@ -65,19 +65,4 @@ namespace Server.Network
return p;
}
}
-
- public sealed class NullFastwalkStack : Packet
- {
- public NullFastwalkStack() : base(0xBF)
- {
- EnsureCapacity(29);
- Stream.Write((short)0x1);
- Stream.Write(0x0);
- Stream.Write(0x0);
- Stream.Write(0x0);
- Stream.Write(0x0);
- Stream.Write(0x0);
- Stream.Write(0x0);
- }
- }
}
diff --git a/Projects/Server/Events/FastwalkEvent.cs b/Projects/Server/Events/FastwalkEvent.cs
deleted file mode 100644
index 61bf9c618..000000000
--- a/Projects/Server/Events/FastwalkEvent.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-/*************************************************************************
- * ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
- * Email: hi@modernuo.com *
- * File: FastwalkEvent.cs *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- *************************************************************************/
-
-using System;
-using System.Runtime.CompilerServices;
-using Server.Network;
-
-namespace Server;
-
-public class FastWalkEventArgs
-{
- public FastWalkEventArgs(NetState state) => NetState = state;
-
- public NetState NetState { get; }
-
- public bool Blocked { get; set; } = true;
-}
-
-public static partial class EventSink
-{
- public static event Action FastWalk;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void InvokeFastWalk(FastWalkEventArgs e) => FastWalk?.Invoke(e);
-}
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index 409611004..ced0936b6 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -556,6 +556,15 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
SendLocalizedMessage(m_Paralyzed ? 502381 : 502382);
_paraTimerToken.Cancel();
}
+
+ if (!value && m_NetState != null)
+ {
+ var now = Core.TickCount;
+ if (now - m_NetState._nextMovementTime > 0)
+ {
+ m_NetState._nextMovementTime = now;
+ }
+ }
}
}
@@ -4232,21 +4241,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
return false;
}
- if (
- CalcMoves.EnableFastwalkPrevention &&
- AccessLevel < CalcMoves.FastwalkExemptionLevel &&
- m_NetState?.AddStep(d) == false
- )
- {
- var fw = new FastWalkEventArgs(m_NetState);
- EventSink.InvokeFastWalk(fw);
-
- if (fw.Blocked)
- {
- return false;
- }
- }
-
LastMoveTime = Core.TickCount;
return true;
@@ -4280,6 +4274,11 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
DisruptiveAction();
}
+ if (m_NetState != null)
+ {
+ m_NetState._nextMovementTime += ComputeMovementSpeed(d);
+ }
+
m_NetState?.SendMovementAck(m_NetState.Sequence, this);
SetLocation(newLocation, false);
@@ -4354,6 +4353,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
}
OnAfterMove(oldLocation);
+
return true;
}
@@ -4565,6 +4565,11 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
///
public virtual void OnSpellCast(ISpell spell)
{
+ var now = Core.TickCount;
+ if (m_NetState != null && now - m_NetState._nextMovementTime > 0)
+ {
+ m_NetState._nextMovementTime = now;
+ }
}
///
diff --git a/Projects/Server/Mobiles/Movement.cs b/Projects/Server/Mobiles/Movement.cs
index 95df72593..0629dd747 100644
--- a/Projects/Server/Mobiles/Movement.cs
+++ b/Projects/Server/Mobiles/Movement.cs
@@ -19,26 +19,19 @@ public static class Movement
{
// Movement implementation algorithm
public static IMovementImpl Impl { get; set; }
- public static int WalkFootDelay { get; set; } = 400;
- public static int RunFootDelay { get; set; } = 200;
- public static int WalkMountDelay { get; set; } = 200;
- public static int RunMountDelay { get; set; } = 100;
-
- public static bool EnableFastwalkPrevention { get; set; } = true;
- public static AccessLevel FastwalkExemptionLevel { get; set; } = AccessLevel.Counselor;
-
- // If this is changed during runtime, then the steps array needs resizing.
- public static int MaxSteps { get; private set; } = 3;
+ public static int WalkFootDelay { get; set; }
+ public static int RunFootDelay { get; set; }
+ public static int WalkMountDelay { get; set; }
+ public static int RunMountDelay { get; set; }
+ public static int TurnDelay { get; set; }
public static void Configure()
{
- EnableFastwalkPrevention = ServerConfiguration.GetOrUpdateSetting("movement.enableFastWalkPrevention", EnableFastwalkPrevention);
- MaxSteps = ServerConfiguration.GetOrUpdateSetting("movement.maxSteps", MaxSteps);
- FastwalkExemptionLevel = ServerConfiguration.GetOrUpdateSetting("movement.fastwalkExemptionLevel", FastwalkExemptionLevel);
- WalkFootDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.walkFoot", WalkFootDelay);
- RunFootDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.runFoot", RunFootDelay);
- WalkMountDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.walkMount", WalkMountDelay);
- RunMountDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.runMount", RunMountDelay);
+ TurnDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.turn", 0);
+ WalkFootDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.walkFoot", 400);
+ RunFootDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.runFoot", 200);
+ WalkMountDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.walkMount", 200);
+ RunMountDelay = ServerConfiguration.GetOrUpdateSetting("movement.delay.runMount", 100);
}
public static bool CheckMovement(Mobile m, Direction d, out int newZ)
diff --git a/Projects/Server/Network/MovementThrottle.cs b/Projects/Server/Network/MovementThrottle.cs
new file mode 100644
index 000000000..d544729e5
--- /dev/null
+++ b/Projects/Server/Network/MovementThrottle.cs
@@ -0,0 +1,71 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2023 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: MovementThrottle.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+
+namespace Server.Network;
+
+public static class MovementThrottle
+{
+ private static long _movementThrottleReset; // 1 second
+ private static long _throttleThreshold; // 400 milliseconds
+
+ public static void Configure()
+ {
+ _movementThrottleReset = ServerConfiguration.GetOrUpdateSetting("movement.throttleReset", 1000);
+ _throttleThreshold = ServerConfiguration.GetOrUpdateSetting("movement.throttleThreshold", 400);
+ }
+
+ public static unsafe void Initialize()
+ {
+ IncomingPackets.RegisterThrottler(0x02, &Throttle);
+ }
+
+ public static bool Throttle(int packetId, NetState ns, out bool drop)
+ {
+ drop = false;
+
+ var from = ns.Mobile;
+
+ if (from?.Deleted != false || from.AccessLevel > AccessLevel.Player)
+ {
+ return true;
+ }
+
+ long now = Core.TickCount;
+ long credit = ns._movementCredit;
+ long nextMove = ns._nextMovementTime;
+
+ // Reset system if idle for more than 1 second
+ if (now - nextMove + _movementThrottleReset > 0)
+ {
+ ns._movementCredit = 0;
+ ns._nextMovementTime = now;
+ return true;
+ }
+
+ long cost = nextMove - now;
+
+ if (credit < cost)
+ {
+ // Not enough credit, therefore throttled
+ return false;
+ }
+
+ // On the next event loop, the player receives up to 400ms in grace latency
+ ns._movementCredit = Math.Min(_throttleThreshold, credit - cost);
+ return true;
+ }
+}
diff --git a/Projects/Server/Network/NetState/NetState.Fastwalk.cs b/Projects/Server/Network/NetState/NetState.Fastwalk.cs
deleted file mode 100644
index e24731eb8..000000000
--- a/Projects/Server/Network/NetState/NetState.Fastwalk.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-/*************************************************************************
- * ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
- * Email: hi@modernuo.com *
- * File: NetState.Fastwalk.cs *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- *************************************************************************/
-
-using System.Runtime.CompilerServices;
-using CalcMoves = Server.Movement.Movement;
-
-namespace Server.Network;
-
-public partial class NetState
-{
- // The next step
- private int _stepIndex;
- // The last index to expire
- private int _expiredIndex;
-
- private long[] _steps;
-
- public bool AddStep(Direction d)
- {
- if (Mobile == null)
- {
- return false;
- }
-
- _steps ??= new long[CalcMoves.MaxSteps + 1]; // Extra index as a sentinel
- var stepsLength = _steps.Length;
-
- var now = Core.TickCount;
-
- var lastIndex = -1;
-
- // Expire old steps
- while (_expiredIndex != _stepIndex)
- {
- var step = _steps[_expiredIndex];
-
- // Is the step ahead of us, or the next step rolled over and we didn't yet
- if (step - now > 0 || lastIndex > -1 && _steps[lastIndex] > step)
- {
- break;
- }
-
- lastIndex = _expiredIndex++;
-
- if (_expiredIndex == stepsLength)
- {
- _expiredIndex -= stepsLength;
- }
- }
-
- var stepsTaken = (_stepIndex < _expiredIndex ? _stepIndex + stepsLength : _stepIndex) - _expiredIndex;
- var maxSteps = _steps.Length - 1;
-
- // Can we take a step?
- if (stepsTaken >= maxSteps)
- {
- return false;
- }
-
- var delay = Mobile.ComputeMovementSpeed(d);
-
- var prev = _stepIndex - 1;
- if (prev < 0)
- {
- prev += stepsLength;
- }
-
- // Give a 5% buffer on the first step
- _steps[_stepIndex++] = stepsTaken > 0 ? _steps[prev] + delay : now + delay * 950 / 1000;
-
- if (_stepIndex == stepsLength)
- {
- _stepIndex -= stepsLength;
- }
-
- // If CalcMoves.MaxSteps is modified, we need to adjust accordingly
- AdjustSteps(CalcMoves.MaxSteps);
-
- return true;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void AdjustSteps(int maxSteps)
- {
- var stepsLength = maxSteps + 1;
-
- if (_steps.Length == stepsLength)
- {
- return;
- }
-
- var oldSteps = _steps;
- _steps = new long[stepsLength];
-
- var expiredIndex = _expiredIndex;
- var newStepIndex = 0;
- while (newStepIndex < maxSteps && expiredIndex != _stepIndex)
- {
- _steps[newStepIndex++] = oldSteps[expiredIndex++];
- if (expiredIndex >= oldSteps.Length)
- {
- expiredIndex -= oldSteps.Length;
- }
- }
-
- _expiredIndex = 0;
- _stepIndex = newStepIndex;
- }
-}
diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs
index 936b3ce0f..6c483f19a 100755
--- a/Projects/Server/Network/NetState/NetState.cs
+++ b/Projects/Server/Network/NetState/NetState.cs
@@ -76,6 +76,10 @@ public partial class NetState : IComparable
public GCHandle Handle => _handle;
+ // Speed Hack Prevention
+ internal long _movementCredit;
+ internal long _nextMovementTime;
+
internal enum ParserState
{
AwaitingNextPacket,
diff --git a/Projects/Server/Network/Packets/OutgoingMovementPackets.cs b/Projects/Server/Network/Packets/OutgoingMovementPackets.cs
index e12894103..bd953293f 100644
--- a/Projects/Server/Network/Packets/OutgoingMovementPackets.cs
+++ b/Projects/Server/Network/Packets/OutgoingMovementPackets.cs
@@ -60,43 +60,6 @@ public static class OutgoingMovementPackets
ns.Send(writer.Span);
}
- public static void SendInitialFastwalkStack(this NetState ns, uint[] keys)
- {
- if (ns.CannotSendPackets())
- {
- return;
- }
-
- var writer = new SpanWriter(stackalloc byte[29]);
- writer.Write((byte)0xBF); // Packet ID
- writer.Write((ushort)29);
- writer.Write((ushort)0x1); // Subpacket
- writer.Write(keys[0]);
- writer.Write(keys[1]);
- writer.Write(keys[2]);
- writer.Write(keys[3]);
- writer.Write(keys[4]);
- writer.Write(keys[5]);
-
- ns.Send(writer.Span);
- }
-
- public static void SendFastwalkStackKey(this NetState ns, uint key = 0)
- {
- if (ns.CannotSendPackets())
- {
- return;
- }
-
- var writer = new SpanWriter(stackalloc byte[9]);
- writer.Write((byte)0xBF); // Packet ID
- writer.Write((ushort)9);
- writer.Write((ushort)0x2); // Subpacket
- writer.Write(key);
-
- ns.Send(writer.Span);
- }
-
public static void SendTimeSyncResponse(this NetState ns)
{
if (ns.CannotSendPackets())
diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs
index 0df10c313..fd89c142a 100644
--- a/Projects/UOContent/Mobiles/PlayerMobile.cs
+++ b/Projects/UOContent/Mobiles/PlayerMobile.cs
@@ -4147,7 +4147,7 @@ namespace Server.Mobiles
{
if (checkTurning && (dir & Direction.Mask) != (Direction & Direction.Mask))
{
- return CalcMoves.RunMountDelay; // We are NOT actually moving (just a direction change)
+ return CalcMoves.TurnDelay; // We are NOT actually moving (just a direction change)
}
var context = TransformationSpellHelper.GetContext(this);
diff --git a/Projects/UOContent/Network/FastwalkDetection.cs b/Projects/UOContent/Network/FastwalkDetection.cs
deleted file mode 100644
index 4328f0f12..000000000
--- a/Projects/UOContent/Network/FastwalkDetection.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using System.Collections.Generic;
-using Server.Commands.Generic;
-
-namespace Server.Network;
-
-public static class FastwalkDetection
-{
- private static readonly HashSet _debugFastwalk = new();
-
- public static void Initialize()
- {
- TargetCommands.Register(new DebugFastwalk());
- EventSink.FastWalk += OnFastwalk;
- }
-
- private static void OnFastwalk(FastWalkEventArgs e)
- {
- var from = e.NetState.Mobile;
- if (from == null)
- {
- return;
- }
-
- if (!_debugFastwalk.Contains(from))
- {
- return;
- }
-
- from.PublicOverheadMessage(MessageType.Emote, from.EmoteHue, false, "Fastwalk Detected", accessLevel: AccessLevel.GameMaster);
- }
-
- public class DebugFastwalk : BaseCommand
- {
- public DebugFastwalk()
- {
- AccessLevel = AccessLevel.GameMaster;
- Supports = CommandSupport.AllMobiles;
- Commands = new[] { "DebugFastwalk" };
- ObjectTypes = ObjectTypes.Mobiles;
- ListOptimized = true;
- Usage = "DebugFastwalk ";
- Description = "Enables fastwalk debug messages";
- }
-
- public override void ExecuteList(CommandEventArgs e, List