mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
5714829c13
The disconnect scenario only ever asserted the server's bookkeeping, and the client's half was failing every run. run_disconnect_host_check ticked 60 physics frames past the reclaim and then shut the server down, so the reconnecting client - whose wiring check waits a 2.0s settle before it looks at anything - had its peer torn out from under it and reported "current_scene is not NetworkedMatch after 2.0s". The host printed PASS throughout, and the host was the side anyone read. The hold is now a real window (8s), and the host also asserts that the reconnected player's input reaches the server and moves the ship the server owns - every other assertion there is slot bookkeeping that would hold identically for a client whose input pipeline came back dead. Both position and connection state are sampled while the peer is still connected: the client leaves on its own schedule, and an end-of-hold sample reported still_connected=false for a good run. New --role=client-reconnect asserts the returning player is not a spectator, owns a slot with its own peer_id, has a real ship, rejoined a live match with the clock already known (§6.2 step 2's bootstrap), and can still drive. That set is chosen because a stale _last_match_config once made a reconnecting player a spectator, and that bug was visible in this scenario's own logs while it reported PASS. Verified 3/3 both sides. Control: rejoining while the slot is still occupied fails on is_player=false - and since the first control run reported it as the generic "lost its ship mid-drive", the spectator case is now diagnosed before the drive rather than after.
178 lines
7.4 KiB
GDScript
178 lines
7.4 KiB
GDScript
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":
|
|
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-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_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()
|