bot-connection.ts•6.34 kB
import mineflayer from 'mineflayer';
import pathfinderPkg from 'mineflayer-pathfinder';
const { pathfinder, Movements } = pathfinderPkg;
import minecraftData from 'minecraft-data';
const SUPPORTED_MINECRAFT_VERSION = '1.21.8';
type ConnectionState = 'connected' | 'connecting' | 'disconnected';
interface BotConfig {
host: string;
port: number;
username: string;
}
interface ConnectionCallbacks {
onLog: (level: string, message: string) => void;
onChatMessage: (username: string, message: string) => void;
}
export class BotConnection {
private bot: mineflayer.Bot | null = null;
private state: ConnectionState = 'disconnected';
private config: BotConfig;
private callbacks: ConnectionCallbacks;
private isReconnecting = false;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private readonly reconnectDelayMs: number;
constructor(config: BotConfig, callbacks: ConnectionCallbacks, reconnectDelayMs = 2000) {
this.config = config;
this.callbacks = callbacks;
this.reconnectDelayMs = reconnectDelayMs;
}
getBot(): mineflayer.Bot | null {
return this.bot;
}
getState(): ConnectionState {
return this.state;
}
getConfig(): BotConfig {
return this.config;
}
isConnected(): boolean {
return this.state === 'connected';
}
connect(): void {
const botOptions = {
host: this.config.host,
port: this.config.port,
username: this.config.username,
plugins: { pathfinder },
};
this.bot = mineflayer.createBot(botOptions);
this.state = 'connecting';
this.isReconnecting = false;
this.registerEventHandlers(this.bot);
}
private registerEventHandlers(bot: mineflayer.Bot): void {
bot.once('spawn', async () => {
this.state = 'connected';
this.callbacks.onLog('info', 'Bot spawned in world');
const mcData = minecraftData(bot.version);
const defaultMove = new Movements(bot, mcData);
bot.pathfinder.setMovements(defaultMove);
bot.chat('LLM-powered bot ready to receive instructions!');
this.callbacks.onLog('info', `Bot connected successfully. Username: ${this.config.username}, Server: ${this.config.host}:${this.config.port}`);
});
bot.on('chat', (username, message) => {
if (username === bot.username) return;
this.callbacks.onChatMessage(username, message);
});
bot.on('kicked', (reason) => {
this.callbacks.onLog('error', `Bot was kicked from server: ${this.formatError(reason)}`);
this.state = 'disconnected';
bot.quit();
});
bot.on('error', (err) => {
const errorCode = (err as { code?: string }).code || 'Unknown error';
const errorMsg = err instanceof Error ? err.message : String(err);
this.callbacks.onLog('error', `Bot error [${errorCode}]: ${errorMsg}`);
if (errorCode === 'ECONNREFUSED' || errorCode === 'ETIMEDOUT') {
this.state = 'disconnected';
}
});
bot.on('login', () => {
this.callbacks.onLog('info', 'Bot logged in successfully');
});
bot.on('end', (reason) => {
this.callbacks.onLog('info', `Bot disconnected: ${this.formatError(reason)}`);
if (this.state === 'connected') {
this.state = 'disconnected';
}
if (this.bot === bot) {
try {
bot.removeAllListeners();
this.bot = null;
this.callbacks.onLog('info', 'Bot instance cleaned up after disconnect');
} catch (err) {
this.callbacks.onLog('warn', `Error cleaning up bot on end event: ${this.formatError(err)}`);
}
}
});
}
attemptReconnect(): void {
if (this.isReconnecting || this.state === 'connecting') {
return;
}
this.isReconnecting = true;
this.state = 'connecting';
this.callbacks.onLog('info', `Attempting to reconnect to Minecraft server in ${this.reconnectDelayMs}ms...`);
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
this.reconnectTimer = setTimeout(() => {
if (this.bot) {
try {
this.bot.removeAllListeners();
this.bot.quit('Reconnecting...');
this.callbacks.onLog('info', 'Old bot instance cleaned up');
} catch (err) {
this.callbacks.onLog('warn', `Error while cleaning up old bot: ${this.formatError(err)}`);
}
}
this.callbacks.onLog('info', 'Creating new bot instance...');
this.connect();
}, this.reconnectDelayMs);
}
async checkConnectionAndReconnect(): Promise<{ connected: boolean; message?: string }> {
const currentState = this.state;
if (currentState === 'disconnected') {
this.attemptReconnect();
const maxWaitTime = this.reconnectDelayMs + 5000;
const pollInterval = 100;
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (this.state === 'connected') {
return { connected: true };
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
const errorMessage =
`Cannot connect to Minecraft server at ${this.config.host}:${this.config.port}\n\n` +
`Please ensure:\n` +
`1. Minecraft server is running on ${this.config.host}:${this.config.port}\n` +
`2. Server is accessible from this machine\n` +
`3. Server version is compatible (latest supported: ${SUPPORTED_MINECRAFT_VERSION})\n\n` +
`For setup instructions, visit: https://github.com/yuniko-software/minecraft-mcp-server`;
return { connected: false, message: errorMessage };
}
if (currentState === 'connecting') {
return { connected: false, message: 'Bot is connecting to the Minecraft server. Please wait a moment and try again.' };
}
return { connected: true };
}
cleanup(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
if (this.bot) {
try {
this.bot.quit('Server shutting down');
} catch (err) {
this.callbacks.onLog('warn', `Error during cleanup: ${this.formatError(err)}`);
}
}
}
private formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
}