Skip to content

Conversation

@ryanbarlow97
Copy link
Contributor

@ryanbarlow97 ryanbarlow97 commented Jan 20, 2026

Description:

adds a join public lobby modal
image

also fixes some issues with clicking/unclicking the button (turnstile rework)

also made a "connecting" to the lobby
image

also allows people to rejoin games as we now store a clientID per gameID locally

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

w.o.n

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

Walkthrough

Adds a public lobby modal and JoinPublicLobbyModal component, a TurnstileManager for join tokens, per-game client IDs, server-side "lobby_info" broadcasts plus socket-replace logic, and integrates these into client join flows, translations, and dev proxy behavior.

Changes

Cohort / File(s) Summary
HTML
index.html
Inserted a new join-public-lobby-modal element into the main content area (hidden/full-page modal).
Translations
resources/lang/en.json
Added common.enabled, public_lobby.*, game_mode.humans_vs_nations, and matchmaking_modal.* keys for public-lobby and matchmaking UI.
Public Lobby Component
src/client/JoinPublicLobbyModal.ts
New Lit web component exported as join-public-lobby-modal; tracks lobby via lobby-info events, renders players/config/countdown, manages join timeout and leave behavior, exposes open(), closeAndLeave(), closeWithoutLeaving().
Turnstile Token Manager
src/client/TurnstileManager.ts
New TurnstileManager class handling token warmup, TTL, refresh scheduling, dedupe, getTokenForJoin() and invalidateToken().
Client wiring & eventing
src/client/Main.ts, src/client/ClientGameRunner.ts, src/client/LangSelector.ts
Wires joinPublicModal, joinAttemptId, TurnstileManager usage, server-config/cosmetics prefetch; dispatches DOM lobby-info on lobby_info messages; includes modal in translation updates; extends JoinLobbyEvent with source and publicLobbyInfo.
Public lobby UI & join flow
src/client/PublicLobby.ts
Simplified lobby card UI, removed debounce/highlight state; dispatches join-lobby with source: "public" and publicLobbyInfo; uses per-game client ID.
Per-game client ID & auth
src/client/Auth.ts, src/client/HostLobbyModal.ts, src/client/JoinPrivateLobbyModal.ts, src/client/Matchmaking.ts, src/client/SinglePlayerModal.ts
Added getClientIDForGame(gameID) stored in sessionStorage; replaced generateID() usage with per-game client IDs; HostLobbyModal.createLobby signature updated to accept creatorClientID and gameID.
Server lobby broadcasting & socket replace
src/server/GameServer.ts, src/core/Schemas.ts
Added ServerLobbyInfoMessage schema (type: "lobby_info", lobby: GameInfo), periodic broadcastLobbyInfo (1s) with start/stopLobbyInfoBroadcast, and replaceClientSocket for socket replacement on reconnects.
Join/update flows & join-time UX
src/client/JoinPrivateLobbyModal.ts, src/client/Matchmaking.ts, src/client/SinglePlayerModal.ts, src/client/HostLobbyModal.ts
Replaced generateID usage with getClientIDForGame, improved active-lobby checks/parsing, preserve deep-link URLs, and wire TurnstileManager prefetch in join flows.
Dev proxy
vite.config.ts
Added devGameHtmlBypass helper and applied it to /w0, /w1, /w2 proxies to bypass WS proxy for certain game HTML requests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Modal as JoinPublicLobbyModal
    participant Client as Main (Client)
    participant TM as TurnstileManager
    participant Server as GameServer
    participant UI as Renderer

    User->>Modal: Open public lobby UI / click join
    Modal->>Client: Dispatch "join-lobby" (source: "public", publicLobbyInfo)
    Client->>TM: getTokenForJoin(gameStartInfo?)
    TM->>TM: ensureToken (reuse or fetch)
    TM-->>Client: Return token or null
    Client->>Server: POST join (clientID, token, gameID)
    Server->>Server: replaceClientSocket() if reconnect
    Server->>Server: startLobbyInfoBroadcast()
    loop every 1s
      Server->>Client: "lobby_info" (GameInfo)
      Client->>Modal: Dispatch DOM "lobby-info"
      Modal->>UI: updateFromLobby() -> render players, countdown, status
    end
    Server->>Client: "start" when game begins
    Server->>Server: stopLobbyInfoBroadcast()
    Modal->>Modal: Cleanup timers / close or transition
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

