extends Node # Manual two-process smoke test for Phase 2 (tasks 2.1-2.5): match_config, # server-authoritative simulation, snapshot broadcast, client interpolation. # Not part of tests/test_runner.tscn — needs real ENet peers and a real # physics-driven ship. Run: # # godot --headless --path Game res://tests/networked_match_smoke.tscn -- --role=host # godot --headless --path Game res://tests/networked_match_smoke.tscn -- --role=client const PORT := 7812 const DEFAULT_SETTLE_SECONDS := 2.0 # time to let match_config + a few snapshots land before checking spawn state const DEFAULT_DRIVE_SECONDS := 2.0 # time to hold forward thrust and let the ship actually move var _role := "" var _settle_seconds := DEFAULT_SETTLE_SECONDS var _drive_seconds := DEFAULT_DRIVE_SECONDS var _exercise_ball_contact := false var _exercise_free_flight := false var _exercise_input_transitions := false var _exercise_match_state := false var _warmup_seconds := 0.0 func _ready() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--role="): _role = arg.substr("--role=".length()) elif arg.begins_with("--settle-seconds="): _settle_seconds = maxf(0.5, arg.get_slice("=", 1).to_float()) elif arg.begins_with("--drive-seconds="): _drive_seconds = maxf(0.5, arg.get_slice("=", 1).to_float()) elif arg == "--exercise-ball-contact": _exercise_ball_contact = true elif arg == "--exercise-free-flight": _exercise_free_flight = true elif arg == "--exercise-input-transitions": _exercise_input_transitions = true elif arg == "--exercise-match-state": _exercise_match_state = true elif arg.begins_with("--warmup-seconds="): _warmup_seconds = maxf(0.0, arg.get_slice("=", 1).to_float()) match _role: "host-disconnect": var derr := NetworkManager.host(PORT) if derr != OK: print("SMOKE FAIL: host() failed: %s" % error_string(derr)) get_tree().quit(1) return print("SMOKE: hosting (disconnect/reconnect scenario) on port %d ..." % PORT) MatchNet.player_joined.connect(_on_disconnect_host_player_joined) "host-latejoin": # §6.3: a spectator takes a vacated slot at the next kickoff. Run # with --slot-reservation-seconds= small, a plain `client` that # leaves, and a `client-latejoin` watching. var lerr := NetworkManager.host(PORT) if lerr != OK: print("SMOKE FAIL: host() failed: %s" % error_string(lerr)) get_tree().quit(1) return print("SMOKE: hosting (late-joiner promotion scenario) on port %d ..." % PORT) MatchNet.player_joined.connect(_on_latejoin_host_player_joined) "host": var err := NetworkManager.host(PORT) if err != OK: print("SMOKE FAIL: host() failed: %s" % error_string(err)) get_tree().quit(1) return print("SMOKE: hosting on port %d, waiting for a client to join the roster..." % PORT) MatchNet.player_joined.connect(_on_host_player_joined) "client": MatchNet.local_player_name = "NetTest" var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: print("SMOKE FAIL: join() failed: %s" % error_string(err)) get_tree().quit(1) return print("SMOKE: joining ...") MatchNet.welcomed.connect(_on_client_welcomed) "client-reconnect": # Same name as --role=client on purpose: §6.4 keys the reservation # to it. Run this as the SECOND life against --role=host-disconnect, # after a plain `client` has joined and dropped. MatchNet.local_player_name = "NetTest" var rerr := NetworkManager.join("127.0.0.1", PORT) if rerr != OK: print("SMOKE FAIL: join() failed: %s" % error_string(rerr)) get_tree().quit(1) return print("SMOKE: rejoining to reclaim a reserved slot ...") MatchNet.welcomed.connect(_on_reconnect_welcomed) "client-latejoin": # A name nobody reserved, so it starts as a spectator and can only # become a player via §6.3's kickoff promotion. MatchNet.local_player_name = "LateComer" var jerr := NetworkManager.join("127.0.0.1", PORT) if jerr != OK: print("SMOKE FAIL: join() failed: %s" % error_string(jerr)) get_tree().quit(1) return print("SMOKE: joining late, expecting to spectate then be promoted ...") MatchNet.welcomed.connect(_on_latejoin_welcomed) "client-spectator": # A name nobody reserved, so the server has no slot for it. MatchNet.local_player_name = "Watcher" var serr := NetworkManager.join("127.0.0.1", PORT) if serr != OK: print("SMOKE FAIL: join() failed: %s" % error_string(serr)) get_tree().quit(1) return print("SMOKE: joining as a spectator ...") MatchNet.welcomed.connect(_on_spectator_welcomed) "client-abuse-malformed", "client-abuse-flood", "client-abuse-flood-dutycycle": # task 3.4's disconnect-abusive-peer paths: joins normally (so # it's a real connected peer, exactly like a hostile custom # client would be — the validation doesn't get to assume # anything about who's on the other end of an authenticated # connection), then deliberately abuses MatchSim._recv_input # directly rather than going through networked_match.gd's own # honest encoder. MatchNet.local_player_name = "Abuser" var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: print("SMOKE FAIL: join() failed: %s" % error_string(err)) get_tree().quit(1) return print("SMOKE: joining to abuse (%s) ..." % _role) MatchNet.welcomed.connect(_on_abuser_welcomed) _: print("SMOKE FAIL: missing or unrecognised --role=") get_tree().quit(1) return func _process(_delta: float) -> void: NetworkManager.poll() func _physics_process(_delta: float) -> void: NetworkManager.poll() func _on_host_player_joined(_peer_id: int, _name: String) -> void: MatchNet.player_joined.disconnect(_on_host_player_joined) print("SMOKE: host loading networked_match.tscn ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) # The host must outlive client settle + drive, plus connection/shutdown # slack. This keeps --drive-seconds useful for sustained prediction QA. hooks.run_host_check.call_deferred(_settle_seconds + _warmup_seconds + _drive_seconds + 4.0, _exercise_match_state) func _on_client_welcomed() -> void: MatchNet.welcomed.disconnect(_on_client_welcomed) print("SMOKE: client loading networked_match.tscn ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_client_check.call_deferred(_settle_seconds, _drive_seconds, _exercise_ball_contact, _exercise_free_flight, _warmup_seconds, _exercise_input_transitions, _exercise_match_state) func _on_disconnect_host_player_joined(_peer_id: int, _name: String) -> void: MatchNet.player_joined.disconnect(_on_disconnect_host_player_joined) print("SMOKE: host loading networked_match.tscn (disconnect scenario) ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_disconnect_host_check.call_deferred(_drive_seconds) func _on_latejoin_host_player_joined(_peer_id: int, _name: String) -> void: MatchNet.player_joined.disconnect(_on_latejoin_host_player_joined) print("SMOKE: host loading networked_match.tscn (late-joiner scenario) ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_late_joiner_host_check.call_deferred(_drive_seconds) func _on_latejoin_welcomed() -> void: MatchNet.welcomed.disconnect(_on_latejoin_welcomed) get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_late_joiner_client_check.call_deferred(_drive_seconds) func _on_reconnect_welcomed() -> void: MatchNet.welcomed.disconnect(_on_reconnect_welcomed) print("SMOKE: reconnecting client loading networked_match.tscn ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_reconnect_client_check.call_deferred(_settle_seconds, _drive_seconds) func _on_spectator_welcomed() -> void: MatchNet.welcomed.disconnect(_on_spectator_welcomed) get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_spectator_check.call_deferred(_drive_seconds) func _on_abuser_welcomed() -> void: MatchNet.welcomed.disconnect(_on_abuser_welcomed) var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) if _role == "client-abuse-malformed": hooks.run_malformed_abuse_check.call_deferred() elif _role == "client-abuse-flood-dutycycle": hooks.run_duty_cycle_flood_abuse_check.call_deferred() else: hooks.run_rate_limit_abuse_check.call_deferred()