Files
tactical-shooter/tests
shawn e7299b17e9 Phase 7: netfox + godot-jolt stack upgrade
Stack installed:
- netfox v1.35.3 (core + extras + noray + internals)
- godot-jolt v0.16.0-stable

Architecture:
- Server: ENet transport (works headless, no netfox deps)
- Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator)

New/modified:
- docs/migration-netfox-plan.md — migration architecture
- scripts/network/network_manager.gd — netfox-aware ENet fallback
- scripts/network/player.gd — clean base player
- client/characters/player_netfox.gd — rollback player w/ WeaponManager
- client/characters/input/player_net_input.gd — BaseNetInput subclass
- client/characters/character/fps_character_controller.gd — netfox input feed
- client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager
- client/scripts/round_replicator.gd — client-side round state bridge
- server/scripts/round_manager.gd — improved state machine
- server/scripts/plugin_api/plugin_manager.gd — refined plugin system
- config: enemy_tag, ally_tag for meatball targeting

Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
2026-07-02 17:39:22 -04:00
..

Tests — Tactical Shooter Netcode Bug Bash

This directory contains tests and tools for the Phase 6 netcode bug bash: netcode edge cases, packet loss simulation, and high ping testing.

Quick Start

Run all tests headlessly:

# Option A: Direct test runner script
./tests/RUN_TESTS.sh

# Option B: Manual Godot invocation
godot --headless --script tests/test_anti_cheat.gd
godot --headless --script tests/test_net_sim.gd
godot --headless --script tests/test_rcon_edge_cases.gd

Run OS-level network simulation (requires root):

# Apply a network profile to the game server port
sudo ./tests/netem_profiles.sh apply cellular

# Test under extreme conditions
sudo ./tests/netem_profiles.sh apply warzone

# Remove all netem rules
sudo ./tests/netem_profiles.sh off

# Show current tc qdisc configuration
sudo ./tests/netem_profiles.sh status

Test Files

File What It Tests
test_utils.gd Minimal assertion framework for headless Godot tests
test_anti_cheat.gd Anti-cheat validation pipeline: sequence, movement, aim (including yaw wraparound), fire rate, command rate, ammo edge cases, first-position-at-origin bypass
test_net_sim.gd Network condition simulator: packet loss, latency, jitter, reordering, duplication, burst loss, bandwidth + 6 new edge case tests (jitter > latency, 25%+500ms combined, burst+reorder, 10k rapid sequence, bandwidth+loss, zero-condition passthrough)
test_rcon_edge_cases.gd RCON protocol edge cases: auth state machine, buffer overflow, malformed commands

Network Simulator (server/scripts/net_sim.gd)

Wraps any MultiplayerPeer (ENet, WebSocket, etc.) to inject simulated adverse network conditions. Use it to test gameplay under real-world network scenarios without needing actual bad connections.

Quick Profiles

var sim = NetSim.new()
sim.set_profile("lan")       # 0% loss, 1ms latency
sim.set_profile("dsl")       # 0.5% loss, 20ms ±5ms
sim.set_profile("cellular")  # 2% loss, 60ms ±20ms
sim.set_profile("satellite") # 1% loss, 600ms ±50ms
sim.set_profile("congested") # 5% loss, 100ms ±40ms, reorder
sim.set_profile("warzone")   # 10% loss, 200ms ±60ms, burst loss

Manual Configuration

sim.packet_loss = 0.05       # 5% packet loss
sim.latency_ms = 100         # 100ms one-way delay
sim.jitter_ms = 30           # ±30ms variable delay
sim.reorder_window = 4       # shuffle groups of 4
sim.duplicate_rate = 0.01    # 1% duplicate packets
sim.bandwidth_limit = 50000  # 50KB/s cap
sim.burst_loss_count = 3     # drop 3 consecutive
sim.burst_loss_interval = 50 # every 50 packets

Usage with ENet

# Create real peer
var real_peer = ENetMultiplayerPeer.new()
real_peer.create_server(7777, 16)

# Wrap with simulator
var sim = NetSim.new()
# IMPORTANT: You must modify game code to call sim.send_packet()/sim.receive_packet()
# instead of the real peer directly. See net_sim.gd for the API.

Debug Stats

print(sim.get_stats_string())
# Example output:
#   Sent:           1500
#   Lost (loss):    75
#   Lost (burst):   12
#   Duplicated:     8
#   Effective loss: 5.1%
#   Config: loss=5.0% lat=100ms jitter=±30ms ...

Bugs Found & Fixed (Phase 6 Bug Bash)

Critical: NetSim packet delivery pipeline broken (net_sim.gd)

