fix(multiplayer): gate allocated matches on full roster

This commit is contained in:
Josh Creek
2026-09-01 16:03:43 +01:00
parent 498f219ab9
commit bdb3c8f4a7
3 changed files with 19 additions and 0 deletions
+10
View File
@@ -83,6 +83,10 @@ func _ready() -> void:
get_tree().quit(1) get_tree().quit(1)
return return
_control = ServerControlScript.new() _control = ServerControlScript.new()
# An allocated process owns exactly the roster issued for this match.
# Never let the general-purpose direct-server default (one player) start
# an allocated match with only a partial assignment admitted.
config.values["min-players"] = required_min_players(true, roster_tokens.size(), int(config.get_value("min-players")))
_control.name = "ServerControl" _control.name = "ServerControl"
_control.drain_requested.connect(_on_drain_requested) _control.drain_requested.connect(_on_drain_requested)
get_tree().root.add_child.call_deferred(_control) get_tree().root.add_child.call_deferred(_control)
@@ -180,3 +184,9 @@ func _on_drain_requested() -> void:
_drain_requested = true _drain_requested = true
MatchNet.admissions_open = false MatchNet.admissions_open = false
ServerLog.info("server_draining", {"reason": "control_request"}) ServerLog.info("server_draining", {"reason": "control_request"})
static func required_min_players(allocated: bool, roster_size: int, configured: int) -> int:
if allocated and roster_size > 0:
return roster_size
return configured
+7
View File
@@ -157,3 +157,10 @@ func test_allocated_mode_rejects_missing_or_expired_assignment_manifest_fields()
assert_true(not missing.is_valid(), "client build and expiry are required") assert_true(not missing.is_valid(), "client build and expiry are required")
var expired = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--client-build=client", "--assignment-expiry-unix=1", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"]) var expired = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--client-build=client", "--assignment-expiry-unix=1", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"])
assert_true(not expired.is_valid(), "expired assignment is rejected") assert_true(not expired.is_valid(), "expired assignment is rejected")
func test_allocated_start_floor_is_the_verified_roster_size() -> void:
var boot = preload("res://scripts/server_boot.gd")
assert_eq(boot.required_min_players(true, 6, 1), 6, "allocated six-player roster cannot start with one player")
assert_eq(boot.required_min_players(true, 2, 6), 2, "allocated casual roster uses its complete size")
assert_eq(boot.required_min_players(false, 1, 1), 1, "direct server keeps its configured floor")
+2
View File
@@ -1396,3 +1396,5 @@ Bounded, but not by much: the attacker must race a genuine disconnect, and they
The current working implementation now wires `deploy/k8s/base/fleet.yaml` to the digest-pinned `game-server` supervisor target, the in-cluster control-plane Service, workload roster materialization, signing/drain secret references, downward-API server/image identity, and the required game-server egress policy. `kubectl kustomize deploy/k8s/base` and `server/security/test_fleet_manifests.py` pass. The older 8.28 narrative above still records the pre-wiring state; live Agones, operator secret/image replacement, and real cluster readiness remain explicit gates. The current working implementation now wires `deploy/k8s/base/fleet.yaml` to the digest-pinned `game-server` supervisor target, the in-cluster control-plane Service, workload roster materialization, signing/drain secret references, downward-API server/image identity, and the required game-server egress policy. `kubectl kustomize deploy/k8s/base` and `server/security/test_fleet_manifests.py` pass. The older 8.28 narrative above still records the pre-wiring state; live Agones, operator secret/image replacement, and real cluster readiness remain explicit gates.
The NA overlay now also patches the allocated childs `--region=NA` argument, keeping it aligned with the NA Fleet label; rendered EU and NA overlays and the adversarial manifest test verify that regional assignment validation cannot silently remain EU in the NA deployment. The NA overlay now also patches the allocated childs `--region=NA` argument, keeping it aligned with the NA Fleet label; rendered EU and NA overlays and the adversarial manifest test verify that regional assignment validation cannot silently remain EU in the NA deployment.
Allocated Godot startup now derives its `min-players` floor from the verified signed roster size, preventing the direct-server default of one player from starting a partially admitted allocated match. A focused regression test covers six-player, casual two-player, and direct-server behavior; the full Godot harness is currently unavailable because Godot cannot open its shared `user://` log and crashes in the macOS renderer before test execution.