feat(multiplayer): publish ranked profile contract

This commit is contained in:
Josh Creek
2026-09-01 21:51:42 +01:00
parent 51f6e19339
commit 47aa196b59
3 changed files with 15 additions and 1 deletions
+2
View File
@@ -1530,3 +1530,5 @@ The ENet gate now auto-detects `/Applications/Godot.app/Contents/MacOS/Godot` wh
The client matchmaking projection now preserves the server's `enqueued_at` timestamp through normalization, snapshots, and recovery, and uses it for the displayed queue wait when available. This prevents a client restart or delayed response from resetting the user's perceived wait to local process uptime; a local timer remains the fallback when older responses omit the timestamp. Godot state and normalization tests cover the projection and restore path.
The ranked profile projection now also carries the active season's authoritative end timestamp from PostgreSQL through the API and Godot client. Ranked matchmaking displays a bounded days-remaining countdown, while providers without an active season remain compatible and omit the countdown.
The versioned OpenAPI contract now declares the implemented `/profile/ranked` surface and its server-authoritative ranked profile schema, including optional active-season metadata. Contract tests reject omission of this operation, extra response fields, and credential leakage.
+5
View File
@@ -19,6 +19,9 @@
"/profile": {
"get": {"operationId": "getProfile", "responses": {"200": {"description": "Profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}
},
"/profile/ranked": {
"get": {"operationId": "getRankedProfile", "responses": {"200": {"description": "Authoritative ranked profile", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RankedProfile"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "404": {"$ref": "#/components/responses/NotFound"}, "503": {"$ref": "#/components/responses/Unavailable"}}}
},
"/queue/tickets": {
"post": {
"operationId": "createQueueTicket",
@@ -73,6 +76,7 @@
"Conflict": {"description": "Revision or idempotency conflict"},
"Invalid": {"description": "Invalid state or schema"},
"NotFound": {"description": "Resource not found"},
"Unavailable": {"description": "Authoritative profile temporarily unavailable"},
"Expired": {"description": "Resource expired"}
},
"schemas": {
@@ -80,6 +84,7 @@
"SteamLogin": {"type": "object", "required": ["web_api_ticket"], "additionalProperties": false, "properties": {"web_api_ticket": {"type": "string", "minLength": 1, "maxLength": 4096}}},
"Session": {"type": "object", "required": ["player_id", "expires_at", "access_token"], "additionalProperties": false, "properties": {"player_id": {"$ref": "#/components/schemas/OpaqueId"}, "expires_at": {"type": "string", "format": "date-time"}, "access_token": {"type": "string"}}},
"Profile": {"type": "object", "required": ["player_id", "rating", "rd", "provisional"], "additionalProperties": false, "properties": {"player_id": {"$ref": "#/components/schemas/OpaqueId"}, "rating": {"type": "number"}, "rd": {"type": "number"}, "provisional": {"type": "boolean"}}},
"RankedProfile": {"type": "object", "required": ["rating", "rd", "volatility", "ranked_games", "tier", "provisional"], "additionalProperties": false, "properties": {"rating": {"type": "number", "minimum": 0}, "rd": {"type": "number", "minimum": 0}, "volatility": {"type": "number", "minimum": 0}, "ranked_games": {"type": "integer", "minimum": 0}, "tier": {"type": "string", "enum": ["PROVISIONAL", "BRONZE", "SILVER", "GOLD", "PLATINUM", "DIAMOND"]}, "provisional": {"type": "boolean"}, "season_id": {"$ref": "#/components/schemas/OpaqueId"}, "season_ends_at": {"type": "string", "format": "date-time"}}},
"QueueCreate": {"type": "object", "required": ["playlist", "client_build", "protocol_version"], "additionalProperties": false, "properties": {"playlist": {"type": "string", "enum": ["casual", "ranked"]}, "client_build": {"type": "string", "minLength": 1, "maxLength": 128}, "protocol_version": {"type": "integer", "minimum": 1}}},
"QueueTicket": {"type": "object", "required": ["ticket_id", "player_id", "playlist", "state", "revision", "enqueued_at", "expires_at"], "additionalProperties": false, "properties": {"ticket_id": {"$ref": "#/components/schemas/OpaqueId"}, "player_id": {"$ref": "#/components/schemas/OpaqueId"}, "playlist": {"type": "string", "enum": ["casual", "ranked"]}, "state": {"$ref": "#/components/schemas/QueueState"}, "revision": {"type": "integer", "minimum": 0}, "enqueued_at": {"type": "string", "format": "date-time"}, "expires_at": {"type": "string", "format": "date-time"}}},
"QueueState": {"type": "string", "enum": ["QUEUED", "PROPOSED", "ACCEPTED", "ALLOCATING", "PROCESS_READY", "ASSIGNMENT_READY", "ASSIGNED", "CONNECTING", "LIVE", "RESULT_PENDING", "COMPLETED", "CANCELLED", "EXPIRED", "FAILED"]},
+8 -1
View File
@@ -27,9 +27,16 @@ class ContractTest(unittest.TestCase):
"createSteamSession", "getProfile", "createQueueTicket",
"heartbeatQueueTicket", "cancelQueueTicket", "acceptProposal",
"declineProposal", "getAssignment", "registerServer",
"submitMatchResult",
"submitMatchResult", "getRankedProfile",
} <= operations)
def test_ranked_profile_contract_is_authoritative_and_optional_season_metadata(self):
schema = self.openapi["components"]["schemas"]["RankedProfile"]
self.assertEqual(schema["required"], ["rating", "rd", "volatility", "ranked_games", "tier", "provisional"])
self.assertFalse(schema["additionalProperties"])
self.assertEqual(schema["properties"]["season_ends_at"]["format"], "date-time")
self.assertNotIn("access_token", json.dumps(schema).lower())
def test_mutations_require_idempotency_and_revision(self):
parameters = self.openapi["components"]["parameters"]
self.assertEqual(parameters["IdempotencyKey"]["name"], "Idempotency-Key")