Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
dist/
package-lock.json
.vscode
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"eventemitter3": "^5.0.0",
"minecraft-protocol": "^1.30.0",
"mineflayer": "git+https://github.com/PrismarineJS/mineflayer.git",
"thread-puddle": "^0.4.0",
"typed-emitter": "^2.0.0"
},
"devDependencies": {
Expand Down
159 changes: 2 additions & 157 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,163 +1,8 @@
import { createBot, Bot } from 'mineflayer';
import { ClientOptions } from 'minecraft-protocol';
import EventEmitter from 'eventemitter3';
import assert from 'assert';
import { createRequire } from 'node:module';

if (typeof process !== 'undefined' && parseInt(process.versions.node.split('.')[0]) < 16) {
console.error('Your node version is currently', process.versions.node);
console.error('Please update it to a version >= 16.x.x from https://nodejs.org/');
process.exit(1);
}

/**
* Creates a new Swarm object. Bots are removed from the swarm on disconnect.
* @param {ConnectionOptions} options - Connection options for the swarm.
* @param {AuthenticationOptions[]} [auths] - A list of initial authentication options for each member of the swarm. Defaults to no members.
* @returns {Swarm} A newly created swarm.
*/
export function createSwarm (options: ConnectionOptions, auths: AuthenticationOptions[] = []): Swarm {
// create swarm object
const swarm = new Swarm(options);

// init swarm
auths.forEach(swarm.addSwarmMember);

return swarm;
}

/**
* Represents a swarm of mineflayer bots. Bots are removed from the swarm on disconnect.
* @see createSwarm to create a swarm object.
*/
export class Swarm extends EventEmitter {
bots: SwarmBot[];
plugins: { [key: string]: Plugin };
options: Partial<ClientOptions>;
requirePlugin = createRequire(import.meta.url);

constructor (options: Partial<ClientOptions>) {
super();
this.bots = [];
this.plugins = {};
this.options = options;

this.on('error', (bot, ...errors) => console.error(...errors));

// remove disconnected members
this.on('end', bot => {
this.bots = this.bots.filter(x => bot.username !== x.username);
});

// plugin injection
this.on('inject_allowed', bot => {
bot.swarmOptions.injectAllowed = true;
for (const name in this.plugins) {
this.plugins[name](bot, bot.swarmOptions.botOptions);
}
});
}

/**
* Check for the presence or absence of a member with a given name.
* @param {AuthenticationOptions} auth - The authentication information to create the swarm member with.
*/
addSwarmMember (auth: AuthenticationOptions): void {
// fix for microsoft auth
if (auth.auth === 'microsoft') auth.authTitle = '00000000402b5328';
// create bot and save its options
const botOptions: Partial<ClientOptions> = { ...this.options, ...auth };
const bot: SwarmBot = createBot(botOptions as ClientOptions);
bot.swarmOptions = new BotSwarmData();
bot.swarmOptions.botOptions = botOptions as ClientOptions;
// monkey patch bot.emit
const oldEmit = bot.emit;
bot.emit = (event, ...args) => {
this.emit(event, this, ...args);
return oldEmit(event, ...args);
};
// add bot to swarm
this.bots.push(bot);
}

/**
* Check for the presence or absence of a member with a given name.
* @param {string} username - The username to query for.
* @returns {boolean} Returns true if the given username is contained in the swarm, otherwise returns false.
*/
isSwarmMember (username: string): boolean {
return this.bots.some(bot => bot.username === username);
}

/**
* Load a plugin
* @param {string} name - The plugin to add.
* @param {Plugin} [plugin] - DEPRECATED OPTION. WILL BE REMOVED IN A FUTURE RELEASE.
* @returns {boolean} Returns true if the given plugin is loaded in the swarm, otherwise returns false.
*/
loadPlugin (name: string, plugin?: Plugin | undefined): void {
let resolvedPlugin: Plugin = plugin as Plugin; // Ugly: fixme
if (typeof plugin === 'undefined') {
resolvedPlugin = this.requirePlugin(name) as Plugin;
}

assert.ok(typeof plugin === 'function', 'plugin needs to be a function');

if (this.hasPlugin(name)) {
return;
}

this.plugins[name] = plugin;

this.bots.forEach(bot => {
if (bot.swarmOptions?.botOptions !== undefined && bot.swarmOptions?.injectAllowed) {
resolvedPlugin(bot, bot.swarmOptions.botOptions);
}
});
}

/**
* Check for the presence or absence of a plugin with a given name.
* @param {string} name - The plugin to query for.
* @returns {boolean} Returns true if the given plugin is loaded in the swarm, otherwise returns false.
*/
hasPlugin (name: string): boolean {
return Object.keys(this.plugins).includes(name);
}
}

/**
* @callback Plugin A plugin that can be loaded into a bot.
* @deprecated
* @param {Bot} The bot to load the plugin into.
* @param {ClientOptions} [opts] The bot's ClientOptions.
*/
export type Plugin = (((bot: Bot) => null) | ((bot: Bot, opts: ClientOptions) => null));

/**
* @typedef {Object} BotSwarmData - Data about bots stored by the swarm.
* @property {ClientOptions} [botOptions] - The bot's ClientOptions.
* @property {boolean} injectAllowed - Whether the bot is ready for plugin injection.
*/
export class BotSwarmData {
botOptions?: ClientOptions;
injectAllowed = false;
}

/**
* @typedef {Object} SwarmBot - A bot in a swarm.
* @property {BotSwarmData} [swarmOptions] - The bot's BotSwarmData.
*/
export interface SwarmBot extends Bot {
swarmOptions?: BotSwarmData
}

/**
* @typedef {Partial<ClientOptions>} AuthenticationOptions - Authentication options for swarms.
*/
export type AuthenticationOptions = Partial<ClientOptions>;

/**
* @typedef {Partial<ClientOptions>} ConnectionOptions - Connection options for swarms.
*/
export type ConnectionOptions = Partial<ClientOptions>;
export * from './types';
export * from './swarm';
92 changes: 92 additions & 0 deletions src/swarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { ClientOptions } from 'minecraft-protocol';
import { createThreadPool } from 'thread-puddle';
import { createRequire } from 'module';
import { ConnectionOptions, AuthenticationOptions, Puddle } from './types';
import { ModifiedBot } from './worker';
import { EventEmitter } from 'eventemitter3';

/**
* Creates a new Swarm object. Bots are removed from the swarm on disconnect.
* @param {ConnectionOptions} options - Connection options for the swarm.
* @param {AuthenticationOptions[]} [auths] - A list of initial authentication options for each member of the swarm. Defaults to no members.
* @returns {Swarm} A newly created swarm.
*/
export async function createSwarm (options: ConnectionOptions, auths: AuthenticationOptions[] = []): Promise<Swarm> {
// create swarm object
const swarm = new Swarm(options);

// init swarm
await Promise.all(auths.map(swarm.addSwarmMember));

return swarm;
}

/**
* Represents a swarm of mineflayer bots. Bots are removed from the swarm on disconnect.
* @see createSwarm to create a swarm object.
*/
export class Swarm extends EventEmitter {
options: Partial<ClientOptions>;
bots: Array<Puddle<ModifiedBot>> = [];
plugins: string[] = [];
requirePlugin = createRequire(import.meta.url);

constructor (options: Partial<ClientOptions>) {
super();
this.options = options;
}

/**
* Check for the presence or absence of a member with a given name.
* @param {AuthenticationOptions} auth - The authentication information to create the swarm member with.
*/
async addSwarmMember (auth: AuthenticationOptions): Promise<void> {
// create bot and save its options
const botOptions = { ...this.options, ...auth } as ClientOptions; // eslint-disable-line @typescript-eslint/consistent-type-assertions
const bot = await createThreadPool<ModifiedBot>('./worker', {
size: 1,
workerOptions: {
workerData: botOptions
}
});

// Load plugins
await Promise.all(this.plugins.map(async plugin => await bot.loadPluginByName(plugin)));

// add bot to swarm
this.bots.push(bot);
}

/**
* Check for the presence or absence of a member with a given name.
* @param {string} username - The username to query for.
* @returns {boolean} Returns true if the given username is contained in the swarm, otherwise returns false.
*/
async isSwarmMember (username: string): Promise<boolean> {
return (await Promise.all(this.bots.map(async bot => bot.getProperty('username')))).some(name => name === username);
}

/**
* Load a plugin
* @param {string} plugin - The plugin to add.
* @returns {boolean} Returns true if the given plugin is loaded in the swarm, otherwise returns false.
*/
async loadPlugin (plugin: string): Promise<void> { // eslint-disable-line @typescript-eslint/no-explicit-any
if (await this.hasPlugin(plugin)) {
return;
}

this.plugins.push(plugin);

await Promise.all(this.bots.map(async bot => await bot.loadPluginByName(plugin)));
}

/**
* Check for the presence or absence of a plugin with a given name.
* @param {string} name - The plugin to query for.
* @returns {boolean} Returns true if the given plugin is loaded in the swarm, otherwise returns false.
*/
async hasPlugin (name: string): Promise<boolean> {
return Object.keys(this.plugins).includes(name);
}
}
6 changes: 6 additions & 0 deletions src/types/authenticationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClientOptions } from 'minecraft-protocol';

/**
* @typedef {Partial<ClientOptions>} AuthenticationOptions - Authentication options for swarms.
*/
export type AuthenticationOptions = Partial<ClientOptions>;
6 changes: 6 additions & 0 deletions src/types/connectionOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClientOptions } from 'minecraft-protocol';

/**
* @typedef {Partial<ClientOptions>} ConnectionOptions - Connection options for swarms.
*/
export type ConnectionOptions = Partial<ClientOptions>;
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './authenticationOptions';
export * from './connectionOptions';
export * from './puddle';
10 changes: 10 additions & 0 deletions src/types/puddle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createThreadPool } from 'thread-puddle';

// Workaround since ReturnType doesn't work well with generics
class Wrapper<T> {
wrapped () { // eslint-disable-line @typescript-eslint/promise-function-async, @typescript-eslint/explicit-function-return-type
return createThreadPool<T>('asdf', {});
}
}

export type Puddle<T> = Awaited<ReturnType<Wrapper<T>['wrapped']>>;
43 changes: 43 additions & 0 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Bot, createBot } from 'mineflayer';
import { ClientOptions } from 'minecraft-protocol';
import { workerData } from 'worker_threads';
import { createRequire } from 'module';

export type ModifiedBot = Bot & { loadPluginByName: (name: string) => void, getProperty: (name: keyof Bot) => any }; // eslint-disable-line @typescript-eslint/no-explicit-any

const options: ClientOptions = workerData;

// fix for microsoft auth
if (options.auth === 'microsoft') options.authTitle = '00000000402b5328';

// create bot and save its options
const bot = createBot(options) as ModifiedBot;

// Add module name-based plugin loading
const requirePlugin = createRequire(import.meta.url);
let injectAllowed = false;
bot.on('inject_allowed', () => {
injectAllowed = true;
});
bot.loadPluginByName = (name: string) => {
if (!injectAllowed) {
bot.on('inject_allowed', () => {
injectAllowed = true;
bot.loadPluginByName(name);
});
} else {
const plugin = requirePlugin(name);
plugin(bot, options);
}
};
bot.getProperty = (name: keyof Bot) => {
return bot[name];
};

// remove disconnected members
bot.on('end', _ => {
process.exit(0);
});

// Return the bot as the worker
export default bot;
2 changes: 1 addition & 1 deletion test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

describe('basic', () => {
test('test', () => {

expect(1).toBe(1);
});
});