done
This commit is contained in:
@@ -2,8 +2,20 @@ FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install wget for healthcheck
|
||||
RUN apk add --no-cache wget
|
||||
# Install Chromium, fonts, and dependencies for Puppeteer
|
||||
RUN apk add --no-cache \
|
||||
wget \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont \
|
||||
font-noto-emoji
|
||||
|
||||
# Tell Puppeteer to use the installed Chromium
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -84,6 +84,20 @@ function initializeDatabase() {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Add player_count column if it doesn't exist (for existing databases)
|
||||
try {
|
||||
db.exec(`ALTER TABLE session_games ADD COLUMN player_count INTEGER`);
|
||||
} catch (err) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Add player_count_check_status column if it doesn't exist (for existing databases)
|
||||
try {
|
||||
db.exec(`ALTER TABLE session_games ADD COLUMN player_count_check_status TEXT DEFAULT 'not_started'`);
|
||||
} catch (err) {
|
||||
// Column already exists, ignore error
|
||||
}
|
||||
|
||||
// Add favor_bias column to games if it doesn't exist
|
||||
try {
|
||||
db.exec(`ALTER TABLE games ADD COLUMN favor_bias INTEGER DEFAULT 0`);
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"csv-parse": "^5.5.3",
|
||||
"csv-stringify": "^6.4.5",
|
||||
"ws": "^8.14.0"
|
||||
"ws": "^8.14.0",
|
||||
"puppeteer": "^24.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
|
||||
@@ -4,6 +4,7 @@ const { authenticateToken } = require('../middleware/auth');
|
||||
const db = require('../database');
|
||||
const { triggerWebhook } = require('../utils/webhooks');
|
||||
const { getWebSocketManager } = require('../utils/websocket-manager');
|
||||
const { startPlayerCountCheck, stopPlayerCountCheck } = require('../utils/player-count-checker');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -355,6 +356,16 @@ router.post('/:id/games', authenticateToken, (req, res) => {
|
||||
console.error('Error triggering notifications:', error);
|
||||
}
|
||||
|
||||
// Automatically start player count check if room code was provided
|
||||
if (room_code) {
|
||||
try {
|
||||
startPlayerCountCheck(req.params.id, result.lastInsertRowid, room_code, game.max_players);
|
||||
} catch (error) {
|
||||
console.error('Error starting player count check:', error);
|
||||
// Don't fail the request if player count check fails
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json(sessionGame);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -569,6 +580,15 @@ router.patch('/:sessionId/games/:gameId/status', authenticateToken, (req, res) =
|
||||
return res.status(404).json({ error: 'Session game not found' });
|
||||
}
|
||||
|
||||
// Stop player count check if game is no longer playing
|
||||
if (status !== 'playing') {
|
||||
try {
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
} catch (error) {
|
||||
console.error('Error stopping player count check:', error);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ message: 'Status updated successfully', status });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
@@ -580,6 +600,13 @@ router.delete('/:sessionId/games/:gameId', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { sessionId, gameId } = req.params;
|
||||
|
||||
// Stop player count check before deleting
|
||||
try {
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
} catch (error) {
|
||||
console.error('Error stopping player count check:', error);
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
DELETE FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
@@ -778,5 +805,101 @@ router.get('/:id/export', authenticateToken, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Start player count check for a session game (admin only)
|
||||
router.post('/:sessionId/games/:gameId/start-player-check', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { sessionId, gameId } = req.params;
|
||||
|
||||
// Get the game to verify it exists and has a room code
|
||||
const game = db.prepare(`
|
||||
SELECT sg.*, g.max_players
|
||||
FROM session_games sg
|
||||
JOIN games g ON sg.game_id = g.id
|
||||
WHERE sg.session_id = ? AND sg.id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (!game) {
|
||||
return res.status(404).json({ error: 'Session game not found' });
|
||||
}
|
||||
|
||||
if (!game.room_code) {
|
||||
return res.status(400).json({ error: 'Game does not have a room code' });
|
||||
}
|
||||
|
||||
// Start the check
|
||||
startPlayerCountCheck(sessionId, gameId, game.room_code, game.max_players);
|
||||
|
||||
res.json({
|
||||
message: 'Player count check started',
|
||||
status: 'waiting'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop player count check for a session game (admin only)
|
||||
router.post('/:sessionId/games/:gameId/stop-player-check', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { sessionId, gameId } = req.params;
|
||||
|
||||
// Stop the check
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
|
||||
res.json({
|
||||
message: 'Player count check stopped',
|
||||
status: 'stopped'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Manually update player count for a session game (admin only)
|
||||
router.patch('/:sessionId/games/:gameId/player-count', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { sessionId, gameId } = req.params;
|
||||
const { player_count } = req.body;
|
||||
|
||||
if (player_count === undefined || player_count === null) {
|
||||
return res.status(400).json({ error: 'player_count is required' });
|
||||
}
|
||||
|
||||
const count = parseInt(player_count);
|
||||
if (isNaN(count) || count < 0) {
|
||||
return res.status(400).json({ error: 'player_count must be a positive number' });
|
||||
}
|
||||
|
||||
// Update the player count
|
||||
const result = db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count = ?, player_count_check_status = 'completed'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(count, sessionId, gameId);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Session game not found' });
|
||||
}
|
||||
|
||||
// Broadcast via WebSocket
|
||||
const wsManager = getWebSocketManager();
|
||||
if (wsManager) {
|
||||
wsManager.broadcastEvent('player-count.updated', {
|
||||
sessionId,
|
||||
gameId,
|
||||
playerCount: count,
|
||||
status: 'completed'
|
||||
}, parseInt(sessionId));
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'Player count updated successfully',
|
||||
player_count: count
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
541
backend/utils/player-count-checker.js
Normal file
541
backend/utils/player-count-checker.js
Normal file
@@ -0,0 +1,541 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const db = require('../database');
|
||||
const { getWebSocketManager } = require('./websocket-manager');
|
||||
|
||||
// Store active check jobs
|
||||
const activeChecks = new Map();
|
||||
|
||||
/**
|
||||
* Check room status via Jackbox API
|
||||
*/
|
||||
async function checkRoomStatus(roomCode) {
|
||||
try {
|
||||
const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const roomData = data.body || data;
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log('[API] Room data:', JSON.stringify(roomData, null, 2));
|
||||
}
|
||||
return {
|
||||
exists: true,
|
||||
locked: roomData.locked || false,
|
||||
full: roomData.full || false,
|
||||
maxPlayers: roomData.maxPlayers || 8,
|
||||
minPlayers: roomData.minPlayers || 0
|
||||
};
|
||||
}
|
||||
return { exists: false };
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error('[API] Error checking room:', e.message);
|
||||
}
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a game from start to finish as audience member
|
||||
* Collects analytics throughout the entire game lifecycle
|
||||
*/
|
||||
async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
|
||||
let browser;
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
|
||||
try {
|
||||
console.log(`[Player Count] Opening audience connection for ${checkKey} (max: ${maxPlayers})`);
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
'--disable-gpu'
|
||||
]
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
||||
|
||||
// Track all player counts we've seen
|
||||
const seenPlayerCounts = new Set();
|
||||
let bestPlayerCount = null;
|
||||
let startPlayerCount = null; // Authoritative count from 'start' action
|
||||
let gameEnded = false;
|
||||
let frameCount = 0;
|
||||
|
||||
// Enable CDP and listen for WebSocket frames BEFORE navigating
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
|
||||
client.on('Network.webSocketFrameReceived', ({ response }) => {
|
||||
if (response.payloadData && !gameEnded) {
|
||||
frameCount++;
|
||||
try {
|
||||
const data = JSON.parse(response.payloadData);
|
||||
|
||||
if (process.env.DEBUG && frameCount % 10 === 0) {
|
||||
console.log(`[Frame ${frameCount}] opcode: ${data.opcode}`);
|
||||
}
|
||||
|
||||
// Check for bc:room with player count data
|
||||
let roomVal = null;
|
||||
|
||||
if (data.opcode === 'client/welcome' && data.result?.entities?.['bc:room']) {
|
||||
roomVal = data.result.entities['bc:room'][1]?.val;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] Found bc:room in client/welcome`);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.opcode === 'object' && data.result?.key === 'bc:room') {
|
||||
roomVal = data.result.val;
|
||||
}
|
||||
|
||||
if (roomVal) {
|
||||
// Check if game has ended
|
||||
if (roomVal.gameResults?.players) {
|
||||
const finalCount = roomVal.gameResults.players.length;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] 🎉 GAME ENDED - Final count: ${finalCount} players`);
|
||||
|
||||
// Verify it matches start count if we had one
|
||||
if (startPlayerCount !== null && startPlayerCount !== finalCount) {
|
||||
console.log(`[Frame ${frameCount}] ⚠️ WARNING: Start count (${startPlayerCount}) != Final count (${finalCount})`);
|
||||
} else if (startPlayerCount !== null) {
|
||||
console.log(`[Frame ${frameCount}] ✓ Verified: Start count matches final count (${finalCount})`);
|
||||
}
|
||||
}
|
||||
bestPlayerCount = finalCount;
|
||||
gameEnded = true;
|
||||
// Update immediately with final count
|
||||
updatePlayerCount(sessionId, gameId, finalCount, 'completed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract player counts from analytics (game in progress)
|
||||
if (roomVal.analytics && Array.isArray(roomVal.analytics)) {
|
||||
for (const analytic of roomVal.analytics) {
|
||||
// Check for 'start' action - this is authoritative
|
||||
if (analytic.action === 'start' && analytic.value && typeof analytic.value === 'number') {
|
||||
if (startPlayerCount === null) {
|
||||
startPlayerCount = analytic.value;
|
||||
bestPlayerCount = analytic.value;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] 🎯 Found 'start' action: ${analytic.value} players (authoritative)`);
|
||||
}
|
||||
// Update UI with authoritative start count
|
||||
updatePlayerCount(sessionId, gameId, startPlayerCount, 'checking');
|
||||
}
|
||||
continue; // Skip to next analytic
|
||||
}
|
||||
|
||||
// If we already have start count, we don't need to keep counting
|
||||
if (startPlayerCount !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, look for any numeric value that could be a player count
|
||||
if (analytic.value && typeof analytic.value === 'number' && analytic.value > 0 && analytic.value <= 100) {
|
||||
seenPlayerCounts.add(analytic.value);
|
||||
|
||||
// Clamp to maxPlayers to avoid cumulative stats inflating count
|
||||
const clampedValue = Math.min(analytic.value, maxPlayers);
|
||||
|
||||
// Update best guess (highest count seen so far, clamped to maxPlayers)
|
||||
if (bestPlayerCount === null || clampedValue > bestPlayerCount) {
|
||||
bestPlayerCount = clampedValue;
|
||||
if (process.env.DEBUG) {
|
||||
if (analytic.value > maxPlayers) {
|
||||
console.log(`[Frame ${frameCount}] 📊 Found player count ${analytic.value} in action '${analytic.action}' (clamped to ${clampedValue})`);
|
||||
} else {
|
||||
console.log(`[Frame ${frameCount}] 📊 Found player count ${analytic.value} in action '${analytic.action}' (best so far)`);
|
||||
}
|
||||
}
|
||||
// Update UI with current best guess
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'checking');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if room is no longer locked (game ended another way)
|
||||
if (roomVal.locked === false && bestPlayerCount !== null) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] Room unlocked, game likely ended. Final count: ${bestPlayerCount}`);
|
||||
}
|
||||
gameEnded = true;
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG && frameCount % 50 === 0) {
|
||||
console.log(`[Frame ${frameCount}] Parse error:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate and join audience
|
||||
if (process.env.DEBUG) console.log('[Audience] Navigating to jackbox.tv...');
|
||||
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Waiting for form...');
|
||||
await page.waitForSelector('input#roomcode', { timeout: 10000 });
|
||||
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Typing room code:', roomCode);
|
||||
const roomInput = await page.$('input#roomcode');
|
||||
await roomInput.type(roomCode.toUpperCase(), { delay: 50 });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Typing name...');
|
||||
const nameInput = await page.$('input#username');
|
||||
await nameInput.type('CountBot', { delay: 30 });
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Waiting for JOIN AUDIENCE button...');
|
||||
await page.waitForFunction(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
return buttons.some(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
|
||||
}, { timeout: 10000 });
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Clicking JOIN AUDIENCE...');
|
||||
await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('button'));
|
||||
const btn = buttons.find(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
|
||||
if (btn) btn.click();
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] 👀 Watching game... (will monitor until game ends)');
|
||||
|
||||
// Keep watching until game ends or we're told to stop
|
||||
// Check every 5 seconds if we should still be watching
|
||||
const checkInterval = setInterval(async () => {
|
||||
// Check if we should stop
|
||||
const game = db.prepare(`
|
||||
SELECT status, player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (!game || game.status === 'skipped' || game.status === 'played' || game.player_count_check_status === 'stopped') {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Audience] Stopping watch - game status changed`);
|
||||
}
|
||||
clearInterval(checkInterval);
|
||||
gameEnded = true;
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if game ended
|
||||
if (gameEnded) {
|
||||
clearInterval(checkInterval);
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if room still exists
|
||||
const roomStatus = await checkRoomStatus(roomCode);
|
||||
if (!roomStatus.exists) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Audience] Room no longer exists - game ended`);
|
||||
}
|
||||
gameEnded = true;
|
||||
clearInterval(checkInterval);
|
||||
if (bestPlayerCount !== null) {
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
} else {
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
}
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Store the interval so we can clean it up
|
||||
const check = activeChecks.get(checkKey);
|
||||
if (check) {
|
||||
check.watchInterval = checkInterval;
|
||||
check.browser = browser;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Audience] Error watching game:', error.message);
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
// If we had a best guess, use it; otherwise fail
|
||||
if (bestPlayerCount !== null) {
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
} else {
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update player count in database
|
||||
*/
|
||||
function updatePlayerCount(sessionId, gameId, playerCount, status) {
|
||||
try {
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count = ?, player_count_check_status = ?
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(playerCount, status, sessionId, gameId);
|
||||
|
||||
// Broadcast via WebSocket
|
||||
const wsManager = getWebSocketManager();
|
||||
if (wsManager) {
|
||||
wsManager.broadcastEvent('player-count.updated', {
|
||||
sessionId,
|
||||
gameId,
|
||||
playerCount,
|
||||
status
|
||||
}, parseInt(sessionId));
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Updated game ${gameId}: ${playerCount} players (${status})`);
|
||||
} catch (error) {
|
||||
console.error('[Player Count] Failed to update database:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start checking player count for a game
|
||||
* New strategy:
|
||||
* 1. Wait 30 seconds
|
||||
* 2. Check if game is locked - if not, wait another 30 seconds
|
||||
* 3. Once locked, join audience and watch entire game
|
||||
* 4. Update UI as we learn more
|
||||
* 5. Finalize when game ends
|
||||
*/
|
||||
async function startPlayerCountCheck(sessionId, gameId, roomCode, maxPlayers = 8) {
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
|
||||
// If already checking, don't start again
|
||||
if (activeChecks.has(checkKey)) {
|
||||
console.log(`[Player Count] Already checking ${checkKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already completed (but allow retrying failed checks)
|
||||
const game = db.prepare(`
|
||||
SELECT player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (game && game.player_count_check_status === 'completed') {
|
||||
console.log(`[Player Count] Check already completed for ${checkKey}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If retrying a failed check, reset the status
|
||||
if (game && game.player_count_check_status === 'failed') {
|
||||
console.log(`[Player Count] Retrying failed check for ${checkKey}`);
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Starting check for game ${gameId} with room code ${roomCode}`);
|
||||
|
||||
// Update status to waiting
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count_check_status = 'waiting'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(sessionId, gameId);
|
||||
|
||||
// Function to check if game is ready (locked)
|
||||
const waitForGameStart = async () => {
|
||||
const roomStatus = await checkRoomStatus(roomCode);
|
||||
|
||||
if (!roomStatus.exists) {
|
||||
console.log(`[Player Count] Room ${roomCode} does not exist`);
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If full, we know the count immediately
|
||||
if (roomStatus.full) {
|
||||
console.log(`[Player Count] Room is FULL - ${roomStatus.maxPlayers} players`);
|
||||
updatePlayerCount(sessionId, gameId, roomStatus.maxPlayers, 'completed');
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If locked, game has started - ready to watch
|
||||
if (roomStatus.locked) {
|
||||
console.log(`[Player Count] Room is LOCKED - game in progress, starting watch`);
|
||||
// Return both status and real maxPlayers from Jackbox
|
||||
return { ready: true, maxPlayers: roomStatus.maxPlayers };
|
||||
}
|
||||
|
||||
// Not ready yet
|
||||
console.log(`[Player Count] Room not ready yet (lobby still open)`);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Wait 30 seconds before first check
|
||||
const initialTimeout = setTimeout(async () => {
|
||||
try {
|
||||
// Update status to checking
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count_check_status = 'checking'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(sessionId, gameId);
|
||||
|
||||
console.log(`[Player Count] Initial check after 30s for ${checkKey}`);
|
||||
const result = await waitForGameStart();
|
||||
|
||||
if (result && result.ready === true) {
|
||||
// Game is locked, start watching with REAL maxPlayers from Jackbox
|
||||
const realMaxPlayers = result.maxPlayers;
|
||||
console.log(`[Player Count] Using real maxPlayers from Jackbox: ${realMaxPlayers} (database had: ${maxPlayers})`);
|
||||
await watchGameAsAudience(sessionId, gameId, roomCode, realMaxPlayers);
|
||||
} else if (result === null) {
|
||||
// Not ready yet, check every 30 seconds
|
||||
const checkInterval = setInterval(async () => {
|
||||
// Check if we should stop
|
||||
const game = db.prepare(`
|
||||
SELECT status, player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (!game || game.status === 'skipped' || game.status === 'played' || game.player_count_check_status === 'stopped' || game.player_count_check_status === 'completed') {
|
||||
console.log(`[Player Count] Stopping check for ${checkKey} - game status changed`);
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await waitForGameStart();
|
||||
if (result && result.ready === true) {
|
||||
// Game is now locked, stop interval and start watching with REAL maxPlayers
|
||||
clearInterval(checkInterval);
|
||||
const check = activeChecks.get(checkKey);
|
||||
if (check) check.interval = null;
|
||||
const realMaxPlayers = result.maxPlayers;
|
||||
console.log(`[Player Count] Using real maxPlayers from Jackbox: ${realMaxPlayers} (database had: ${maxPlayers})`);
|
||||
await watchGameAsAudience(sessionId, gameId, roomCode, realMaxPlayers);
|
||||
} else if (result === false) {
|
||||
// Check failed or completed, stop
|
||||
clearInterval(checkInterval);
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
}
|
||||
}, 30000); // Check every 30 seconds
|
||||
|
||||
// Store the interval
|
||||
const check = activeChecks.get(checkKey);
|
||||
if (check) check.interval = checkInterval;
|
||||
}
|
||||
// If ready === false, check already stopped/completed
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Player Count] Error starting check for ${checkKey}:`, error.message);
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
stopPlayerCountCheck(sessionId, gameId);
|
||||
}
|
||||
}, 30000); // Wait 30 seconds before first check
|
||||
|
||||
// Store the check references
|
||||
activeChecks.set(checkKey, {
|
||||
sessionId,
|
||||
gameId,
|
||||
roomCode,
|
||||
initialTimeout,
|
||||
interval: null,
|
||||
watchInterval: null,
|
||||
browser: null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop checking player count for a game
|
||||
*/
|
||||
async function stopPlayerCountCheck(sessionId, gameId) {
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
const check = activeChecks.get(checkKey);
|
||||
|
||||
if (check) {
|
||||
if (check.initialTimeout) {
|
||||
clearTimeout(check.initialTimeout);
|
||||
}
|
||||
if (check.interval) {
|
||||
clearInterval(check.interval);
|
||||
}
|
||||
if (check.watchInterval) {
|
||||
clearInterval(check.watchInterval);
|
||||
}
|
||||
if (check.browser) {
|
||||
try {
|
||||
await check.browser.close();
|
||||
} catch (e) {
|
||||
// Ignore errors closing browser
|
||||
}
|
||||
}
|
||||
activeChecks.delete(checkKey);
|
||||
|
||||
// Update status to stopped if not already completed or failed
|
||||
const game = db.prepare(`
|
||||
SELECT player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (game && game.player_count_check_status !== 'completed' && game.player_count_check_status !== 'failed') {
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count_check_status = 'stopped'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(sessionId, gameId);
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Stopped check for ${checkKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all active checks (for graceful shutdown)
|
||||
*/
|
||||
async function cleanupAllChecks() {
|
||||
for (const [checkKey, check] of activeChecks.entries()) {
|
||||
if (check.initialTimeout) {
|
||||
clearTimeout(check.initialTimeout);
|
||||
}
|
||||
if (check.interval) {
|
||||
clearInterval(check.interval);
|
||||
}
|
||||
if (check.watchInterval) {
|
||||
clearInterval(check.watchInterval);
|
||||
}
|
||||
if (check.browser) {
|
||||
try {
|
||||
await check.browser.close();
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
activeChecks.clear();
|
||||
console.log('[Player Count] Cleaned up all active checks');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startPlayerCountCheck,
|
||||
stopPlayerCountCheck,
|
||||
cleanupAllChecks
|
||||
};
|
||||
Reference in New Issue
Block a user