mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
142 lines
5.9 KiB
GDScript
142 lines
5.9 KiB
GDScript
extends Node
|
|
|
|
# Two-process real end-to-end proposal smoke test: extends
|
|
# control_plane_smoke.gd's single-player login/queue/heartbeat/cancel
|
|
# coverage to the matcher path -- two real Godot clients, two real queued
|
|
# tickets, a real running server/cmd/matcher pairing them, both clients
|
|
# observing the resulting proposal over the real WebSocket event stream and
|
|
# accepting it for real. multiplayer-next.md 8.40 names this "a two-player
|
|
# proposal round trip" as the next scoped extension to this harness.
|
|
#
|
|
# ControlPlaneClient is a singleton autoload, so one process can only ever be
|
|
# one player -- this mirrors net_smoke.gd's own host/client two-process
|
|
# pattern rather than trying to simulate two players in one process:
|
|
#
|
|
# godot --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- \
|
|
# --control-plane-url=http://127.0.0.1:PORT --role=player-a
|
|
# godot --headless --path Game res://tests/control_plane_proposal_smoke.tscn -- \
|
|
# --control-plane-url=http://127.0.0.1:PORT --role=player-b
|
|
#
|
|
# Prints one "SMOKE PASS/FAIL: ..." line and exits 0/1.
|
|
|
|
const TIMEOUT_SECONDS := 20.0
|
|
|
|
var _role := ""
|
|
var _finished := false
|
|
var _ticket_id := ""
|
|
var _accept_sent := false
|
|
|
|
|
|
func _ready() -> void:
|
|
var control_plane_url := ""
|
|
for arg in OS.get_cmdline_user_args():
|
|
if arg.begins_with("--control-plane-url="):
|
|
control_plane_url = arg.substr("--control-plane-url=".length())
|
|
elif arg.begins_with("--role="):
|
|
_role = arg.substr("--role=".length())
|
|
if control_plane_url.is_empty() or (_role != "player-a" and _role != "player-b"):
|
|
_finish(false, "missing --control-plane-url or --role=player-a|player-b")
|
|
return
|
|
if not ControlPlaneClient.configure(control_plane_url, "0:0"):
|
|
_finish(false, "configure() rejected a valid-looking base URL")
|
|
return
|
|
|
|
_ticket_id = "proposal-smoke-%s-%d" % [_role, Time.get_unix_time_from_system()]
|
|
ControlPlaneClient.request_succeeded.connect(_on_request_succeeded)
|
|
ControlPlaneClient.request_failed.connect(_on_request_failed)
|
|
ControlPlaneClient.state.changed.connect(_on_state_changed)
|
|
|
|
var web_api_ticket := "proposal-smoke-web-api-ticket-%s-%d" % [_role, Time.get_ticks_usec()]
|
|
var err := ControlPlaneClient.login_steam(web_api_ticket)
|
|
if err != OK:
|
|
_finish(false, "login_steam() failed to start: %s" % error_string(err))
|
|
return
|
|
print("SMOKE[%s]: logging in against %s..." % [_role, control_plane_url])
|
|
|
|
var timer := Timer.new()
|
|
timer.wait_time = TIMEOUT_SECONDS
|
|
timer.one_shot = true
|
|
timer.timeout.connect(func(): _finish(false, "timed out after %.1fs waiting for a proposal" % TIMEOUT_SECONDS))
|
|
add_child(timer)
|
|
timer.start()
|
|
|
|
|
|
func _on_request_succeeded(operation: String, payload: Dictionary) -> void:
|
|
if _finished:
|
|
return
|
|
if operation == "steam_session":
|
|
print("SMOKE[%s]: logged in as %s, queueing for a 2-player casual match..." % [_role, ControlPlaneClient.player_id])
|
|
var err := ControlPlaneClient.queue_create(_ticket_id, "casual", "smoke-build", 1)
|
|
if err != OK:
|
|
_finish(false, "queue_create() failed to start: %s" % error_string(err))
|
|
return
|
|
if operation == "queue_create":
|
|
print("SMOKE[%s]: ticket %s QUEUED, waiting for the matcher to propose a match..." % [_role, _ticket_id])
|
|
return
|
|
if operation.begins_with("proposal_"):
|
|
if payload.get("state", "") != "ACCEPTED" and payload.get("state", "") != "OPEN":
|
|
_finish(false, "unexpected proposal response after accept: %s" % payload)
|
|
return
|
|
if payload.get("state", "") == "ACCEPTED":
|
|
_finish(true, "both players queued, the real matcher formed a proposal, and this client's accept was recorded")
|
|
else:
|
|
print("SMOKE[%s]: accepted; waiting for the other player..." % _role)
|
|
var recovery_timer := get_tree().create_timer(1.0)
|
|
recovery_timer.timeout.connect(_recover_after_accept)
|
|
|
|
|
|
func _recover_after_accept() -> void:
|
|
if _finished or ControlPlaneClient._operation != "" or ControlPlaneClient.state.proposal_id.is_empty():
|
|
return
|
|
var err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id)
|
|
if err != OK and err != ERR_BUSY:
|
|
_finish(false, "proposal recovery after accept failed to start: %s" % error_string(err))
|
|
|
|
|
|
func _on_state_changed(snapshot: Dictionary) -> void:
|
|
if _finished or _accept_sent or String(snapshot.get("proposal_state", "")) != "OPEN":
|
|
return
|
|
if ControlPlaneClient._operation != "":
|
|
return # Already mid-request (e.g. the accept itself); avoid double-sending.
|
|
print("SMOKE[%s]: proposal %s is OPEN at revision %d, accepting..." % [_role, ControlPlaneClient.state.proposal_id, ControlPlaneClient.state.proposal_revision])
|
|
_accept_sent = true
|
|
if _role == "player-b":
|
|
var delay := get_tree().create_timer(0.5)
|
|
delay.timeout.connect(_send_accept)
|
|
return
|
|
_send_accept()
|
|
|
|
|
|
func _send_accept() -> void:
|
|
if _finished:
|
|
return
|
|
var err := ControlPlaneClient.respond_to_proposal(ControlPlaneClient.state.proposal_id, true, ControlPlaneClient.state.proposal_revision)
|
|
if err != OK and err != ERR_BUSY:
|
|
_accept_sent = false
|
|
_finish(false, "respond_to_proposal() failed to start: %s" % error_string(err))
|
|
|
|
|
|
func _on_request_failed(operation: String, http_code: int, detail: String) -> void:
|
|
if _finished:
|
|
return
|
|
if operation == "proposal_accept" and http_code == 409 and not ControlPlaneClient.state.proposal_id.is_empty():
|
|
print("SMOKE[%s]: concurrent accept conflicted at an old revision; recovering the authoritative proposal..." % _role)
|
|
_accept_sent = false
|
|
var err := ControlPlaneClient.recover_proposal(ControlPlaneClient.state.proposal_id)
|
|
if err != OK and err != ERR_BUSY:
|
|
_finish(false, "proposal recovery after concurrent accept failed to start: %s" % error_string(err))
|
|
return
|
|
_finish(false, "%s failed: http=%d detail=%s" % [operation, http_code, detail])
|
|
|
|
|
|
func _finish(passed: bool, detail: String) -> void:
|
|
if _finished:
|
|
return
|
|
_finished = true
|
|
if passed:
|
|
print("SMOKE PASS: [%s] %s" % [_role, detail])
|
|
get_tree().quit(0)
|
|
else:
|
|
print("SMOKE FAIL: [%s] %s" % [_role, detail])
|
|
get_tree().quit(1)
|