extends Node # ═══════════════════════════════════════════════════════════════════════════ # Anti-Cheat Unit Tests # # Tests the AntiCheat validation pipeline for edge cases: # - Sequence validation (overflow, replay, timestamp regression) # - Movement validation (teleport, speed, multi-jump) # - Aim validation (snap clamping) # - Fire rate (interval, ammo consistency) # - Command rate (sliding window limits) # - Cross-category interactions # # Run with: godot --headless --script tests/test_anti_cheat.gd # ═══════════════════════════════════════════════════════════════════════════ const AC_SCRIPT = preload("res://server/scripts/anti_cheat.gd") const UTILS_SCRIPT = preload("res://tests/test_utils.gd") # Loaded from the AC script in _ready enum ViolationCategory { INPUT_SEQUENCE = 0, MOVEMENT_SPEED = 1, MOVEMENT_TELEPORT = 2, MOVEMENT_MULTI_JUMP = 3, AIM_SNAP = 4, FIRE_RATE_INTERVAL = 5, FIRE_RATE_AMMO = 6, COMMAND_RATE = 7, } enum AcLevel { OFF = 0, LOG = 1, CORRECT = 2, KICK = 3, } var ac var tu # ── Helpers ──────────────────────────────────────────────────────────────── func _make_input(overrides: Dictionary = {}) -> Dictionary: """Build a standard input packet dict with overrides.""" var base := { "seq": 1, "time": 1000.0, "movement": Vector3.ZERO, "view_angles": Vector2(0.0, 0.0), "buttons": 0, "ammo": 30, "position": Vector3.ZERO, "max_speed": 320.0, "weapon_fire_rate_ms": 100.0, } for k in overrides: base[k] = overrides[k] return base func _register_and_validate(pkt: Dictionary, delta: float = 1.0/128.0) -> Dictionary: """Register peer 1 and validate the packet in one call.""" if not ac.is_tracking(1): ac.register_player(1) return ac.validate_input(1, pkt, delta) # ── Test Suites ──────────────────────────────────────────────────────────── func _test_sequence_validation() -> void: tu.describe("Sequence Validation — edge cases") # Normal first packet var r = _register_and_validate(_make_input({"seq": 1, "time": 1000.0})) tu.assert_no_violation(r, "first packet accepted") # Normal second packet — monotonic r = ac.validate_input(1, _make_input({"seq": 2, "time": 1000.016}), 0.0078) tu.assert_no_violation(r, "monotonic seq accepted") # Replay — same seq r = ac.validate_input(1, _make_input({"seq": 2, "time": 1001.0}), 0.0078) tu.assert_violation(r, ViolationCategory.INPUT_SEQUENCE, "replay detected") tu.assert_corrected(r, "seq", "replay seq corrected") # Regression — lower seq r = ac.validate_input(1, _make_input({"seq": 1, "time": 1002.0}), 0.0078) tu.assert_violation(r, ViolationCategory.INPUT_SEQUENCE, "regression detected") # Timestamp regression r = ac.validate_input(1, _make_input({"seq": 5, "time": 999.0}), 0.0078) tu.assert_violation(r, ViolationCategory.INPUT_SEQUENCE, "timestamp regression detected") # Large gap (jump forward) — should be accepted as valid (player may lag) r = ac.validate_input(1, _make_input({"seq": 1000, "time": 1010.0}), 0.0078) tu.assert_no_violation(r, "large forward jump accepted (possible lag)") # Very large seq number (near overflow boundary) r = ac.validate_input(1, _make_input({"seq": 2147483640, "time": 1020.0}), 0.0078) tu.assert_no_violation(r, "large seq number accepted") # Overflow wraparound — negative seq after positive is regression r = ac.validate_input(1, _make_input({"seq": -5, "time": 1030.0}), 0.0078) tu.assert_violation(r, ViolationCategory.INPUT_SEQUENCE, "negative seq after positive is regression") # First packet always accepted (last_seq was -1) ac.unregister_player(2) ac.register_player(2) r = ac.validate_input(2, _make_input({"seq": 99999, "time": 1.0}), 0.0078) tu.assert_no_violation(r, "arbitrary first seq accepted") ac.unregister_player(1) func _test_movement_teleport() -> void: tu.describe("Movement — Teleport Detection Edge Cases") # Reset: register fresh peer 3 ac.unregister_player(3) ac.register_player(3) # First packet at origin → teleport check skipped (init position) var r = ac.validate_input(3, _make_input({"seq": 1, "position": Vector3(0, 0, 0), "time": 1000.0}), 0.0078) tu.assert_no_violation(r, "first position accepted") # Legitimate small movement r = ac.validate_input(3, _make_input({"seq": 2, "position": Vector3(10, 0, 10), "time": 1000.016}), 0.0078) tu.assert_no_violation(r, "small movement accepted") # Teleport — large displacement in one tick r = ac.validate_input(3, _make_input({"seq": 3, "position": Vector3(1000, 500, 1000), "time": 1000.032}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_TELEPORT, "teleport > threshold detected") tu.assert_corrected(r, "position", "position corrected to prev") # Reset player and test teleport threshold exactly at boundary ac.unregister_player(4) ac.register_player(4) r = ac.validate_input(4, _make_input({"seq": 1, "position": Vector3(0, 0, 0), "time": 2000.0}), 0.0078) tu.assert_no_violation(r, "fresh start") # Move just under threshold (sv_ac_teleport_threshold = 100.0) r = ac.validate_input(4, _make_input({"seq": 2, "position": Vector3(99.9, 0, 0), "time": 2000.016}), 0.0078) tu.assert_no_violation(r, "movement just under teleport threshold") # At exactly threshold (boundary) r = ac.validate_input(4, _make_input({"seq": 3, "position": Vector3(199.9, 0, 0), "time": 2000.032}), 0.0078) tu.assert_no_violation(r, "exactly at teleport threshold (100.0 units, not over)") # Just over threshold r = ac.validate_input(4, _make_input({"seq": 4, "position": Vector3(300.1, 0, 0), "time": 2000.048}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_TELEPORT, "just over teleport threshold detected") ac.unregister_player(4) func _test_movement_speed() -> void: tu.describe("Movement — Speed Limit Edge Cases") ac.unregister_player(5) ac.register_player(5) # First packet var r = ac.validate_input(5, _make_input({"seq": 1, "position": Vector3(0, 0, 0), "time": 3000.0, "max_speed": 320.0}), 0.0078) tu.assert_no_violation(r, "first position accepted") # Legitimate max-speed movement: 320 u/s * 1/128 s/tick = ~2.5 u/tick # At tolerance 1.2: max_allowed = 320 * 1.2 * 0.0078 = 2.995 u/tick r = ac.validate_input(5, _make_input({"seq": 2, "position": Vector3(2.9, 0, 0), "time": 3000.016, "max_speed": 320.0}), 0.0078) tu.assert_no_violation(r, "speed within tolerance accepted") # Slight overspeed: 3.1 u/tick > 2.995 = violation r = ac.validate_input(5, _make_input({"seq": 3, "position": Vector3(6.0, 0, 0), "time": 3000.032, "max_speed": 320.0}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_SPEED, "overspeed detected") # Speed test with player at non-zero prev position ac.unregister_player(6) ac.register_player(6) r = ac.validate_input(6, _make_input({"seq": 1, "position": Vector3(100, 100, 100), "time": 4000.0}), 0.0078) # Valid movement from non-zero position r = ac.validate_input(6, _make_input({"seq": 2, "position": Vector3(102, 100, 100), "time": 4000.016}), 0.0078) tu.assert_no_violation(r, "valid movement from non-zero position") ac.unregister_player(5) ac.unregister_player(6) func _test_multi_jump_detection() -> void: tu.describe("Movement — Multi-Jump Detection Edge Cases") # Key fix: jump_count must NOT reset on button release while airborne ac.unregister_player(7) ac.register_player(7) # Player on ground, jumps ac.set_ground_contact(7, true) var r = ac.validate_input(7, _make_input({"seq": 1, "buttons": 1, "position": Vector3(0, 0, 0), "time": 5000.0}), 0.0078) tu.assert_no_violation(r, "first jump from ground accepted") # After physics: player is now airborne ac.set_ground_contact(7, false) # Player still holding jump in air — second jump attempt r = ac.validate_input(7, _make_input({"seq": 2, "buttons": 1, "position": Vector3(0, 0.1, 0), "time": 5000.016}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_MULTI_JUMP, "double jump detected while holding") tu.assert_corrected(r, "buttons", "jump flag corrected") # Now test the bypass that was fixed: player releases and re-presses jump mid-air ac.unregister_player(8) ac.register_player(8) ac.set_ground_contact(8, true) r = ac.validate_input(8, _make_input({"seq": 1, "buttons": 1, "position": Vector3(0, 0, 0), "time": 6000.0}), 0.0078) tu.assert_no_violation(r, "player 8 first jump") ac.set_ground_contact(8, false) # Release jump while still airborne r = ac.validate_input(8, _make_input({"seq": 2, "buttons": 0, "position": Vector3(0, 0.1, 0), "time": 6000.016}), 0.0078) tu.assert_no_violation(r, "button release in air passes (no jump flag)") # Re-press jump in air — should be caught because jump_count did NOT reset # (fix: jump_count only resets on ground contact, not button release) r = ac.validate_input(8, _make_input({"seq": 3, "buttons": 1, "position": Vector3(0, 0.2, 0), "time": 6000.032}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_MULTI_JUMP, "tap-jump bypass prevented") # Release and re-press again to confirm counting continues r = ac.validate_input(8, _make_input({"seq": 4, "buttons": 0, "position": Vector3(0, 0.2, 0), "time": 6000.048}), 0.0078) tu.assert_no_violation(r, "another release passes") r = ac.validate_input(8, _make_input({"seq": 5, "buttons": 1, "position": Vector3(0, 0.3, 0), "time": 6000.064}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_MULTI_JUMP, "third aerial tap-jump attempt prevented") # Landing resets the counter ac.set_ground_contact(8, true) r = ac.validate_input(8, _make_input({"seq": 6, "buttons": 0, "position": Vector3(0, 0, 0), "time": 6001.0}), 0.0078) tu.assert_no_violation(r, "ground contact passes") r = ac.validate_input(8, _make_input({"seq": 7, "buttons": 1, "position": Vector3(0, 0, 0), "time": 6001.016}), 0.0078) tu.assert_no_violation(r, "jump from ground after landing accepted (counter reset)") ac.unregister_player(7) ac.unregister_player(8) func _test_aim_snap() -> void: tu.describe("Aim Validation — Snap Detection Edge Cases") ac.unregister_player(9) ac.register_player(9) # First packet initialises aim var r = ac.validate_input(9, _make_input({"seq": 1, "view_angles": Vector2(0, 0), "time": 7000.0}), 0.0078) tu.assert_no_violation(r, "first aim accepted") # Small rotation r = ac.validate_input(9, _make_input({"seq": 2, "view_angles": Vector2(5, 10), "time": 7000.016}), 0.0078) tu.assert_no_violation(r, "small rotation accepted") # Large snap — 181 degrees > 180 max r = ac.validate_input(9, _make_input({"seq": 3, "view_angles": Vector2(0, 190), "time": 7000.032}), 0.0078) tu.assert_violation(r, ViolationCategory.AIM_SNAP, "aim snap > 180° detected") tu.assert_corrected(r, "view_angles", "view_angles corrected") # Exactly at boundary (180 degrees) ac.unregister_player(10) ac.register_player(10) r = ac.validate_input(10, _make_input({"seq": 1, "view_angles": Vector2(0, 0), "time": 8000.0}), 0.0078) r = ac.validate_input(10, _make_input({"seq": 2, "view_angles": Vector2(0, 180), "time": 8000.016}), 0.0078) tu.assert_no_violation(r, "exactly 180° aim change at boundary accepted") # Just over boundary (180.1) r = ac.validate_input(10, _make_input({"seq": 3, "view_angles": Vector2(0, 360.1), "time": 8000.032}), 0.0078) tu.assert_violation(r, ViolationCategory.AIM_SNAP, "just over 180° snap detected") ac.unregister_player(9) ac.unregister_player(10) func _test_fire_rate() -> void: tu.describe("Fire Rate — Interval Edge Cases") ac.unregister_player(11) ac.register_player(11) # First fire always accepted var r = ac.validate_input(11, _make_input({ "seq": 1, "buttons": 2, # fire bitmask = 2 "time": 9000.0, "weapon_fire_rate_ms": 80.0, }), 0.0078) tu.assert_no_violation(r, "first fire always accepted") # Fire too soon — 40ms < (80-10=70ms) r = ac.validate_input(11, _make_input({ "seq": 2, "buttons": 2, "time": 9000.04, "weapon_fire_rate_ms": 80.0, }), 0.0078) tu.assert_violation(r, ViolationCategory.FIRE_RATE_INTERVAL, "fire too soon detected") tu.assert_corrected(r, "buttons", "fire flag corrected") # Fire at exactly min interval (80ms, with 10ms tolerance → 70ms allowed) r = ac.validate_input(11, _make_input({ "seq": 3, "buttons": 2, "time": 9000.11, "weapon_fire_rate_ms": 80.0, }), 0.0078) tu.assert_no_violation(r, "fire at exactly min interval after tolerance accepted") # Weapon with very fast fire rate (pistol: 200ms) r = ac.validate_input(11, _make_input({ "seq": 4, "buttons": 2, "time": 9000.32, "weapon_fire_rate_ms": 200.0, }), 0.0078) tu.assert_no_violation(r, "fire with pistol rate accepted") ac.unregister_player(11) func _test_command_rate() -> void: tu.describe("Command Rate — Sliding Window Edge Cases") ac.unregister_player(12) ac.register_player(12) # Rapid commands to fill the window var base_time = 10000.0 var r: Dictionary var violations_found = false var rapid_seq = 0 for i in range(150): rapid_seq += 1 var t = base_time + i * 0.004 # 250 Hz command rate r = ac.validate_input(12, _make_input({"seq": rapid_seq, "time": t}), 0.0078) if r.get("violations", []).size() > 0: violations_found = true break tu.assert_true(violations_found, "command rate limit kicks in after ~128+ commands in window") # Burst allowance for new connections ac.unregister_player(13) ac.register_player(13) # Player 13 connected within last 3 seconds — has burst allowance var burst_violations = false rapid_seq = 0 for i in range(140): rapid_seq += 1 var t = 11000.0 + i * 0.004 r = ac.validate_input(13, _make_input({"seq": rapid_seq, "time": t}), 0.0078) if r.get("violations", []).size() > 0: burst_violations = true break tu.assert_true(burst_violations, "burst allowance exceeded") ac.unregister_player(12) ac.unregister_player(13) func _test_ammo_consistency() -> void: tu.describe("Ammo Consistency — Edge Cases") ac.unregister_player(14) ac.register_player(14) # First ammo report var r = ac.validate_input(14, _make_input({ "seq": 1, "buttons": 0, "ammo": 30, "time": 12000.0 }), 0.0078) tu.assert_no_violation(r, "first ammo report accepted") # Fire weapon: ammo decreases by 1 r = ac.validate_input(14, _make_input({ "seq": 2, "buttons": 2, "ammo": 29, "time": 12000.1 }), 0.0078) tu.assert_no_violation(r, "ammo decreased by 1 after fire accepted") # Ammo increases while firing — cheat r = ac.validate_input(14, _make_input({ "seq": 3, "buttons": 2, "ammo": 30, "time": 12000.2 }), 0.0078) tu.assert_violation(r, ViolationCategory.FIRE_RATE_AMMO, "ammo increase while firing detected") # Ammo doesn't change while firing (should decrease by burst_count=1) r = ac.validate_input(14, _make_input({ "seq": 4, "buttons": 2, "ammo": 30, "time": 12000.3, "burst_count": 1, }), 0.0078) tu.assert_violation(r, ViolationCategory.FIRE_RATE_AMMO, "ammo not decreasing while firing detected") ac.unregister_player(14) func _test_enforcement_levels() -> void: tu.describe("Enforcement — Level Interaction Edge Cases") # Save and set to OFF var saved_level = ac.sv_ac_level ac.sv_ac_level = AcLevel.OFF ac.unregister_player(15) ac.register_player(15) # Should return no violations at OFF level var r = ac.validate_input(15, _make_input({ "seq": 1, "position": Vector3(0, 0, 0), "time": 13000.0 }), 0.0078) tu.assert_no_violation(r, "no violations at OFF level") # Even blatant cheats should pass r = ac.validate_input(15, _make_input({ "seq": 99999, "position": Vector3(9999, 9999, 9999), "time": 13000.1 }), 0.0078) tu.assert_no_violation(r, "blatant cheat passes at OFF level") ac.unregister_player(15) ac.sv_ac_level = saved_level func _test_concurrent_players() -> void: tu.describe("Concurrent Players — State Isolation") # Register 3 simultaneous players ac.register_player(20) ac.register_player(21) ac.register_player(22) ac.set_ground_contact(20, true) ac.set_ground_contact(21, true) ac.set_ground_contact(22, true) # All send first packets var r20 = ac.validate_input(20, _make_input({"seq": 1, "time": 14000.0}), 0.0078) var r21 = ac.validate_input(21, _make_input({"seq": 1, "time": 14000.0}), 0.0078) var r22 = ac.validate_input(22, _make_input({"seq": 1, "time": 14000.0}), 0.0078) tu.assert_no_violation(r20, "player 20 first") tu.assert_no_violation(r21, "player 21 first") tu.assert_no_violation(r22, "player 22 first") # Player 20 jumps while airborne ac.set_ground_contact(20, false) r20 = ac.validate_input(20, _make_input({"seq": 2, "buttons": 1, "position": Vector3(0, 1, 0), "time": 14000.016}), 0.0078) r21 = ac.validate_input(21, _make_input({"seq": 2, "position": Vector3(2, 0, 0), "time": 14000.016}), 0.0078) r22 = ac.validate_input(22, _make_input({"seq": 2, "position": Vector3(0, 0, 2), "time": 14000.016}), 0.0078) tu.assert_no_violation(r21, "player 21 clean") tu.assert_no_violation(r22, "player 22 clean") # Player 20 jumps again while airborne (3rd tick with jump button) r20 = ac.validate_input(20, _make_input({"seq": 3, "buttons": 1, "position": Vector3(0, 2, 0), "time": 14000.032}), 0.0078) tu.assert_violation(r20, ViolationCategory.MOVEMENT_MULTI_JUMP, "player 20 multi-jump detected independently") # Verify violation counts are isolated var v20_count = ac.get_player_violation_count(20) var v21_count = ac.get_player_violation_count(21) var v22_count = ac.get_player_violation_count(22) tu.assert_true(v20_count > 0, "player 20 has violations") tu.assert_eq(v21_count, 0, "player 21 has zero violations") tu.assert_eq(v22_count, 0, "player 22 has zero violations") ac.unregister_player(20) ac.unregister_player(21) ac.unregister_player(22) func _test_missing_data_fields() -> void: tu.describe("Missing/Null Data — Graceful Handling") ac.unregister_player(30) ac.register_player(30) # Packet missing 'seq' entirely var r = ac.validate_input(30, {"time": 15000.0, "buttons": 0}, 0.0078) tu.assert_no_violation(r, "missing seq field handled gracefully") # Packet missing 'position' r = ac.validate_input(30, {"seq": 2, "time": 15000.016, "buttons": 0}, 0.0078) tu.assert_no_violation(r, "missing position handled gracefully") # Packet missing 'view_angles' r = ac.validate_input(30, {"seq": 3, "time": 15000.032, "buttons": 0, "position": Vector3(0, 0, 0)}, 0.0078) tu.assert_no_violation(r, "missing view_angles handled gracefully") # Packet missing 'time' r = ac.validate_input(30, {"seq": 4, "position": Vector3.ZERO, "buttons": 0}, 0.0078) tu.assert_no_violation(r, "missing time handled gracefully") # Packet with wrong types r = ac.validate_input(30, {"seq": "not_an_int", "time": "not_a_float", "buttons": null, "position": "wrong_type"}, 0.0078) tu.assert_no_violation(r, "wrong types handled gracefully (no crash)") ac.unregister_player(30) func _test_reset_player() -> void: tu.describe("Reset Player State — State Cleanup") ac.register_player(40) ac.set_ground_contact(40, true) var r = ac.validate_input(40, _make_input({"seq": 1, "time": 16000.0}), 0.0078) tu.assert_no_violation(r, "initial validation ok") # Reset ac.reset_player_violations(40) tu.assert_eq(ac.get_player_violation_count(40), 0, "violations reset to 0") tu.assert_eq(ac.get_player_violations(40).size(), 0, "violations log cleared") ac.unregister_player(40) func _test_clear_all() -> void: tu.describe("Clear All — Global State Cleanup") ac.register_player(50) ac.register_player(51) ac.register_player(52) tu.assert_true(ac.is_tracking(50), "player 50 tracked") tu.assert_true(ac.is_tracking(51), "player 51 tracked") tu.assert_true(ac.is_tracking(52), "player 52 tracked") ac.clear_all() tu.assert_false(ac.is_tracking(50), "player 50 cleared") tu.assert_false(ac.is_tracking(51), "player 51 cleared") tu.assert_false(ac.is_tracking(52), "player 52 cleared") # Should still work after clearing ac.register_player(50) tu.assert_true(ac.is_tracking(50), "re-register after clear works") # ═══════════════════════════════════════════════════════════════════════════ # NEW: Edge Case Tests (Phase 6 bug bash) # ═══════════════════════════════════════════════════════════════════════════ func _test_aim_yaw_wraparound() -> void: tu.describe("Aim — Yaw Wraparound (359° → 1° should NOT flag)") ac.unregister_player(60) ac.register_player(60) # First packet initialises aim at yaw=359 var r = ac.validate_input(60, _make_input({"seq": 1, "view_angles": Vector2(0, 359), "time": 17000.0}), 0.0078) tu.assert_no_violation(r, "first aim at 359° accepted") # Small rotation around the 0° boundary: 359→1 = 2° delta (shortest path) # Without yaw wraparound fix, this would be |1-359| = 358° → false snap flag r = ac.validate_input(60, _make_input({"seq": 2, "view_angles": Vector2(0, 1), "time": 17000.016}), 0.0078) tu.assert_no_violation(r, "359° → 1° (2° shortest path) not flagged as snap") # Full wraparound: 1→359 = 2° delta backward (shortest path) r = ac.validate_input(60, _make_input({"seq": 3, "view_angles": Vector2(0, 359), "time": 17000.032}), 0.0078) tu.assert_no_violation(r, "1° → 359° (2° shortest path, reverse) not flagged as snap") # Legitimate large snap still caught: 359→200 = 159° which is still less than 180° r = ac.validate_input(60, _make_input({"seq": 4, "view_angles": Vector2(0, 200), "time": 17000.048}), 0.0078) tu.assert_no_violation(r, "359° → 200° (159° delta, under 180° max) accepted") # True snap that wraps: 200→10 = 190° shortest path → should flag r = ac.validate_input(60, _make_input({"seq": 5, "view_angles": Vector2(0, 10), "time": 17000.064}), 0.0078) tu.assert_violation(r, ViolationCategory.AIM_SNAP, "200° → 10° (190° shortest path) flagged as snap") ac.unregister_player(60) func _test_movement_first_position_origin() -> void: tu.describe("Movement — First Position at Origin (Vector3.ZERO)") # Bug: If a player starts at position (0,0,0), the old code skipped # the second tick movement validation entirely, allowing undetected teleport. ac.unregister_player(61) ac.register_player(61) # First packet at origin var r = ac.validate_input(61, _make_input({"seq": 1, "position": Vector3(0, 0, 0), "time": 18000.0}), 0.0078) tu.assert_no_violation(r, "first position at origin accepted") # Second packet — teleport far away (should be flagged now with the fix) r = ac.validate_input(61, _make_input({"seq": 2, "position": Vector3(999, 999, 999), "time": 18000.016}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_TELEPORT, "teleport from origin on tick 2 flagged (bypass fixed)") ac.unregister_player(61) func _test_movement_second_packet_speed_check() -> void: tu.describe("Movement — Speed check on second tick after origin") ac.unregister_player(62) ac.register_player(62) # First packet var r = ac.validate_input(62, _make_input({"seq": 1, "position": Vector3.ZERO, "time": 19000.0}), 0.0078) tu.assert_no_violation(r, "first position accepted") # Second packet: valid speed (within tolerance from origin) # At max_speed=320, tol=1.2, delta=1/128: max this tick = 320*1.2*0.0078 ≈ 2.995 r = ac.validate_input(62, _make_input({"seq": 2, "position": Vector3(2.5, 0, 0), "time": 19000.016, "max_speed": 320.0}), 0.0078) tu.assert_no_violation(r, "valid speed at tick 2 from origin accepted") # Third packet: overspeed at 6.0 units from 2.5 = 3.5 u > 2.995 max r = ac.validate_input(62, _make_input({"seq": 3, "position": Vector3(6.0, 0, 0), "time": 19000.032, "max_speed": 320.0}), 0.0078) tu.assert_violation(r, ViolationCategory.MOVEMENT_SPEED, "overspeed on tick 3 flagged") ac.unregister_player(62) # ── Main Entry Point ────────────────────────────────────────────────────── func _ready() -> void: """Run all tests and exit.""" print("\n══════════════════════════════════════════════") print(" Anti-Cheat Unit Tests") print("══════════════════════════════════════════════\n") # Instantiate anti-cheat from preload ac = AC_SCRIPT.new() add_child(ac) # Force specific values for deterministic tests ac.sv_ac_level = AcLevel.KICK ac.sv_ac_speed_tolerance = 1.2 ac.sv_ac_max_aim_snap = 180.0 ac.sv_ac_fire_tolerance_ms = 10 ac.sv_ac_command_rate_max = 128 ac.sv_ac_teleport_threshold = 100.0 ac.sv_ac_kick_threshold = 5 ac.sv_ac_check_multi_jump = true ac.sv_ac_check_ammo = true tu = UTILS_SCRIPT.new() add_child(tu) # Run all test suites _test_sequence_validation() _test_movement_teleport() _test_movement_speed() _test_multi_jump_detection() _test_aim_snap() _test_fire_rate() _test_command_rate() _test_ammo_consistency() _test_enforcement_levels() _test_concurrent_players() _test_missing_data_fields() _test_reset_player() _test_clear_all() # Phase 6 bug bash edge case tests _test_aim_yaw_wraparound() _test_movement_first_position_origin() _test_movement_second_packet_speed_check() # Summary print("══════════════════════════════════════════════") var total = tu._passed + tu._failed print(" Total: %d passed, %d failed out of %d" % [tu._passed, tu._failed, total]) print("══════════════════════════════════════════════\n") get_tree().quit(tu.exit_code())