Files
tactical-shooter/tests/README.md
T
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

218 lines
8.3 KiB
Markdown

# 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:
```bash
# 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):
```bash
# 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
```gdscript
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
```gdscript
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
```gdscript
# 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
```gdscript
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:
```gdscript
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`