Root cause: _deliver_to_game() was a no-op (pass). When _process() ran every frame, it found packets whose delay had expired, removed them from the queue, and called _deliver_to_game() which did nothing. The packets vanished into the void. Additionally, _send_to_real_peer() was defined but never called — packets were queued internally but never forwarded to the real ENet peer.

Fix:

  • _send_to_real_peer() is now called from _process() when outbound delay expires
  • _deliver_to_game() removed; the outbound pipeline is now: send_packet → queue → _process_send_to_real_peer → real peer's put_packet()
  • Separated outbound and inbound queues (_outbound_queue for game→network, _inbound_queue for network→game). Previously everything used _inbound_queue.
  • receive_packet() and has_packet() now fall through to the real peer when the inbound simulation queue is empty
  • Added _queue_delayed_outbound() and _queue_delayed_inbound() as dedicated queue functions
  • Removed dead _queue_delayed_packet() function

Bug: _outbound_queue dead code (net_sim.gd)

Root cause: var _outbound_queue was declared (line 82) but never written to. All delayed packets went into _inbound_queue, mixing outbound and inbound traffic in the same queue with no directional separation.

Fix: send_packet() now queues in _outbound_queue. _process() forwards expired outbound packets to the real peer. _inbound_queue is now truly for inbound packets read from the real peer.

Bug: Anti-cheat aim yaw wraparound false positives (anti_cheat.gd)

Root cause: _validate_aim() computed (angles - state.last_view_angles).abs() without handling yaw circular wraparound. If a player's last yaw was 359° and they turned right 2° to 1°, the delta was |1 - 359| = 358°, triggering a false positive aim snap violation.

Fix: Added shortest-angular-distance calculation using fmod(raw_delta, 360.0) with wraparound correction: values > 180° are subtracted from 360° to get the true shortest path. Applied to both violation detection and aim angle clamping.

Bug: First-position-at-origin teleport bypass (anti_cheat.gd)

Root cause: _validate_movement() had this early-return guard:

if prev_pos == Vector3.ZERO and state.last_seq != -1:
    if state.violation_count == 0:
        return  # SKIPS ALL MOVEMENT VALIDATION

If a player spawned at Vector3(0,0,0) (a valid map origin), the second tick's movement validation was entirely skipped, allowing undetected teleport on tick 2.

Fix: Removed the entire guard. The first packet is still protected by the state.last_seq != -1 check in the teleport/speed tests (which correctly skips the first-ever packet). All subsequent packets are validated normally regardless of position value.

Bug: Companion file net_sim_peer.gd referenced but missing

Root cause: net_sim.gd line 254 references a companion file net_sim_peer.gd for the "full MultiplayerPeer adapter," but this file does not exist anywhere in the project.

Status: Documented. The architecture relies on game code calling send_packet() and receive_packet() directly. A transparent MultiplayerPeer wrapper would be needed for drop-in replacement without game code changes.

Missing Gitea Issues (to be created manually)

  1. net_sim.gd: Packet delivery pipeline broken — packets never reach real peer
  2. anti_cheat.gd: Aim validation yaw wraparound causes false positives
  3. anti_cheat.gd: First position at origin bypasses movement validation on tick 2
  4. net_sim.gd: _outbound_queue dead code (minor)
  5. net_sim.gd: Bandwidth check order — random loss reduces effective bandwidth

Bugs Found & Fixed

Bug: Multi-jump detection bypass (anti_cheat.gd)

Root cause: jump_count was reset to 0 every time the jump button was released, regardless of whether the player was airborne. This let players tap-jump repeatedly while mid-air, bypassing the single-jump limit.

Fix: jump_count now only resets on ground contact (was_on_ground = true). Button release while airborne no longer resets the counter.

Bug: Sequence number not corrected in output (anti_cheat.gd)

Root cause: When _validate_sequence detected a non-monotonic seq number, the corrected output packet was never updated — it still contained the invalid seq.

Fix: On seq regression, corrected["seq"] is now set to state.last_seq + 1.

Bug: Duplicate cleanup loop (anti_cheat.gd)

Root cause: The command rate sliding window cleanup had two identical while state.command_times.size() > limit + 8 loops, the second being dead code.

Fix: Removed the duplicate loop. One cleanup pass is sufficient.

Adding New Tests

  1. Create a .gd file in this directory
  2. Extend Node and implement _ready() to run tests
  3. Use TestUtils for assertions:
    var tu = TestUtils.new()
    add_child(tu)
    tu.describe("My Feature")
    tu.assert_eq(result, expected, "what we're checking")
    tu.finish()
    get_tree().quit(tu.exit_code())
    
  4. Add to RUN_TESTS.sh