using Server.Network; using Xunit; namespace Server.Tests.Network; /// /// Unit tests for MovementThrottle rate calculation and detection logic. /// These tests are CI/CD safe - they don't depend on real timing. /// [Collection("Sequential Server Tests")] public class MovementThrottleTests { /// /// Creates a test NetState with movement history pre-populated. /// private static NetState CreateNetStateWithHistory(params (short interval, ushort targetSpeed)[] records) { var ns = PacketTestUtilities.CreateTestNetState(); if (records.Length == 0) { return ns; } // Initialize history buffer ns._movementHistory = new NetState.MovementRecord[20]; ns._movementHistoryIndex = 0; ns._movementHistoryFull = false; foreach (var (interval, targetSpeed) in records) { ns._movementHistory[ns._movementHistoryIndex] = new NetState.MovementRecord { Interval = interval, TargetSpeed = targetSpeed, QueueDepth = 0, Flags = 0 }; ns._movementHistoryIndex++; if (ns._movementHistoryIndex >= 20) { ns._movementHistoryIndex = 0; ns._movementHistoryFull = true; } } return ns; } [Fact] public void CalculateMovementRate_NoHistory_ReturnsOne() { var ns = CreateNetStateWithHistory(); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); Assert.Equal(1.0f, rate); Assert.Equal(0, sampleCount); } [Fact] public void CalculateMovementRate_InsufficientSamples_ReturnsOne() { // Less than 8 samples (minSamplesForRate default) var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100) ); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); Assert.Equal(1.0f, rate); // When insufficient samples, method returns early with sampleCount = 0 Assert.Equal(0, sampleCount); } [Fact] public void CalculateMovementRate_NormalSpeed_ReturnsOne() { // 10 samples at exactly expected speed (100ms interval, 100ms target) var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100) ); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); Assert.Equal(1.0f, rate); Assert.Equal(10, sampleCount); } [Fact] public void CalculateMovementRate_SpeedHack_ReturnsHighRate() { // Moving at 50ms intervals when 100ms is expected = 2x speed var ns = CreateNetStateWithHistory( (50, 100), (50, 100), (50, 100), (50, 100), (50, 100), (50, 100), (50, 100), (50, 100), (50, 100), (50, 100) ); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); Assert.Equal(2.0f, rate); Assert.Equal(10, sampleCount); } [Fact] public void CalculateMovementRate_SlowMovement_ReturnsLowRate() { // Moving at 200ms intervals when 100ms is expected = 0.5x speed var ns = CreateNetStateWithHistory( (200, 100), (200, 100), (200, 100), (200, 100), (200, 100), (200, 100), (200, 100), (200, 100), (200, 100), (200, 100) ); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); Assert.Equal(0.5f, rate); Assert.Equal(10, sampleCount); } [Fact] public void CalculateMovementRate_MixedSpeeds_ReturnsAverageRate() { // Mix of mounted running (100ms target) at varying speeds // Total target: 1000ms, Total actual: 900ms = 1.11 rate var ns = CreateNetStateWithHistory( (80, 100), (100, 100), (90, 100), (100, 100), (80, 100), (90, 100), (100, 100), (80, 100), (90, 100), (90, 100) ); var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount); // 1000 / 900 = 1.111... Assert.True(rate > 1.10f && rate < 1.12f, $"Expected rate ~1.11, got {rate}"); Assert.Equal(10, sampleCount); } [Fact] public void DetectRecentBurst_NoBurst_ReturnsZero() { // Normal intervals, no burst var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100) ); var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns); Assert.Equal(0, burstSize); } [Fact] public void DetectRecentBurst_BurstDetected_ReturnsBurstSizeAndGap() { // Last 4 packets arrived in burst (< 15ms intervals), preceded by 500ms gap var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (500, 100), (5, 100), (5, 100), (5, 100), (5, 100) ); var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns); Assert.Equal(4, burstSize); Assert.Equal(500, precedingGap); } [Fact] public void DetectRecentBurst_SmallBurst_DetectsCorrectly() { // 2 packets in burst var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (200, 100), (10, 100) ); var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns); Assert.Equal(1, burstSize); Assert.Equal(200, precedingGap); } [Fact] public void AnalyzeMovement_NormalMovement_ReturnsNormal() { var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (100, 100) ); var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); Assert.Equal(MovementThrottle.DetectionVerdict.Normal, verdict); Assert.Equal(1.0f, rate); Assert.Equal(10, sampleCount); } [Fact] public void AnalyzeMovement_ClearSpeedHack_ReturnsDefinite() { // 30% faster than expected - clear speed hack var ns = CreateNetStateWithHistory( (77, 100), (77, 100), (77, 100), (77, 100), (77, 100), (77, 100), (77, 100), (77, 100), (77, 100), (77, 100) ); var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict); Assert.True(rate > 1.25f, $"Expected rate > 1.25, got {rate}"); } [Fact] public void AnalyzeMovement_ModerateSpeedHack_ReturnsPossibleOrLikely() { // 8% faster than expected - moderate, should be Possible or Likely var ns = CreateNetStateWithHistory( (93, 100), (93, 100), (93, 100), (93, 100), (93, 100), (93, 100), (93, 100), (93, 100), (93, 100), (93, 100) ); // Set up stable, low-latency RTT (required for Likely verdict path) ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50]; ns._rttSampleCount = 8; ns._rttVariance = 4; // Low variance var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); Assert.True( verdict == MovementThrottle.DetectionVerdict.Possible || verdict == MovementThrottle.DetectionVerdict.Likely, $"Expected Possible or Likely, got {verdict}. Rate={rate}, Confidence={confidence}" ); Assert.True(rate > 1.05f && rate < 1.15f, $"Expected rate ~1.07, got {rate}"); } [Fact] public void AnalyzeMovement_BurstAfterLargeGap_ReducesConfidence() { // Simulate lag recovery: large gap followed by burst of packets var ns = CreateNetStateWithHistory( (100, 100), (100, 100), (100, 100), (100, 100), (100, 100), (5, 100), (5, 100), (5, 100), (5, 100), (5, 100) ); // Set up the gap duration (simulates what RecordMovement does) ns._lastGapDuration = 1500; // Gap > maxChainGap/2 (1000) var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); // Even though rate is high, confidence should be reduced due to burst forgiveness // The gap duration should trigger the forgiveness path Assert.True(confidence < 0.5f, $"Expected low confidence due to burst forgiveness, got {confidence}"); // After analysis, gap duration should be reset Assert.Equal(0, ns._lastGapDuration); } [Fact] public void AnalyzeMovement_HighQueue_IncreasesConfidence() { var ns = CreateNetStateWithHistory( (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100) ); // Simulate high queue depth (modified client indicator) ns._movementQueue = new System.Collections.Generic.Queue(); for (var i = 0; i < 5; i++) { ns._movementQueue.Enqueue(new NetState.QueuedMovement()); } var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); // Queue > 4 indicates modified client - should be Definite Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict); } [Fact] public void AnalyzeMovement_StableConnectionLowLatency_IncreasesConfidence() { var ns = CreateNetStateWithHistory( (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100), (95, 100) ); // Set up stable, low-latency RTT ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50]; ns._rttSampleCount = 8; ns._rttVariance = 4; // Low variance var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence); // With stable connection, even small anomalies are more suspicious Assert.True(confidence > 0.2f, $"Expected higher confidence for stable connection, got {confidence}"); } [Fact] public void AnalyzeMovement_UnstableConnection_ReducesConfidence() { var ns = CreateNetStateWithHistory( (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100) ); // Set up unstable connection (high variance) ns._rttHistory = [50, 200, 80, 350, 100, 250, 90, 300]; ns._rttSampleCount = 8; ns._rttVariance = 15000; // High variance var verdictUnstable = MovementThrottle.AnalyzeMovement(ns, out _, out _, out var confidenceUnstable); // Reset and test with stable connection for comparison var nsStable = CreateNetStateWithHistory( (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100), (90, 100) ); nsStable._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50]; nsStable._rttSampleCount = 8; nsStable._rttVariance = 4; var verdictStable = MovementThrottle.AnalyzeMovement(nsStable, out _, out _, out var confidenceStable); // Unstable connection should have lower confidence Assert.True( confidenceUnstable < confidenceStable, $"Expected unstable ({confidenceUnstable}) < stable ({confidenceStable})" ); } [Fact] public void HasStableConnection_LowLatencyLowVariance_ReturnsTrue() { var ns = PacketTestUtilities.CreateTestNetState(); // Stable: low variance, enough samples, low latency ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50]; ns._rttSampleCount = 8; ns._rttVariance = 4; // Low variance < 2500 Assert.True(ns.HasStableConnection); } [Fact] public void HasStableConnection_HighLatency_ReturnsFalse() { var ns = PacketTestUtilities.CreateTestNetState(); // High but consistent latency (500ms) - should NOT be considered stable ns._rttHistory = [500, 502, 498, 501, 499, 500, 501, 500]; ns._rttSampleCount = 8; ns._rttVariance = 4; // Low variance, but latency > 200ms Assert.False(ns.HasStableConnection, "High latency connection should not be considered stable"); } [Fact] public void HasStableConnection_HighVariance_ReturnsFalse() { var ns = PacketTestUtilities.CreateTestNetState(); // Low average latency but high variance ns._rttHistory = [50, 200, 30, 180, 40, 150, 60, 170]; ns._rttSampleCount = 8; ns._rttVariance = 5000; // High variance > 2500 Assert.False(ns.HasStableConnection); } [Fact] public void HasStableConnection_InsufficientSamples_ReturnsFalse() { var ns = PacketTestUtilities.CreateTestNetState(); // Only 2 samples ns._rttHistory = [50, 50, 0, 0, 0, 0, 0, 0]; ns._rttSampleCount = 2; ns._rttVariance = 0; Assert.False(ns.HasStableConnection, "Needs at least 3 samples"); } }