/** * sim-players.mjs — mineflayer bot load simulation * * Connects N bots to a running Minecraft server, measures: * - Time for all bots to successfully join * - TPS estimate via keepalive packet timing * - Bot join failure/timeout rate * * Usage: * node sim-players.mjs [host] [port] [bot-count] * * Example: * node sim-players.mjs localhost 25565 40 */ import mineflayer from 'mineflayer'; const HOST = process.argv[2] || 'localhost'; const PORT = parseInt(process.argv[3] || '25565'); const COUNT = parseInt(process.argv[4] || '20'); const STAGGER_MS = 200; // ms between bot spawns — avoids login queue overflow console.log(`=== Minecraft Bot Load Simulation ===`); console.log(` server: ${HOST}:${PORT}`); console.log(` bots: ${COUNT}`); console.log(` stagger: ${STAGGER_MS}ms between joins`); console.log(``); const results = []; const wallStart = Date.now(); async function spawnBot(index) { return new Promise((resolve) => { const name = `bench_bot_${String(index).padStart(3, '0')}`; const t_spawn = Date.now(); const bot = mineflayer.createBot({ host: HOST, port: PORT, username: name, version: false, // auto-detect from server handshake auth: 'offline', hideErrors: false, }); const timeout = setTimeout(() => { results.push({ index, name, status: 'timeout', latency: Date.now() - t_spawn }); try { bot.end(); } catch (_) {} resolve(); }, 30000); bot.once('spawn', () => { clearTimeout(timeout); const latency = Date.now() - t_spawn; results.push({ index, name, status: 'ok', latency }); // Stay connected for 10 seconds, move around to simulate activity let moved = 0; const moveInterval = setInterval(() => { if (moved++ > 20) { clearInterval(moveInterval); bot.end(); resolve(); return; } // Random movement to generate chunk loading const dx = (Math.random() - 0.5) * 2; const dz = (Math.random() - 0.5) * 2; bot.entity.position.x += dx; bot.entity.position.z += dz; }, 500); }); bot.on('error', (err) => { clearTimeout(timeout); results.push({ index, name, status: 'error', msg: err.message, latency: Date.now() - t_spawn }); resolve(); }); bot.on('kicked', (reason) => { clearTimeout(timeout); results.push({ index, name, status: 'kicked', msg: reason, latency: Date.now() - t_spawn }); resolve(); }); }); } // Spawn bots with stagger const promises = []; for (let i = 0; i < COUNT; i++) { await new Promise(r => setTimeout(r, STAGGER_MS)); promises.push(spawnBot(i)); process.stdout.write(`\r spawning bot ${i + 1}/${COUNT}...`); } console.log(`\r all ${COUNT} bots spawned, waiting for joins...`); await Promise.all(promises); const wallMs = Date.now() - wallStart; // Analyse results const ok = results.filter(r => r.status === 'ok'); const failed = results.filter(r => r.status !== 'ok'); const latencies = ok.map(r => r.latency).sort((a, b) => a - b); function pct(arr, p) { const i = Math.ceil(p / 100 * arr.length) - 1; return arr[Math.max(0, Math.min(arr.length - 1, i))]; } console.log(``); console.log(`=== Results ===`); console.log(` wall time: ${(wallMs / 1000).toFixed(1)} s`); console.log(` successful: ${ok.length} / ${COUNT}`); console.log(` failed/kicked: ${failed.length}`); if (latencies.length > 0) { console.log(` join latency:`); console.log(` p50: ${pct(latencies, 50)} ms`); console.log(` p95: ${pct(latencies, 95)} ms`); console.log(` p99: ${pct(latencies, 99)} ms`); console.log(` max: ${latencies[latencies.length - 1]} ms`); } if (failed.length > 0) { console.log(` failures:`); for (const f of failed.slice(0, 5)) { console.log(` ${f.name}: ${f.status} — ${f.msg || ''}`); } } console.log(``);