mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): submit allocated match results
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
class_name ServerResultClient
|
||||
extends Node
|
||||
|
||||
# The allocated server is the sole authority able to finish a match. Keep the
|
||||
# match in RESULTS until the control plane has durably acknowledged this exact,
|
||||
# idempotent payload: exiting first would strand the match in LIVE forever.
|
||||
|
||||
signal accepted
|
||||
signal retrying(http_code: int)
|
||||
|
||||
const RETRY_SECONDS := 1.0
|
||||
|
||||
var _base_url := ""
|
||||
var _workload_token := ""
|
||||
var _match_id := ""
|
||||
var _server_id := ""
|
||||
var _submitting := false
|
||||
|
||||
|
||||
func configure(base_url: String, workload_token: String, match_id: String, server_id: String) -> bool:
|
||||
base_url = base_url.strip_edges().trim_suffix("/")
|
||||
workload_token = workload_token.strip_edges()
|
||||
if not valid_configuration(base_url, workload_token, match_id, server_id):
|
||||
return false
|
||||
_base_url = base_url
|
||||
_workload_token = workload_token
|
||||
_match_id = match_id
|
||||
_server_id = server_id
|
||||
return true
|
||||
|
||||
|
||||
func submit(team_0: int, team_1: int, integrity_state := "CERTIFIED") -> void:
|
||||
if _submitting or team_0 < 0 or team_1 < 0 or integrity_state != "CERTIFIED":
|
||||
return
|
||||
_submitting = true
|
||||
var nonce := result_nonce(_match_id, _server_id, team_0, team_1, integrity_state)
|
||||
var key := "server-result-" + nonce
|
||||
var payload := {
|
||||
"match_id": _match_id,
|
||||
"result_nonce": nonce,
|
||||
"score": {"team_0": team_0, "team_1": team_1},
|
||||
"integrity_state": integrity_state,
|
||||
}
|
||||
while is_inside_tree():
|
||||
var response := await _send(payload, key)
|
||||
if response_is_accepted(int(response.get("code", 0))):
|
||||
_submitting = false
|
||||
accepted.emit()
|
||||
return
|
||||
retrying.emit(int(response.get("code", 0)))
|
||||
await get_tree().create_timer(RETRY_SECONDS).timeout
|
||||
_submitting = false
|
||||
|
||||
|
||||
func _send(payload: Dictionary, key: String) -> Dictionary:
|
||||
var request := HTTPRequest.new()
|
||||
request.timeout = 5.0
|
||||
add_child(request)
|
||||
var err := request.request("%s/v1/servers/%s/result" % [_base_url, _server_id.uri_encode()], [
|
||||
"Authorization: Bearer " + _workload_token,
|
||||
"Content-Type: application/json",
|
||||
"Idempotency-Key: " + key,
|
||||
], HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if err != OK:
|
||||
request.queue_free()
|
||||
return {"code": 0}
|
||||
var raw: Array = await request.request_completed
|
||||
request.queue_free()
|
||||
if int(raw[0]) != HTTPRequest.RESULT_SUCCESS:
|
||||
return {"code": 0}
|
||||
return {"code": int(raw[1])}
|
||||
|
||||
|
||||
static func result_nonce(match_id: String, server_id: String, team_0: int, team_1: int, integrity_state: String) -> String:
|
||||
# Result score is immutable once NetworkedMatch enters RESULTS. A deterministic
|
||||
# nonce makes retries after a lost response provably the same submission.
|
||||
return "result-" + (match_id + "\n" + server_id + "\n" + str(team_0) + "\n" + str(team_1) + "\n" + integrity_state).sha256_text()
|
||||
|
||||
|
||||
static func response_is_accepted(http_code: int) -> bool:
|
||||
# The documented endpoint acknowledges only after its serializable result
|
||||
# transaction commits. Do not treat a generic 2xx as proof of completion.
|
||||
return http_code == 202
|
||||
|
||||
|
||||
static func valid_configuration(base_url: String, workload_token: String, match_id: String, server_id: String) -> bool:
|
||||
if not (base_url.begins_with("http://") or base_url.begins_with("https://")) or base_url.contains("\n") or base_url.contains("\r") or base_url.contains("?") or base_url.contains("#"):
|
||||
return false
|
||||
if workload_token.is_empty() or workload_token.contains("\n") or workload_token.contains("\r"):
|
||||
return false
|
||||
return match_id.length() >= 8 and server_id.length() >= 8 and not match_id.contains("/") and not server_id.contains("/")
|
||||
Reference in New Issue
Block a user