feat: Adds robust speed hack detection and movement throttling (#2266)

## Summary

   Server-side movement throttle that prevents speed hacking while accurately identifying cheaters with detection of lagging connections.

   **Key features:**
   - Credit buffer (200ms) absorbs timing jitter from legitimate players
   - Movement queue handles larger bursts, draining at proper game-tick intervals
   - RTT measurement distinguishes network lag from speed hacks
   - Queue depth detection catches ACK-throttled speed hacks (going straight)

   ## How It Works

   **Throttle** (prevention): Movements arriving too early either consume credit or get queued. The queue drains at
   correct intervals, so speed hackers can't move faster regardless of what they send.

   **Detection** (identification): Combines multiple signals to identify cheaters:
   | Signal | What it catches |
   |--------|-----------------|
   | Queue depth ≥4 sustained | ACK-throttled speed hacks (client limits unacked moves to 5) |
   | Movement rate >1.05x | Direction-change speed hacks where timing is visible |
   | Stable RTT + high queue | Eliminates false positives from laggy players |

   **RTT-Aware Logic:**
   - Probes only sent to players actively moving (event-driven, not global loop)
   - Stable low-latency + problems = suspicious
   - Unstable/high-latency + problems = probably just lag, throttle handles it

   ## Configuration

   ```json
   {
     "movementThrottle.maxCredit": 200,
     "movementThrottle.softQueueLimit": 6,
     "movementThrottle.hardQueueLimit": 10,
     "movementThrottle.debugLogging": false
   }
   ```
This commit is contained in:
Kamron Batman 2026-03-07 11:44:37 -08:00 committed by GitHub
parent 6745cf2075
commit 04d438239d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2474 additions and 74 deletions

View file

@ -2522,7 +2522,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
if (ns != null)
{
ns.Sequence = 0;
ns.ResetMovementState();
if (m_Map != null)
{
@ -2758,7 +2758,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
{
if (sendUpdate)
{
ourState.Sequence = 0;
ourState.ResetMovementState();
ourState.SendMobileUpdate(this);
}
@ -4309,7 +4309,32 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
if (m_NetState != null)
{
m_NetState._nextMovementTime += ComputeMovementSpeed(d);
var cost = ComputeMovementSpeed(d);
var now = Core.TickCount;
var delta = now - m_NetState._nextMovementTime;
// Cap drift to prevent banking "lateness" for speed hacking later.
// maxDrift should match the credit buffer so the systems are symmetric.
const int maxDrift = 200;
if (cost == 0)
{
// Direction-only turn: reset to now to prevent turn accumulation
m_NetState._nextMovementTime = now;
}
else
{
// Clamp _nextMovementTime so it's never more than maxDrift behind now.
// This limits how much "lateness" can be banked.
if (delta > maxDrift)
{
m_NetState._nextMovementTime = now - maxDrift;
}
// Accumulative timing: add cost to current baseline
m_NetState._nextMovementTime += cost;
}
m_NetState.SendMovementAck(m_NetState.Sequence, this);
}
@ -7249,7 +7274,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
if (isTeleport && (!m_NetState.HighSeas || !NoMoveHS))
{
m_NetState.Sequence = 0;
m_NetState.ResetMovementState();
m_NetState.SendMobileUpdate(this);
}
}