From ce0ad5c0d0e18c26d3c4d83f8140ca554e087a3d Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 12 Jul 2026 19:58:49 -0400 Subject: [PATCH] fix(queue): free "Next block" no longer pops the payment modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- html/js/network/socketHandlers.js | 2 +- src/network/queueHandler.js | 45 ++++++++++++++++++------------- src/network/queueManager.js | 14 +++++----- src/network/socketHandlers.js | 7 ++--- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index 86248c1..c7dc0be 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -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; } diff --git a/src/network/queueHandler.js b/src/network/queueHandler.js index c106668..e9570e2 100644 --- a/src/network/queueHandler.js +++ b/src/network/queueHandler.js @@ -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(); diff --git a/src/network/queueManager.js b/src/network/queueManager.js index a6c8eda..1ae7f28 100644 --- a/src/network/queueManager.js +++ b/src/network/queueManager.js @@ -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); diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index f0d56aa..b835be3 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -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