Add usable client proxy
continuous-integration/drone/push Build is passing Details

master
May B. 2020-04-28 21:06:52 +02:00
parent 095c5c8ce2
commit 07b5aa96c0
9 changed files with 248 additions and 35 deletions

View File

@ -47,26 +47,27 @@ export default class Cubbot {
this._registeredModules.set(n, module)
}
public loadModule<T extends Module<{}>>(module: IModuleType): T {
return this.loadModuleByName(module.name) as T
public getModule<T extends Module<{}>>(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<string, Module<{}>>()
this.loadModule<Client>(Client).setEngine(this)
Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(this))
this.getModule<Client>(Client, true)!.setEngine(this)
Object.keys(this.config.modules).forEach(m => this.getModuleByName(m, true))
}
}

View File

@ -78,6 +78,7 @@ export default class Connection extends Module<IConfig> {
this.client.on('error', error => {
this.logger.error({ msg: 'Error', value: error })
})
// MAYBE: on 'end'
}
protected getConf() {

View File

@ -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 => {

View File

@ -38,6 +38,12 @@ export default class Meal extends Module<IConf> {
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<IConf> {
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(() => {

197
src/modules/Proxy.ts Normal file
View File

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

View File

@ -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) {

View File

@ -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[]

View File

@ -1,7 +1,7 @@
import { Client } from 'minecraft-protocol'
import { Logger } from 'pino'
type ModuleLoader = <T extends Module<{}>>(module: IModuleType) => T
type ModuleLoader = <T extends Module<{}>>(module: IModuleType, load: boolean) => T | undefined
/** Typeof Module */
export interface IModuleType {
@ -37,8 +37,13 @@ export default abstract class Module<T extends object> {
/** Setup listener on client */
protected abstract mount(): void
/** Get reference to another module */
/** Load reference to another module */
protected load<M extends Module<{}>>(module: IModuleType): M {
return this.getModule<M>(module)
return this.getModule<M>(module, true)!
}
/** TryGet reference to another module */
protected get<M extends Module<{}>>(module: IModuleType): M | undefined {
return this.getModule<M>(module, false)
}
}

View File

@ -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