Skip to content

Feature: Quick Play - one-click matchmaking into an ordinary NetPlay room - #23

Closed
JaviAir wants to merge 3 commits into
Project-Plus-Development-Team:masterfrom
JaviAir:quickplay-pr-a
Closed

JaviAir wants to merge 3 commits into
Project-Plus-Development-Team:masterfrom
JaviAir:quickplay-pr-a

Conversation

@JaviAir

@JaviAir JaviAir commented Oct 16, 2026

Copy link
Copy Markdown

Quick Play: one-click matchmaking into an ordinary NetPlay room

Quick Play finds another Project+ player and puts you both in a normal NetPlay room. There are
no NetPlay protocol changes and no emulation changes: once matched, it's the same traversal
room you'd get from Host/Join, with the Netplay Launcher selected.

Video & Screenshots

ppqp_hover_lobbypref ppqp_hover_hostpref ppqp_searching_dialog ppqp_searching_dialog_customcoord ppqp_searching_dialog_join ppqp_searching_dialog_listed
PP_QP_DEMO_1_1080p_small.2.mp4

What players see

  • Quickplay button in the toolbar, next to Netplay, plus a small gear for Quick Play
    Settings
    .
  • Clicking Quickplay opens a small dialog: Searching (Dolphin lobby)..., the regions
    searched and the host preference, then Opponent found, Creating room... /
    Waiting for host..., and finally the ordinary NetPlay lobby on both sides. Cancel stops
    the search at any time.
  • Quick Play Settings:
    • Matchmaking: the Dolphin lobby (default), or a custom coordinator URL (empty by
      default) with a Test button.
    • Regions: a checklist (US East, US South, US Midwest, US West, Canada, Mexico & Central
      America, Caribbean, South America, Europe, Asia, Australia). Players match if they share at
      least one region.
    • Host preference: Prefer to host / No preference / Prefer to join. It's a tie-break,
      never a constraint, and never delays a match.
  • With a game running, the button asks you to close the game first. While Quick Play owns a room
    setup, manual NetPlay Host/Join is refused.
  • The room's game is the Project+ Netplay Launcher.

How it works

  • Default backend: the Dolphin lobby. No server of our own: the rendezvous runs on
    Dolphin's NetPlay index, which Dolphin already uses for the server browser. Each searcher
    keeps at most one listing whose name carries a small handshake (PPQP1, below).
  • Optional backend: a coordinator. Any HTTP server that speaks the Quick Play coordinator
    protocol (summarized below) can be set in Quick Play Settings. There is no
    default URL. If the lobby is unreachable and a coordinator is configured, the search
    continues there.
  • Handoff. After a match, the chosen host opens a traversal NetPlay room
    (NetPlayServer with a Quick Play attempt id) and publishes its room code through the
    backend; the other player joins with that code. When the guest arrives, Quick Play steps
    back and the players are in an ordinary NetPlay lobby. They pick settings and press Start
    as usual.
  • No wire-format change. The NetPlay handshake, packets and version check are upstream's.
    A Quick Play room differs only locally on the host: it isn't listed on the index, it
    accepts a single guest (others get The server is full.), and it reports its traversal
    state to Quick Play instead of the generic traversal error dialog.
  • Quick Play builds only find other Quick Play builds of the same version: listings use the
    version <scm describe>+ppqp1 (e.g. v3.2.1+ppqp1), and the coordinator matches the exact
    build revision.

File map

File What
DolphinQt/QuickPlay/QuickPlayIndexMatchmaker.{h,cpp} Dolphin lobby backend (PPQP1 handshake), runs on one worker thread
DolphinQt/QuickPlay/QuickPlayClient.{h,cpp} Coordinator backend
DolphinQt/QuickPlay/QuickPlayMatchmaker.{h,cpp} Shared backend interface, regions, host preference, host-code check
DolphinQt/QuickPlay/QuickPlayController.{h,cpp} Attempt state machine (search, match, validate, host/join, timeouts)
DolphinQt/QuickPlay/QuickPlayLauncher.{h,cpp} Finds the Netplay Launcher in the game list
DolphinQt/QuickPlay/QuickPlayDialog.{h,cpp} Search/progress dialog
DolphinQt/QuickPlay/QuickPlaySettingsDialog.{h,cpp} Quick Play Settings
DolphinQt/QuickPlay/QuickPlaySession.{h,cpp} Owns Quick Play in the main window; QuickPlayWindow is the 5-method interface MainWindow implements
DolphinQt/MainWindow.{h,cpp} Creates the session; host/join split so Quick Play can force traversal and pass its attempt id; refuses manual host/join while Quick Play owns NetPlay
DolphinQt/ToolBar.{h,cpp} Quickplay button and settings gear
DolphinQt/NetPlay/NetPlayDialog.{h,cpp} Two signals: the host's traversal state/room code, and the guest's arrival
Core/NetPlayServer.{h,cpp}, Core/NetPlayClient.h Optional Quick Play attempt id: no index listing, one guest, traversal state callback (NetPlayUI::OnHostTraversalStateChanged, default no-op)
Core/Config/NetplaySettings.{h,cpp} QuickPlayMatchmaking, QuickPlayCoordinator, QuickPlayRegions, QuickPlayRegion (legacy), QuickPlayHostPreference
UICommon/NetPlayIndex.{h,cpp} Reads the listing timestamp; a session may set its own (escaped) version
Common/HttpRequest.{h,cpp} Delete, SetTimeout, SetSensitive (keeps ticket URLs out of the log)
DolphinQt/CMakeLists.txt, DolphinQt.vcxproj New files

