diff --git a/src/Cubbot.ts b/src/Cubbot.ts index 5dd3338..f6f6966 100644 --- a/src/Cubbot.ts +++ b/src/Cubbot.ts @@ -47,26 +47,27 @@ export default class Cubbot { this._registeredModules.set(n, module) } - public loadModule>(module: IModuleType): T { - return this.loadModuleByName(module.name) as T + public getModule>(module: IModuleType, load: boolean): T | undefined { + return this.getModuleByName(module.name, load) as T | undefined } - public loadModuleByName(name: string) { + public getModuleByName(name: string, load: boolean) { if (this.modules!.has(name)) { return this.modules!.get(name)! } + if (load) { + this.logger.debug('Loading module %s', name) + assert.ok(this._registeredModules.has(name), `Unknown module ${name}`) + const mType = this._registeredModules.get(name)! - this.logger.debug('Loading module %s', name) - assert.ok(this._registeredModules.has(name), `Unknown module ${name}`) - const mType = this._registeredModules.get(name)! + const conf: { log?: string } = this.config.modules[name] || {} + const mLogger = this.logger.child({ name, level: conf.log }) + mLogger.trace('Logger created') - const conf: { log?: string } = this.config.modules[name] || {} - const mLogger = this.logger.child({ name, level: conf.log }) - mLogger.trace('Logger created') - - const module = new mType(this.client!, mLogger, conf, this.loadModule.bind(this)) - this._modules!.set(name, module) - return module + const module = new mType(this.client!, mLogger, conf, this.getModule.bind(this)) + this._modules!.set(name, module) + return module + } } /** Start bot */ @@ -77,8 +78,8 @@ export default class Cubbot { this.logger.debug('Loading modules') this._modules = new Map>() - this.loadModule(Client).setEngine(this) - Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(this)) + this.getModule(Client, true)!.setEngine(this) + Object.keys(this.config.modules).forEach(m => this.getModuleByName(m, true)) } } diff --git a/src/modules/Connection.ts b/src/modules/Connection.ts index f050b19..c5fa18c 100644 --- a/src/modules/Connection.ts +++ b/src/modules/Connection.ts @@ -78,6 +78,7 @@ export default class Connection extends Module { this.client.on('error', error => { this.logger.error({ msg: 'Error', value: error }) }) + // MAYBE: on 'end' } protected getConf() { diff --git a/src/modules/Inventory.ts b/src/modules/Inventory.ts index 0a574f1..204f0de 100644 --- a/src/modules/Inventory.ts +++ b/src/modules/Inventory.ts @@ -85,6 +85,7 @@ export default class Inventory extends Module<{}> { } private _held = 0 + private _onHeld = new Array<(slot: number) => void>() public get mainHand() { return this._held } @@ -108,6 +109,10 @@ export default class Inventory extends Module<{}> { public setHeld(slotId: number) { this.client.write('held_item_slot', { slotId }) this._held = slotId + this._onHeld.forEach(cb => cb(slotId)) + } + public registerHeld(cb: (slot: number) => void) { + this._onHeld.push(cb) } public swapOffItem() { @@ -164,7 +169,7 @@ export default class Inventory extends Module<{}> { // TODO: action list if (!packet.accepted) { this.logger.warn('Conflict') - this.client.write('transaction', packet) + this.client.write('transaction', packet) // NOTE: may conflict with proxy } }) this.client.on('set_slot', packet => { diff --git a/src/modules/Meal.ts b/src/modules/Meal.ts index 34707af..238a73d 100644 --- a/src/modules/Meal.ts +++ b/src/modules/Meal.ts @@ -38,6 +38,12 @@ export default class Meal extends Module { const ready = () => { this.life.events.on('update', this.eat.bind(this)) + this.client.on('entity_status', packet => { + if (this._eating && packet.entityId === this.state.state.entityId && packet.entityStatus === 9) { + clearInterval(this._eating) + this._eating = undefined + } + }) this.eat() } @@ -74,12 +80,6 @@ export default class Meal extends Module { const food = this.inv.findFirst(this.conf.foodsIds!) if (food.slot >= 0) { - this.client.on('entity_status', packet => { - if (this._eating && packet.entityId === this.state.state.entityId && packet.entityStatus === 9) { - clearTimeout(this._eating) - this._eating = undefined - } - }) this.logger.info('Eating %s', this.conf.foods[food.value]) this.inv.useItem(food.slot) this._eating = setTimeout(() => { diff --git a/src/modules/Proxy.ts b/src/modules/Proxy.ts new file mode 100644 index 0000000..d3fb261 --- /dev/null +++ b/src/modules/Proxy.ts @@ -0,0 +1,197 @@ +import { Client, createServer, Server, ServerOptions } from 'minecraft-protocol' +import Module from '../utils/Module' +import { IPositionPacket } from '../utils/types' +import Inventory from './Inventory' +import State from './State' + +interface IConfig { + cache: boolean + commandPrefix: string + autoClose: boolean + server: ServerOptions +} + +const CLIENT_CUSTOM_EVENTS = ['chat', 'tab_complete', 'keep_alive', 'update_time'] +const SERVER_CUSTOM_EVENTS = ['tab_complete', 'declare_commands'] + +export default class Proxy extends Module { + + private buffer: any[] = [] + private proxyClient?: Client + private server?: Server + + public umount() { + if (this.proxyClient) { + this.logger.info('Kicking player') + this.proxyClient.end('Connection reset by target server.') + this.proxyClient = undefined + } + if (this.server) { + this.logger.info('Closing server') + this.server.close() + this.server = undefined + } + } + + protected mount(): void { + this.client.once('login', lstate => { + this.logger.debug({ msg: 'Starting server', port: this.conf.server.port }) + this.server = createServer({ + errorHandler: (_, err) => this.logger.error(err.message), + motd: `Proxy for ${this.client.socket.remoteAddress}:${this.client.socket.remotePort}`, + version: (this.client as any).version, + ...this.conf.server, + }) + + this.server.on('login', (proxyClient: Client) => { // handle login + this.logger.info('Player connected') + if (this.conf.cache) { + this.buffer.forEach(data => proxyClient.writeRaw(data)) + this.conf.cache = this.conf.cache && !this.conf.autoClose + if (!this.conf.cache) { + this.buffer = [] + } + } else { + proxyClient.write('login', { ...lstate, maxPlayers: this.server!.maxPlayers }) + proxyClient.write('position', { + x: 0, + y: 1.62, + z: 0, + pitch: 0, + yaw: 0, + flags: 0x00, + }) + } + + proxyClient.on('raw', (data, meta) => { + if (meta.state === 'play') { + if (!CLIENT_CUSTOM_EVENTS.includes(meta.name)) { + this.client.writeRaw(data) + } + } + }) + + proxyClient.on('chat', ({ message }: { message: string }) => { + if (message.startsWith(this.conf.commandPrefix)) { + const cmd = message.slice(this.conf.commandPrefix.length) + this.logger.info(cmd) + } else { + this.client.write('chat', { message }) + } + }) + proxyClient.on('tab_complete', data => { + /* Sent when the client needs to tab-complete a minecraft:ask_server suggestion type. */ + this.logger.error({ msg: 'TODO:', data }) + /* + { + transactionId: number, + start: number, + length: number, + matches: [ + { + match: string, + tooltip: option | string + } + ] + }*/ + }) + + // TODO: override declare_commands + + proxyClient.on('end', reason => { + this.logger.info({ msg: 'Player disconnected', reason }) + if (this.conf.autoClose) { + this.umount() + } // FIXME: must handle and reemit full state to reconnect properly without storing all packets + }) + + // TODO: reemit actions from proxyClient (position, ...) + + // TODO: reemit actions from other modules + + // Binding with Inventory + const inv = this.get(Inventory) + if (inv) { + // MAYBE: add this.client.out.on('held_item_slot', { slotId }) + inv.registerHeld(slot => proxyClient.write('held_item_slot', { slot })) + proxyClient.on('held_item_slot', ({ slotId }) => + this.client.emit('held_item_slot', { slot: slotId })) + proxyClient.on('window_click', packet => this.logger.info({ msg: 'click', packet })) + // TODO: set_slot + } + + // Binding with State + const state = this.get(State) + if (state) { + // tslint:disable: no-bitwise + // TODO: onTranslate ? + proxyClient.on('position', ({ x, y, z }) => { + const position: IPositionPacket = { + flags: 0x08 | 0x10, // Relative look + pitch: 0, + yaw: 0, + x, y, z, + teleportId: 0, // Disable confirm_teleport + } + this.client.emit('position', position) + }) + proxyClient.on('look', ({ pitch, yaw }) => { + const position: IPositionPacket = { + flags: 0x01 | 0x02 | 0x4, // Relative position + x: 0, + y: 0, + z: 0, + pitch, yaw, + teleportId: 0, // Disable confirm_teleport + } + this.client.emit('position', position) + }) + proxyClient.on('position_look', ({ x, y, z, pitch, yaw }) => { + const position: IPositionPacket = { + flags: 0, // Absolute + x, y, z, pitch, yaw, + teleportId: 0, // Disable confirm_teleport + } + this.client.emit('position', position) + }) + } + + // MAYBE: add cheats + /* + "0x11": "tab_complete", + "0x12": "declare_commands", + "0x17": "set_slot", + "0x18": "set_cooldown", + */ + + this.proxyClient = proxyClient + }) + }) + + this.client.on('raw', (buffer, meta) => { + if (meta.state === 'play') { + if (this.conf.cache) { + this.buffer.push(buffer) + } + if (this.proxyClient && !SERVER_CUSTOM_EVENTS.includes(meta.name)) { + this.proxyClient.writeRaw(buffer) + } + } + }) + } + + protected getConf(): IConfig { + return { + cache: false, + commandPrefix: '%', + autoClose: true, + server: { + host: '0.0.0.0', + port: 25565, + maxPlayers: 1, + 'online-mode': false, + }, + } + } + +} diff --git a/src/modules/State.ts b/src/modules/State.ts index 8ed6269..eff81c0 100644 --- a/src/modules/State.ts +++ b/src/modules/State.ts @@ -1,5 +1,5 @@ import Module from '../utils/Module' -import { IDelta, IPosition, IState, gameMode } from '../utils/types' +import { gameMode, IDelta, IPosition, IPositionPacket, IState } from '../utils/types' import Spawn from './Spawn' // MAYBE: split @@ -15,11 +15,6 @@ interface IAbilities { walkingSpeed: number } -interface IPositionPacket extends IPosition { - flags: number - teleportId: number -} - interface IPlayerInfo { UUID: string name: string @@ -57,7 +52,7 @@ export default class State extends Module<{ positionConfirm: boolean }> { } public translate(delta: IDelta, onGround: boolean = true) { - this.client.write('position', { + this.client.write('position', { // TODO: optionally re-emit to proxy onGround, x: this.position.x += delta.dX, y: this.position.y += delta.dY, @@ -102,13 +97,15 @@ export default class State extends Module<{ positionConfirm: boolean }> { } } - if (this.conf.positionConfirm) { - this.client.write('teleport_confirm', { teleportId: data.teleportId }) + if (this.conf.positionConfirm && data.teleportId) { + this.client.write('teleport_confirm', { teleportId: data.teleportId }) // NOTE: may conflict proxy + this.logger.debug({ msg: 'Teleported', type: 'position', value: this.position }) } - this.logger.debug({ msg: 'Teleported', type: 'position', value: this.position }) spawn.validateCheck('position') }) + // TODO: player movement + this.client.on('player_info', (packet: { action: number, data: IPlayerInfo[] }) => { for (const player of packet.data) { switch (packet.action) { diff --git a/src/modules/index.ts b/src/modules/index.ts index 57441a6..0b79122 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -6,6 +6,7 @@ import Entities from './Entities' import Inventory from './Inventory' import Life from './Life' import Meal from './Meal' +import Proxy from './Proxy' import Spawn from './Spawn' import State from './State' import Time from './Time' @@ -22,4 +23,5 @@ export default [ Time, Inventory, Spawn, + Proxy, ] as IModuleType[] diff --git a/src/utils/Module.ts b/src/utils/Module.ts index 30aee9d..346115d 100644 --- a/src/utils/Module.ts +++ b/src/utils/Module.ts @@ -1,7 +1,7 @@ import { Client } from 'minecraft-protocol' import { Logger } from 'pino' -type ModuleLoader = >(module: IModuleType) => T +type ModuleLoader = >(module: IModuleType, load: boolean) => T | undefined /** Typeof Module */ export interface IModuleType { @@ -37,8 +37,13 @@ export default abstract class Module { /** Setup listener on client */ protected abstract mount(): void - /** Get reference to another module */ + /** Load reference to another module */ protected load>(module: IModuleType): M { - return this.getModule(module) + return this.getModule(module, true)! + } + + /** TryGet reference to another module */ + protected get>(module: IModuleType): M | undefined { + return this.getModule(module, false) } } diff --git a/src/utils/types.ts b/src/utils/types.ts index 33de114..90f1ba4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -24,6 +24,11 @@ export interface IDelta { export interface IPosition extends ICoordinates, IRotation { } export interface IMovable extends IPosition, IVelocity { } +export interface IPositionPacket extends IPosition { + flags: number + teleportId: number +} + export interface ISlot { present: boolean itemId: number