A modal opens where public players meet,
Turnstile hums a token, fresh and neat,
Per-game IDs remember every name,
Servers whisper lobby state each second the same,
Countdown ticks — the match takes its frame. 🎮✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Publiclobbymodal' is vague and lacks clarity—it's merely a component name without context about the primary change or purpose. Revise to a clear, descriptive title such as 'Add public lobby join modal with connecting state' to communicate the main change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly describes the main changes: adding a join public lobby modal, fixing Turnstile-related click issues, implementing a connecting state, and enabling game rejoin via per-game client ID storage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ryanbarlow97 ryanbarlow97 added this to the v29 milestone Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/client/JoinPublicLobbyModal.ts`:
- Around line 302-311: The lobby callback in startLobbySocket currently only
handles non-null lobbies via updateFromLobby, so when WorkerLobbySocket emits
null on lobby_closed the UI remains stale; update startLobbySocket's callback to
explicitly handle lobby === null by closing the JoinPublicLobbyModal (call the
modal close/dismiss method used in this class) and trigger a redirect/navigation
to the appropriate page (or emit the same event path used elsewhere), otherwise
call updateFromLobby(lobby) as before; reference startLobbySocket,
this.lobbySocket, WorkerLobbySocket, and updateFromLobby to locate where to add
the null branch.
- Around line 24-36: The countdown freezes because render() uses Date.now() but
no timer triggers re-renders; add a 1-second setInterval when lobbyStartAt is
non-null that calls this.requestUpdate() to force updates while the lobby start
time is set (store the interval id on the instance, e.g.
this._countdownTimerId), ensure you only create one timer and clear it when
lobbyStartAt becomes null and also clear it in onClose() using
clearInterval(this._countdownTimerId) to avoid leaks; reference render(),
lobbyStartAt, requestUpdate(), and onClose() when making these changes.
- Around line 43-47: The onBack handler is passed as this.closeAndLeave which
loses the class instance context; update the modalHeader call to wrap the method
so it preserves this (e.g., use an arrow wrapper or bind) — replace onBack:
this.closeAndLeave with a bound wrapper such as onBack: () =>
this.closeAndLeave() (or onBack: this.closeAndLeave.bind(this)) so
closeAndLeave() can safely call this.close() and other instance methods.
🧹 Nitpick comments (4)
src/client/LobbySocket.ts (2)

215-240: Consider handling config fetch failure more gracefully.

If getServerConfigFromClient() throws, handleConnectError is called, which increments attempts and may schedule reconnect. However, a persistent config fetch failure (e.g., network down) will silently exhaust retries without user feedback.

This is acceptable for now since the feature is new, but consider adding an onError callback option for surfacing connection failures to the UI.


274-301: No fallback mechanism after max attempts.

Unlike PublicLobbySocket which falls back to HTTP polling, WorkerLobbySocket silently stops after maxWsAttempts. The user will see stale data with no indication of failure.

Consider either:

  1. Adding an HTTP fallback endpoint
  2. Passing an onMaxAttemptsReached callback to notify the UI
src/client/Main.ts (1)

206-216: Use a discriminated union for JoinLobbyEvent.

source and publicLobbyInfo can drift. A union makes publicLobbyInfo required only when source: "public" and keeps other cases clean.

♻️ Suggested typing
-export interface JoinLobbyEvent {
-  clientID: string;
-  // Multiplayer games only have gameID, gameConfig is not known until game starts.
-  gameID: string;
-  // GameConfig only exists when playing a singleplayer game.
-  gameStartInfo?: GameStartInfo;
-  // GameRecord exists when replaying an archived game.
-  gameRecord?: GameRecord;
-  source?: "public" | "private" | "host" | "matchmaking" | "singleplayer";
-  publicLobbyInfo?: GameInfo;
-}
+type JoinLobbyEventBase = {
+  clientID: string;
+  gameID: string;
+  gameStartInfo?: GameStartInfo;
+  gameRecord?: GameRecord;
+};
+
+export type JoinLobbyEvent =
+  | (JoinLobbyEventBase & {
+      source: "public";
+      publicLobbyInfo: GameInfo;
+    })
+  | (JoinLobbyEventBase & {
+      source?: "private" | "host" | "matchmaking" | "singleplayer";
+      publicLobbyInfo?: undefined;
+    });
src/client/JoinPublicLobbyModal.ts (1)

12-13: Prefer composition over inheritance for the modal.

Extending BaseModal couples lifecycle and UI. A composed modal wrapper keeps this component focused on state/render and is easier to test.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/LobbySocket.ts`:
- Around line 225-228: The WebSocket URL construction in LobbySocket.ts can
produce a double-slash if config.workerPath(this.lobbyId) returns
leading/trailing slashes; update the wsUrl build to normalize the worker path
before concatenation by trimming any leading/trailing slashes (use the value
returned by config.workerPath(this.lobbyId)), then join with a single slash when
constructing wsUrl (referencing protocol, window.location.host,
config.workerPath, this.lobbyId, and wsUrl) so the final URL never contains
duplicated slashes.
- Around line 276-304: The reconnect logic in handleClose and handleConnectError
currently just logs when wsConnectionAttempts >= maxWsAttempts and doesn't
notify the rest of the system; add a single terminal notification and stop
further retries: when wsConnectionAttempts >= this.maxWsAttempts in both
handleClose and handleConnectError, call a single new/explicit notification
method (e.g. this.onTerminalFailure() or emit a "terminalFailure" event) and
ensure it's only invoked once (reuse this.wsAttemptCounted or add a dedicated
flag) instead of scheduling reconnects via scheduleReconnect; update
callers/consumers to handle that notification.
🧹 Nitpick comments (1)
src/client/JoinPublicLobbyModal.ts (1)

