Add usable client proxy
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
095c5c8ce2
commit
07b5aa96c0
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue