From 251ace4635b1dd4197ff3431afc1eb59d849b9a5 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Wed, 7 Jan 2026 17:44:47 -0500 Subject: [PATCH] Fix: make reconnect more robust and make spectate window re-openable --- html/js/input/inputHandler.js | 2 +- src/db/databaseManager.js | 14 ++++++++++-- src/network/paymentHandlers.js | 18 ++++++++++++++-- src/network/queueHandler.js | 10 +++++++++ src/network/queueManager.js | 31 ++++++++++++++++++++++++--- src/network/socketHandlers.js | 39 ++++++++++++++++++++++++++++++---- 6 files changed, 102 insertions(+), 12 deletions(-) diff --git a/html/js/input/inputHandler.js b/html/js/input/inputHandler.js index 38de57b..972d0e8 100644 --- a/html/js/input/inputHandler.js +++ b/html/js/input/inputHandler.js @@ -215,7 +215,7 @@ const InputHandler = { } else { // If not showing waiting screen, add helpful message to chat if missing address if (addressRequired && !hasAddress) { - $('#messages').append($('
  • ').text("⚠️ Please set a payout address before starting.")); + $('#messages').append($('
  • ').text("⚠️ Please set a payout address using the button below before starting.")); UI.scrollChat(); } } diff --git a/src/db/databaseManager.js b/src/db/databaseManager.js index 4ca46f2..73ce2e4 100644 --- a/src/db/databaseManager.js +++ b/src/db/databaseManager.js @@ -27,9 +27,19 @@ class QueryValidator { } // Check for dangerous patterns in the query text itself - const upperText = text.toUpperCase(); for (const keyword of this.dangerousKeywords) { - if (upperText.includes(keyword)) { + // Use regex to avoid false positives (e.g., "DESCRIPTION" containing "SCRIPT") + // Alphanumeric keywords use word boundaries; others use literal match + let regex; + if (/^[A-Z0-9_]+$/i.test(keyword)) { + regex = new RegExp(`\\b${keyword}\\b`, 'i'); + } else { + // Escape special characters for literal match + const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + regex = new RegExp(escaped, 'i'); + } + + if (regex.test(text)) { // Allow legitimate DDL in migrations only const stack = new Error().stack; if (!stack.includes('migrations')) { diff --git a/src/network/paymentHandlers.js b/src/network/paymentHandlers.js index 4408fd5..fbc8385 100644 --- a/src/network/paymentHandlers.js +++ b/src/network/paymentHandlers.js @@ -7,13 +7,14 @@ const { normalizeError } = require('../utils/errors'); class PaymentHandlers { - constructor({ io, gameModeManager, walletService, debugManager, queueManager, broadcastManager }) { + constructor({ io, gameModeManager, walletService, debugManager, queueManager, broadcastManager, sessionManager }) { this.io = io; this.gameModeManager = gameModeManager; this.walletService = walletService; this.debugManager = debugManager; this.queueManager = queueManager; this.broadcastManager = broadcastManager; + this.sessionManager = sessionManager; this.mempoolNotified = new Set(); this.paymentMonitors = new Map(); // Track pending payment metadata per socket (may reuse existing) @@ -239,7 +240,20 @@ class PaymentHandlers { socket.emit('payment_detected', { paymentId: paymentRequest.id, message: 'Payment detected in mempool! Adding you to the game queue...', amount: status.amount, confirmations: 0 }); const existingIdx = this.queueManager.getPlayerIndex(socket.id); if (existingIdx === -1) { - this.queueManager.addPlayer({ serverId: socket.id, clientId: currentUser ? currentUser.clientId : null, paymentId: paymentRequest.id, requiresConfirmation: true, confirmed: false }); + // Try to get DB userId from session + let userId = null; + if (this.sessionManager?.sessions?.has(socket.id)) { + userId = this.sessionManager.sessions.get(socket.id).id; + } + + this.queueManager.addPlayer({ + serverId: socket.id, + clientId: currentUser ? currentUser.clientId : null, + userId: userId, + paymentId: paymentRequest.id, + requiresConfirmation: true, + confirmed: false + }); } socket.emit('queue_joined', { position: (existingIdx === -1 ? this.queueManager.getQueueLength() : existingIdx + 1), message: 'Payment received! Waiting for next block to start game...', currentBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null, nextBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() + 1 : null }); } else if (status.confirmed) { diff --git a/src/network/queueHandler.js b/src/network/queueHandler.js index 4134f60..c106668 100644 --- a/src/network/queueHandler.js +++ b/src/network/queueHandler.js @@ -73,10 +73,20 @@ class QueueHandler { // Record the queue attempt await this.rateLimiter.recordAttempt(socket.id, 'game:queue'); + // Get session info to pass userId + let userId = null; + if (this.paymentHandlers?.gameModeManager?.getOrCreateUser) { + try { + const dbUser = await this.paymentHandlers.gameModeManager.getOrCreateUser(socket.id); + userId = dbUser.id; + } catch (e) {} + } + // Add to waiting queue (in free mode or already authorized paid mode) this.queueManager.addPlayer({ serverId: socket.id, clientId: currentUser.clientId, + userId: userId, requiresConfirmation: false, confirmed: true }); diff --git a/src/network/queueManager.js b/src/network/queueManager.js index 67b2181..5ae4c16 100644 --- a/src/network/queueManager.js +++ b/src/network/queueManager.js @@ -22,14 +22,39 @@ class QueueManager { this._waitingPlayers = []; // internal queue } - addPlayer({ serverId, clientId, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) { + addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) { if (this.getPlayerIndex(serverId) !== -1) return; // already queued - this._waitingPlayers.push({ serverId, clientId, entryTime, paymentId, requiresConfirmation, confirmed }); + this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed }); if (this.CONSOLE_LOGGING) { - console.log(`[QueueManager] Added player ${serverId} (confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`); + console.log(`[QueueManager] Added player ${serverId} (userId=${userId}, confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`); } } + /** + * Update the serverId (socket.id) for a queued player when their session resumes + */ + updateSocketId(userId, newSocketId) { + const entry = this._waitingPlayers.find(p => p.userId === userId); + if (entry) { + if (this.CONSOLE_LOGGING) { + console.log(`[QueueManager] Updating socket ID for user ${userId}: ${entry.serverId} -> ${newSocketId}`); + } + entry.serverId = newSocketId; + return true; + } + return false; + } + + /** + * Check if a player entry has persistent data that should be preserved on disconnect + */ + isValuableEntry(serverId) { + const idx = this.getPlayerIndex(serverId); + if (idx === -1) return false; + const entry = this._waitingPlayers[idx]; + return !!(entry.paymentId || entry.userId); + } + removePlayer(serverId) { const idx = this.getPlayerIndex(serverId); if (idx !== -1) { diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index eb2a46f..a415d4a 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -98,7 +98,8 @@ class SocketHandlers { walletService: this.walletService, debugManager: this.debugManager, queueManager: this.queueManager, - broadcastManager: this.broadcastManager + broadcastManager: this.broadcastManager, + sessionManager: this.sessionManager }); // Address / payout handling encapsulated in AddressManager @@ -341,6 +342,26 @@ class SocketHandlers { const connectionResult = await this.connectionHandler.handleConnection(socket); if (!connectionResult) return; // Connection was rejected or failed + const { sessionInfo } = connectionResult; + + // Restore queue status if session resumed + if (sessionInfo && sessionInfo.resumed && sessionInfo.user && this.queueManager) { + const wasUpdated = this.queueManager.updateSocketId(sessionInfo.user.id, socket.id); + if (wasUpdated) { + const position = this.queueManager.getQueuePosition(socket.id); + if (position !== -1) { + this.broadcastManager.sendStatusUpdate(socket.id, 'queue', + `Welcome back! You are still in the queue at position ${position}.`); + socket.emit('queue_joined', { + position: position, + message: `Welcome back! You are still in queue.`, + currentBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null, + nextBlock: (this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : 0) + 1 + }); + } + } + } + // Send game mode info to client if (this.gameModeManager) { socket.emit('game_mode_info', this.gameModeManager.getGameModeInfo()); @@ -428,7 +449,7 @@ class SocketHandlers { try { const dbUser = await this.gameModeManager.getOrCreateUser(socket.id); if (!dbUser.payout_address) { - this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm. Payment request will appear automatically.'); + this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Use the "Manage Payout Address" button or paste your payout address here and type confirm. Payment request will appear automatically.'); return; } } catch (e) { @@ -649,8 +670,18 @@ class SocketHandlers { // Clean up active games this.activeGames.delete(socket.id); - // Remove from queue manager (idempotent) - this.queueManager.removePlayer(socket.id); + // Remove from queue manager if not a "valuable" entry (persistent session or payment) + // This allows users who paid but disconnected to stay in the queue for session resumption + if (this.queueManager && typeof this.queueManager.removePlayer === 'function') { + if (this.queueManager.isValuableEntry && !this.queueManager.isValuableEntry(socket.id)) { + this.queueManager.removePlayer(socket.id); + } else if (this.debugManager.CONSOLE_LOGGING && this.queueManager.isValuableEntry && this.queueManager.isValuableEntry(socket.id)) { + console.log(`[SocketHandlers] Preserving queue entry for ${socket.id} (user has session/payment)`); + } else if (!this.queueManager.isValuableEntry) { + // Fallback for safety if method not found + this.queueManager.removePlayer(socket.id); + } + } // Clear any pending address confirmations if (this.chatHandler) {