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
JavaScript
;
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';
};