Skip to content

fix(pvp): pause on opponent disconnect/leave mid-run instead of instant win - #504

Open
ChronoFinale wants to merge 1 commit into
Balatro-Multiplayer:mqttfrom
ChronoFinale:pr/disconnect-grace
Open

fix(pvp): pause on opponent disconnect/leave mid-run instead of instant win#504
ChronoFinale wants to merge 1 commit into
Balatro-Multiplayer:mqttfrom
ChronoFinale:pr/disconnect-grace

Conversation

@ChronoFinale

Copy link
Copy Markdown
Contributor

An opponent disconnecting or leaving mid-run instantly showed the win screen (any player_left triggered an immediate forfeit). Route departures through the grace/pause path instead; only forfeit after the grace window.

Rebased onto current mqtt — re-expressed against upstream's result-passing gamemode convention + reconnect-tail handler. Test: luajit tests/test_disconnect_grace.lua — 22/22 green.

Closes #483

Any opponent departure mid-run -- including a transient network drop --
instantly showed the local player the win screen. The MQTT bridge's
PLAYER_LEFT handler called the gamemode's on_player_forfeit as soon as
G.STAGE == RUN, with no distinction between a deliberate leave and a
dropped connection, and it never subscribed to the API's
PLAYER_DISCONNECTED/PLAYER_RECONNECTED events at all -- so the existing
pause/countdown machinery (action_enemyDisconnected/action_enemyReconnected,
MP.enemy_disconnect_countdown) was dead code in the MQTT flow.

The API's lobby event stream also cannot reliably distinguish a deliberate
leave from a drop: `player_left` fires both for an explicit lobby:leave()
and for an ungraceful connection drop whose retained players/<id>/info
topic clears, which can arrive before, instead of, or without ever seeing
a player_disconnected event first, with no reason field to tell them
apart. Given that ambiguity, every mid-run opponent departure now routes
into the same pause/grace flow (pvp_api/disconnect_grace.lua's
MP.decide_departure_action), and only local grace expiry ends the match --
there is no server anymore to send stopGame on timeout, so the client
resolves the forfeit itself, exactly once, through the existing
host-authoritative on_player_forfeit path.

- pvp_api/disconnect_grace.lua: new pure decision core -- event + state in,
  ignore/start_grace/cancel_grace out -- plus the single-fire expiry guard.
- pvp_api/lobby_bridge.lua: subscribe to PLAYER_DISCONNECTED/PLAYER_RECONNECTED,
  route all three departure events through the decision core, and add the
  grace-expiry forfeit shell (MP.resolve_enemy_disconnect_forfeit).
- networking/action_handlers.lua: track the departed player_id on the
  countdown, resolve it locally on expiry instead of waiting on a server
  stopGame that no longer exists.
- tests/test_disconnect_grace.lua: pure-core scenarios, a RED control
  proving the old routing forfeited instantly on a network drop, and a
  shell-wiring test against a fake MPAPI lobby.
@ChronoFinale
ChronoFinale marked this pull request as ready for review July 23, 2026 23:23
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.

1 participant