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)
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'sput_packet()- Separated outbound and inbound queues (
_outbound_queuefor game→network,_inbound_queuefor network→game). Previously everything used_inbound_queue. receive_packet()andhas_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)
net_sim.gd: Packet delivery pipeline broken — packets never reach real peeranti_cheat.gd: Aim validation yaw wraparound causes false positivesanti_cheat.gd: First position at origin bypasses movement validation on tick 2net_sim.gd:_outbound_queuedead code (minor)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
- Create a
.gdfile in this directory - Extend
Nodeand implement_ready()to run tests - Use
TestUtilsfor 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()) - Add to
RUN_TESTS.sh