mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-23 21:05:29 +00:00
a5480ec7f7b2ad7cdbca40565e690a484fc55a45
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7e073dda0b |
fix(server): keep matchmaking games out of lobby reports; salvage invalid entries (#4641)
## Problem Prod masters log `Invalid IPC message from worker` for every ranked 1v1/2v2 match created: 1. The matchmaking poll loop creates the match game with the 1v1/2v2 playlist config (`gameType: Public`) but no `publicGameType` — correctly, since ranked games are invite-only and should never be advertised. 2. While that game sits in its ~7s lobby window, `GameManager.publicLobbies()` includes it (it only checks phase + `isPublic()`), and `WorkerLobbyService.sendMyLobbiesToMaster()` reports it with `publicGameType: gi.publicGameType!` — the non-null assertion hides the `undefined`, and IPC serialization drops the key. 3. The master's `WorkerMessageSchema` parse fails on the missing required field and **discards the worker's entire lobby report**. So for the lobby window of every ranked match, the master's view of that worker freezes: stale player counts and countdowns broadcast to all clients, `maybeScheduleLobby` repeatedly resetting the next lobby's countdown, and occasional duplicate scheduled lobbies (with no cleanup path for scheduled types). With matchmaking running continuously, some worker is blocked at any given moment on a busy server. ## Fix - **Worker** (`WorkerLobbyService`): filter games without a `publicGameType` out of the report. Matchmaking games are never advertised — which also guarantees they can't leak into the public lobby browser. - **Master** (`MasterLobbyService` + `IPCBridgeSchema`): validate lobby entries individually instead of failing the whole message. Malformed entries are logged and dropped; valid lobbies survive. A future bug of this class now degrades to one missing entry instead of a frozen worker view. The send side keeps compile-time safety via the existing per-entry `satisfies` annotations. ## Testing - New test: a matchmaking-style game (Public, `allowedPublicIds`, no `publicGameType`) is excluded from the worker's `lobbyList` report while a normal FFA lobby still goes through. - New test: a report containing a malformed entry keeps its valid lobbies in the next broadcast instead of being rejected wholesale. - Full `tests/server` suite passes (173 tests); `tsc --noEmit` and eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46b9fe88bb |
fix: survive host modal close during listed-lobby auto-start (#4514)
Follow-up to the listed-lobby auto-start feature (`2a82b01e3`, on main). ## Bug When a listed lobby auto-started (and in some paths on manual start), the game began server-side but a player got ejected back to the menu before it loaded. Root cause: `BaseModal.close()` navigates via `showPage()`, which **force-closes whichever page-modal is currently visible**. During the game-start transition that is the *other* lobby modal — closing the join modal first cascade-closed the host's modal with `leaveLobbyOnClose` still armed (host disconnected → host-left teardown killed the game); reversing the order just moved the victim to the joiner. No close order fixes both sides. ## Fix - **Disarm both lobby modals before closing either** in the prestart transition (`disarmLeaveOnClose()` on `HostLobbyModal` and `JoinLobbyModal`), so no cascade order can disconnect anyone mid game-start. - Both modals re-arm `leaveLobbyOnClose` in `onOpen` instead of `onClose`'s state reset, so once disarmed no later cascade close can re-arm it until the modal is reopened. - `HostLobbyModal.closeWithoutLeaving()` (pattern `JoinLobbyModal` already used), called explicitly instead of the generic modal-close loop. - **Server hardening:** `handleClientDisconnect` bails on `hasStarted()` (which includes prestart) instead of raw `_hasStarted`, so host socket churn during the lobby → game transition can never tear down a starting game regardless of client behavior. Regression test included. ## Verification Headless end-to-end (two browser contexts, host + joiner, `leave-lobby` stack-trace tripwire armed on both pages): - **Manual start:** host and joiner both receive prestart/start and enter the game; zero `leave-lobby` dispatches. - **Auto-start:** same, with nobody interacting after listing. Also confirmed manually in a real browser. The auto-start deadline was temporarily 30s during testing and is restored to 5 minutes in the final commit. Full suite green (1843 + 161), tsc/eslint/prettier clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2a82b01e30 |
feat: auto-start listed lobbies after 5 minutes
Hosts can't sit on a public listing: setListed(true) records listedAt, and once listedAt + HOSTED_LOBBY_AUTO_START_MS passes, GameManager's tick arms the normal start countdown (same path as the host's Start button, honoring startDelay). Cancelling the countdown re-arms on the next tick; unlisting cancels the deadline and relisting starts a fresh one. Duplicate setListed(true) calls don't extend it. The deadline rides to the host as autoStartAt in lobby info, rendered as an amber countdown next to the Public/Private switch in the host modal header (hidden once the real start countdown takes over). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5d21be826d |
feat: subscriber-hosted public lobby listing (#4480)
Part of #4040 (v1 scope: listing + browser + per-subscriber limit; custom lobby name/description left for a follow-up). ## What Subscribers can toggle their **private lobby** to be **publicly listed**; a browsable **"Open Lobbies"** list appears in the Join Lobby modal. Hard limit of **one listed lobby per subscriber**, enforced cluster-wide. ## How **Semantics** — a listed lobby stays `GameType.Private`: the host keeps full control and starts the game manually; the toggle only controls visibility. The `listed` flag lives on `GameServer` (not `GameConfig`), so it cannot be smuggled in through `update_game_config` and never touches core/sim/records. **Distribution** — reuses the existing public-lobby pipeline end to end: a new `"hosted"` `PublicGameType` bucket flows worker → master IPC → `/lobbies` websocket → `PublicLobbySocket`. Master scheduling now iterates only `SCHEDULED_PUBLIC_GAME_TYPES` (`ffa`/`team`/`special`), so it never sets countdowns on or schedules replacements for hosted lobbies. Lobbies delist automatically when the game starts/fills/dies (phase change). The broadcast fingerprint now includes browser-visible config, so host edits (map/mode) refresh the list even though the gameID doesn't change. **Gating** — new authenticated endpoint `POST /api/game/:id/listing`: - creator-only (403), private + not-started only (409) - fresh subscription check via server-side `getUserMe` using the shared `hasActiveSubscription()` helper (`active`/`trialing`); skipped in `GameEnv.Dev` (same precedent as Turnstile) so it's testable locally - one-lobby-per-creator (409): a SHA-256 hash of the creator's persistentID rides worker↔master IPC (`PublicGameInfo.creatorID`); the master dedupes as a race backstop. The hash — and host-only config (whitelist, name reveals) — are **stripped from every client payload** (broadcast + primed snapshot). **Client** — subscriber-gated "List lobby publicly" toggle in the host modal (server rejection reverts the toggle and shows a translated message); "Open Lobbies" rows (map, mode, player count) in the Join Lobby modal that reuse the existing private-join flow. **Compat** — `PublicGames.games` is now a `partialRecord`, so newer clients tolerate servers that don't send every bucket. Note: already-open old clients will fail to parse broadcasts containing the new `hosted` key until refreshed (closed Zod enum) — same class of break as previous wire-schema changes. ## Testing - `tests/server/HostedLobbyListing.test.ts` (15 tests): listed-lobby filtering, flag not settable via config intent, master aggregation + creator dedupe + no scheduling of hosted, creatorID stripping (broadcast + primed snapshot), `creatorHasListedLobby` (broadcast + local), fingerprint refresh on config change - `hasActiveSubscription` cases in `ApiSchemas.test.ts`; hosted counts-delta patch in `LobbySocket.test.ts` - Full suite green (1723 + 141 tests), tsc/eslint/prettier clean - **E2E in the real app** (headless Chromium, two browser contexts): host lists lobby → appears in second browser's Join Lobby list (creatorID absent from payload) → join succeeds (2 players in lobby) → same creator's second lobby rejected 409 with toggle revert → unlist removes it from a fresh browser's list. Curl negatives: missing auth 400, bad token 401, non-creator 403, missing game 404, bad body 400. ## Known follow-ups - Custom lobby name/description in the browser (needs the censor pipeline) — rest of #4040 - A listed lobby whose host closes the tab stays advertised indefinitely (an empty private lobby never leaves the Lobby phase) — pre-existing lifecycle, now more visible; consider delisting on creator disconnect 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |