fix(multiplayer): bound workload credential lifetime

This commit is contained in:
Josh Creek
2026-09-03 21:27:26 +01:00
parent 8507472635
commit ea1c65acfb
12 changed files with 46 additions and 11 deletions
+2 -2
View File
@@ -451,10 +451,10 @@ func configure_result_submission(callback: Callable) -> void:
_result_submit = callback
func submit_authoritative_result(score: Dictionary) -> bool:
func submit_authoritative_result(score: Dictionary, integrity_state := "CERTIFIED") -> bool:
if not _result_submit.is_valid() or not score.has(0) or not score.has(1):
return false
_result_submit.call(int(score[0]), int(score[1]))
_result_submit.call(int(score[0]), int(score[1]), integrity_state)
return true
+13 -2
View File
@@ -320,6 +320,8 @@ static var server_bot_fill_override := false
var _max_spectators := -1
var _last_emitted_countdown := -1
var _in_overtime := false
var _max_overtime_seconds := 900.0
var _overtime_deadline_tick := -1
var _match_over := false
var _planned_server_shutdown := false
var _awaiting_result_submission := false
@@ -349,6 +351,7 @@ func _ready() -> void:
# FULL_TIME/OVERTIME/RESULTS, so allow an override. Server-side only —
# a client cannot shorten anyone's match.
match_length_seconds = maxf(1.0, float(config.get_value("match-length")))
_max_overtime_seconds = maxf(1.0, float(config.get_value("max-overtime-seconds")))
var smoke_after := float(config.get_value("smoke-force-goal-after"))
if smoke_after >= 0.0:
_smoke_force_goal_tick = -2 # arm when PLAYING begins; -1 remains disabled
@@ -685,6 +688,8 @@ func _apply_match_state(new_state: int, at_tick: int) -> void:
# Kickoff is over: bodies move again, and the clock resumes.
_pending_freeze_tick = -1
_set_bodies_frozen(false)
if new_state == MatchState.State.OVERTIME:
_overtime_deadline_tick = at_tick + int(_max_overtime_seconds * SimConstants.TICK_HZ)
# The clock only advances during live play (§6.2 step 9). Derived here
# rather than tracked separately so it cannot disagree with the state.
var was_running := _clock_running
@@ -1055,12 +1060,12 @@ func _update_clock() -> void:
# --- §6.2 step 10: full time, overtime, results (task 5.5) -----------------
func _enter_results(winning_team: int) -> void:
func _enter_results(winning_team: int, integrity_state := "CERTIFIED") -> void:
_match_over = true
_clock_running = false
_set_bodies_frozen(true)
match_ended.emit(winning_team, score.duplicate())
if multiplayer.is_server() and MatchNet.submit_authoritative_result(score):
if multiplayer.is_server() and MatchNet.submit_authoritative_result(score, integrity_state):
_awaiting_result_submission = true
ServerLog.info("match_ended", {"score_0": score.get(0, 0), "score_1": score.get(1, 0), "overtime": _in_overtime})
_set_match_state(MatchState.State.RESULTS)
@@ -1103,6 +1108,12 @@ func _update_match_state() -> void:
else:
_enter_results(_winning_team())
return
if match_state == MatchState.State.OVERTIME and _overtime_deadline_tick >= 0 and now >= _overtime_deadline_tick:
# Golden goal remains clockless to players, but an operational bound is
# necessary: a stalled draw must finish while its allocated credential is
# valid. REVIEW completes lifecycle delivery without rating either side.
_enter_results(-1, "REVIEW")
return
if _state_deadline_tick < 0 or now < _state_deadline_tick:
return
match match_state:
+3
View File
@@ -56,6 +56,7 @@ static func specs() -> Array[Spec]:
out.append(Spec.new("log-level", Kind.STRING, "info", "logging", "One of debug, info, warn, error"))
out.append(Spec.new("replay-log", Kind.STRING, "", "logging", "Path to record a binary replay log to; empty disables (see tools/replay_dump.gd)"))
out.append(Spec.new("match-length", Kind.FLOAT, 150.0, "match", "Regulation length in seconds"))
out.append(Spec.new("max-overtime-seconds", Kind.FLOAT, 900.0, "match", "Safety cap for sudden death; expiry records a REVIEW result without rating changes"))
out.append(Spec.new("max-matches", Kind.INT, 0, "match", "Exit cleanly after this many completed matches; 0 runs forever"))
out.append(Spec.new("min-players", Kind.INT, 1, "match", "Players required before a match starts"))
out.append(Spec.new("start-countdown", Kind.FLOAT, 5.0, "match", "Seconds to wait after min-players is met before starting"))
@@ -255,6 +256,8 @@ func _validate() -> void:
errors.append("--max-clients must be at least 1, got %d" % int(values["max-clients"]))
if float(values["match-length"]) <= 0.0:
errors.append("--match-length must be positive, got %s" % str(values["match-length"]))
if float(values["max-overtime-seconds"]) <= 0.0:
errors.append("--max-overtime-seconds must be positive, got %s" % str(values["max-overtime-seconds"]))
if int(values["max-matches"]) < 0:
errors.append("--max-matches must be 0 or more, got %d" % int(values["max-matches"]))
if int(values["min-players"]) < 1:
+1 -1
View File
@@ -30,7 +30,7 @@ func configure(base_url: String, workload_token: String, match_id: String, serve
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":
if _submitting or team_0 < 0 or team_1 < 0 or not integrity_state in ["CERTIFIED", "REVIEW"]:
return
_submitting = true
var nonce := result_nonce(_match_id, _server_id, team_0, team_1, integrity_state)