const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const mineflayer = require('mineflayer'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*", methods: ["GET", "POST"] }, pingTimeout: 60000, pingInterval: 25000 }); const PORT = process.env.PORT || 7860; // Store active bots const activeBots = new Map(); // Middleware app.use(express.json()); app.use(express.static('public')); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: true, uptime: process.uptime(), bots: activeBots.size }); }); // API Routes app.get('/api/bots', (req, res) => { const bots = Array.from(activeBots.entries()).map(([id, data]) => ({ id, username: data.bot?.username || 'unknown', host: data.host, port: data.port, connected: data.connected, players: data.players || [] })); res.json(bots); }); // Socket.IO Connection io.on('connection', (socket) => { console.log('Client connected:', socket.id); let currentBot = null; let botId = null; let reconnectTimer = null; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 5; const RECONNECT_DELAY = 5000; let botConfig = null; let isIntentionalDisconnect = false; // Connect bot to Minecraft server socket.on('connect-bot', (config) => { const { username, host, port = 25565, version } = config; if (!username || !host) { socket.emit('error', { message: 'Username and host are required' }); return; } // Save config for reconnect botConfig = { username, host, port, version }; isIntentionalDisconnect = false; reconnectAttempts = 0; // Disconnect existing bot for this socket if (currentBot) { isIntentionalDisconnect = true; currentBot.quit(); activeBots.delete(botId); } // Clear any pending reconnect if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } botId = `${socket.id}_${Date.now()}`; createBotConnection(config, socket); }); function createBotConnection(config, socket) { const { username, host, port = 25565, version } = config; try { console.log(`Creating bot ${username} for ${host}:${port}...`); const bot = mineflayer.createBot({ host: host, port: parseInt(port), username: username, version: version || false, auth: 'offline', hideErrors: false, checkTimeoutInterval: 30000, closeTimeout: 10000, keepAlive: true, // Add some delay to avoid rate limiting connectTimeout: 30000 }); currentBot = bot; const botData = { bot: bot, socket: socket, host: host, port: port, connected: false, players: [], chatHistory: [], spawnTimeout: null, keepAliveInterval: null }; activeBots.set(botId, botData); // Bot Events bot.on('login', () => { console.log(`Bot ${username} logged in to ${host}:${port}`); botData.connected = true; reconnectAttempts = 0; socket.emit('bot-status', { status: 'online', username: bot.username, host: host, port: port }); }); bot.on('spawn', () => { console.log(`Bot ${username} spawned`); socket.emit('bot-spawned', { position: bot.entity?.position, health: bot.health, food: bot.food }); updatePlayersList(bot, socket); // Start keep-alive mechanism startKeepAlive(bot, botData); // Clear spawn timeout if (botData.spawnTimeout) { clearTimeout(botData.spawnTimeout); botData.spawnTimeout = null; } }); bot.on('chat', (username, message) => { if (username === bot.username) return; const chatData = { username: username, message: message, timestamp: new Date().toISOString(), type: 'chat' }; botData.chatHistory.push(chatData); socket.emit('chat-message', chatData); }); bot.on('message', (jsonMsg) => { const text = jsonMsg.toString(); if (text && !text.includes(bot.username)) { socket.emit('system-message', { message: text, timestamp: new Date().toISOString() }); } }); bot.on('playerJoined', (player) => { socket.emit('player-joined', { username: player.username, timestamp: new Date().toISOString() }); updatePlayersList(bot, socket); }); bot.on('playerLeft', (player) => { socket.emit('player-left', { username: player.username, timestamp: new Date().toISOString() }); updatePlayersList(bot, socket); }); bot.on('health', () => { socket.emit('bot-health', { health: bot.health, food: bot.food }); }); bot.on('death', () => { socket.emit('bot-death', { message: 'Bot died!', timestamp: new Date().toISOString() }); }); bot.on('kicked', (reason) => { console.log(`Bot ${username} kicked:`, reason); const reasonStr = reason.toString ? reason.toString() : JSON.stringify(reason); socket.emit('bot-kicked', { reason: reasonStr, timestamp: new Date().toISOString() }); // Try to reconnect if not intentional if (!isIntentionalDisconnect) { attemptReconnect(socket, `Kicked: ${reasonStr}`); } }); bot.on('error', (err) => { console.error(`Bot ${username} error:`, err.message); socket.emit('bot-error', { message: err.message, timestamp: new Date().toISOString() }); // Only attempt reconnect for connection errors, not auth errors if (err.message && ( err.message.includes('ETIMEDOUT') || err.message.includes('ECONNRESET') || err.message.includes('ECONNREFUSED') || err.message.includes('timeout') || err.message.includes('disconnected') )) { if (!isIntentionalDisconnect) { attemptReconnect(socket, `Error: ${err.message}`); } } }); bot.on('end', (reason) => { console.log(`Bot ${username} disconnected. Reason:`, reason, 'Intentional:', isIntentionalDisconnect); // Stop keep-alive stopKeepAlive(botData); socket.emit('bot-disconnected', { reason: reason || 'Connection closed', timestamp: new Date().toISOString() }); // Cleanup but keep the bot data for potential reconnect cleanupBotListeners(botId); // Attempt reconnect if not intentional disconnect if (!isIntentionalDisconnect) { attemptReconnect(socket, reason || 'Connection closed'); } }); // Spawn timeout - if bot doesn't spawn within 30 seconds, reconnect botData.spawnTimeout = setTimeout(() => { if (!botData.connected) { console.log(`Bot ${username} spawn timeout, reconnecting...`); socket.emit('bot-error', { message: 'Spawn timeout, reconnecting...', timestamp: new Date().toISOString() }); if (!isIntentionalDisconnect) { bot.quit(); attemptReconnect(socket, 'Spawn timeout'); } } }, 30000); } catch (error) { console.error('Failed to create bot:', error); socket.emit('error', { message: error.message }); if (!isIntentionalDisconnect) { attemptReconnect(socket, `Create error: ${error.message}`); } } } function attemptReconnect(socket, reason) { if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.log(`Max reconnect attempts reached for ${botConfig?.username}`); socket.emit('bot-error', { message: `Max reconnect attempts reached. Please check server details or try again later.`, timestamp: new Date().toISOString() }); return; } reconnectAttempts++; console.log(`Reconnect attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} for ${botConfig?.username} in ${RECONNECT_DELAY}ms`); socket.emit('bot-reconnecting', { attempt: reconnectAttempts, maxAttempts: MAX_RECONNECT_ATTEMPTS, delay: RECONNECT_DELAY, reason: reason, timestamp: new Date().toISOString() }); reconnectTimer = setTimeout(() => { if (botConfig && !isIntentionalDisconnect) { console.log(`Executing reconnect for ${botConfig.username}...`); createBotConnection(botConfig, socket); } }, RECONNECT_DELAY); } function startKeepAlive(bot, botData) { // Send periodic position updates to keep connection alive botData.keepAliveInterval = setInterval(() => { if (bot && bot.entity) { // Small random movement to prevent AFK kick try { const random = Math.random(); if (random < 0.1) { // 10% chance to look around const yaw = Math.random() * Math.PI * 2; bot.look(yaw, 0, false); } } catch (e) { // Ignore look errors } } }, 15000); // Every 15 seconds } function stopKeepAlive(botData) { if (botData.keepAliveInterval) { clearInterval(botData.keepAliveInterval); botData.keepAliveInterval = null; } if (botData.spawnTimeout) { clearTimeout(botData.spawnTimeout); botData.spawnTimeout = null; } } // Send chat message socket.on('send-chat', (message) => { if (currentBot && currentBot.chat && botConfig) { try { currentBot.chat(message); socket.emit('chat-sent', { username: currentBot.username, message: message, timestamp: new Date().toISOString() }); } catch (e) { socket.emit('error', { message: 'Failed to send chat: ' + e.message }); } } }); // Send command socket.on('send-command', (command) => { if (currentBot && currentBot.chat && botConfig) { try { currentBot.chat(command); } catch (e) { socket.emit('error', { message: 'Failed to send command: ' + e.message }); } } }); // Bot actions socket.on('bot-action', (action) => { if (!currentBot || !botConfig) return; try { switch(action.type) { case 'jump': currentBot.setControlState('jump', true); setTimeout(() => currentBot.setControlState('jump', false), 500); break; case 'sneak': currentBot.setControlState('sneak', true); setTimeout(() => currentBot.setControlState('sneak', false), 1000); break; case 'look': if (action.x !== undefined && action.y !== undefined && action.z !== undefined) { const Vec3 = require('vec3'); currentBot.lookAt(new Vec3(action.x, action.y, action.z)); } break; case 'move': if (action.direction) { currentBot.setControlState(action.direction, true); setTimeout(() => currentBot.setControlState(action.direction, false), 500); } break; } } catch (e) { socket.emit('error', { message: 'Action failed: ' + e.message }); } }); // Disconnect bot socket.on('disconnect-bot', () => { isIntentionalDisconnect = true; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (currentBot) { try { currentBot.quit(); } catch (e) {} cleanupBot(botId); currentBot = null; botId = null; } socket.emit('bot-disconnected', { reason: 'Disconnected by user', timestamp: new Date().toISOString() }); }); // Get players list socket.on('get-players', () => { if (currentBot) { updatePlayersList(currentBot, socket); } }); // Disconnect handler socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); isIntentionalDisconnect = true; if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (currentBot) { try { currentBot.quit(); } catch (e) {} cleanupBot(botId); } }); }); function updatePlayersList(bot, socket) { if (!bot.players) return; const players = Object.values(bot.players).map(p => ({ username: p.username, ping: p.ping, uuid: p.uuid })); socket.emit('players-list', players); } function cleanupBotListeners(id) { if (activeBots.has(id)) { const data = activeBots.get(id); stopKeepAlive(data); if (data.bot) { try { data.bot.removeAllListeners(); } catch (e) {} } activeBots.delete(id); } } function cleanupBot(id) { if (activeBots.has(id)) { const data = activeBots.get(id); stopKeepAlive(data); if (data.bot) { try { data.bot.quit(); data.bot.removeAllListeners(); } catch (e) {} } activeBots.delete(id); } } function stopKeepAlive(botData) { if (botData) { if (botData.keepAliveInterval) { clearInterval(botData.keepAliveInterval); botData.keepAliveInterval = null; } if (botData.spawnTimeout) { clearTimeout(botData.spawnTimeout); botData.spawnTimeout = null; } } } // Start server server.listen(PORT, () => { console.log(`🎮 Minecraft Bot Controller running on port ${PORT}`); console.log(`🌐 Access the web interface at http://localhost:${PORT}`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); activeBots.forEach((data, id) => { cleanupBot(id); }); server.close(() => { process.exit(0); }); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); activeBots.forEach((data, id) => { cleanupBot(id); }); server.close(() => { process.exit(0); }); });