Prepare v0.6.7-alpha discovery release
This commit is contained in:
parent
ef30369865
commit
54937fad4b
7 changed files with 75 additions and 12 deletions
|
|
@ -79,6 +79,9 @@ See [`docs/API.md`](docs/API.md) for the versioned HTTP contract.
|
|||
|
||||
## Repository relationship
|
||||
|
||||
This is deliberately a separate repository from the Godot game. The game and service can version,
|
||||
deploy, and roll back independently. The discovery API version is independent from NETfishing's
|
||||
save schema and gameplay network protocol.
|
||||
This is deliberately a separate repository from the Godot game so the service can deploy and roll
|
||||
back independently. Production tags follow the coordinated NETfishing release train: a
|
||||
`vX.Y.Z-alpha` tag identifies the discovery snapshot tested for the same game and dedicated-server
|
||||
release. The versioned HTTP contract remains independent from NETfishing's save schema and gameplay
|
||||
network protocol. Each tagged deployment accepts room advertisements only from its matching game
|
||||
release so incompatible rooms are never presented as publicly available.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ All request and response bodies use `application/json`. Production clients must
|
|||
|
||||
Returns service status, package version, configured build revision, and the number of active,
|
||||
unexpired room leases. Deployments should set `NETFISHING_DISCOVERY_BUILD_REVISION` to the exact
|
||||
Git commit they are running.
|
||||
Git commit they are running. The package version follows the coordinated NETfishing release tag.
|
||||
|
||||
## `GET /v1/rooms`
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ trusting an address supplied by the game client.
|
|||
"port": 7777,
|
||||
"current_players": 0,
|
||||
"max_players": 8,
|
||||
"game_version": "0.6.4-alpha",
|
||||
"game_version": "0.6.7-alpha",
|
||||
"protocol_version": 3
|
||||
}
|
||||
```
|
||||
|
|
@ -47,6 +47,10 @@ excluded from public listings until the service observes that endpoint.
|
|||
The service applies configured global and per-observed-address active-room limits. Exceeding one
|
||||
returns `429 room_limit`; expired leases stop counting automatically.
|
||||
|
||||
The discovery release accepts room advertisements only from the matching NETfishing game release.
|
||||
A mismatched `game_version` returns `409 game_version_mismatch` with `required_game_version`; the
|
||||
room is not created or updated.
|
||||
|
||||
## `PUT /v1/rooms/{room_id}`
|
||||
|
||||
Renews and updates a room lease. Send the complete room payload and:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""NETfishing discovery service."""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.6.7-alpha"
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ class DiscoveryRequestHandler(BaseHTTPRequestHandler):
|
|||
payload = self._read_json_object()
|
||||
if payload is None:
|
||||
return
|
||||
if self._reject_incompatible_game_version(payload):
|
||||
return
|
||||
try:
|
||||
room, token, verification_token = self.server.registry.create(
|
||||
self._client_address(), payload
|
||||
|
|
@ -211,6 +213,8 @@ class DiscoveryRequestHandler(BaseHTTPRequestHandler):
|
|||
payload = self._read_json_object()
|
||||
if payload is None:
|
||||
return
|
||||
if self._reject_incompatible_game_version(payload):
|
||||
return
|
||||
try:
|
||||
room = self.server.registry.update(
|
||||
room_id,
|
||||
|
|
@ -300,6 +304,22 @@ class DiscoveryRequestHandler(BaseHTTPRequestHandler):
|
|||
except ValueError:
|
||||
return peer_text
|
||||
|
||||
def _reject_incompatible_game_version(self, payload: dict[str, Any]) -> bool:
|
||||
game_version = payload.get("game_version")
|
||||
if not isinstance(game_version, str) or game_version.strip() == __version__:
|
||||
return False
|
||||
self._send_error(
|
||||
HTTPStatus.CONFLICT,
|
||||
"game_version_mismatch",
|
||||
(
|
||||
f"This discovery server only lists NETfishing {__version__} rooms. "
|
||||
"Your room will not be listed until the game and discovery server "
|
||||
"use the same version."
|
||||
),
|
||||
{"required_game_version": __version__},
|
||||
)
|
||||
return True
|
||||
|
||||
def _room_id_from_path(self) -> str | None:
|
||||
path = urlsplit(self.path).path
|
||||
prefix = "/v1/rooms/"
|
||||
|
|
@ -341,8 +361,17 @@ class DiscoveryRequestHandler(BaseHTTPRequestHandler):
|
|||
raise ValueError
|
||||
return values[0]
|
||||
|
||||
def _send_error(self, status: HTTPStatus, code: str, message: str) -> None:
|
||||
self._send_json(status, {"error": {"code": code, "message": message}})
|
||||
def _send_error(
|
||||
self,
|
||||
status: HTTPStatus,
|
||||
code: str,
|
||||
message: str,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
error: dict[str, Any] = {"code": code, "message": message}
|
||||
if details is not None:
|
||||
error.update(details)
|
||||
self._send_json(status, {"error": error})
|
||||
|
||||
def _send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "netfishing-discovery-server"
|
||||
version = "0.2.0"
|
||||
version = "0.6.7-alpha"
|
||||
description = "Ephemeral public-room directory for NETfishing"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ VALID_ROOM = {
|
|||
"port": 7777,
|
||||
"current_players": 1,
|
||||
"max_players": 8,
|
||||
"game_version": "0.6.4-alpha",
|
||||
"game_version": "0.6.7-alpha",
|
||||
"protocol_version": 3,
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +91,19 @@ class DiscoveryHTTPTests(unittest.TestCase):
|
|||
listed_rooms = listing["rooms"]
|
||||
self.assertTrue(any(candidate["room_id"] == room_id for candidate in listed_rooms))
|
||||
|
||||
status, updated = self.request(
|
||||
"PUT",
|
||||
f"/v1/rooms/{room_id}",
|
||||
dict(VALID_ROOM, game_version="0.6.6-alpha"),
|
||||
str(token),
|
||||
)
|
||||
self.assertEqual(status, 409)
|
||||
assert updated is not None
|
||||
self.assertEqual(updated["error"]["code"], "game_version_mismatch")
|
||||
self.assertEqual(
|
||||
updated["error"]["required_game_version"], "0.6.7-alpha"
|
||||
)
|
||||
|
||||
status, updated = self.request(
|
||||
"PUT",
|
||||
f"/v1/rooms/{room_id}",
|
||||
|
|
@ -110,6 +123,20 @@ class DiscoveryHTTPTests(unittest.TestCase):
|
|||
assert body is not None
|
||||
self.assertEqual(body["error"]["code"], "invalid_room")
|
||||
|
||||
def test_outdated_game_cannot_list_a_room(self) -> None:
|
||||
status, body = self.request(
|
||||
"POST",
|
||||
"/v1/rooms",
|
||||
dict(VALID_ROOM, game_version="0.6.6-alpha"),
|
||||
)
|
||||
self.assertEqual(status, 409)
|
||||
assert body is not None
|
||||
error = body["error"]
|
||||
assert isinstance(error, dict)
|
||||
self.assertEqual(error["code"], "game_version_mismatch")
|
||||
self.assertEqual(error["required_game_version"], "0.6.7-alpha")
|
||||
self.assertIn("will not be listed", str(error["message"]))
|
||||
|
||||
def test_empty_dedicated_room_can_be_listed(self) -> None:
|
||||
status, created = self.request(
|
||||
"POST", "/v1/rooms", dict(VALID_ROOM, current_players=0)
|
||||
|
|
@ -173,7 +200,7 @@ class DiscoveryHTTPTests(unittest.TestCase):
|
|||
self.assertEqual(status, 200)
|
||||
assert body is not None
|
||||
self.assertEqual(body["status"], "ok")
|
||||
self.assertEqual(body["version"], "0.2.0")
|
||||
self.assertEqual(body["version"], "0.6.7-alpha")
|
||||
self.assertEqual(body["build_revision"], "test-build")
|
||||
|
||||
def test_udp_rendezvous_does_not_spawn_per_packet_threads(self) -> None:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ VALID_ROOM = {
|
|||
"port": 7777,
|
||||
"current_players": 1,
|
||||
"max_players": 8,
|
||||
"game_version": "0.6.4-alpha",
|
||||
"game_version": "0.6.7-alpha",
|
||||
"protocol_version": 3,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue