mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): constrain ranked arena paths in postgres
This commit is contained in:
+2
-1
@@ -240,7 +240,8 @@ rating loss because no rated match began.
|
||||
until the trained-policy restriction is lifted. It chooses from that list
|
||||
deterministically from the proposal ID, so retrying a proposal cannot change
|
||||
its arena. Every durable proposal, match, and allocation boundary rechecks
|
||||
the same allowlist rather than accepting an arbitrary non-empty path. The selected scene is
|
||||
the same allowlist rather than accepting an arbitrary non-empty path; the
|
||||
PostgreSQL constraints enforce it for new direct SQL writes as well. The selected scene is
|
||||
persisted with the proposal/match plan, included in the allocation identity,
|
||||
and passed through the Agones GameServer annotation into the allocated
|
||||
server's validated `--arena-path` flag.
|
||||
|
||||
+1
-1
@@ -1448,7 +1448,7 @@ The same allocation path now carries the matcher-selected playlist, preventing a
|
||||
|
||||
Ranked proposal admission no longer trusts the matcher’s `--ranked-random-arena` boolean. The Go domain now owns a named allowlist for the three floor-goal `ArenaRegistry` entries, and rejects unknown and elevated IDs before any proposal is created.
|
||||
|
||||
The arena hand-off is now durable: the matcher deterministically selects an eligible floor-goal arena from the proposal ID, migration 0008 stores that path on proposals and matches, each durable transition rechecks the same allowlist, allocation claims and idempotency digests retain it, Agones applies it as a match-scoped annotation, and the supervisor overlays the allocated child’s `--arena-path`. Godot accepts only the same floor-goal `ArenaRegistry` paths and requires one for allocated ranked matches, so a stale Fleet default, an elevated variant, or an altered retry cannot substitute a ranked arena.
|
||||
The arena hand-off is now durable: the matcher deterministically selects an eligible floor-goal arena from the proposal ID, migration 0008 stores that path on proposals and matches and enforces it for new direct SQL writes, each durable transition rechecks the same allowlist, allocation claims and idempotency digests retain it, Agones applies it as a match-scoped annotation, and the supervisor overlays the allocated child’s `--arena-path`. Godot accepts only the same floor-goal `ArenaRegistry` paths and requires one for allocated ranked matches, so a stale Fleet default, an elevated variant, or an altered retry cannot substitute a ranked arena.
|
||||
|
||||
The Godot control-plane client now retains the exact last idempotent mutation and exposes `retry_last_mutation()` for transport, timeout, rate-limit, and 5xx failures. Retries reuse the original idempotency key and expected revision, while 401 and 409 responses remain non-retryable; the harness covers the policy boundary. This closes the local duplicate-action recovery mechanism for heartbeat/cancel/proposal calls, with broader live UI retry verification still remaining.
|
||||
|
||||
|
||||
@@ -4,5 +4,31 @@
|
||||
ALTER TABLE proposals
|
||||
ADD COLUMN match_arena_path TEXT;
|
||||
|
||||
ALTER TABLE proposals
|
||||
ADD CONSTRAINT proposals_ranked_arena_path
|
||||
CHECK (
|
||||
playlist <> 'ranked' OR (
|
||||
match_arena_path IS NOT NULL AND
|
||||
match_arena_path IN (
|
||||
'res://scenes/arena_01.tscn',
|
||||
'res://scenes/arena_02.tscn',
|
||||
'res://scenes/arena_03.tscn'
|
||||
)
|
||||
)
|
||||
) NOT VALID;
|
||||
|
||||
ALTER TABLE matches
|
||||
ADD COLUMN arena_path TEXT;
|
||||
|
||||
ALTER TABLE matches
|
||||
ADD CONSTRAINT matches_ranked_arena_path
|
||||
CHECK (
|
||||
playlist <> 'ranked' OR (
|
||||
arena_path IS NOT NULL AND
|
||||
arena_path IN (
|
||||
'res://scenes/arena_01.tscn',
|
||||
'res://scenes/arena_02.tscn',
|
||||
'res://scenes/arena_03.tscn'
|
||||
)
|
||||
)
|
||||
) NOT VALID;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
ALTER TABLE matches
|
||||
DROP CONSTRAINT IF EXISTS matches_ranked_arena_path,
|
||||
DROP COLUMN IF EXISTS arena_path;
|
||||
ALTER TABLE proposals
|
||||
DROP CONSTRAINT IF EXISTS proposals_ranked_arena_path,
|
||||
DROP COLUMN IF EXISTS match_arena_path;
|
||||
|
||||
@@ -7,6 +7,7 @@ import unittest
|
||||
SQL = (Path(__file__).parent / "0001_initial.sql").read_text()
|
||||
ASSIGNMENTS_SQL = (Path(__file__).parent / "0002_assignments.sql").read_text()
|
||||
QUOTAS_SQL = (Path(__file__).parent / "0007_allocation_quotas.sql").read_text()
|
||||
ARENAS_SQL = (Path(__file__).parent / "0008_match_arena_paths.sql").read_text()
|
||||
|
||||
|
||||
class MigrationTest(unittest.TestCase):
|
||||
@@ -59,6 +60,14 @@ class MigrationTest(unittest.TestCase):
|
||||
self.assertIn(fragment, QUOTAS_SQL)
|
||||
self.assertIn("region IN ('EU', 'NA')", QUOTAS_SQL)
|
||||
|
||||
def test_ranked_arena_paths_are_database_enforced_for_new_rows(self):
|
||||
for fragment in (
|
||||
"proposals_ranked_arena_path", "matches_ranked_arena_path", "NOT VALID",
|
||||
"match_arena_path IS NOT NULL", "arena_path IS NOT NULL",
|
||||
"res://scenes/arena_01.tscn", "res://scenes/arena_02.tscn", "res://scenes/arena_03.tscn",
|
||||
):
|
||||
self.assertIn(fragment, ARENAS_SQL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user