199-237: Use GameMode constant instead of string literal.
This keeps comparisons typed and avoids drift if the literal ever changes. Please verify GameMode.FFA matches the actual value.

Suggested update
-    const modeName =
-      c.gameMode === "Free For All"
+    const modeName =
+      c.gameMode === GameMode.FFA
         ? translateText("game_mode.ffa")
         : translateText("game_mode.teams");
@@
-        ${c.gameMode !== "Free For All" && c.playerTeams
+        ${c.gameMode !== GameMode.FFA && c.playerTeams
           ? this.renderConfigItem(

@ryanbarlow97 ryanbarlow97 added UI/UX UI/UX changes including assets, menus, QoL, etc. Bugfix Fixes a bug labels Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/JoinPublicLobbyModal.ts`:
- Around line 335-375: In pollLobby, the code mistakenly subtracts Date.now()
from the server-provided duration msUntilStart; update pollLobby (in the private
async pollLobby(lobbyId: string) function) to pass msUntilStart through
unchanged (or keep it undefined if absent) when calling this.updateFromLobby so
it remains a duration (matching updateFromLobby's handling), i.e. remove the
"Math.max(0, msUntilStart - Date.now())" adjustment and send msUntilStart
directly.
🧹 Nitpick comments (2)
src/client/JoinPublicLobbyModal.ts (2)

12-25: Prefer composition over extending BaseModal for new UI components.

Using inheritance couples this element to BaseModal behavior. If you can, wrap <o-modal> and delegate instead of extending, and consider a small typed union for lobby status to reduce null checks.


194-240: Replace string literals with GameMode.FFA enum value.

The code currently compares gameMode against the string "Free For All" in two places. Since GameMode is already imported and GameMode.FFA = "Free For All", use the enum constant instead to avoid drift if the enum value changes.

🔧 Suggested changes
     const modeName =
-      c.gameMode === "Free For All"
+      c.gameMode === GameMode.FFA
         ? translateText("game_mode.ffa")
         : translateText("game_mode.teams");
     ...
-        ${c.gameMode !== "Free For All" && c.playerTeams
+        ${c.gameMode !== GameMode.FFA && c.playerTeams
           ? this.renderConfigItem(

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/JoinPublicLobbyModal.ts`:
- Around line 151-172: The element can be removed from the DOM without calling
close(), leaking countdownTimerId and lobbyPollInterval; implement a
disconnectedCallback on the JoinPublicLobbyModal class that performs the same
cleanup as onClose (or simply call this.onClose()) to call
clearCountdownTimer(), stopLobbyPolling(), and handle leaving the lobby if
leaveLobbyOnClose is set, ensuring timers are stopped when the element is
detached.
🧹 Nitpick comments (1)
src/client/JoinPublicLobbyModal.ts (1)

251-265: Consider extracting unitKeys to a shared constant.

This mapping duplicates knowledge about unit names and their translation keys. If unit names change or new units are added, this could drift out of sync with other parts of the codebase.

The fallback to the raw unit name (line 277) is a good safety net. For now this works, but you might want to extract this mapping to a shared file (e.g., UnitTranslations.ts) if other components need the same lookup.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/JoinPublicLobbyModal.ts`:
- Around line 211-215: The mode label only inspects c.gameMode (compared to
GameMode.FFA) when computing modeName, so HumansVsNations lobbies (which are
represented via c.playerTeams) are incorrectly labeled as "teams"; update the
logic that sets modeName to also check c.playerTeams for the HumansVsNations
value and return the appropriate translation key (e.g.,
translateText("game_mode.humans_vs_nations")) when matched, otherwise fall back
to the existing GameMode.FFA vs teams branch; look for the modeName assignment
and uses of c.gameMode, GameMode.FFA, translateText(...) and c.playerTeams to
implement the conditional.
♻️ Duplicate comments (1)
src/client/JoinPublicLobbyModal.ts (1)

150-166: Add disconnectedCallback to stop timers when the element is detached.

onClose() clears timers, but if the element is removed from the DOM without close(), the countdown timer and polling interval will keep running. Add a disconnectedCallback() cleanup so timers always stop.

🔧 Suggested fix
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    this.clearCountdownTimer();
+    this.stopLobbyPolling();
+  }
+
   protected onClose(): void {
     this.clearCountdownTimer();
     this.stopLobbyPolling();

@ryanbarlow97 ryanbarlow97 requested a review from a team as a code owner January 21, 2026 18:58
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 23, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 23, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 24, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/Main.ts`:
- Around line 625-631: The onHashUpdate handler currently closes modals and
calls handleLeaveLobby but doesn't cancel any in-flight join/prefetch, allowing
a previous join Promise to complete after navigation; add an explicit
cancellation mechanism (e.g., an AbortController or incremental joinId token
stored on the instance like this.joinAbortController or this.currentJoinId) that
is set/created when starting the join/prefetch and checked/cancelled in the join
completion path, then update onHashUpdate to call abort() or increment/clear the
token before closing this.joinModal/this.joinPublicModal and invoking
handleLeaveLobby so any ongoing join is invalidated and cannot complete. Ensure
join logic (where the Promise resolves to actually join) respects the
abort/token (checking controller.signal.aborted or matching currentJoinId) and
returns early when cancelled.

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Jan 24, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/Main.ts`:
- Around line 626-633: onHashUpdate can be triggered by a hashchange fired after
a popstate rollback and close the lobby unexpectedly; add a one-shot guard flag
(e.g. skipNextHashChange) that you set right before you perform the
popstate/rollback action and check at the top of onHashUpdate to return early
and reset the flag; update the rollback code path to set skipNextHashChange =
true before calling history.back()/popstate emulation and ensure onHashUpdate
checks skipNextHashChange first (and clears it) before calling
cancelJoinInFlight(), this.joinModal?.close(), this.joinPublicModal?.close(), or
this.handleLeaveLobby() so the rollback cannot immediately re-close the lobby.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bugfix Fixes a bug UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

4 participants