Files
client/matchmaking-api/app/main.py
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

59 lines
1.8 KiB
Python

import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy import select
from app.config import settings
from app.database import Base, async_session, engine
from app.models import GameServer, Mode, ServerStatus
from app.queue_manager import queue_manager
from app.routers import matchmaking, ranks, servers, stats
async def _seed_dev_server() -> None:
async with async_session() as db:
for mode in (Mode.casual, Mode.ranked):
existing = (
await db.execute(
select(GameServer).where(
GameServer.ip == settings.dev_seed_server_ip,
GameServer.port == settings.dev_seed_server_port,
GameServer.mode == mode,
)
)
).scalars().first()
if existing is None:
db.add(
GameServer(
ip=settings.dev_seed_server_ip,
port=settings.dev_seed_server_port,
mode=mode,
status=ServerStatus.available,
)
)
await db.commit()
@asynccontextmanager
async def lifespan(app: FastAPI):
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await _seed_dev_server()
matching_task = asyncio.create_task(queue_manager.run_matching_loop())
yield
matching_task.cancel()
app = FastAPI(title="Spacewar Matchmaking API", lifespan=lifespan)
app.include_router(matchmaking.router)
app.include_router(stats.router)
app.include_router(ranks.router)
app.include_router(servers.router)
@app.get("/health")
async def health() -> dict:
return {"status": "ok"}