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, }, } } }