fix: Windows client compile errors — get_world_3d on Node, array type mismatch, null plugin guard, maps format
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
[configuration]
|
||||
entry_symbol = "gdextension_entry"
|
||||
compatibility_minimum = "4.2"
|
||||
|
||||
[libraries]
|
||||
linux.x86_64 = "res://gdextension/simulation/gdextension/bin/linux/libsimulation.so"
|
||||
windows.x86_64 = ""
|
||||
macos.universal = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://4ivoytu0kqkn
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnopn0wj58j83
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtvf72mhgdat7
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdsyn134gsca8
|
||||
+356830
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
[configuration]
|
||||
entry_symbol = "gdextension_init"
|
||||
entry_symbol = "gdextension_entry"
|
||||
compatibility_minimum = "4.2"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3r5nkuddarbb
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
uid://ci55pc8ou2iuq
|
||||
@@ -0,0 +1 @@
|
||||
uid://bn4sdresufqbo
|
||||
@@ -0,0 +1 @@
|
||||
uid://dktmtadg8o0qa
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4yrh2xnjfrch
|
||||
@@ -0,0 +1 @@
|
||||
uid://duwmyvcdn55qe
|
||||
@@ -72,7 +72,7 @@ func _ready() -> void:
|
||||
print("[ServerMain] Port: %d" % effective_port)
|
||||
print("[ServerMain] Name: \"%s\"" % ServerConfig.server_name)
|
||||
print("[ServerMain] Tick rate: %d Hz" % ServerConfig.tick_rate)
|
||||
print("[ServerMain] Maps: %s" % ServerConfig.map_list)
|
||||
print("[ServerMain] Maps: %s" % str(ServerConfig.map_list))
|
||||
print("[ServerMain] Headless: %s" % (DisplayServer.get_name() == &"headless"))
|
||||
print("[ServerMain] Spawn pts: Team A=%d, Team B=%d" % [spawn_points_a.size(), spawn_points_b.size()])
|
||||
var lag_comp_ms: float = 64.0 * 1000.0 / ServerConfig.tick_rate
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://vxt3rh3f2j35
|
||||
@@ -0,0 +1 @@
|
||||
uid://dixk0c0uniyp6
|
||||
@@ -0,0 +1 @@
|
||||
uid://w0y1tql3o5vd
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1hh1yrejn46f
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1ij0okuw1wy2
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkhwke1658h31
|
||||
@@ -0,0 +1 @@
|
||||
uid://ynhkn35cch8b
|
||||
@@ -0,0 +1 @@
|
||||
uid://nsrju5h3twl8
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3bjem6a0wy0e
|
||||
@@ -0,0 +1 @@
|
||||
uid://8m62kk21r2ek
|
||||
@@ -109,7 +109,7 @@ func _ready() -> void:
|
||||
# These work alongside the C++ SimulationServer for the GDScript
|
||||
# weapon path.
|
||||
weapon_server = WeaponServer.new()
|
||||
weapon_server.physics_world = get_world_3d()
|
||||
weapon_server.physics_world = get_viewport().get_world_3d()
|
||||
add_child(weapon_server)
|
||||
|
||||
lag_compensation = LagCompensation.new()
|
||||
@@ -177,7 +177,10 @@ func _ready() -> void:
|
||||
|
||||
# Register bomb sites from the scene tree
|
||||
if is_inside_tree():
|
||||
var bomb_sites: Array[Area3D] = get_tree().get_nodes_in_group("bomb_sites")
|
||||
var bomb_sites: Array[Area3D] = []
|
||||
for n in get_tree().get_nodes_in_group("bomb_sites"):
|
||||
if n is Area3D:
|
||||
bomb_sites.append(n)
|
||||
if bomb_sites.size() > 0:
|
||||
bomb_objective.register_bomb_sites(bomb_sites)
|
||||
print("[GameServer] Registered %d bomb sites with BombObjective" % bomb_sites.size())
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxn8cn5v4rv0r
|
||||
@@ -251,6 +251,9 @@ func _load_plugin_from_manifest(name: String, manifest: PluginManifest) -> bool:
|
||||
if "plugin_author" in instance:
|
||||
instance.plugin_author = manifest.author
|
||||
|
||||
if instance == null:
|
||||
push_error("[PluginManager] Failed to instantiate plugin: %s" % name)
|
||||
return
|
||||
instance.name = "Plugin_%s" % name
|
||||
add_child(instance)
|
||||
loaded_plugins[name] = instance
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dukkopo6xcbn7
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmvykm8r7koqv
|
||||
@@ -0,0 +1 @@
|
||||
uid://c57s6fj8odgir
|
||||
@@ -0,0 +1,8 @@
|
||||
[configuration]
|
||||
entry_symbol = "gdextension_entry"
|
||||
compatibility_minimum = "4.2"
|
||||
|
||||
[libraries]
|
||||
linux.x86_64 = "res://gdextension/simulation/gdextension/bin/linux/libsimulation.so"
|
||||
windows.x86_64 = ""
|
||||
macos.universal = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://cx4pxeohkkyaa
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env godot
|
||||
# tests/benchmarks/bench_128hz.gd
|
||||
# 128Hz Load Test Benchmark for GDExtension Simulation Core
|
||||
#
|
||||
# Tests SimulationServer at N=8,16,32,64 bots running at 128Hz for
|
||||
# BENCH_DURATION virtual-seconds each. Measures achieved tick rate,
|
||||
# frame time (GDScript + tick overhead), C++ tick time (from get_stats()),
|
||||
# memory delta, and missed ticks.
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script tests/benchmarks/bench_128hz.gd
|
||||
#
|
||||
# Exit code: 0 = benchmark complete, 1 = GDExtension not loadable
|
||||
|
||||
extends SceneTree
|
||||
|
||||
const TICK_HZ := 128
|
||||
const BENCH_DURATION_SEC := 10.0
|
||||
const POPULATION_LEVELS := [8, 16, 32, 64]
|
||||
const TICK_INTERVAL := 1.0 / TICK_HZ
|
||||
const EXPECTED_TICKS := int(TICK_HZ * BENCH_DURATION_SEC)
|
||||
|
||||
var _results := []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _init() -> void:
|
||||
print("")
|
||||
print("=".repeat(70))
|
||||
print(" 128Hz Load Test Benchmark — GDExtension Simulation Server")
|
||||
print("=".repeat(70))
|
||||
print("")
|
||||
|
||||
var vi = Engine.get_version_info()
|
||||
print(" Engine: Godot %d.%d.%d" % [vi.major, vi.minor, vi.patch])
|
||||
print(" Platform: %s" % OS.get_name())
|
||||
print(" Headless: %s" % str(DisplayServer.get_name() == "headless"))
|
||||
print(" Tick Rate: %d Hz" % TICK_HZ)
|
||||
print(" Duration: %.1f sec per level" % BENCH_DURATION_SEC)
|
||||
print(" Expected ticks: %d" % EXPECTED_TICKS)
|
||||
print("")
|
||||
|
||||
# ---- Diagnostics: is the GDExtension loadable? ----
|
||||
if not ClassDB.class_exists("SimulationServer"):
|
||||
printerr("")
|
||||
printerr(" [FATAL] SimulationServer class is NOT registered in ClassDB.")
|
||||
printerr("")
|
||||
printerr(" Diagnostic:")
|
||||
printerr(" The GDExtension shared library could not be loaded.")
|
||||
printerr("")
|
||||
printerr(" Expected configuration:")
|
||||
printerr(" .gdextension: res://gdextension/simulation.gdextension")
|
||||
printerr(" Linux .so: res://gdextension/simulation/gdextension/bin/linux/libsimulation.so")
|
||||
printerr("")
|
||||
printerr(" Possible causes:")
|
||||
printerr(" 1. The .so has not been compiled — run: cd gdextension/simulation && scons -j$(nproc)")
|
||||
printerr(" 2. The .gdextension file points to the wrong path")
|
||||
printerr(" 3. The .so is incompatible with this Godot version (need 4.2+ godot-cpp)")
|
||||
printerr(" 4. Missing symbol — rebuild with a clean build")
|
||||
printerr("")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print(" [OK] SimulationServer class is registered in ClassDB")
|
||||
print("")
|
||||
|
||||
# Run benchmarks for each population level
|
||||
for pop in POPULATION_LEVELS:
|
||||
_run_benchmark(pop)
|
||||
|
||||
# Print summary table
|
||||
_print_summary()
|
||||
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Single population-level benchmark
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_benchmark(pop: int) -> void:
|
||||
print("─".repeat(70))
|
||||
print(" Benchmark: %d bots" % pop)
|
||||
print("─".repeat(70))
|
||||
|
||||
# Create a fresh SimulationServer for each population level
|
||||
# Use ClassDB.instantiate because SimulationServer is a GDExtension
|
||||
# class that isn't known to the parser at compile time.
|
||||
var server = ClassDB.instantiate("SimulationServer")
|
||||
if server == null:
|
||||
printerr(" [ERROR] Failed to create SimulationServer instance!")
|
||||
return
|
||||
|
||||
# Configure and populate
|
||||
server.set_tick_rate(TICK_HZ)
|
||||
server.populate_bots(pop)
|
||||
server.start()
|
||||
server.reset_stats()
|
||||
|
||||
var entity_count = server.get_entity_count()
|
||||
print(" Entities spawned: %d" % entity_count)
|
||||
assert(entity_count == pop, "Entity count should match requested population")
|
||||
|
||||
# ---- Warm-up: a few ticks to stabilise caches / allocators ----
|
||||
for _i in range(10):
|
||||
if server.can_tick(TICK_INTERVAL):
|
||||
server.tick()
|
||||
server.reset_stats()
|
||||
|
||||
# Memory snapshot BEFORE
|
||||
var mem_before := OS.get_static_memory_usage()
|
||||
|
||||
# ---- Main benchmark loop (EXPECTED_TICKS iterations) ----
|
||||
var tick_count := 0
|
||||
var frame_times := []
|
||||
var bench_start := Time.get_ticks_usec()
|
||||
|
||||
for i in range(EXPECTED_TICKS):
|
||||
var frame_start := Time.get_ticks_usec()
|
||||
|
||||
if server.can_tick(TICK_INTERVAL):
|
||||
var snap = server.tick()
|
||||
tick_count += 1
|
||||
|
||||
var frame_end := Time.get_ticks_usec()
|
||||
frame_times.append(frame_end - frame_start)
|
||||
|
||||
var bench_end := Time.get_ticks_usec()
|
||||
|
||||
# Memory snapshot AFTER
|
||||
var mem_after := OS.get_static_memory_usage()
|
||||
var mem_delta := mem_after - mem_before
|
||||
|
||||
# Internal C++ timing (from get_stats)
|
||||
var stats = server.get_stats()
|
||||
var cpp_avg_usec := float(stats["avg_tick_usec"])
|
||||
var cpp_peak_usec := float(stats["peak_tick_usec"])
|
||||
var cpp_tick_count := int(stats["tick_count"])
|
||||
|
||||
# Wall-clock timing
|
||||
var wall_usec := bench_end - bench_start
|
||||
var wall_sec := wall_usec / 1_000_000.0
|
||||
var achieved_hz := float(tick_count) / wall_sec if wall_sec > 0.0 else 0.0
|
||||
|
||||
# Frame-time statistics
|
||||
var avg_frame_usec := 0.0
|
||||
var min_frame_usec := 0
|
||||
var max_frame_usec := 0
|
||||
if frame_times.size() > 0:
|
||||
var total := 0
|
||||
min_frame_usec = frame_times[0]
|
||||
max_frame_usec = frame_times[0]
|
||||
for ft in frame_times:
|
||||
total += ft
|
||||
if ft < min_frame_usec:
|
||||
min_frame_usec = ft
|
||||
if ft > max_frame_usec:
|
||||
max_frame_usec = ft
|
||||
avg_frame_usec = float(total) / float(frame_times.size())
|
||||
|
||||
# Missed ticks (process fewer than expected)
|
||||
var missed_ticks: int = max(0, EXPECTED_TICKS - tick_count)
|
||||
|
||||
# ---- Print per-level results ----
|
||||
print("")
|
||||
print(" ┌─ Results ──────────────────────────────────────────────┐")
|
||||
print(" │ Wall clock: %8.3f sec │" % wall_sec)
|
||||
print(" │ Total ticks: %d / %d (expected) │" % [tick_count, EXPECTED_TICKS])
|
||||
print(" │ Missed ticks: %d │" % missed_ticks)
|
||||
print(" │ Achieved tick rate: %.1f Hz │" % achieved_hz)
|
||||
print(" │ │")
|
||||
print(" │ Frame time (GDScript + tick overhead): │")
|
||||
print(" │ Avg: %8.1f µs │" % avg_frame_usec)
|
||||
print(" │ Min: %8d µs │" % min_frame_usec)
|
||||
print(" │ Max: %8d µs │" % max_frame_usec)
|
||||
print(" │ │")
|
||||
print(" │ Tick time (C++ internal, from get_stats()): │")
|
||||
print(" │ Avg: %8.1f µs │" % cpp_avg_usec)
|
||||
print(" │ Peak: %8.1f µs │" % cpp_peak_usec)
|
||||
print(" │ Count: %d │" % cpp_tick_count)
|
||||
print(" │ │")
|
||||
print(" │ Memory: │")
|
||||
print(" │ Before: %s │" % _pad(_fmt_bytes(mem_before), 12))
|
||||
print(" │ After: %s │" % _pad(_fmt_bytes(mem_after), 12))
|
||||
print(" │ Delta: %s │" % _pad(_fmt_bytes(mem_delta), 12))
|
||||
print(" └─────────────────────────────────────────────────────────┘")
|
||||
print("")
|
||||
|
||||
# Save for summary
|
||||
_results.append({
|
||||
"pop": pop,
|
||||
"achieved_hz": achieved_hz,
|
||||
"avg_frame_usec": avg_frame_usec,
|
||||
"min_frame_usec": min_frame_usec,
|
||||
"max_frame_usec": max_frame_usec,
|
||||
"cpp_avg_usec": cpp_avg_usec,
|
||||
"cpp_peak_usec": cpp_peak_usec,
|
||||
"wall_sec": wall_sec,
|
||||
"total_ticks": tick_count,
|
||||
"expected_ticks": EXPECTED_TICKS,
|
||||
"missed_ticks": missed_ticks,
|
||||
"mem_before": mem_before,
|
||||
"mem_after": mem_after,
|
||||
"mem_delta": mem_delta,
|
||||
})
|
||||
|
||||
# Cleanup
|
||||
server.stop()
|
||||
server = null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
print("")
|
||||
print("=".repeat(70))
|
||||
print(" BENCHMARK SUMMARY")
|
||||
print("=".repeat(70))
|
||||
print("")
|
||||
|
||||
# Table header
|
||||
print(" %-8s %-12s %-10s %-10s %-8s %-10s" % [
|
||||
"Players", "Tick Rate", "Frame Avg", "C++ Tick", "Missed", "Mem Δ",
|
||||
])
|
||||
print(" %-8s %-12s %-10s %-10s %-8s %-10s" % [
|
||||
"", "(Hz)", "(µs)", "Avg (µs)", "Ticks", "",
|
||||
])
|
||||
print(" " + "─".repeat(60))
|
||||
|
||||
for r in _results:
|
||||
var rate_str := "%.1f/%d" % [r["achieved_hz"], TICK_HZ]
|
||||
var missed_str := "%d/%d" % [r["missed_ticks"], r["expected_ticks"]]
|
||||
print(" %-8s %-12s %-10.1f %-10.1f %-8s %-10s" % [
|
||||
str(r["pop"]),
|
||||
rate_str,
|
||||
r["avg_frame_usec"],
|
||||
r["cpp_avg_usec"],
|
||||
missed_str,
|
||||
_fmt_bytes(r["mem_delta"]),
|
||||
])
|
||||
|
||||
print(" " + "─".repeat(60))
|
||||
print("")
|
||||
print(" Legend:")
|
||||
print(" Players: number of simulated bots")
|
||||
print(" Tick Rate: achieved Hz / target Hz (128)")
|
||||
print(" Frame Avg: average frame time incl. GDScript + tick overhead")
|
||||
print(" C++ Tick: average tick processing time inside C++ (get_stats)")
|
||||
print(" Missed: ticks short of expected / total expected ticks")
|
||||
print(" Mem Δ: static memory delta before/after the benchmark")
|
||||
print("")
|
||||
if _results.size() > 0:
|
||||
var all_zero_missed := true
|
||||
var all_above_target := true
|
||||
for r in _results:
|
||||
if r["missed_ticks"] > 0:
|
||||
all_zero_missed = false
|
||||
if r["achieved_hz"] < TICK_HZ:
|
||||
all_above_target = false
|
||||
|
||||
if all_zero_missed and all_above_target:
|
||||
print(" ✓ ALL BENCHMARKS PASSED — simulation maintains 128 Hz at all population levels")
|
||||
elif all_above_target:
|
||||
print(" ⚠ All levels achieved ≥128 Hz but some missed ticks recorded (check timing)")
|
||||
else:
|
||||
print(" ✗ Some levels FAILED to maintain 128 Hz — simulation may need optimisation")
|
||||
print(" for the higher population levels.")
|
||||
print("")
|
||||
print("=".repeat(70))
|
||||
print(" Benchmark complete.")
|
||||
print("=".repeat(70))
|
||||
print("")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
static func _fmt_bytes(bytes: int) -> String:
|
||||
if bytes < 0:
|
||||
return "-" + _fmt_bytes(-bytes)
|
||||
if bytes < 1024:
|
||||
return "%d B" % bytes
|
||||
elif bytes < 1024 * 1024:
|
||||
return "%.1f KB" % (float(bytes) / 1024.0)
|
||||
else:
|
||||
return "%.2f MB" % (float(bytes) / (1024.0 * 1024.0))
|
||||
|
||||
|
||||
# Left-pad a string to at least `width` characters by adding spaces.
|
||||
# Godot 4 GDScript doesn't have rjust(), so we roll our own.
|
||||
static func _pad(s: String, width: int) -> String:
|
||||
var needed := width - s.length()
|
||||
if needed <= 0:
|
||||
return s
|
||||
var buf := ""
|
||||
for _i in range(needed):
|
||||
buf += " "
|
||||
return buf + s
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0a04532axyqo
|
||||
@@ -0,0 +1,734 @@
|
||||
## Netcode Bug Bash — Automated Netcode Test Suite
|
||||
##
|
||||
## Tests connection lifecycle, prediction & reconciliation, round lifecycle,
|
||||
## economy & weapons, and RCON functionality — all without real ENet traffic
|
||||
## by calling internal APIs directly.
|
||||
##
|
||||
## Usage:
|
||||
## godot --headless --script tests/netcode/netcode_bugbash.gd
|
||||
##
|
||||
## Exit code: 0 = all pass, 1 = any failure
|
||||
|
||||
extends Node
|
||||
|
||||
# Preload TeamData for enum access (it's a class_name Resource, not an autoload)
|
||||
const TeamData = preload("res://scripts/teams/team_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test counters
|
||||
# ---------------------------------------------------------------------------
|
||||
var _passed: int = 0
|
||||
var _failed: int = 0
|
||||
var _total: int = 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test infrastructure references
|
||||
# ---------------------------------------------------------------------------
|
||||
var _game_server: Node = null
|
||||
var _world_3d: World3D = null
|
||||
var _net_mgr: Node = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
func check_pass(name: String) -> void:
|
||||
_passed += 1
|
||||
_total += 1
|
||||
print(" ✓ PASS: %s" % name)
|
||||
|
||||
func check_fail(name: String, reason: String) -> void:
|
||||
_failed += 1
|
||||
_total += 1
|
||||
print(" ✗ FAIL: %s — %s" % [name, reason])
|
||||
|
||||
func assert_eq(name: String, got, expected, msg: String = "") -> void:
|
||||
if got == expected:
|
||||
check_pass(name)
|
||||
else:
|
||||
var extra: String = ": " + msg if msg else ""
|
||||
check_fail(name, "expected %s, got %s%s" % [str(expected), str(got), extra])
|
||||
|
||||
func assert_true(name: String, cond: bool, msg: String = "") -> void:
|
||||
if cond:
|
||||
check_pass(name)
|
||||
else:
|
||||
var extra: String = ": " + msg if msg else ""
|
||||
check_fail(name, "condition was false%s" % [extra])
|
||||
|
||||
func assert_false(name: String, cond: bool, msg: String = "") -> void:
|
||||
if not cond:
|
||||
check_pass(name)
|
||||
else:
|
||||
var extra: String = ": " + msg if msg else ""
|
||||
check_fail(name, "condition was true%s" % [extra])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Infrastructure setup
|
||||
# ---------------------------------------------------------------------------
|
||||
func _setup_infrastructure() -> void:
|
||||
# Create NetworkManager manually (autoloads not loaded in --script mode)
|
||||
var nm_script = load("res://scripts/network/network_manager.gd")
|
||||
_net_mgr = nm_script.new()
|
||||
add_child(_net_mgr)
|
||||
print(" Created NetworkManager node")
|
||||
|
||||
# Create a 3D physics world so physics-dependent subsystems work
|
||||
_world_3d = World3D.new()
|
||||
get_window().world_3d = _world_3d
|
||||
|
||||
# Add a static body so raycasts have something to hit
|
||||
var sb := StaticBody3D.new()
|
||||
var col := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(10, 10, 10)
|
||||
col.shape = box
|
||||
sb.add_child(col)
|
||||
add_child(sb)
|
||||
sb.global_position = Vector3(0, 0, -20)
|
||||
|
||||
# Create GameServer with all subsystems
|
||||
var gs_script = load("res://server/scripts/game_server.gd")
|
||||
_game_server = gs_script.new()
|
||||
add_child(_game_server)
|
||||
|
||||
# Manually ensure the simulation server is running
|
||||
if _game_server.has_method("start_simulation"):
|
||||
_game_server.start_simulation()
|
||||
|
||||
print(" Setup complete: GameServer + all subsystems created")
|
||||
print("")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main entry
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
print("")
|
||||
print("=".repeat(64))
|
||||
print(" Netcode Bug Bash — Automated Test Suite")
|
||||
print("=".repeat(64))
|
||||
print("")
|
||||
|
||||
var vi = Engine.get_version_info()
|
||||
print(" Engine: Godot %d.%d.%d" % [vi.major, vi.minor, vi.patch])
|
||||
print(" Platform: %s" % OS.get_name())
|
||||
print(" Headless: %s" % str(DisplayServer.get_name() == "headless"))
|
||||
print("")
|
||||
|
||||
_setup_infrastructure()
|
||||
|
||||
# === GROUP 1: Connection Lifecycle (5 tests) ===
|
||||
print("─".repeat(64))
|
||||
print(" GROUP 1: Connection Lifecycle")
|
||||
print("─".repeat(64))
|
||||
test_conn_client_connects()
|
||||
test_conn_client_disconnect_cleanup()
|
||||
test_conn_client_reconnect_fresh()
|
||||
test_conn_full_server_rejected()
|
||||
test_conn_invalid_data_no_crash()
|
||||
|
||||
# === GROUP 2: Prediction & Reconciliation (4 tests) ===
|
||||
print("")
|
||||
print("─".repeat(64))
|
||||
print(" GROUP 2: Prediction & Reconciliation")
|
||||
print("─".repeat(64))
|
||||
test_pred_client_input_processed()
|
||||
test_pred_server_snapshot_reconcile()
|
||||
test_pred_lag_compensation_rewind()
|
||||
test_pred_packet_loss_recovery()
|
||||
|
||||
# === GROUP 3: Round Lifecycle (3 tests) ===
|
||||
print("")
|
||||
print("─".repeat(64))
|
||||
print(" GROUP 3: Round Lifecycle")
|
||||
print("─".repeat(64))
|
||||
test_round_transitions()
|
||||
test_round_bomb_explosion()
|
||||
test_round_bomb_defuse()
|
||||
|
||||
# === GROUP 4: Economy & Weapons (3 tests) ===
|
||||
print("")
|
||||
print("─".repeat(64))
|
||||
print(" GROUP 4: Economy & Weapons")
|
||||
print("─".repeat(64))
|
||||
test_econ_kill_reward()
|
||||
test_econ_round_win_loss()
|
||||
test_econ_weapon_purchase()
|
||||
|
||||
# === GROUP 5: RCON (3 tests) ===
|
||||
print("")
|
||||
print("─".repeat(64))
|
||||
print(" GROUP 5: RCON")
|
||||
print("─".repeat(64))
|
||||
test_rcon_auth_command()
|
||||
test_rcon_invalid_password()
|
||||
test_rcon_start_match_end_round()
|
||||
|
||||
# === SUMMARY ===
|
||||
_print_summary()
|
||||
get_tree().quit(0 if _failed == 0 else 1)
|
||||
|
||||
func _print_summary() -> void:
|
||||
print("")
|
||||
print("=".repeat(64))
|
||||
print(" RESULTS: %d / %d passed, %d failed" % [_passed, _total, _failed])
|
||||
print("=".repeat(64))
|
||||
if _failed == 0:
|
||||
print(" ALL TESTS PASSED")
|
||||
else:
|
||||
print(" SOME TESTS FAILED — see above for details")
|
||||
print("=".repeat(64))
|
||||
|
||||
# ===================================================================
|
||||
# GROUP 1: Connection Lifecycle
|
||||
# ===================================================================
|
||||
|
||||
func test_conn_client_connects() -> void:
|
||||
if not _net_mgr or not _net_mgr.has_signal("player_connected"):
|
||||
check_fail("conn_01_client_connects", "NetworkManager not available")
|
||||
return
|
||||
|
||||
var err = _net_mgr.start_server(0)
|
||||
if err != OK:
|
||||
check_fail("conn_01_client_connects", "Failed to start server: " + error_string(err))
|
||||
return
|
||||
|
||||
assert_true("conn_01_server_started", _net_mgr.is_server,
|
||||
"NetworkManager should be in server mode after start_server")
|
||||
_net_mgr.stop()
|
||||
|
||||
func test_conn_client_disconnect_cleanup() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("conn_02_disconnect_cleanup", "GameServer not available")
|
||||
return
|
||||
|
||||
var gs = _game_server
|
||||
if not gs.is_running:
|
||||
gs.start_simulation()
|
||||
|
||||
var entity_id: int = gs.spawn_player_entity(1001, Vector3(2, 0, 3))
|
||||
assert_true("conn_02_spawned", entity_id >= 0, "Player should spawn successfully")
|
||||
|
||||
var peer = gs.get_peer_for_entity(entity_id)
|
||||
assert_eq("conn_02_entity_to_peer_mapped", peer, 1001,
|
||||
"entity_to_peer should map to peer_id=1001")
|
||||
|
||||
# Despawn (simulating disconnect)
|
||||
gs.despawn_player_entity(entity_id)
|
||||
|
||||
var peer_after = gs.get_peer_for_entity(entity_id)
|
||||
assert_eq("conn_02_entity_cleaned", peer_after, -1,
|
||||
"entity_to_peer should be cleared after despawn")
|
||||
|
||||
func test_conn_client_reconnect_fresh() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("conn_03_reconnect_fresh", "GameServer not available")
|
||||
return
|
||||
|
||||
var gs = _game_server
|
||||
|
||||
var eid1: int = gs.spawn_player_entity(2001, Vector3(5, 0, 5))
|
||||
gs.despawn_player_entity(eid1)
|
||||
|
||||
var eid2: int = gs.spawn_player_entity(2001, Vector3(10, 0, 10))
|
||||
assert_true("conn_03_re_spawned", eid2 >= 0, "Re-spawning should succeed")
|
||||
assert_true("conn_03_new_entity_id", eid2 != eid1, "Re-spawn should get a new entity_id")
|
||||
|
||||
var old_peer = gs.get_peer_for_entity(eid1)
|
||||
assert_eq("conn_03_old_entity_cleaned", old_peer, -1, "Old entity_id should be unmapped")
|
||||
gs.despawn_player_entity(eid2)
|
||||
|
||||
func test_conn_full_server_rejected() -> void:
|
||||
if not _net_mgr:
|
||||
check_fail("conn_04_full_server", "NetworkManager not available")
|
||||
return
|
||||
|
||||
_net_mgr.max_clients = 1
|
||||
var err = _net_mgr.start_server(0)
|
||||
if err != OK:
|
||||
check_fail("conn_04_full_server", "Failed to start server: " + error_string(err))
|
||||
return
|
||||
|
||||
assert_true("conn_04_max_clients_set", _net_mgr.max_clients == 1,
|
||||
"max_clients should be 1")
|
||||
_net_mgr.stop()
|
||||
|
||||
func test_conn_invalid_data_no_crash() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("conn_05_invalid_data", "GameServer not available")
|
||||
return
|
||||
|
||||
var gs = _game_server
|
||||
var sim = gs.get_simulation_server()
|
||||
|
||||
if sim and sim.has_method("apply_input"):
|
||||
sim.apply_input(-999, {"move_direction": "not_a_vector"})
|
||||
check_pass("conn_05_invalid_entity_input")
|
||||
|
||||
var eid = gs.spawn_player_entity(3001, Vector3.ZERO)
|
||||
sim.apply_input(eid, {})
|
||||
check_pass("conn_05_empty_input_dict")
|
||||
gs.despawn_player_entity(eid)
|
||||
else:
|
||||
check_fail("conn_05_invalid_data", "Simulation server missing apply_input method")
|
||||
|
||||
var bad_eid = gs.spawn_player_entity(3002, Vector3(NAN, NAN, NAN))
|
||||
gs.despawn_player_entity(bad_eid)
|
||||
check_pass("conn_05_nan_position")
|
||||
|
||||
# ===================================================================
|
||||
# GROUP 2: Prediction & Reconciliation
|
||||
# ===================================================================
|
||||
|
||||
func test_pred_client_input_processed() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("pred_01_input_processed", "GameServer not available")
|
||||
return
|
||||
|
||||
var gs = _game_server
|
||||
var sim = gs.get_simulation_server()
|
||||
|
||||
# Test simulation_server.apply_input directly
|
||||
if sim and sim.has_method("apply_input"):
|
||||
var eid = gs.spawn_player_entity(4001, Vector3(0, 0, 0))
|
||||
sim.apply_input(eid, {"move_direction": Vector3(1, 0, 0), "sprint": false})
|
||||
check_pass("pred_01_apply_input_direct")
|
||||
gs.despawn_player_entity(eid)
|
||||
else:
|
||||
check_fail("pred_01_input_processed", "Simulation server missing apply_input")
|
||||
|
||||
func test_pred_server_snapshot_reconcile() -> void:
|
||||
var snapshot_script = load("res://scripts/network/snapshot.gd")
|
||||
var snap = snapshot_script.new()
|
||||
snap.timestamp = 42
|
||||
snap.position = Vector3(10, 5, 3)
|
||||
snap.rotation = Quaternion.IDENTITY
|
||||
snap.velocity = Vector3(2, 0, 1)
|
||||
snap.grounded = true
|
||||
|
||||
var data = snap.to_dict()
|
||||
assert_eq("pred_02_serialize_timestamp", data.get("timestamp"), 42)
|
||||
assert_eq("pred_02_serialize_position", data.get("position"), [10.0, 5.0, 3.0])
|
||||
assert_eq("pred_02_serialize_grounded", data.get("grounded"), true)
|
||||
|
||||
var restored = snapshot_script.from_dict(data)
|
||||
assert_eq("pred_02_deserialize_timestamp", restored.timestamp, 42)
|
||||
assert_eq("pred_02_deserialize_position", restored.position, Vector3(10, 5, 3))
|
||||
assert_eq("pred_02_deserialize_grounded", restored.grounded, true)
|
||||
|
||||
# Test ClientPrediction ring buffer
|
||||
var cp_script = load("res://scripts/network/client_prediction.gd")
|
||||
var cp = cp_script.new()
|
||||
cp._snapshot_buffer.resize(64)
|
||||
|
||||
var test_snap = snapshot_script.new()
|
||||
test_snap.timestamp = 5
|
||||
test_snap.position = Vector3(1, 2, 3)
|
||||
cp._snapshot_buffer[5 % 64] = test_snap
|
||||
cp._pending_inputs[5] = {"move_direction": Vector3(0, 0, -1)}
|
||||
|
||||
assert_true("pred_02_buffer_has_snapshot",
|
||||
cp._snapshot_buffer[5 % 64] != null,
|
||||
"Snapshot should be stored in ring buffer")
|
||||
check_pass("pred_02_buffer_and_serialization_ok")
|
||||
|
||||
func test_pred_lag_compensation_rewind() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("pred_03_lag_comp", "GameServer not available")
|
||||
return
|
||||
|
||||
var lc = _game_server.lag_compensation
|
||||
if lc == null:
|
||||
check_fail("pred_03_lag_comp", "LagCompensation not wired in GameServer")
|
||||
return
|
||||
|
||||
var test_node := Node3D.new()
|
||||
test_node.global_position = Vector3(10, 0, 20)
|
||||
add_child(test_node)
|
||||
|
||||
var entity_id = 9999
|
||||
lc.register_player_node(entity_id, test_node)
|
||||
lc.record_tick(0)
|
||||
|
||||
test_node.global_position = Vector3(30, 0, 40)
|
||||
lc.record_tick(1)
|
||||
|
||||
# Move node away for rewind test
|
||||
test_node.global_position = Vector3(50, 0, 60)
|
||||
|
||||
var origin := Vector3(0, 0, 0)
|
||||
var direction := Vector3(0, 0, -1).normalized()
|
||||
var result = lc.rewind_and_raycast(0, origin, direction, 100.0, [])
|
||||
|
||||
assert_true("pred_03_rewind_no_crash", true,
|
||||
"rewind_and_raycast completed without error")
|
||||
|
||||
var pos_after: Vector3 = test_node.global_position
|
||||
assert_eq("pred_03_node_restored", pos_after, Vector3(50, 0, 60),
|
||||
"Node position should be restored after rewind")
|
||||
|
||||
lc.unregister_player_node(entity_id)
|
||||
test_node.queue_free()
|
||||
|
||||
func test_pred_packet_loss_recovery() -> void:
|
||||
var cp_script = load("res://scripts/network/client_prediction.gd")
|
||||
var snapshot_script = load("res://scripts/network/snapshot.gd")
|
||||
var cp = cp_script.new()
|
||||
cp._snapshot_buffer.resize(64)
|
||||
|
||||
for t in range(5):
|
||||
var snap = snapshot_script.new()
|
||||
snap.timestamp = t
|
||||
snap.position = Vector3(t * 2.0, 0, 0)
|
||||
snap.rotation = Quaternion.IDENTITY
|
||||
snap.velocity = Vector3(2, 0, 0)
|
||||
snap.grounded = true
|
||||
cp._snapshot_buffer[t % 64] = snap
|
||||
cp._pending_inputs[t] = {"move_direction": Vector3(1, 0, 0), "input_sequence": t}
|
||||
|
||||
cp._last_confirmed_tick = -1
|
||||
|
||||
var pending_after: Array[int] = []
|
||||
for tick in cp._pending_inputs.keys():
|
||||
if tick > 2:
|
||||
pending_after.append(tick)
|
||||
pending_after.sort()
|
||||
|
||||
assert_eq("pred_04_pending_count", pending_after.size(), 2,
|
||||
"Should have 2 pending inputs after tick 2 (ticks 3, 4)")
|
||||
assert_eq("pred_04_pending_tick_3", pending_after[0], 3,
|
||||
"First pending tick should be 3")
|
||||
assert_eq("pred_04_pending_tick_4", pending_after[1], 4,
|
||||
"Second pending tick should be 4")
|
||||
check_pass("pred_04_packet_loss_recovery_logic")
|
||||
|
||||
# ===================================================================
|
||||
# GROUP 3: Round Lifecycle
|
||||
# ===================================================================
|
||||
|
||||
func test_round_transitions() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("round_01_transitions", "GameServer not available")
|
||||
return
|
||||
|
||||
var rm = _game_server.round_manager
|
||||
if rm == null:
|
||||
check_fail("round_01_transitions", "RoundManager not available")
|
||||
return
|
||||
|
||||
assert_eq("round_01_initial_warmup", rm.get_phase(), rm.RoundPhase.WARMUP,
|
||||
"Initial phase should be WARMUP")
|
||||
|
||||
rm.start_match()
|
||||
assert_eq("round_01_after_start_match", rm.get_phase(), rm.RoundPhase.PREP,
|
||||
"After start_match, phase should be PREP")
|
||||
assert_eq("round_01_round_1", rm.get_round_num(), 1,
|
||||
"Round number should be 1")
|
||||
assert_true("round_01_match_started", rm.is_match_started(),
|
||||
"Match should be started")
|
||||
|
||||
rm._phase_timer = 0.001
|
||||
rm._advance_timer(0.1)
|
||||
assert_eq("round_01_live", rm.get_phase(), rm.RoundPhase.LIVE,
|
||||
"After PREP timer expires, phase should be LIVE")
|
||||
|
||||
rm.end_round(TeamData.Team.COUNTER_TERRORIST, "test")
|
||||
assert_eq("round_01_post", rm.get_phase(), rm.RoundPhase.POST,
|
||||
"After end_round, phase should be POST")
|
||||
|
||||
var ct_score = rm.team_manager.get_team_score(TeamData.Team.COUNTER_TERRORIST)
|
||||
assert_eq("round_01_score_ct", ct_score, 1,
|
||||
"CT should have 1 point after winning")
|
||||
check_pass("round_01_transition_sequence_ok")
|
||||
|
||||
func test_round_bomb_explosion() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("round_02_bomb_explosion", "GameServer not available")
|
||||
return
|
||||
|
||||
var rm = _game_server.round_manager
|
||||
if rm == null:
|
||||
check_fail("round_02_bomb_explosion", "RoundManager not available")
|
||||
return
|
||||
|
||||
var tm = _game_server.team_manager
|
||||
if tm == null:
|
||||
check_fail("round_02_bomb_explosion", "TeamManager not available")
|
||||
return
|
||||
|
||||
rm.start_match()
|
||||
rm.start_live()
|
||||
|
||||
rm.end_round(TeamData.Team.TERRORIST, "bomb_exploded")
|
||||
|
||||
assert_eq("round_02_phase", rm.get_phase(), rm.RoundPhase.POST,
|
||||
"Phase should be POST after bomb explosion")
|
||||
|
||||
var t_score = tm.get_team_score(TeamData.Team.TERRORIST)
|
||||
assert_eq("round_02_t_score", t_score, 1,
|
||||
"Terrorist should have 1 point")
|
||||
check_pass("round_02_bomb_explosion_ok")
|
||||
|
||||
func test_round_bomb_defuse() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("round_03_bomb_defuse", "GameServer not available")
|
||||
return
|
||||
|
||||
var rm = _game_server.round_manager
|
||||
if rm == null:
|
||||
check_fail("round_03_bomb_defuse", "RoundManager not available")
|
||||
return
|
||||
|
||||
var tm = _game_server.team_manager
|
||||
if tm == null:
|
||||
check_fail("round_03_bomb_defuse", "TeamManager not available")
|
||||
return
|
||||
|
||||
rm.start_round(2)
|
||||
rm.start_live()
|
||||
|
||||
rm.end_round(TeamData.Team.COUNTER_TERRORIST, "bomb_defused")
|
||||
|
||||
assert_eq("round_03_phase", rm.get_phase(), rm.RoundPhase.POST,
|
||||
"Phase should be POST after bomb defuse")
|
||||
|
||||
var ct_score = tm.get_team_score(TeamData.Team.COUNTER_TERRORIST)
|
||||
assert_eq("round_03_ct_score_updated", ct_score, 2,
|
||||
"CT should have 2 points (1 from prev + 1 from defuse)")
|
||||
check_pass("round_03_bomb_defuse_ok")
|
||||
|
||||
# ===================================================================
|
||||
# GROUP 4: Economy & Weapons
|
||||
# ===================================================================
|
||||
|
||||
func test_econ_kill_reward() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("econ_01_kill_reward", "GameServer not available")
|
||||
return
|
||||
|
||||
var em = _game_server.economy_manager
|
||||
if em == null:
|
||||
check_fail("econ_01_kill_reward", "EconomyManager not available")
|
||||
return
|
||||
|
||||
em.register_player(5001)
|
||||
var initial_money = em.get_money(5001)
|
||||
assert_eq("econ_01_initial_money", initial_money, 800,
|
||||
"Starting money should be 800")
|
||||
|
||||
em.award_kill_reward(5001)
|
||||
var after_kill = em.get_money(5001)
|
||||
assert_eq("econ_01_after_kill", after_kill, 1100,
|
||||
"Money should increase by 300 (kill reward)")
|
||||
|
||||
var signal_data: Dictionary = {}
|
||||
var signal_conn = func(pid, old_amt, new_amt, reason):
|
||||
signal_data = {"player_id": pid, "old": old_amt, "new": new_amt, "reason": reason}
|
||||
em.money_changed.connect(signal_conn)
|
||||
|
||||
em.award_kill_reward(5001)
|
||||
assert_eq("econ_01_signal_reason", signal_data.get("reason", ""), "kill",
|
||||
"Signal reason should be 'kill'")
|
||||
assert_eq("econ_01_signal_new_amount", signal_data.get("new", 0), 1400,
|
||||
"Signal should report new balance after kill reward")
|
||||
em.money_changed.disconnect(signal_conn)
|
||||
|
||||
check_pass("econ_01_kill_reward_ok")
|
||||
|
||||
func test_econ_round_win_loss() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("econ_02_win_loss", "GameServer not available")
|
||||
return
|
||||
|
||||
var em = _game_server.economy_manager
|
||||
var tm = _game_server.team_manager
|
||||
if em == null or tm == null:
|
||||
check_fail("econ_02_win_loss", "EconomyManager or TeamManager not available")
|
||||
return
|
||||
|
||||
em.register_player(6001)
|
||||
em.register_player(6002)
|
||||
tm.assign_player(6001, TeamData.Team.COUNTER_TERRORIST)
|
||||
tm.assign_player(6002, TeamData.Team.TERRORIST)
|
||||
|
||||
em.set_money(6001, 0, "admin")
|
||||
em.set_money(6002, 0, "admin")
|
||||
|
||||
var ct_players = [6001]
|
||||
em.award_round_win(TeamData.Team.COUNTER_TERRORIST, ct_players)
|
||||
assert_eq("econ_02_ct_after_win", em.get_money(6001), 3250,
|
||||
"CT player should get 3250 round win bonus")
|
||||
|
||||
var t_players = [6002]
|
||||
em.award_round_loss(TeamData.Team.TERRORIST, t_players)
|
||||
assert_eq("econ_02_t_after_first_loss", em.get_money(6002), 1900,
|
||||
"T player should get 1900 base loss money on first loss")
|
||||
|
||||
# Second consecutive loss with escalated bonus
|
||||
em.set_money(6002, 0, "admin")
|
||||
em.award_round_loss(TeamData.Team.TERRORIST, t_players)
|
||||
assert_eq("econ_02_t_after_second_loss", em.get_money(6002), 2400,
|
||||
"T player should get 1900 + 500 = 2400 on second consecutive loss")
|
||||
|
||||
check_pass("econ_02_win_loss_ok")
|
||||
|
||||
# Clean up
|
||||
tm.unregister_player(6001)
|
||||
tm.unregister_player(6002)
|
||||
em.unregister_player(6001)
|
||||
em.unregister_player(6002)
|
||||
|
||||
func test_econ_weapon_purchase() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("econ_03_purchase", "GameServer not available")
|
||||
return
|
||||
|
||||
var em = _game_server.economy_manager
|
||||
var ws = _game_server.weapon_server
|
||||
if em == null:
|
||||
check_fail("econ_03_purchase", "EconomyManager not available")
|
||||
return
|
||||
|
||||
em.register_player(7001)
|
||||
em.set_money(7001, 5000, "admin")
|
||||
|
||||
assert_true("econ_03_can_afford_rifle", em.can_afford(7001, 2700),
|
||||
"Player with 5000 should afford a 2700 rifle")
|
||||
assert_true("econ_03_can_afford_pistol", em.can_afford(7001, 500),
|
||||
"Player should afford a 500 pistol")
|
||||
|
||||
var success = em.spend_money(7001, 2700)
|
||||
assert_true("econ_03_spend_rifle", success, "Spending 2700 should succeed")
|
||||
assert_eq("econ_03_after_rifle", em.get_money(7001), 2300,
|
||||
"After spending 2700, balance should be 2300")
|
||||
|
||||
var fail_spend = em.spend_money(7001, 99999)
|
||||
assert_false("econ_03_insufficient_funds", fail_spend,
|
||||
"Spending more than balance should fail")
|
||||
|
||||
if ws:
|
||||
ws.give_weapon(7001, "rifle")
|
||||
var can_fire = ws.can_fire(7001, "rifle")
|
||||
assert_true("econ_03_can_fire_rifle", can_fire,
|
||||
"Player should be able to fire rifle after purchasing it")
|
||||
var ammo_info = ws.get_ammo_info(7001, "rifle")
|
||||
assert_eq("econ_03_rifle_ammo", ammo_info.get("ammo", 0), 30,
|
||||
"Rifle should start with full magazine (30 rounds)")
|
||||
else:
|
||||
check_fail("econ_03_purchase", "WeaponServer not available")
|
||||
|
||||
em.unregister_player(7001)
|
||||
if ws:
|
||||
ws.unregister_player(7001)
|
||||
|
||||
# ===================================================================
|
||||
# GROUP 5: RCON
|
||||
# ===================================================================
|
||||
|
||||
func test_rcon_auth_command() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("rcon_01_auth_command", "GameServer not available")
|
||||
return
|
||||
|
||||
var rcon_script = load("res://server/scripts/rcon_server.gd")
|
||||
var rcon = rcon_script.new()
|
||||
rcon.password = "testpass"
|
||||
rcon.enabled = false
|
||||
add_child(rcon)
|
||||
|
||||
var handler = null
|
||||
for child in rcon.get_children():
|
||||
if child.has_method("handle_command"):
|
||||
handler = child
|
||||
break
|
||||
|
||||
if handler == null:
|
||||
var handler_path = "res://server/scripts/rcon_command_handler.gd"
|
||||
if ResourceLoader.exists(handler_path):
|
||||
handler = load(handler_path).new()
|
||||
add_child(handler)
|
||||
|
||||
if handler:
|
||||
var response = handler.handle_command("echo", ["hello"])
|
||||
assert_true("rcon_01_echo_response", "hello" in response,
|
||||
"Echo command should return the argument")
|
||||
|
||||
response = handler.handle_command("help", [])
|
||||
assert_true("rcon_01_help_response", "Commands" in response or "help" in response,
|
||||
"Help command should return command list")
|
||||
|
||||
response = handler.handle_command("echo", [])
|
||||
assert_true("rcon_01_ping_response", "Pong" in response,
|
||||
"Echo without args should return 'Pong'")
|
||||
|
||||
response = handler.handle_command("status", [])
|
||||
assert_true("rcon_01_status_response",
|
||||
"Status" in response or "Server" in response,
|
||||
"Status command should return server info")
|
||||
else:
|
||||
check_fail("rcon_01_auth_command", "Command handler not created")
|
||||
|
||||
rcon.queue_free()
|
||||
if handler and handler.get_parent():
|
||||
handler.queue_free()
|
||||
|
||||
func test_rcon_invalid_password() -> void:
|
||||
var rcon_script = load("res://server/scripts/rcon_server.gd")
|
||||
var rcon = rcon_script.new()
|
||||
rcon.password = "secretpass"
|
||||
rcon.enabled = false
|
||||
add_child(rcon)
|
||||
|
||||
assert_eq("rcon_02_password_set", rcon.password, "secretpass",
|
||||
"Password should be set correctly")
|
||||
|
||||
# Test IP ban logic
|
||||
rcon._banned_ips["127.0.0.1"] = Time.get_unix_time_from_system() + 5.0
|
||||
assert_true("rcon_02_ip_banned", rcon._is_ip_banned("127.0.0.1"),
|
||||
"IP should be banned after 3 failures")
|
||||
|
||||
rcon.queue_free()
|
||||
check_pass("rcon_02_bad_password_rejected")
|
||||
|
||||
func test_rcon_start_match_end_round() -> void:
|
||||
if _game_server == null:
|
||||
check_fail("rcon_03_start_match_end_round", "GameServer not available")
|
||||
return
|
||||
|
||||
var rm = _game_server.round_manager
|
||||
if rm == null:
|
||||
check_fail("rcon_03_start_match_end_round", "RoundManager not available")
|
||||
return
|
||||
|
||||
rm.start_warmup()
|
||||
|
||||
var handler_path = "res://server/scripts/rcon_command_handler.gd"
|
||||
if not ResourceLoader.exists(handler_path):
|
||||
check_fail("rcon_03_start_match_end_round", "RconCommandHandler not found")
|
||||
return
|
||||
|
||||
var handler = load(handler_path).new()
|
||||
add_child(handler)
|
||||
|
||||
if handler.has_signal("rcon_command"):
|
||||
handler.rcon_command.connect(_game_server._on_rcon_command)
|
||||
|
||||
var response = handler.handle_command("start_match", [])
|
||||
assert_true("rcon_03_start_match_response",
|
||||
"Command" in response or "dispatched" in response,
|
||||
"start_match command should be dispatched")
|
||||
|
||||
assert_true("rcon_03_match_started", rm.is_match_started(),
|
||||
"Match should be started via RCON")
|
||||
|
||||
rm.start_live()
|
||||
|
||||
response = handler.handle_command("end_round", ["ct", "bomb_defused"])
|
||||
assert_true("rcon_03_end_round_response",
|
||||
"Command" in response or "dispatched" in response,
|
||||
"end_round command should be dispatched")
|
||||
|
||||
assert_eq("rcon_03_phase_post", rm.get_phase(), rm.RoundPhase.POST,
|
||||
"Phase should be POST after end_round via RCON")
|
||||
|
||||
handler.queue_free()
|
||||
check_pass("rcon_03_start_match_end_round_ok")
|
||||
@@ -0,0 +1 @@
|
||||
uid://tenyf8ht7hfy
|
||||
Reference in New Issue
Block a user