From 146abad36ee5a7f7f26ada7fb67ea22608b5d27b Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 27 Sep 2023 20:53:22 -0700
Subject: [PATCH] feat: Adds a movement throttle system. Removes Fastwalk
system. (#1511)
### Summary
- Removes Fastwalk system
- Removed the following settings:
- `movement.enableFastWalkPrevention`
- `movement.fastwalkExemptionLevel`
- Adds movement throttle system.
- Adds the following settings:
- `movement.throttleReset` - Default value is `1000` (1 second).
- `movement.throttleThreshold` - Default value is `400` (400ms).
### Movement Throttling
This new system will trigger if a player requests 400ms (configurable) worth of movements quicker than wall clock time. When this happens, the player is throttled (all incoming packets to the server are halted) until wall clock time catches up with the requests. Upon each throttle, the player receives enough credit to handle up to 400ms of "lag" as a grace/catch-up.
### Developer Notes
We use two throttle queues to prevent an infinite loop.
---
.../Packets/Outgoing/MovementPacketTests.cs | 12 --
.../Packets/Outgoing/MovementPackets.cs | 15 ---
Projects/Server/Events/FastwalkEvent.cs | 37 ------
Projects/Server/Mobiles/Mobile.cs | 35 ++---
Projects/Server/Mobiles/Movement.cs | 27 ++--
Projects/Server/Network/MovementThrottle.cs | 71 ++++++++++
.../Network/NetState/NetState.Fastwalk.cs | 121 ------------------
Projects/Server/Network/NetState/NetState.cs | 4 +
.../Packets/OutgoingMovementPackets.cs | 37 ------
Projects/UOContent/Mobiles/PlayerMobile.cs | 2 +-
.../UOContent/Network/FastwalkDetection.cs | 67 ----------
11 files changed, 106 insertions(+), 322 deletions(-)
delete mode 100644 Projects/Server/Events/FastwalkEvent.cs
create mode 100644 Projects/Server/Network/MovementThrottle.cs
delete mode 100644 Projects/Server/Network/NetState/NetState.Fastwalk.cs
delete mode 100644 Projects/UOContent/Network/FastwalkDetection.cs
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