Prepare v0.6.7-alpha discovery release

This commit is contained in:
Alexander Sellite 2026-08-11 20:31:02 -04:00
parent ef30369865
commit 54937fad4b
7 changed files with 75 additions and 12 deletions

View file

@ -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.

View file

@ -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:

View file

@ -1,3 +1,3 @@
"""NETfishing discovery service."""
__version__ = "0.2.0"
__version__ = "0.6.7-alpha"

View file

@ -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")

View file

@ -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 = []

View file

@ -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:

View file

@ -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,
}