Skip to content

Add 2v2 ranked matchmaking#4596

Merged
evanpelle merged 7 commits into
mainfrom
matchmaking-2v2
Jul 13, 2026
Merged

Add 2v2 ranked matchmaking#4596
evanpelle merged 7 commits into
mainfrom
matchmaking-2v2

Conversation

@evanpelle

@evanpelle evanpelle commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Description:

Implements 2v2 ranked matchmaking end-to-end against the matchmaking API's 2v2 queues (API PR #419): core team pinning, the server's second checkin loop and game creation, and the client UI.

Core — deterministic team pinning

The matcher's assignment specifies exactly who plays with whom (teams: [[a,d],[b,c]]), but team assignment previously only did clan/friend balancing and could scramble the ELO-balanced split.

  • PlayerSchema/PlayerInfo gain an optional teamIndex — a server-stamped index into the game's team list, part of GameStartInfo so it's identical on every client (same category as clanTag/friends, which already feed deterministic team assignment).
  • assignTeams honors pins unconditionally — before clan/friend grouping and past maxTeamSize (the matcher's balancing is authoritative) — and seeds the counts that balancing of any unpinned players sees. Pinned players still participate in the friend graph, so an unpinned friend is pulled toward a pinned player's team.
  • publicIds never enter core: the game server resolves publicId → teamIndex per client at game start.

Server

  • One checkin long-poll per mode. Both loops send mode explicitly (the API deployed ahead of the client, so no omit-for-back-compat needed).
  • get2v2Config(): Team mode, playerTeams: 2, maxPlayers: 4, always-compact map, donations enabled (matching public team games), rankedType: "2v2" (the API's 2v2 ingestion has shipped; RankedType gains TwoVTwo).
  • The assignment payload is now used (it was previously discarded): playersallowedPublicIds so only the matched accounts can take the slots (also hardens 1v1), and teamsteamIndex stamps at game start. A malformed assignment logs a warning and falls back to creating the game without pins rather than stranding matched players.
  • The 3-clients-per-IP cap on public games applies to matchmade games too (an allowlist doesn't stop one person multi-tabbing multiple accounts). It is now skipped in dev, where local testing (multi-tab, the 4-player e2e) is inherently same-IP — matching the existing dev/prod gating of Turnstile and the duplicate-account kick.

Client

  • Ranked modal's 2v2 card is enabled; it passes the mode through open-matchmaking (dispatchers without a detail — homepage button, requeue URL — still mean 1v1).
  • Matchmaking modal joins with &mode=1v1/&mode=2v2, shows a 2v2 title (matchmaking_modal.title_2v2 in en.json), and shows the real 2v2 ELO from the new leaderboard.twoVtwo field in /users/@me (the ranked modal's 2v2 card does too).
  • WinModal shows requeue for any ranked game and carries the mode back into the right queue (/?requeue=2v2).
  • 2v2 ranked stats surface in the player stats tree (labeled via player_stats_tree.ranked_2v2).

Harnesses (tests/matchmaking/)

  • Contained: the fake server captures the mode query param; asserts each queue sends its mode explicitly. 10/10.
  • E2E: MM_MODE=2v2 runs four real browser players through the real local worker's 2v2 queue and rides the flow into the started game. Asserts same gameId for all four, the 2v2 config, allowlist admission, and a deterministic 2 vs 2 in-game split read from each client's GameView (the software-WebGL gate is spoofed in test pages only). 8/8. 1v1 e2e still 6/6.

Verification

  • npm test: 2,053 tests pass, including 7 new (6 assignTeams pinning unit tests + a full-game pinned-split test through setup()).
  • npx tsc --noEmit, ESLint clean.
  • Live e2e against a local wrangler dev API worker: 1v1 (6/6) and 2v2 (8/8) as above.

🤖 Generated with Claude Code

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

(UI changes — the ranked modal's 1v1/2v2 cards — were verified with before/after screenshots in the live app during development.)

Integrates the 2v2 queue from the matchmaking API (API PR #419) across
core, server, and client.

Core (deterministic team pinning):
- PlayerSchema/PlayerInfo gain an optional teamIndex - a server-stamped
  slot into the game's team list. It feeds deterministic team assignment
  like clanTag/friends, identical for every client.
- assignTeams honors pins unconditionally (before clan/friend grouping
  and past maxTeamSize - the matcher's balancing is authoritative) and
  seeds the counts the remaining balancing sees.

Server:
- One checkin long-poll loop per mode; 1v1 still omits the mode field so
  an API without mode support keeps working.
- get2v2Config: Team mode, 2 teams, 4 players, donations on. No
  rankedType - ingestion still only accepts "1v1", so 2v2 games are
  reported like ordinary team games until 2v2 ingestion ships.
- The assignment payload is now used: players -> allowedPublicIds (only
  the matched accounts can take the slots, also hardening 1v1), teams ->
  teamIndex stamped per client at game start via publicId lookup.
- The 3-clients-per-IP cap is skipped for identity-gated (allowlisted)
  games: it added nothing on top of the account allowlist and locked out
  legitimate same-NAT players the matcher grouped together.

Client:
- Ranked modal's 2v2 card is enabled and passes mode through
  open-matchmaking; the matchmaking modal joins with mode=2v2 (omitted
  for 1v1), shows a 2v2 title, and shows "No ELO yet" until 2v2
  ingestion ships.

Harnesses:
- Contained: fake server captures the mode param; asserts 1v1 omits it
  and 2v2 sends it.
- E2E: MM_MODE=2v2 runs four browser players through the real queue and
  into the started game, asserting the 2v2 config, allowlist admission,
  and a deterministic 2 vs 2 in-game split (WebGL software gate spoofed
  in test pages only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Matchmaking now supports explicit 1v1 and 2v2 modes. The server creates mode-specific games, carries assigned team slots to clients, and applies pinned team assignments. Tests cover queue payloads, game setup, allowlists, and deterministic 2v2 teams.

Changes

Matchmaking mode flow

Layer / File(s) Summary
Client matchmaking mode flow
src/client/Main.ts, src/client/Matchmaking.ts, src/client/components/RankedModal.ts, src/client/hud/layers/WinModal.ts, src/core/ApiSchemas.ts, src/core/game/Game.ts, resources/lang/en.json, src/client/components/baseComponents/stats/PlayerStatsTree.ts
The ranked UI sends the selected mode, while matchmaking and requeue flows use mode-specific titles, URLs, labels, and ELO values.
Mode-aware matchmaking polling
src/server/Worker.ts, src/server/MapPlaylist.ts, src/server/GameManager.ts, src/server/GameServer.ts
Workers poll 1v1 and 2v2 queues separately, validate assignments, select configurations, and pass matched identities and teams into game servers.
Pinned team propagation and assignment
src/core/Schemas.ts, src/core/game/Game.ts, src/core/GameRunner.ts, src/core/game/TeamAssignment.ts, src/server/GameServer.ts
Player team indexes flow into game state, where pinned slots are assigned first and remaining players are balanced afterward.
Team and matchmaking validation
tests/Team.test.ts, tests/TeamAssignment.test.ts, tests/matchmaking/*, docs/API.md
Tests and documentation cover pinned assignments, mode-specific queue joins, game configuration, allowlists, client joins, and deterministic 2v2 team splits.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant MatchmakingModal
  participant Worker
  participant GameManager
  participant GameServer
  participant GameClient
  Player->>MatchmakingModal: select 1v1 or 2v2
  MatchmakingModal->>Worker: join mode-specific queue
  Worker->>GameManager: create configured game
  GameManager->>GameServer: pass identities and team groups
  GameServer-->>GameClient: send game assignment
  GameClient->>GameServer: join assigned game
  GameServer-->>GameClient: start game with teamIndex values
Loading

Possibly related PRs

Suggested labels: UI/UX, Backend, Feature

Suggested reviewers: mushroomlamp, zixer1, berkelmali, variablevince, scottanderson

Poem

Two queues hum beneath the light,
One for day, and two for night.
Pinned teams march where servers say,
Four brave players join the play.
Red and blue, in order true—
Matchmaking finds the crew!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: adding 2v2 ranked matchmaking.
Description check ✅ Passed The description is directly related to the PR and summarizes the 2v2 matchmaking changes.

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.

@evanpelle evanpelle added this to the v33 milestone Jul 13, 2026
The 1v1/2v2 cards only scaled 1.02 on hover, which barely reads. Adopt
the quick-action card hover treatment (brightness bump + blue glow
shadow + scale) with a stronger brightness since the cards are large
and dark.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/server/Worker.ts (1)

756-758: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid logging the full matchmaking response payload.

data can include assignment.players (matched players' account public IDs). Logging the whole object at info level puts stable user identifiers into logs unnecessarily; log only what's needed to confirm a successful poll (e.g., whether an assignment was present).

🔒 Suggested redaction
-        const data = await response.json();
-        log.info(`Lobby ${mode} poll successful:`, data);
+        const data = await response.json();
+        log.info(`Lobby ${mode} poll successful, assignment present: ${!!data.assignment}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/Worker.ts` around lines 756 - 758, Update the successful poll
logging in Worker’s response handling to stop passing the full data payload to
log.info. Log only a non-sensitive success indicator, such as whether
data.assignment is present, while preserving the existing poll success message.
src/server/GameServer.ts (1)

543-548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse hasJoinWhitelist() instead of re-deriving the same condition.

identityGated duplicates the exact expression already encapsulated in hasJoinWhitelist() (Lines 1294-1296). Calling the existing method keeps both call sites in sync if the "what counts as identity-gated" definition ever changes.

♻️ Suggested fix
-    const identityGated = (this.gameConfig.allowedPublicIds?.length ?? 0) > 0;
+    const identityGated = this.hasJoinWhitelist();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/GameServer.ts` around lines 543 - 548, Update the identityGated
assignment in the matchmade-game same-IP-cap logic to call the existing
hasJoinWhitelist() method instead of directly checking allowedPublicIds.length.
Preserve the surrounding conditional behavior while reusing that method as the
single source of truth.
src/server/MapPlaylist.ts (1)

417-449: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared ranked-map-selection logic from get1v1Config/get2v2Config.

The maps array, isCompact roll, and maxTimerValue/difficulty logic here are byte-for-byte duplicates of get1v1Config (Lines 386-415). Any future tweak to the ranked map pool now has to be kept in sync by hand in two places.

♻️ Suggested extraction
+  private pickRankedMapAndCompact(): { map: GameMapType; isCompact: boolean } {
+    const maps = [
+      GameMapType.Australia, // 40%
+      GameMapType.Australia,
+      GameMapType.Iceland, // 20%
+      GameMapType.Asia, // 20%
+      GameMapType.EuropeClassic, // 20%
+    ];
+    return {
+      map: maps[Math.floor(Math.random() * maps.length)],
+      isCompact: Math.random() < 0.2,
+    };
+  }
+
   public get1v1Config(): GameConfig {
-    const maps = [...];
-    const isCompact = Math.random() < 0.2;
+    const { map, isCompact } = this.pickRankedMapAndCompact();
     return {
       ...
-      gameMap: maps[Math.floor(Math.random() * maps.length)],
+      gameMap: map,
       ...
     } satisfies GameConfig;
   }

   public get2v2Config(): GameConfig {
-    const maps = [...];
-    const isCompact = Math.random() < 0.2;
+    const { map, isCompact } = this.pickRankedMapAndCompact();
     return {
       ...
-      gameMap: maps[Math.floor(Math.random() * maps.length)],
+      gameMap: map,
       ...
     } satisfies GameConfig;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/MapPlaylist.ts` around lines 417 - 449, Extract the duplicated
ranked map-selection setup from get1v1Config and get2v2Config into a shared
helper or reusable symbol. Reuse it in both methods for the maps array,
isCompact roll, selected gameMap, maxTimerValue, and difficulty values, while
preserving each method’s distinct player-count and game-mode configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/GameServer.ts`:
- Around line 543-548: Update the identityGated assignment in the matchmade-game
same-IP-cap logic to call the existing hasJoinWhitelist() method instead of
directly checking allowedPublicIds.length. Preserve the surrounding conditional
behavior while reusing that method as the single source of truth.

In `@src/server/MapPlaylist.ts`:
- Around line 417-449: Extract the duplicated ranked map-selection setup from
get1v1Config and get2v2Config into a shared helper or reusable symbol. Reuse it
in both methods for the maps array, isCompact roll, selected gameMap,
maxTimerValue, and difficulty values, while preserving each method’s distinct
player-count and game-mode configuration.

In `@src/server/Worker.ts`:
- Around line 756-758: Update the successful poll logging in Worker’s response
handling to stop passing the full data payload to log.info. Log only a
non-sensitive success indicator, such as whether data.assignment is present,
while preserving the existing poll success message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2082b801-bf5d-4c20-bba3-dcd38391cb49

📥 Commits

Reviewing files that changed from the base of the PR and between 290d692 and 7b696ce.

📒 Files selected for processing (18)
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/components/RankedModal.ts
  • src/core/GameRunner.ts
  • src/core/Schemas.ts
  • src/core/game/Game.ts
  • src/core/game/TeamAssignment.ts
  • src/server/GameManager.ts
  • src/server/GameServer.ts
  • src/server/MapPlaylist.ts
  • src/server/Worker.ts
  • tests/Team.test.ts
  • tests/TeamAssignment.test.ts
  • tests/matchmaking/README.md
  • tests/matchmaking/contained.mjs
  • tests/matchmaking/e2e.mjs
  • tests/matchmaking/fakeServer.mjs

Use the solo button palette (bg-malibu-blue, hover:bg-aquarius,
active:bg-malibu-blue/80) on the 1v1/2v2 cards, keeping the hover
glow/scale. Subtitle opacity bumped to white/80 for readability on the
brighter background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
The API now supports 2v2: ingestion accepts rankedType "2v2" and
/users/@me returns a twoVtwo leaderboard row. Unblocks the pieces that
were deliberately deferred:

- RankedType gains TwoVTwo; get2v2Config sets it, so 2v2 games are
  ingested as ranked 2v2 instead of ordinary team games.
- UserMeResponse schema gains leaderboard.twoVtwo; the matchmaking and
  ranked modals show the real 2v2 ELO instead of "No ELO yet".
- WinModal shows requeue for any ranked game and carries the mode
  (/?requeue=2v2); Main routes it back into the right queue.
- Player stats tree labels 2v2 ranked stats (they surface automatically
  now that the enum value exists).
- e2e 2v2 harness asserts the game config carries rankedType "2v2".
- docs/API.md rankedType filter now lists 2v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/server/MapPlaylist.ts (1)

417-447: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting shared logic between get1v1Config() and get2v2Config().

Both methods pick from the same map pool, roll isCompact the same way, and reuse the same maxTimerValue/spawnImmunityDuration values. Factoring the common parts into one small private helper would keep the two configs from drifting apart as either mode evolves.

♻️ Suggested refactor sketch
-  public get1v1Config(): GameConfig {
-    const maps = [
-      GameMapType.Australia, // 40%
-      GameMapType.Australia,
-      GameMapType.Iceland, // 20%
-      GameMapType.Asia, // 20%
-      GameMapType.EuropeClassic, // 20%
-    ];
-    const isCompact = Math.random() < 0.2;
+  private smallRankedMapAndCompact(): { map: GameMapType; isCompact: boolean } {
+    const maps = [
+      GameMapType.Australia, // 40%
+      GameMapType.Australia,
+      GameMapType.Iceland, // 20%
+      GameMapType.Asia, // 20%
+      GameMapType.EuropeClassic, // 20%
+    ];
+    return {
+      map: maps[Math.floor(Math.random() * maps.length)],
+      isCompact: Math.random() < 0.2,
+    };
+  }
+
+  public get1v1Config(): GameConfig {
+    const { map, isCompact } = this.smallRankedMapAndCompact();
     return {
       donateGold: false,
       donateTroops: false,
-      gameMap: maps[Math.floor(Math.random() * maps.length)],
+      gameMap: map,
       maxPlayers: 2,
       ...
     } satisfies GameConfig;
   }

   public get2v2Config(): GameConfig {
-    const maps = [ ... ];
-    const isCompact = Math.random() < 0.2;
+    const { map, isCompact } = this.smallRankedMapAndCompact();
     return {
       donateGold: true,
       donateTroops: true,
-      gameMap: maps[Math.floor(Math.random() * maps.length)],
+      gameMap: map,
       maxPlayers: 4,
       ...
     } satisfies GameConfig;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/MapPlaylist.ts` around lines 417 - 447, Extract the shared map
selection, compact-mode roll, timer calculation, and spawn-immunity
configuration from get1v1Config() and get2v2Config() into a small private helper
in MapPlaylist. Update both methods to reuse that helper while preserving their
mode-specific settings and existing configuration values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/MapPlaylist.ts`:
- Around line 417-447: Extract the shared map selection, compact-mode roll,
timer calculation, and spawn-immunity configuration from get1v1Config() and
get2v2Config() into a small private helper in MapPlaylist. Update both methods
to reuse that helper while preserving their mode-specific settings and existing
configuration values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e98e0bc8-186a-4533-aa3f-15a35753af54

📥 Commits

Reviewing files that changed from the base of the PR and between 38ca3b7 and 13bc7f8.

📒 Files selected for processing (11)
  • docs/API.md
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/components/RankedModal.ts
  • src/client/components/baseComponents/stats/PlayerStatsTree.ts
  • src/client/hud/layers/WinModal.ts
  • src/core/ApiSchemas.ts
  • src/core/game/Game.ts
  • src/server/MapPlaylist.ts
  • tests/matchmaking/e2e.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/game/Game.ts
  • resources/lang/en.json
  • tests/matchmaking/e2e.mjs

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
evanpelle and others added 2 commits July 13, 2026 14:22
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The API deployed ahead of the client, so the omit-mode-for-1v1
backward compatibility is no longer needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
An account allowlist doesn't stop one person multi-tabbing multiple
accounts into a ranked game, so the IP heuristic stays. Dev is exempt
(like Turnstile and the duplicate-account kick) because local testing
(multi-tab, the 4-player e2e) is inherently same-IP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/GameServer.ts (1)

950-950: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail closed on missing matchmade team pins. In matchmade games, a missing teamIndex is turned into null on the client and treated as unpinned in src/core/game/TeamAssignment.ts, so an incomplete split can drift from the matchmaker’s intended teams. Reject start or validate every active client before starting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/GameServer.ts` at line 950, Update the matchmade game start flow
around GameServer’s matchmakingTeamIndex(c) assignment to fail closed when any
active client lacks a valid teamIndex. Validate all active clients before
starting, or reject the start when a missing pin is detected, rather than
allowing it to become null and be treated as unpinned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/server/GameServer.ts`:
- Line 950: Update the matchmade game start flow around GameServer’s
matchmakingTeamIndex(c) assignment to fail closed when any active client lacks a
valid teamIndex. Validate all active clients before starting, or reject the
start when a missing pin is detected, rather than allowing it to become null and
be treated as unpinned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d8bba7e-d9e5-4c71-ac12-e6e70abddb9b

📥 Commits

Reviewing files that changed from the base of the PR and between 30cb568 and c8039dc.

📒 Files selected for processing (1)
  • src/server/GameServer.ts

@evanpelle evanpelle merged commit 3a5fba2 into main Jul 13, 2026
16 checks passed
@evanpelle evanpelle deleted the matchmaking-2v2 branch July 13, 2026 22:22
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

5 participants