Files
tactical-shooter/tests/test_net_sim.gd
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

649 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
# ═══════════════════════════════════════════════════════════════════════════
# Network Simulator — Unit & Integration Tests
#
# Tests the NetSim network condition simulator for correctness:
# - Packet loss rate (statistical, within confidence)
# - Latency simulation (timing)
# - Jitter distribution
# - Packet reordering
# - Duplication rate
# - Burst loss pattern
# - Bandwidth throttling
# - Silent passthrough with all conditions at 0
# - Profile presets
# - Multi-peer isolation
#
# Run with: godot --headless --script tests/test_net_sim.gd
# ═══════════════════════════════════════════════════════════════════════════
const SIM_SCRIPT = preload("res://server/scripts/net_sim.gd")
const UTILS_SCRIPT = preload("res://tests/test_utils.gd")
var sim # NetSim — loaded via preload
var tu # TestUtils — loaded via preload
# ─── Test Suites ──────────────────────────────────────────────────────────
func _test_silent_passthrough() -> void:
tu.describe("Silent Passthrough — zero conditions, no side effects")
sim.packet_loss = 0.0
sim.latency_ms = 0
sim.jitter_ms = 0
sim.reorder_window = 0
sim.duplicate_rate = 0.0
sim.bandwidth_limit = 0
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
var sent = 100
var received = 0
for i in range(sent):
var pkt = SIM_SCRIPT.make_test_packet(i, "test")
if sim.send_packet(pkt, SIM_SCRIPT.CHANNEL_UNRELIABLE):
pass
# All should be in inbound queue (no loss)
received = sim._outbound_queue.size()
tu.assert_eq(received, sent, "all packets queued with no conditions")
# No dropped stats
tu.assert_eq(sim._total_lost, 0, "zero loss")
tu.assert_eq(sim._total_burst_dropped, 0, "zero burst drops")
tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bw drops")
# Drain queue
sim._outbound_queue.clear()
func _test_packet_loss_rate() -> void:
tu.describe("Packet Loss — statistical rate verification")
sim.packet_loss = 0.2 # 20% loss
sim.latency_ms = 0
sim.jitter_ms = 0
sim.reorder_window = 0
sim.duplicate_rate = 0.0
sim.bandwidth_limit = 0
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
sim.reset_stats()
var total = 5000
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "payload_%d" % i)
sim.send_packet(pkt)
# With 20% loss on 5000 packets: expected ~1000 lost
# Acceptable range: 8001200 (within 4 sigma of binomial)
var lost = sim._total_lost
tu.assert_true(lost >= 700 and lost <= 1300,
"loss count %d in acceptable range [700, 1300] for 20%% on %d" % [lost, total])
sim.reset_stats()
sim.packet_loss = 0.0
func _test_latency_timing() -> void:
tu.describe("Latency — timing accuracy")
sim.packet_loss = 0.0
sim.latency_ms = 100 # 100ms one-way
sim.jitter_ms = 0
sim.reorder_window = 0
sim.duplicate_rate = 0.0
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
sim.reset_stats()
var pkt = SIM_SCRIPT.make_test_packet(1, "latency_test")
var before = Time.get_ticks_msec()
sim.send_packet(pkt)
# Packet should be in queue with delay
tu.assert_eq(sim._outbound_queue.size(), 1, "one packet queued")
var queued = sim._outbound_queue[0]
var expected_delivery = before + 100
var actual_delivery = queued.time
var diff = actual_delivery - before
tu.assert_true(diff >= 95 and diff <= 105,
"latency %dms within [95, 105]ms tolerance (got %dms)" % [100, diff])
sim.reset_stats()
func _test_jitter_distribution() -> void:
tu.describe("Jitter — distribution within range")
sim.packet_loss = 0.0
sim.latency_ms = 50
sim.jitter_ms = 30 # ±30ms -> effective range: [20, 80]
sim.reset_stats()
var delays: Array[float] = []
var samples = 200
var before = Time.get_ticks_msec()
for i in range(samples):
var pkt = SIM_SCRIPT.make_test_packet(i, "jitter_test")
sim.send_packet(pkt)
var queued = sim._outbound_queue[i]
delays.append(queued.time - before)
before = Time.get_ticks_msec() # time moves forward naturally
# Check bounds: each delay should be latency ± jitter
# Min: max(0, 50-30) = 20, Max: 50+30 = 80
var within_bounds = true
var min_seen = 999.0
var max_seen = 0.0
for d in delays:
if d < 20.0 or d > 80.0:
within_bounds = false
min_seen = min(min_seen, d)
max_seen = max(max_seen, d)
tu.assert_true(within_bounds,
"all jittered delays within [20, 80]ms (min=%.1f max=%.1f)" % [min_seen, max_seen])
sim.reset_stats()
func _test_packet_duplication() -> void:
tu.describe("Duplication — statistical rate verification")
sim.packet_loss = 0.0
sim.latency_ms = 0
sim.duplicate_rate = 0.15 # 15%
sim.reset_stats()
var total = 2000
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "dupe_test")
sim.send_packet(pkt)
var dupe = sim._total_duplicated
tu.assert_true(dupe >= 150 and dupe <= 450,
"duplicate count %d in acceptable range [150, 450] for 15%% on %d" % [dupe, total])
sim.duplicate_rate = 0.0
sim.reset_stats()
func _test_burst_loss() -> void:
tu.describe("Burst Loss — pattern verification")
sim.packet_loss = 0.0
sim.latency_ms = 0
sim.burst_loss_count = 5 # drop 5 consecutive packets
sim.burst_loss_interval = 10 # every 10 packets
sim.reset_stats()
var total = 50
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "burst")
sim.send_packet(pkt)
# With burst_loss_interval=10 and burst_loss_count=5:
# Packets 10-14 dropped, 20-24 dropped, 30-34 dropped, 40-44 dropped = 20 drops
var burst_drops = sim._total_burst_dropped
tu.assert_eq(burst_drops, 20, "expected 20 burst drops out of 50 (5/10 pattern = 20)")
# The sum of inbound + burst_dropped should equal total
var queued = sim._outbound_queue.size()
tu.assert_eq(queued + burst_drops, total, "queued %d + burst_dropped %d = total %d" % [queued, burst_drops, total])
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
sim.reset_stats()
func _test_bandwidth_throttling() -> void:
tu.describe("Bandwidth — throttling verification")
sim.packet_loss = 0.0
sim.latency_ms = 0
var pkt_size = 100 # bytes per test packet
sim.bandwidth_limit = 2000 # 2000 bytes/s
sim.reset_stats()
var total = 100
for i in range(total):
var data = PackedByteArray()
data.resize(pkt_size)
data[0] = i
sim.send_packet(data)
# At 2000 bytes/s with 100-byte packets: max ~20 packets should pass
var bw_drops = sim._total_bandwidth_dropped
tu.assert_true(bw_drops >= 70,
"at least 70 of 100 packets dropped by bandwidth limit (got %d)" % bw_drops)
tu.assert_true(sim._outbound_queue.size() <= 30,
"at most 30 packets pass bandwidth limit (got %d)" % sim._outbound_queue.size())
sim.bandwidth_limit = 0
sim.reset_stats()
func _test_reorder_window() -> void:
tu.describe("Packet Reordering — window flipping")
sim.packet_loss = 0.0
sim.latency_ms = 1
sim.jitter_ms = 0
sim.reorder_window = 5
sim.reset_stats()
# Send 20 packets
for i in range(20):
var pkt = SIM_SCRIPT.make_test_packet(i, "reorder")
sim.send_packet(pkt)
# Flush the reorder buffer
sim._flush_reorder_buffer()
# The reorder buffer shuffles groups of 5.
# This doesn't guarantee reorder happened (shuffle may produce same order),
# but it should have triggered the reorder path
tu.assert_true(sim._total_reordered > 0,
"reordering occurred (>0 packets shuffled)")
sim.reorder_window = 0
sim.reset_stats()
func _test_profile_presets() -> void:
tu.describe("Profile Presets — quick-set correctness")
tu.assert_true(sim.set_profile("lan"), "lan profile set")
tu.assert_eq(sim.packet_loss, 0.0, "lan: no loss")
tu.assert_eq(sim.latency_ms, 1, "lan: 1ms latency")
tu.assert_true(sim.set_profile("cellular"), "cellular profile set")
tu.assert_eq(sim.packet_loss, 0.02, "cellular: 2% loss")
tu.assert_eq(sim.latency_ms, 60, "cellular: 60ms latency")
tu.assert_true(sim.set_profile("satellite"), "satellite profile set")
tu.assert_eq(sim.packet_loss, 0.01, "satellite: 1% loss")
tu.assert_eq(sim.latency_ms, 600, "satellite: 600ms latency")
tu.assert_true(sim.set_profile("warzone"), "warzone profile set")
tu.assert_eq(sim.packet_loss, 0.10, "warzone: 10% loss")
tu.assert_eq(sim.latency_ms, 200, "warzone: 200ms latency")
tu.assert_eq(sim.burst_loss_count, 3, "warzone: burst loss 3")
# Unknown profile should fail
tu.assert_false(sim.set_profile("nonexistent"), "unknown profile fails")
func _test_stats_report() -> void:
tu.describe("Stats Report — format correctness")
sim.packet_loss = 0.1
sim.reset_stats()
for i in range(100):
var pkt = SIM_SCRIPT.make_test_packet(i, "stats")
sim.send_packet(pkt)
var report = sim.get_stats_string()
tu.assert_true(report.contains("Sent:"), "report contains Sent:")
tu.assert_true(report.contains("Config:"), "report contains Config:")
tu.assert_true(report.contains("Effective loss:"), "report contains Effective loss:")
func _test_high_loss_extreme() -> void:
tu.describe("Extreme Conditions — 100% packet loss")
sim.packet_loss = 1.0
sim.latency_ms = 0
sim.reset_stats()
for i in range(100):
var pkt = SIM_SCRIPT.make_test_packet(i, "all_lost")
sim.send_packet(pkt)
tu.assert_eq(sim._total_lost, 100, "all 100 packets lost at 100% loss")
tu.assert_eq(sim._outbound_queue.size(), 0, "no packets in queue at 100% loss")
sim.packet_loss = 0.0
sim.reset_stats()
func _test_zero_bandwidth() -> void:
tu.describe("Edge: zero bandwidth limit")
sim.packet_loss = 0.0
sim.latency_ms = 0
sim.bandwidth_limit = 0 # 0 = unlimited
sim.reset_stats()
for i in range(10):
var pkt = SIM_SCRIPT.make_test_packet(i, "no_bw_limit")
sim.send_packet(pkt)
# Should not drop any — 0 means unlimited
tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bandwidth limit treated as unlimited")
tu.assert_eq(sim._outbound_queue.size(), 10, "all 10 packets queued with unlimited bw")
sim.reset_stats()
func _test_complex_profile() -> void:
tu.describe("Complex Profile — combined conditions")
# Simulate a realistic bad network: 3% loss, 80ms latency, 25ms jitter
sim.packet_loss = 0.03
sim.latency_ms = 80
sim.jitter_ms = 25
sim.duplicate_rate = 0.01
sim.reorder_window = 0
sim.bandwidth_limit = 0
sim.burst_loss_count = 0
sim.reset_stats()
var total = 1000
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "complex")
sim.send_packet(pkt)
var queued = sim._outbound_queue.size()
var lost = sim._total_lost
var duped = sim._total_duplicated
var delayed = sim._total_delayed
# At 3% loss: ~30 lost (range: 10-60)
tu.assert_true(lost >= 5 and lost <= 80,
"loss %d in range for 3%% on %d" % [lost, total])
# At 1% dupe: ~10 dupes
tu.assert_true(duped >= 0 and duped <= 40,
"dupes %d in range for 1%% on %d" % [duped, total])
# All non-lost packets should be delayed (latency > 0)
tu.assert_true(delayed >= queued,
"all queued packets marked as delayed (%d >= %d)" % [delayed, queued])
sim.reset_stats()
# ═══════════════════════════════════════════════════════════════════════════
# NEW: Edge Case Tests (Phase 6 bug bash)
# ═══════════════════════════════════════════════════════════════════════════
func _test_jitter_exceeds_latency() -> void:
tu.describe("Edge: Jitter exceeds latency — negative delay clamping")
# When jitter > latency, the per-packet delay can go negative.
# max(0, delay) clamps to 0. Test that all packets still get some delay
# and no crashes occur.
sim.packet_loss = 0.0
sim.latency_ms = 30
sim.jitter_ms = 100 # ±100ms on 30ms base → effective range [-70, +130] → clamped to [0, 130]
sim.reset_stats()
var total = 500
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "neg_jitter")
sim.send_packet(pkt)
# All packets should be queued (no loss)
var queued = sim._outbound_queue.size()
tu.assert_eq(queued, total, "all %d packets queued despite negative jitter" % total)
# All should be marked as delayed (some have 0ms due to clamping but
# the _total_delayed counter counts packets that got any non-zero delay)
tu.assert_true(sim._total_delayed > 0,
"at least some packets received non-zero delay (got %d)" % sim._total_delayed)
# Verify no negative delivery times
for entry in sim._outbound_queue:
tu.assert_true(entry.time >= 0, "all delivery times >= 0 (no wrap)")
sim.reset_stats()
func _test_combined_high_loss_latency() -> void:
tu.describe("Edge: High loss (25%) + high latency (500ms) combined")
# Simulate extreme network conditions: 25% loss, 500ms latency, 100ms jitter
sim.packet_loss = 0.25
sim.latency_ms = 500
sim.jitter_ms = 100
sim.duplicate_rate = 0.0
sim.bandwidth_limit = 0
sim.burst_loss_count = 0
sim.reset_stats()
var total = 2000
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "extreme")
sim.send_packet(pkt)
var lost = sim._total_lost
var queued = sim._outbound_queue.size()
# At 25% loss on 2000: expected ~500 lost (range: 350-650)
tu.assert_true(lost >= 300 and lost <= 700,
"loss %d in acceptable range [300, 700] for 25%% on %d" % [lost, total])
# Queued = total - lost
tu.assert_eq(queued, total - lost,
"queued %d + lost %d = total %d" % [queued, lost, total])
# All queued packets should have delay near 500ms
var min_delay = 99999.0
var max_delay = 0.0
var now = Time.get_ticks_msec()
for entry in sim._outbound_queue:
var effective = entry.time - now
min_delay = min(min_delay, effective)
max_delay = max(max_delay, effective)
# With 500ms latency + 100ms jitter: effective range [400, 600]
tu.assert_true(min_delay >= 0,
"minimum effective delay >= 0 (got %.1f)" % min_delay)
tu.assert_true(max_delay <= 700,
"maximum effective delay <= 700 (got %.1f)" % max_delay)
sim.reset_stats()
func _test_burst_with_reorder() -> void:
tu.describe("Edge: Burst loss + reorder window combined")
sim.packet_loss = 0.0
sim.latency_ms = 5
sim.jitter_ms = 0
sim.burst_loss_count = 3
sim.burst_loss_interval = 10
sim.reorder_window = 4
sim.reset_stats()
var total = 60
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "burst_reorder")
sim.send_packet(pkt)
# Flush reorder buffer
sim._flush_reorder_buffer()
# With interval=10, count=3: ~6 bursts = 18 drops
var burst_drops = sim._total_burst_dropped
var queued = sim._outbound_queue.size()
var triggered = sim._total_reordered
tu.assert_eq(queued + burst_drops, total,
"queued %d + burst_drops %d = total %d" % [queued, burst_drops, total])
tu.assert_true(triggered > 0,
"reordering was triggered (%d packets reordered)" % triggered)
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
sim.reorder_window = 0
sim.reset_stats()
func _test_rapid_sequence_10k() -> void:
tu.describe("Performance: 10,000 rapid packets — timing precision")
# Simulate rapid-fire packet generation at 128 tickrate
sim.packet_loss = 0.02
sim.latency_ms = 50
sim.jitter_ms = 10
sim.duplicate_rate = 0.005
sim.reset_stats()
var total = 10000
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "rapid_%d" % i)
sim.send_packet(pkt)
var lost = sim._total_lost
var duped = sim._total_duplicated
var queued = sim._outbound_queue.size()
tu.assert_eq(queued + lost, total,
"queued %d + lost %d = total %d (no phantom packets)" % [queued, lost, total])
# At 2% loss on 10000: ~200 lost (range: 100-400)
tu.assert_true(lost >= 50 and lost <= 500,
"loss %d in range for 2%% on %d" % [lost, total])
# At 0.5% dupe: ~50 dupes
tu.assert_true(duped >= 5 and duped <= 200,
"dupes %d in range for 0.5%% on %d" % [duped, total])
# Verify no negative delivery times
for entry in sim._outbound_queue:
tu.assert_true(entry.time >= 0, "all delivery times >= 0 (no integer overflow)")
sim.reset_stats()
func _test_bandwidth_with_loss() -> void:
tu.describe("Edge: Bandwidth limit combined with packet loss")
# Loss removes some packets before bandwidth check has a chance to drop them.
# Effective throughput = min(bandwidth adjusted, surviving packets).
sim.packet_loss = 0.1 # 10% loss
sim.latency_ms = 0
sim.bandwidth_limit = 5000 # 5000 bytes/s
sim.reset_stats()
var pkt_size = 100
var total = 200
for i in range(total):
var data = PackedByteArray()
data.resize(pkt_size)
data[0] = i
sim.send_packet(data)
var lost_loss = sim._total_lost
var bw_drops = sim._total_bandwidth_dropped
var queued = sim._outbound_queue.size()
# 10% of 200 = ~20 lost to random loss (range: 10-30)
tu.assert_true(lost_loss >= 5 and lost_loss <= 40,
"loss %d in range for 10%% on %d" % [lost_loss, total])
# Remaining 180 packets compete for 5000/100 = 50 packet slots
# But since bandwidth is checked after loss, at most ~50-60 should pass
tu.assert_true(bw_drops >= 100,
"at least 100 packets dropped by bandwidth limit (got %d)" % bw_drops)
tu.assert_true(queued + bw_drops + lost_loss == total,
"accounting: queued %d + bw %d + lost %d = total %d" % [queued, bw_drops, lost_loss, total])
sim.bandwidth_limit = 0
sim.reset_stats()
func _test_zero_packet_loss_timing() -> void:
tu.describe("Edge: Zero loss + zero latency — no side effects")
sim.packet_loss = 0.0
sim.latency_ms = 0
sim.jitter_ms = 0
sim.reorder_window = 0
sim.duplicate_rate = 0.0
sim.bandwidth_limit = 0
sim.burst_loss_count = 0
sim.burst_loss_interval = 0
sim.reset_stats()
var total = 500
var seq = 0
for i in range(total):
var pkt = SIM_SCRIPT.make_test_packet(i, "zero")
var ok = sim.send_packet(pkt)
tu.assert_true(ok, "packet %d send returned true" % i)
# All should queue with 0 delay (delivery at now + 0ms)
var queued = sim._outbound_queue.size()
tu.assert_eq(queued, total, "all %d packets queued" % total)
tu.assert_eq(sim._total_lost, 0, "zero loss")
tu.assert_eq(sim._total_duplicated, 0, "zero dupes")
tu.assert_eq(sim._total_burst_dropped, 0, "zero burst drops")
tu.assert_eq(sim._total_bandwidth_dropped, 0, "zero bw drops")
# Delivery times should all be at or near current time
var now = Time.get_ticks_msec()
for entry in sim._outbound_queue:
tu.assert_true(entry.time >= now - 5 and entry.time <= now + 5,
"zero-delay packet delivery at current time (time=%d, now=%d)" % [entry.time, now])
sim.reset_stats()
# ── Main Entry Point ──────────────────────────────────────────────────────
func _ready() -> void:
"""Run all tests and exit."""
print("\n══════════════════════════════════════════════")
print(" Network Simulator — Unit & Integration Tests")
print("══════════════════════════════════════════════\n")
sim = SIM_SCRIPT.new()
add_child(sim)
# Use deterministic seed for reproducibility
sim.seed_value = 42
sim._rng.seed = 42
tu = UTILS_SCRIPT.new()
add_child(tu)
# Run all test suites
_test_silent_passthrough()
_test_packet_loss_rate()
_test_latency_timing()
_test_jitter_distribution()
_test_packet_duplication()
_test_burst_loss()
_test_bandwidth_throttling()
_test_reorder_window()
_test_profile_presets()
_test_stats_report()
_test_high_loss_extreme()
_test_zero_bandwidth()
_test_complex_profile()
# Phase 6 bug bash edge case tests
_test_jitter_exceeds_latency()
_test_combined_high_loss_latency()
_test_burst_with_reorder()
_test_rapid_sequence_10k()
_test_bandwidth_with_loss()
_test_zero_packet_loss_timing()
# 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())