Commits:

  1. NetPlay: Add the hooks Quick Play needs from NetPlay
  2. DolphinQt: Add Quick Play matchmaking
  3. DolphinQt: Add the Quick Play button and dialogs

The lobby handshake (PPQP1)

The NetPlay index is a passive directory (list, session/add, session/active,
session/remove; a session times out 15 s after its last keepalive; at most 5 sessions per
IP; no locks). Every agreement is built from listings each player reads and writes on its own.

Listings. Each is an index session with name = P+ Quick Play · PPQP1 <tokens>,
game = Project+, version = <scm describe>+ppqp1, password = 1, method =
traversal, in_game = 0 for W and 1 otherwise (so "hide in-game" hides the handshake),
region = the index continent of the first region (display only).

Kind Tokens Meaning
W W <mask> <pref> <digest> <nonce> waiting for a claim
C C <mask> <pref> <digest> <waiter> <nonce> claims that waiter
A A <host> <digest> <waiter> <claimer> the waiter accepted that claimer; <host> is w or c
H H <digest> <waiter> <claimer> the host's room is up; server_id carries the obfuscated room code

<mask>: 3 hex digits, one bit per built-in region. <pref>: h, n or j. <digest>:
always 00000000 in this PR (a reserved field; listings with any other value are ignored).
Nonces: 6 random hex digits per search.

Flow.

  • Searching: claim a random compatible W (shared region bit, not blocked). If there is
    none, list our own W: at once when preferring to host, after a random 0–2 s with no
    preference, after 15 s when preferring to join.
  • Waiting: accept the claim with the smallest claimer nonce (every claimer can predict
    it), pick the host, replace W with A. If a waiter with a smaller nonce appears, claim it
    instead, so two waiters never wait on each other.
  • Claiming: list C, then poll every 2 s. An A/H for us means accepted. Another
    claimer, the waiter missing twice, or 12 s without an answer means lost: block that waiter
    for 60 s and search again.
  • Matched: both players re-check that the peer's listing is still there (up to 3 lists).
    The host opens the room and lists H; the client polls for H, decrypts the code and
    joins.
  • Who hosts: the coordinator's soft rule. The claimer hosts if the waiter prefers to join
    and the claimer doesn't, or the waiter has no preference and the claimer prefers to host.
    Otherwise the waiter hosts.
  • Agreed region: the first shared bit in checklist order.
  • Room code: NetPlaySession::EncryptID with key PPQP1:none. This keeps ordinary
    browsers out; it isn't secret.
  • Limits: search lifetime 180 s; 25 s room setup after a match; ghost filter (listings
    more than 15 s older than the newest one are ignored); per-IP cap handled by waiting for a
    free slot.

The coordinator protocol

For anyone running their own server. All JSON over HTTP; the ticket is a bearer capability, so
the client keeps ticket URLs out of the log:

  • POST /v1/queue with protocol, build (GetScmRevGitStr()), session
    ("ordinary"), pplus_version (GetScmDescStr()), regions (1–12 strings),
    host_preference (host/none/join), platform and a random player_nonce. The response
    carries a ticket.
  • GET /v1/queue/{ticket}, polled once a second: state is searching, matched (with
    match_id, role = host/client, and the agreed region) or expired.
  • The host publishes its room code with POST /v1/matches/{id}/host (ticket, host_code).
    The client polls GET /v1/matches/{id} until state is ready with host_code
    (waiting_for_host until then).
  • DELETE /v1/queue/{ticket} cancels; cancelling a matched ticket also ends the match, so the
    peer's next poll gets expired.
  • Pairing rule: the same protocol, build and session, and at least one shared region.
    First come, first served. The host is chosen by the same soft rule as above.
  • Lifetimes: queue 180 s; 30 s from match to host code.

