fix(queue): free "Next block" no longer pops the payment modal

The consolidated entry modal Free Play + Next block path emitted a bare join_queue with no free
intent, so handleGameQueue ran full payment eligibility and (on a mixed paid+free instance) fired
show_payment_options — the old "Choose How to Play" modal appeared on top of an already-queued free
game. Thread the free flag end-to-end, mirroring auto_start{free}: join_queue{free} -> handleJoinQueue
-> handleGameQueue (skips _checkPaymentEligibility when free+freePlayEnabled, tags the queued entry
free) -> queueManager.addPlayer stores free -> startGamesForWaiting/startGameImmediately pass
{ forceFree } into processGameStart, so the block-landing start takes no credit/payment and lands on
the Pleb board. Ranked queued path (plain join_queue) unchanged. Suite 62/435 green.
This commit is contained in:
jwinterm
2026-07-12 19:58:49 -04:00
parent 6879d3d943
commit ce0ad5c0d0
4 changed files with 40 additions and 28 deletions
+1 -1
View File
@@ -1792,7 +1792,7 @@ const SocketHandlers = {
$('#ecX').on('click', function () { $('#entryChoiceOverlay').remove(); });
$('#ecFree').on('click', function () {
if (timing === 'now') begin('⚡ Free game — dropping in...', function () { socket.emit('auto_start', { free: true }); });
else begin('🛡️ Free game — queued for the next block...', function () { socket.emit('join_queue'); });
else begin('🛡️ Free game — queued for the next block...', function () { socket.emit('join_queue', { free: true }); });
});
$('#ecRank').on('click', function () {
if (!hasCredits) { pay(); return; }
+27 -18
View File
@@ -19,8 +19,13 @@ class QueueHandler {
/**
* Handle game queue request (typing "enter") with rate limiting
*/
async handleGameQueue(socket, getUserBySocket) {
async handleGameQueue(socket, getUserBySocket, opts = {}) {
try {
// The player explicitly chose FREE play (Pleb board). Only honoured when the instance
// allows free play; otherwise fall through to the paid eligibility check below. Mirrors
// the auto_start free path so the "Next block · Free Play" choice doesn't wrongly pop the
// payment modal for a free queued game.
const wantsFree = opts.free === true && this.gameModeManager?.freePlayEnabled;
// Rate limiting for queue attempts
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'game:queue');
if (!rateLimitResult.allowed) {
@@ -52,22 +57,24 @@ class QueueHandler {
return;
}
// Check payment eligibility and payout address requirements
const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
if (!paymentCheckResult.allowed) {
// Handle the specific reason for denial
switch (paymentCheckResult.action) {
case 'set_address':
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm.');
break;
case 'make_payment':
await this.paymentHandlers.createAndShowPaymentRequest(socket);
break;
default:
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
paymentCheckResult.reason || 'Not allowed to join queue');
// Check payment eligibility and payout address requirements — SKIPPED for free play.
if (!wantsFree) {
const paymentCheckResult = await this._checkPaymentEligibility(socket.id);
if (!paymentCheckResult.allowed) {
// Handle the specific reason for denial
switch (paymentCheckResult.action) {
case 'set_address':
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm.');
break;
case 'make_payment':
await this.paymentHandlers.createAndShowPaymentRequest(socket);
break;
default:
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
paymentCheckResult.reason || 'Not allowed to join queue');
}
return;
}
return;
}
// Record the queue attempt
@@ -82,13 +89,15 @@ class QueueHandler {
} catch (e) {}
}
// Add to waiting queue (in free mode or already authorized paid mode)
// Add to waiting queue (in free mode or already authorized paid mode). `free` carries the
// Pleb-board intent to processGameStart when the block lands, so no credit/payment is taken.
this.queueManager.addPlayer({
serverId: socket.id,
clientId: currentUser.clientId,
userId: userId,
requiresConfirmation: false,
confirmed: true
confirmed: true,
free: wantsFree
});
const currentBlock = this.debugManager.getCurrentBlockHeight();
+8 -6
View File
@@ -22,9 +22,9 @@ class QueueManager {
this._waitingPlayers = []; // internal queue
}
addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) {
addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true, free = false }) {
if (this.getPlayerIndex(serverId) !== -1) return; // already queued
this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed });
this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed, free });
if (this.CONSOLE_LOGGING) {
console.log(`[QueueManager] Added player ${serverId} (userId=${userId}, confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`);
}
@@ -117,9 +117,10 @@ class QueueManager {
gameState.proof = game.getProofCommitment();
}
// Process game start (credits deduction / payment link)
// Process game start (credits deduction / payment link). forceFree honours a queued
// Pleb-board entry so no credit/payment is taken when the block lands.
if (this.gameModeManager) {
const startRes = await this.gameModeManager.processGameStart(serverId, game.id);
const startRes = await this.gameModeManager.processGameStart(serverId, game.id, { forceFree: !!entry.free });
if (!startRes.success) {
// Abort game + clean up orphaned DB record
if (this.activeGames) this.activeGames.delete(serverId);
@@ -187,9 +188,10 @@ class QueueManager {
gameState.proof = game.getProofCommitment();
}
// Process game start (credits deduction / payment link)
// Process game start (credits deduction / payment link). forceFree honours a queued
// Pleb-board entry confirmed after the tick.
if (this.gameModeManager) {
const startRes = await this.gameModeManager.processGameStart(serverId, game.id);
const startRes = await this.gameModeManager.processGameStart(serverId, game.id, { forceFree: !!entry.free });
if (!startRes.success) {
// Abort game + clean up orphaned DB record
if (this.activeGames) this.activeGames.delete(serverId);
+4 -3
View File
@@ -480,7 +480,7 @@ class SocketHandlers {
});
socket.on('auto_start', (data) => this.handleAutoStart(socket, (data && typeof data === 'object') ? data : {})); // New handler for start button
socket.on('play_free', (data) => this.handleAutoStart(socket, { ...((data && typeof data === 'object') ? data : {}), free: true })); // Explicit free-play choice
socket.on('join_queue', () => this.handleJoinQueue(socket)); // Queue instead of auto-start
socket.on('join_queue', (data) => this.handleJoinQueue(socket, (data && typeof data === 'object') ? data : {})); // Queue instead of auto-start ({ free: true } for Pleb-board free play)
socket.on('early_entry', () => this.handleEarlyEntry(socket)); // Early entry without waiting for block
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
@@ -662,11 +662,12 @@ class SocketHandlers {
/**
* Handle join_queue request — adds player to block queue instead of starting immediately
*/
async handleJoinQueue(socket) {
async handleJoinQueue(socket, opts = {}) {
try {
await this.queueHandler.handleGameQueue(
socket,
this.connectionHandler.getUserBySocket.bind(this.connectionHandler)
this.connectionHandler.getUserBySocket.bind(this.connectionHandler),
opts
);
// queueHandler already emits queue status and adds to queue
// Client receives queue_joined event from broadcastManager