UNPKG

mcraft-fun-mineflayer

Version:

Mineflayer viewer (connector) for mcraft.fun project and vanilla Minecraft client! Both TCP and WebSockets servers are supported.

601 lines (600 loc) 23 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createMineflayerPluginServer = void 0; const minecraft_protocol_1 = require("minecraft-protocol"); const wsServer_1 = __importStar(require("./wsServer")); const exit_hook_1 = __importDefault(require("exit-hook")); const mineflayerPacketHandler_1 = require("./mineflayerPacketHandler"); const customChannel_1 = require("./customChannel"); const os_1 = require("os"); const fs_1 = require("fs"); const https_1 = require("https"); const ssl_1 = require("./ssl"); const worldState_1 = require("./worldState"); const packetsLogger_1 = require("./packetsLogger"); const fs_2 = __importDefault(require("fs")); const createMineflayerPluginServer = (bot, options) => { if (bot.game?.gameMode !== undefined) { throw new Error('[mcraft-fun-mineflayer] Bot is already in-game. You MUST register the plugin just right after creating the bot, not in login callback'); } if (options.tcpEnabled !== false && options.websocketEnabled !== false) { console.log('Starting servers...'); } // #region start servers const TCP_PORT = options.tcpPort ?? 25587; const WS_PORT = options.websocketPort ?? 25588; const TCP_HOST = options.tcpHost ?? undefined; const WS_HOST = options.websocketHost ?? undefined; let tcpServer; let wsServer; let httpsServer; if (options.tcpEnabled !== false) { if (options.password) { console.log('TCP server (Vanilla Minecraft) is disabled because it does not support password'); } else { tcpServer = (0, minecraft_protocol_1.createServer)({ "online-mode": false, version: bot.version, port: TCP_PORT, host: TCP_HOST, }); } } const websitePreview = 'https://s.mcraft.fun'; (0, wsServer_1.setNextWebsocketOptions)(undefined); if (options.websocketEnabled !== false) { if (options.ssl?.enabled) { let sslOptions; if (options.ssl.selfSigned) { sslOptions = (0, ssl_1.generateSelfSignedCertificate)(); } else if (options.ssl.cert && options.ssl.key) { sslOptions = { cert: (0, fs_1.readFileSync)(options.ssl.cert), key: (0, fs_1.readFileSync)(options.ssl.key) }; } else { console.warn('SSL is enabled but no certificate provided. Falling back to non-SSL.'); } if (sslOptions) { httpsServer = (0, https_1.createServer)(sslOptions); httpsServer.on('request', (req, res) => { // Check if this is a WebSocket upgrade request if (req.headers.upgrade?.toLowerCase() !== 'websocket') { res.writeHead(302, { 'Location': websitePreview + '/?viewerConnect=wss://' + req.headers.host }); res.end(); return; } }); (0, wsServer_1.setNextWebsocketOptions)({ server: httpsServer }); wsServer = (0, minecraft_protocol_1.createServer)({ "online-mode": false, version: bot.version, Server: wsServer_1.default, port: WS_PORT, host: WS_HOST, customPackets: {}, }); httpsServer.listen(WS_PORT, WS_HOST); } else { wsServer = (0, minecraft_protocol_1.createServer)({ "online-mode": false, version: bot.version, Server: wsServer_1.default, port: WS_PORT, host: WS_HOST, customPackets: {}, }); } } else { wsServer = (0, minecraft_protocol_1.createServer)({ "online-mode": false, version: bot.version, Server: wsServer_1.default, port: WS_PORT, host: WS_HOST, customPackets: {}, }); } wsServer['options'] = options; } const serverPromises = []; if (wsServer) { serverPromises.push(new Promise(resolve => wsServer.once('listening', resolve)).then(() => console.log('WebSocket server is ready'))); } if (tcpServer) { serverPromises.push(new Promise(resolve => tcpServer.once('listening', resolve)).then(() => console.log('TCP server is ready'))); } void Promise.all(serverPromises).then((arr) => { if (arr.length === 0) return; console.log(`Viewer servers are ready:`); if (options.showConnectionInstructions !== false) { const defaultIp = getDefaultIp(); if (wsServer) { const wsDisplayHost = WS_HOST ?? (!options.ssl?.enabled ? 'localhost' : defaultIp); const protocol = options.ssl?.enabled ? 'wss' : 'ws'; const webLink = options.ssl?.enabled ? `https://${wsDisplayHost}:${WS_PORT}` : `${websitePreview}/?viewerConnect=${protocol}://${wsDisplayHost}:${WS_PORT}`; console.log(`Web Link: ${webLink}`); if (!options.ssl?.enabled) { console.log('Use SSL cert or tunnel like cloudflared to connect from outside the network'); } } if (tcpServer) { const tcpDisplayHost = TCP_HOST ?? defaultIp; console.log(`TCP (Vanilla Minecraft): ${tcpDisplayHost}:${TCP_PORT} (${bot.version})`); } } }); // #endregion const fakeClients = []; const writeClients = (name, data, clients) => { if (clients) { for (const client of clients) { client.write(name, data); } } else { const getClients = (clients) => Object.values(clients).filter(c => c.state === minecraft_protocol_1.states.PLAY); tcpServer?.writeToClients(getClients(tcpServer.clients), name, data); wsServer?.writeToClients(getClients(wsServer.clients), name, data); fakeClients.forEach(c => c.write(name, data)); } }; const packetHandler = new mineflayerPacketHandler_1.MineflayerPacketHandler(bot, { writeToAuxClients(name, data) { writeClients(name, data); }, }); bot.on('resourcePack', (url) => { packetHandler.loginState = 'Bot is waiting for resource pack to be accepted'; }); bot._client.on('login', (packet) => { packetHandler.loginState = 'in-world'; }); bot.on('kicked', (reason) => { packetHandler.loginState = `Kicked from server: ${reason}`; }); bot.on('end', (reason) => { packetHandler.loginState ||= `Disconnected from server`; }); bot._client.on('respawn', (packet) => { packetHandler.loginState = 'has-respawned'; }); // send custom channel packets const customChannel = (0, customChannel_1.registerCustomChannel)(bot, options, () => { return [...(tcpServer?.clients ? Object.values(tcpServer.clients) : []), ...(wsServer?.clients ? Object.values(wsServer.clients) : [])] .filter((c) => c?.state === minecraft_protocol_1.states.PLAY); }); const login = (client, isTcp = false) => { customChannel.registerChannel(client); //@ts-ignore if (!client.supportFeature('hasConfigurationState')) { newConnection(client, isTcp); } }; const newConnection = (client, isTcp = false) => { if (client['handledLogin']) return; client['handledLogin'] = true; customChannel.registerChannel(client); packetHandler.handleNewConnection(client); // force selected slot (dont allow viewer to change it) client.on('held_item_slot', () => { packetHandler.updateSlot([client]); }); customChannel.newConnection(client); // Send all existing UI definitions to new connection for (const [id, def] of uiDefinitions.entries()) { customChannel.send({ type: 'ui', update: { id, data: def } }, client); } // Add chat forwarding handler if (options.forwardChat) { client.on('chat', ({ message }) => { bot.chat(message); }); client.on('chat_message', ({ message }) => { bot.chat(message); }); client.on('chat_command', ({ command }) => { bot.chat(`/${command}`); }); client.on('tab_complete', (packet) => { bot._client.write('tab_complete', packet); let start = Date.now(); bot._client.once('tab_complete', (packet) => { if (Date.now() - start > 5000) return; client.write('tab_complete', packet); }); }); } }; const handlePlayerJoin = (client, isTcp = false) => { //@ts-ignore if (client.supportFeature('hasConfigurationState')) { newConnection(client, isTcp); } }; tcpServer?.on('playerJoin', client => handlePlayerJoin(client, true)); wsServer?.on('playerJoin', client => handlePlayerJoin(client, false)); wsServer?.on('login', client => login(client, false)); tcpServer?.on('login', client => login(client, true)); const hookMethod = (_name, callback) => { const name = _name; const oldMethod = bot[name].bind(bot); bot[name] = (...args) => { callback(...args); oldMethod(...args); }; }; // todo patch swingArm hookMethod('closeWindow', () => { writeClients('closeWindow', { windowId: 0 }); }); hookMethod('setQuickBarSlot', () => { packetHandler.updateSlot(); }); bot.on('end', () => { if (options.stopServersOnDisconnect !== false) { tcpServer?.close(); wsServer?.close(); if (httpsServer) httpsServer.close(); } }); (0, exit_hook_1.default)(() => { tcpServer?.close(); wsServer?.close(); if (httpsServer) httpsServer.close(); }); const lils = {}; // Add storage for UI definitions const uiDefinitions = new Map(); const sendLil = (id) => { const lil = lils[id]; if (!lil) { return; } const lilDef = { type: 'lil', ...lil, params: Object.fromEntries(Object.entries(lil.params).filter(x => { return typeof x[1] === 'string' || typeof x[1] === 'number' || typeof x[1] === 'boolean'; }).map(x => { return [x[0], x[1]]; })), buttons: Object.entries(lil.params).filter(x => typeof x[1] === 'function').map(x => x[0]) }; // Store lil definition uiDefinitions.set(id, lilDef); customChannel.send({ type: 'ui', update: { id, data: lilDef } }); }; const uiController = { updateUI: (id, ui) => { uiDefinitions.set(id, ui); customChannel.send({ type: 'ui', update: { id, data: ui } }); }, removeUI: (id) => { uiDefinitions.delete(id); customChannel.send({ type: 'ui', update: { id, data: null } }); }, updateText: (id, text) => { if (!uiDefinitions.has(id) || uiDefinitions.get(id)?.type !== 'text') { return; } const ui = uiDefinitions.get(id); ui.text = text; customChannel.send({ type: 'ui', update: { id, data: ui } }); }, updateLil: (id, object, params = {}) => { lils[id] = { ...params, params: object }; sendLil(id); }, removeLil: (id) => { delete lils[id]; // Remove from storage uiDefinitions.delete(id); customChannel.send({ type: 'ui', update: { id, data: null } }); } }; const abortController = new AbortController(); const startTime = Date.now(); const interval = setInterval(() => { if (options.sendStats !== false) { customChannel.send({ type: 'stats', botPing: -1, botUptime: Date.now() - startTime }); } }, 1000); abortController.signal.addEventListener('abort', () => { clearInterval(interval); }); customChannel.receivedProcessor = (packet) => { if (packet.type === 'eval') { if (options.allowEval) { try { const func = new Function('bot', packet.code); const result = func(bot); customChannel.send({ type: 'eval', result: result, isError: false }); } catch (error) { customChannel.send({ type: 'eval', result: String(error), isError: true }); } } } if (packet.type === 'method') { // const allowMethods = options.allowMethods; const allowMethods = true; if (allowMethods) { try { const result = bot[packet.method](...packet.args); if (result instanceof Promise) { result.then(result => { customChannel.send({ type: 'method', result: result }); }); } else { customChannel.send({ type: 'method', result: result }); } } catch (err) { bot.emit('error', err); } } } if (packet.type === 'ui') { const lil = lils[packet.id]; if (lil) { const oldValue = lil.params[packet.param]; if (typeof oldValue === 'function') { try { oldValue(); } catch (err) { bot.emit('error', err); } } else { if (lil.onUpdate) { lil.params[packet.param] = packet.value; try { lil.onUpdate(packet.param, packet.value, oldValue); } catch (err) { bot.emit('error', err); } } else { lil.params[packet.param] = packet.value; } // update for other clients sendLil(packet.id); } } } if (packet.type === 'setControlState') { // todo: do full control override bot.setControlState(packet.control, packet.value); } if (packet.type === 'setLook') { bot.look(packet.yaw, packet.pitch, true); } }; // intercept console messages if (options.sendConsole) { const originalConsole = { ...console }; const interceptConsole = (method) => { console[method] = (...args) => { originalConsole[method](...args); customChannel.send({ type: 'console', level: method, message: args.map(arg => typeof arg === 'string' ? arg : typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ') }); }; }; interceptConsole('log'); interceptConsole('warn'); interceptConsole('error'); // Restore console on cleanup abortController.signal.addEventListener('abort', () => { Object.assign(console, originalConsole); }); } let recordingLogger; const startRecording = (adjustPacketsLogger) => { const stateCaptureFileBase = (0, worldState_1.createStateCaptureFile)(bot, adjustPacketsLogger); recordingLogger = stateCaptureFileBase.logger; fakeClients.push(stateCaptureFileBase.client); newConnection(stateCaptureFileBase.client); }; const stopRecording = (saveFileName) => { if (!recordingLogger) throw new Error('No current recording session'); fakeClients.pop(); if (saveFileName) { fs_2.default.writeFileSync(`${saveFileName}.${worldState_1.PACKETS_REPLAY_FILE_EXTENSION}`, recordingLogger.contents); } recordingLogger = undefined; }; const createStateCaptureFile = (fileName, adjustPacketsLogger) => { const { logger: newLogger, client } = (0, worldState_1.createStateCaptureFile)(bot, adjustPacketsLogger); fakeClients.push(client); newConnection(client); fakeClients.pop(); if (fileName) { fs_2.default.mkdirSync(fileName, { recursive: true }); fs_2.default.writeFileSync(`${fileName}.${worldState_1.WORLD_STATE_FILE_EXTENSION}`, newLogger.contents); } return newLogger; }; const unstableApi = { createStateCaptureFile, startRecording, stopRecording, debugWorldCapture() { console.time('debugWorldCapture'); const recordingLogger = createStateCaptureFile(); if (!recordingLogger) throw new Error('No current recording session'); const contents = recordingLogger.contents; console.log(`Captured state size: ${contents.length / 1024 / 1024} MB`); const { packets } = (0, packetsLogger_1.parseReplayContents)(contents); // Count total occurrences of each packet const packetCounts = {}; for (const packet of packets) { const packetName = packet.name; packetCounts[packetName] = (packetCounts[packetName] || 0) + 1; } // Create flattened sequence of repeated packets const packetsFlattened = []; let currentPacket = ''; let currentCount = 0; for (const packet of packets) { if (packet.name === currentPacket) { currentCount++; } else { if (currentCount > 0) { packetsFlattened.push(`${currentPacket} ${currentCount}x`); } currentPacket = packet.name; currentCount = 1; } } if (currentCount > 0) { packetsFlattened.push(`${currentPacket} ${currentCount}x`); } console.log('\nSequential packets:'); console.log(packetsFlattened.join(', ')); console.log('\nTotal packet counts:'); Object.entries(packetCounts) .sort(([, a], [, b]) => b - a) .forEach(([name, count]) => { console.log(`${name}: ${count}`); }); console.timeEnd('debugWorldCapture'); } }; const plugin = { ui: uiController, methods: {}, _customChannel: customChannel, _tcpServer: tcpServer, _wsServer: wsServer, captureWorldIntoFile: createStateCaptureFile, _unstable: unstableApi }; bot.webViewer = plugin; return plugin; }; exports.createMineflayerPluginServer = createMineflayerPluginServer; const getDefaultIp = () => { const interfaces = (0, os_1.networkInterfaces)(); // Try common interface names first const commonNames = ['eth0', 'en0', 'wlan0', 'Wi-Fi', 'Ethernet']; for (const name of commonNames) { const iface = interfaces[name]; if (iface?.length) { const ipv4 = iface.find(addr => addr.family === 'IPv4' && !addr.internal); if (ipv4) return ipv4.address; } } // If common names didn't work, try all interfaces for (const iface of Object.values(interfaces)) { if (!iface) continue; const ipv4 = iface.find(addr => addr.family === 'IPv4' && !addr.internal); if (ipv4) return ipv4.address; } return 'localhost'; };