Testing

Test Result
Builds: Linux (each commit), Windows (CMake/MSBuild) Linux: pass at each commit. Windows: pass at 7757cba325 via CMake-generated MSBuild; runtime reports v3.2.1-3
Upstream unit tests 1,351 pass; PatchAllowlist.VerifyHashes fails on master as well
Dolphin lobby (local index), two Quick Play builds Pass: both reach an ordinary NetPlay lobby; listing version confirmed as v3.2.1-3+ppqp1
Coordinator (local server), two Quick Play builds Pass: custom-coordinator search and ordinary NetPlay lobby on both
Ordinary NetPlay, this PR ⇄ plain master, both directions, one match each Pass: Direct, Fair Input Delay, one match each role; PR source built with master’s revision for its exact-revision handshake. No gameplay desync
Public Dolphin lobby, Linux ⇄ Windows, one match Pass on separate networks, Linux hosts; successful match and Stop.

Test setup on v3.2.1: upstream master automatically assigns Wii ports alongside GC ports. For
GC-only play, clear the Wii assignments in the room’s Assign Controller Ports dialog
after both players join.

Generative AI disclosure

Most of this PR's code was generated with an AI coding assistant (Claude/Codex) under my direction.
I designed the feature, reviewed the changes, tested them on Linux and Windows, and take
responsibility for them. It contains no changes to emulated-console behavior.

JaviAir and others added 3 commits October 15, 2026 15:25
Quick Play hands a match off to an ordinary traversal NetPlay room. This adds
what that handoff needs, without changing the NetPlay wire format:

- NetPlayServer takes an optional Quick Play attempt id. Such a room is not
  listed on the NetPlay index, accepts a single guest (later joins get "The
  server is full."), and reports its traversal state and room code to the
  dialog through NetPlayUI::OnHostTraversalStateChanged instead of the
  generic traversal error.
- NetPlayDialog forwards that state and the guest's arrival as signals.
- NetPlayIndex reads the listing timestamp and lets a session set its own
  version string (escaped, since it may contain '+').
- Common::HttpRequest gains DELETE, a per-request timeout, and a "sensitive"
  mode that keeps URLs carrying bearer tickets out of the log.

Ordinary NetPlay sessions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quick Play pairs two Project+ players and hands them to an ordinary
traversal NetPlay room. This adds the matchmaking, with no UI yet:

- QuickPlayIndexMatchmaker (default): a serverless handshake (PPQP1) through
  Dolphin's NetPlay index. Each searcher keeps at most one listing whose name
  carries the handshake: wait, claim, accept, host ready. The room code is
  obfuscated in the host-ready listing, and listings use the version
  "<scm desc>+ppqp1" so ordinary browsers grey them out.
- QuickPlayClient: an optional coordinator backend for any server that speaks
  the Quick Play coordinator protocol (version 3). No default URL.
- QuickPlayMatchmaker: the shared interface, regions and host preference.
- QuickPlayController: the attempt state machine on the GUI thread (search,
  match, validate, host or join, room setup timeout), with a fallback from
  an unreachable index to a configured coordinator.
- QuickPlayLauncher: finds the Project+ Netplay Launcher in the game list;
  it is the room's game.
- NetPlay settings for the backend, coordinator URL, regions and host
  preference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Quickplay toolbar button, next to Netplay, starts a search and shows its
  progress. A gear button opens Quick Play Settings: the backend (Dolphin
  lobby or a custom coordinator, with a connection test), regions and host
  preference.
- QuickPlaySession owns Quick Play in the main window. On a match it hosts
  the Netplay Launcher in a traversal room, or joins the opponent's room
  with the code, then leaves the ordinary NetPlay lobby to the players.
  Cancel and closing the room end the attempt.
- MainWindow implements the small QuickPlayWindow interface, refuses manual
  NetPlay host/join while Quick Play owns NetPlay, and forces traversal for
  a Quick Play room.
- With a game running, the button asks the player to close it first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@WhiteTPoison

Copy link
Copy Markdown
Collaborator
  • Functionality that wasn't requested
  • Functionality that doesn't fit into our ecosystem
  • Functionality that was developed via AI

Yep, vibecoded slop. Closing.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants