Skip to content

feat(matchmaking): queue guard for run start + reusable guard_queued helper#7

Merged
V-rtualized merged 3 commits into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/queue-blocks-run-start
Jul 21, 2026
Merged

feat(matchmaking): queue guard for run start + reusable guard_queued helper#7
V-rtualized merged 3 commits into
Balatro-Multiplayer:mainfrom
ChronoFinale:feat/queue-blocks-run-start

Conversation

@ChronoFinale

@ChronoFinale ChronoFinale commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

A queued player could still click Play → New Run (also Continue, Challenges, and the in-run restart). The run tore down the main menu while the search stayed active server-side, with no feedback. This PR ships two things: a run-start gate applied here, and the reusable MPAPI.matchmaking.guard_queued(replay) helper the consumer-mod PRs build on to guard lobby create/join.

The bug

Nothing on the run-start path checked matchmaking state; G.FUNCS.start_run tears down the menu (clear_queue, Game:start_run) regardless.

The fix

G.FUNCS.start_run is the single vanilla funnel for New Run, Continue, Challenges, and the in-run restart. Gating there runs before menu teardown: one wrap covers every entry point, and the menu stays untouched when blocked.

function MPAPI.matchmaking.guard_queued(replay)
	if not MPAPI.matchmaking.is_queued() then
		return false
	end
	mm.pending_action = replay
	G.SETTINGS.paused = true
	MPAPI.queue_guard_overlay:as_overlay()
	return true
end

Returns false when not queued (caller proceeds), true when queued (caller aborts; the blocked call is stashed; the overlay offers Leave Queue & Continue / Leave Queue / Stay Queued).

The replay closure must re-enter the caller's OWN complete entry point, not a lower-level primitive. Replaying MPAPI.join_lobby directly would join server-side but skip the consumer's post-join setup, stranding the player. The replay re-enters a guarded entry point, so the gate is re-checked: is_queued() is false after a successful leave; if the leave did not take, it re-blocks instead of soft-locking. Only Leave Queue & Continue consumes the stash; otherwise the next guard_queued call overwrites it.

Behavior change: leave() fires left

handle:leave() now fires the left event locally, before the server round-trip, idempotent via the existing _left guard. Cancel-search UI resets regardless of which path initiated the leave.

Deliberately not here: lobby create/join guarding

An earlier revision guarded MPAPI.create_lobby/join_lobby in the API; replaying the primitive skipped the consumer's post-join setup and stranded the client. It was removed; each consumer mod guards its own entry points instead (see Related).

How to review

  1. api/matchmaking/queue_guard.lua — the core gate and the start_run wrap.
  2. ui/queue_guard_overlay.lua — the overlay, leave_all_handles, stash consume/replay. Esc/back = Stay Queued.
  3. api/matchmaking/handle.lua — the left event change.
  4. localization/en-us.lua — strings only.
  5. dev/test_queue_guard.lua — tests.

Tests

luajit dev/test_queue_guard.lua from the repo root; only a LuaJIT binary needed. The script stubs the Balatro/MPAPI surface and drives the real module source. Covers the guard_queued contract, the run-start gate (block, stash, replay-after-leave, re-block while still searching), a pre-fix control that fails without the gate, and handle:left firing exactly once.

Related

What it looks like — every entry point, every button

Captured by scripted visual test scenarios (maintained alongside the dev tooling, not part of this PR; each scene fakes the queued state and goes through the real wrapped G.FUNCS paths). Full annotated gallery: VERIFICATION.md.

The guard, with the search visibly running ("Queueing m:ss", left panel):

guard overlay

The story from the New Run setup screen — queued, then blocked on Play. Note the guard replaces the setup overlay rather than stacking, so dismissing lands on the main menu (worth a maintainer opinion on whether that's acceptable):

setup while queued
guard replaces setup

Outcomes: Stay Queued (search still ticking) · Leave Queue (search gone, no run) · Leave Queue & Continue (queue left AND the blocked run really starts):

stay queued
leave queue
leave and continue

@ChronoFinale
ChronoFinale marked this pull request as draft July 13, 2026 02:35
@ChronoFinale
ChronoFinale force-pushed the feat/queue-blocks-run-start branch from 31d7133 to ba9035a Compare July 13, 2026 13:42
Nothing stopped a queued player from clicking Play -> New Run (or
Continue / a Challenge): the run would tear down the main menu while the
queue stayed active server-side with no feedback.

Wrap G.FUNCS.start_run -- the single vanilla chokepoint every run-entry
flow funnels through -- and, while MPAPI.matchmaking.is_queued(), show a
'Matchmaking In Progress' dialog with three ways out: Leave Queue & Play
(leaves every handle, then replays the blocked run-start back through the
gate so it re-blocks if still searching), Leave Queue (return to menu),
and Stay Queued (back button / Esc). No dismiss path reads search state,
so it can't soft-lock if the search ends while open. Inert when not
searching -- vanilla flow untouched.
handle:leave() marked the handle left and told the server, but never
notified the handle's own 'left' subscribers -- so any leave initiated
outside a consumer mod's own cancel path (e.g. the queue-guard overlay)
left the mod's search UI stale, still showing Cancel Search. Fire the
event right after the local state update, before the server round-trip,
so UI resets even if the network call fails.
@ChronoFinale
ChronoFinale force-pushed the feat/queue-blocks-run-start branch from ba9035a to bb6f5c7 Compare July 13, 2026 15:31
@ChronoFinale
ChronoFinale marked this pull request as ready for review July 13, 2026 16:15
@ChronoFinale

Copy link
Copy Markdown
Contributor Author

What shows up if you try to start a new run, continue a run, or start a challenge when you are queued up.

  1. Tested leaving queue,
  2. Tested leaving queue + play correctly plays the selected game option
  3. Tested stay queued
Screenshot 2026-07-13 at 11 13 07 AM

@ChronoFinale ChronoFinale changed the title feat(matchmaking): block singleplayer run start while queued feat(matchmaking): block run start and lobby create/join while queued Jul 13, 2026
@ChronoFinale

Copy link
Copy Markdown
Contributor Author

Updated to block create/join lobby when queued

Screenshot 2026-07-13 at 12 13 35 PM

Generalize the run-start queue gate into a shared mechanism consumer mods
can reuse for their own queue-conflicting actions (e.g. lobby buttons).

- Rename run_guard.lua -> queue_guard.lua and add
  MPAPI.matchmaking.guard_queued(replay): if searching, stash the blocked
  call as a replay closure, show the leave-or-stay overlay, and return true
  so the caller aborts; else return false. The start_run wrap now goes
  through it. guard_queued documents that `replay` must re-enter the
  caller's OWN complete entry point (a consumer guarding a lobby button
  replays its own join/create function, not MPAPI.join_lobby -- replaying
  the API primitive would join server-side but skip the consumer's
  post-join setup and strand the player outside the lobby).
- Generalize the overlay's stashed action from a start_run-specific
  { e, args } (pending_run) to a replay closure (pending_action), so the
  one overlay serves run-start and any consumer action. Button copy
  "Leave Queue & Play" -> "Leave Queue & Continue"; description mentions
  lobbies since consumers surface the same overlay for them.

dev/test_queue_guard.lua covers the guard_queued contract (blocks/stashes/
shows overlay when searching, no-ops when not) and the run-start
application (block/allow, stash-and-replay incl. re-block-while-searching),
plus a pre-fix control that strips the gate to prove it reproduces the bug.
@ChronoFinale
ChronoFinale force-pushed the feat/queue-blocks-run-start branch from afe1130 to 3f2e1b3 Compare July 13, 2026 22:07
@ChronoFinale ChronoFinale changed the title feat(matchmaking): block run start and lobby create/join while queued feat(matchmaking): queue guard for run start + guard_queued helper; fix api_client response routing Jul 13, 2026
@ChronoFinale
ChronoFinale force-pushed the feat/queue-blocks-run-start branch from 3f2e1b3 to 794790c Compare July 13, 2026 22:18
@ChronoFinale ChronoFinale changed the title feat(matchmaking): queue guard for run start + guard_queued helper; fix api_client response routing feat(matchmaking): queue guard for run start + reusable guard_queued helper Jul 13, 2026
@ChronoFinale
ChronoFinale force-pushed the feat/queue-blocks-run-start branch from 6dd6636 to 794790c Compare July 18, 2026 02:50
@V-rtualized
V-rtualized merged commit c315838 into Balatro-Multiplayer:main Jul 21, 2026
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