Files
client/matchmaking-api/README.md
T
anekdotin edcda92812 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>
2026-07-12 22:37:59 -04:00

82 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.