Add ENet multiplayer (movement/combat sync) and matchmaking API
Networked play, end to end: connect, get matched, spawn, fight, respawn. Godot side: - Replace the single hardcoded Player with per-peer networked ships (MultiplayerSpawner + custom spawn_function), driven by client-side prediction with server reconciliation for movement. - Server-authoritative combat: bullets, health, death/respawn all decided by the server and broadcast to every peer over RPC. - Fix a Camera2D bug where Godot auto-promotes the first camera to ever enter the scene tree as the active one regardless of its own `current` value — with MultiplayerSpawner catch-up replication that was almost never the local player's own ship. `make_current()` on the owner's camera fixes it; property assignment doesn't reliably override the auto-claim. - Fix two "late joiner never learns already-established state" gaps (health, visibility) by folding both into the existing per-tick position broadcast instead of relying on one-shot RPCs that only reach peers already connected when they fire. - Make the 2 offered factions in team select match for every player in a match: the server decides once and clients either read it directly (server's own instance) or request it over RPC, rather than each client rolling its own random pair. - New MatchmakingClient autoload wires the main menu's CASUAL/RANKED buttons to the matchmaking API instead of connecting directly. New matchmaking-api/ (FastAPI + Postgres, Docker Compose): - Queue/match/stats/ranks endpoints. In-memory matchmaking queue in a single API process — no Redis until there's an actual reason to shard the queue across instances. - Background loop forms a match once enough players are queued for a mode, assigns the first available registered game server. - No real server pool yet: one dev server is auto-seeded from DEV_SEED_SERVER_IP/PORT; POST /servers/register exists for real servers to self-register later but nothing calls it yet. Docs: CLAUDE.md and overview/tech.md updated to match — networking checklist mostly checked off, matchmaking backend section rewritten to describe what was actually built vs. the original Go/Redis plan, and Current Tasks reordered around what's actually left (bot fill, real server pool, ranked/MMR refinement, lag compensation). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Spacewar Matchmaking API
|
||||
|
||||
FastAPI service for matchmaking, stats, and ranks (chat planned — see below).
|
||||
One process, one Postgres database, no Redis: matchmaking tickets live
|
||||
in-memory in the API process, which is enough at this scale — add Redis
|
||||
later only if the API needs to run as more than one instance.
|
||||
|
||||
## Run it
|
||||
|
||||
```
|
||||
cp .env.example .env # already done for local dev; edit if needed
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
API is at `http://localhost:8100`, interactive docs at `/docs`. Postgres is
|
||||
exposed on host port `5433` (mapped from its usual `5432`, to avoid clashing
|
||||
with any other local Postgres) for local inspection.
|
||||
|
||||
```
|
||||
docker compose down # stop
|
||||
docker compose down -v # stop + wipe the postgres volume
|
||||
```
|
||||
|
||||
Code in `app/` is bind-mounted with `--reload`, so edits take effect without
|
||||
rebuilding the image. Rebuild (`docker compose up -d --build`) only when
|
||||
`requirements.txt` changes.
|
||||
|
||||
## How matchmaking works right now
|
||||
|
||||
- `POST /matchmaking/queue/join` `{callsign, mode, mmr?}` — creates the
|
||||
player row if it doesn't exist yet, returns a `ticket_id`.
|
||||
- `GET /matchmaking/queue/status/{ticket_id}` — poll this; once matched it
|
||||
returns the assigned server's `server_ip`/`server_port`/`team`.
|
||||
- `POST /matchmaking/queue/leave?ticket_id=...` — cancel a queued ticket.
|
||||
|
||||
A background loop (`app/queue_manager.py`) runs every 2s, and once enough
|
||||
players are queued for a mode (`CASUAL_TEAM_SIZE`/`RANKED_TEAM_SIZE` × 2) it
|
||||
forms a match, splits them into two teams, and assigns the first available
|
||||
`GameServer` for that mode.
|
||||
|
||||
There's no real game-server pool yet — one dev server is auto-seeded on API
|
||||
startup from `DEV_SEED_SERVER_IP`/`DEV_SEED_SERVER_PORT` (defaults to
|
||||
`127.0.0.1:7777`, i.e. a Godot server run on the host via
|
||||
`godot4 --headless --path spacewar -- --server`). That IP is handed straight
|
||||
to game clients to connect to, so it must be reachable from wherever the
|
||||
*client* runs, not the API container — `host.docker.internal` would resolve
|
||||
inside the api container but not on the client's machine, which is why this
|
||||
isn't a docker-internal address. Real servers should eventually call
|
||||
`POST /servers/register` on boot and periodically as a heartbeat — that
|
||||
endpoint exists but nothing calls it yet.
|
||||
|
||||
## Client integration
|
||||
|
||||
The Godot client is wired up (`spacewar/autoload/matchmaking_client.gd`):
|
||||
CASUAL/RANKED on the main menu calls `POST /matchmaking/queue/join`, polls
|
||||
`GET /matchmaking/queue/status/{ticket_id}` every 1.5s, and once `matched`
|
||||
connects `NetworkManager` directly to the returned `server_ip`/`server_port`.
|
||||
Point it at a non-default API with `godot4 ... -- --matchmaking-api=http://host:port`.
|
||||
|
||||
## Adding a new domain (stats/ranks did this; chat will too)
|
||||
|
||||
1. Add/extend a model in `app/models.py` if it needs its own table.
|
||||
2. Add request/response shapes to `app/schemas.py`.
|
||||
3. Add a router file under `app/routers/`, `include_router()` it in
|
||||
`app/main.py`.
|
||||
|
||||
Chat isn't scaffolded yet (no data model or requirements decided), but it
|
||||
fits the same shape — likely a WebSocket router using FastAPI's native
|
||||
support, in this same service.
|
||||
|
||||
## Not set up yet, on purpose
|
||||
|
||||
- **Migrations**: tables are created via `Base.metadata.create_all()` on
|
||||
startup. Fine while the schema is still moving; switch to Alembic before
|
||||
this holds real data.
|
||||
- **Auth**: `callsign` is the only player identity, matching the game
|
||||
client's current state (no accounts yet). Real identity arrives with the
|
||||
GodotSteam auth checklist item in `overview/tech.md`.
|
||||
- **Ranked MMR-window widening / bot-fill timeouts**: current matching is a
|
||||
simple threshold (enough players queued → form a match). Refine once
|
||||
there's real queue volume to tune against.
|
||||
Reference in New Issue
Block a user