198 lines
7.4 KiB
TypeScript
198 lines
7.4 KiB
TypeScript
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<IConfig> {
|
|
|
|
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>(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>(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,
|
|
},
|
|
}
|
|
}
|
|
|
|
}
|