mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): actually connect once matchmaking assigns a match
A player who completed the entire queue -> proposal -> allocate -> assign pipeline would reach ASSIGNED, see "Your match server is ready", and then simply sit there forever. connect_to_assignment() existed in control_plane_client.gd, fully validated (checks the assignment is available and fresh, splits and validates the endpoint, carries the join authorisation via MatchNet's hello payload rather than the URL) with its own assignment_connection_started/ assignment_connection_failed signals, and multiplayer-next.md's own §8.41 row already described it as wired -- but grepping the whole client found zero callers. Nothing anywhere in matchmaking.gd or control_plane_client.gd itself ever invoked it. ControlPlaneClient._connect_when_assigned() now calls it automatically the moment state.phase reaches ASSIGNED. Wired into the single call site every queue-shaped HTTP response already shares (heartbeat, recover, and resync-triggered recover alike, since the WebSocket match-lifecycle path always funnels into a REST resync first), so both the ordinary poll path and the WebSocket-push path are covered without a second call site to keep in sync. Two orderings are handled: if the assignment fetch triggered earlier by ASSIGNMENT_READY has already completed, it connects immediately; if not, it defers via _pending_connect_match_id and resolves once the assignment becomes available. _connect_attempted_match_id guards against a duplicate or replayed ASSIGNED event reattempting the connection. Verified against the real Godot 4.7.1 binary now that headless testing has resumed: two new test_control_plane_client.gd tests cover both orderings and the duplicate-attempt guard directly (216/216 total, 0 failed, no crash, no engine-level error, stable across repeated runs); full make verify-multiplayer-local and the complete make verify-enet-integration suite (all five cases, including the 3-process match) both pass clean; zero new crash reports throughout. multiplayer-next.md's §8.41 row is corrected to describe what was actually true (connect_to_assignment existed but was never called) rather than repeating the prior, inaccurate 'already wired' claim.
This commit is contained in:
@@ -38,6 +38,16 @@ var _authoritative_recovery_seconds := AUTHORITATIVE_RECOVERY_INTERVAL_SECONDS
|
||||
var _pending_proposal_id := ""
|
||||
var _pending_assignment_match_id := ""
|
||||
var _pending_resync_resource_id := ""
|
||||
# The final wiring step of the matchmaking pipeline: once state.phase reaches
|
||||
# ASSIGNED, the client must actually start the game transport. connect_to_assignment()
|
||||
# already existed with correct validation/signal behavior, but nothing ever
|
||||
# called it -- a player would sit on "Your match server is ready" forever.
|
||||
# These two fields defer the connect attempt until the assignment fetch
|
||||
# (triggered independently, earlier, by ASSIGNMENT_READY) has actually
|
||||
# completed, and prevent a duplicate/replayed ASSIGNED update from firing a
|
||||
# second connection attempt for the same match.
|
||||
var _pending_connect_match_id := ""
|
||||
var _connect_attempted_match_id := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -85,9 +95,39 @@ func _process(_delta: float) -> void:
|
||||
var match_id := _pending_assignment_match_id
|
||||
_pending_assignment_match_id = ""
|
||||
fetch_assignment(match_id)
|
||||
if not _pending_connect_match_id.is_empty() and _assignment_ready_for(_pending_connect_match_id):
|
||||
var match_id := _pending_connect_match_id
|
||||
_pending_connect_match_id = ""
|
||||
_connect_attempted_match_id = match_id
|
||||
connect_to_assignment()
|
||||
_poll_authoritative_recovery(_delta)
|
||||
|
||||
|
||||
# The assignment fetch (triggered independently by ASSIGNMENT_READY, which
|
||||
# always precedes ASSIGNED) and the ASSIGNED transition that should start the
|
||||
# transport can arrive in either order. This is the shared readiness check
|
||||
# both _connect_when_assigned and the deferred _process retry above use.
|
||||
func _assignment_ready_for(match_id: String) -> bool:
|
||||
return assignment != null and assignment.available and assignment.match_id == match_id and _assignment_is_fresh(assignment)
|
||||
|
||||
|
||||
# Starts (or defers, if the assignment fetch triggered by the earlier
|
||||
# ASSIGNMENT_READY event hasn't completed yet) the game transport once the
|
||||
# ticket-state machine reaches ASSIGNED. connect_to_assignment() itself
|
||||
# already existed with full validation and failure signalling; nothing ever
|
||||
# called it, so a player reaching "Your match server is ready" never actually
|
||||
# connected. _connect_attempted_match_id guards against a duplicate/replayed
|
||||
# ASSIGNED update firing a second connection attempt for the same match.
|
||||
func _connect_when_assigned(match_id: String) -> void:
|
||||
if state.phase != MatchmakingState.ASSIGNED or not is_valid_resource_id(match_id) or match_id == _connect_attempted_match_id:
|
||||
return
|
||||
if _assignment_ready_for(match_id):
|
||||
_connect_attempted_match_id = match_id
|
||||
connect_to_assignment()
|
||||
else:
|
||||
_pending_connect_match_id = match_id
|
||||
|
||||
|
||||
func configure(url: String, token: String) -> bool:
|
||||
var normalized := url.strip_edges().trim_suffix("/")
|
||||
var normalized_token := token.strip_edges()
|
||||
@@ -508,6 +548,7 @@ func _on_request_completed(result: HTTPRequest.Result, response_code: int, _head
|
||||
if state.apply_ticket_update(normalize_ticket(payload), operation == "queue_recover"):
|
||||
_queue_proposal_if_ready(payload)
|
||||
_queue_assignment_if_ready(payload)
|
||||
_connect_when_assigned(String(payload.get("match_id", "")))
|
||||
elif operation.begins_with("proposal_"):
|
||||
state.apply_proposal_update(normalize_proposal(payload))
|
||||
elif operation == "ranked_profile":
|
||||
|
||||
Reference in New Issue
Block a user