mcbot / index.js
wpms's picture
Update index.js
f39bfe4 verified
Raw History Blame Contribute Delete
17 kB
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);
});
});