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 list) - { - var on = e.Arguments.Length == 0 || e.GetBoolean(0); - - foreach (var o in list) - { - if (o is not Mobile m) - { - continue; - } - - if (on) - { - _debugFastwalk.Add(m); - } - else - { - _debugFastwalk.Remove(m); - } - } - } - } -}