Add usable client proxy
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
095c5c8ce2
commit
07b5aa96c0
|
@ -47,26 +47,27 @@ export default class Cubbot {
|
||||||
this._registeredModules.set(n, module)
|
this._registeredModules.set(n, module)
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadModule<T extends Module<{}>>(module: IModuleType): T {
|
public getModule<T extends Module<{}>>(module: IModuleType, load: boolean): T | undefined {
|
||||||
return this.loadModuleByName(module.name) as T
|
return this.getModuleByName(module.name, load) as T | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadModuleByName(name: string) {
|
public getModuleByName(name: string, load: boolean) {
|
||||||
if (this.modules!.has(name)) {
|
if (this.modules!.has(name)) {
|
||||||
return this.modules!.get(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)
|
const conf: { log?: string } = this.config.modules[name] || {}
|
||||||
assert.ok(this._registeredModules.has(name), `Unknown module ${name}`)
|
const mLogger = this.logger.child({ name, level: conf.log })
|
||||||
const mType = this._registeredModules.get(name)!
|
mLogger.trace('Logger created')
|
||||||
|
|
||||||
const conf: { log?: string } = this.config.modules[name] || {}
|
const module = new mType(this.client!, mLogger, conf, this.getModule.bind(this))
|
||||||
const mLogger = this.logger.child({ name, level: conf.log })
|
this._modules!.set(name, module)
|
||||||
mLogger.trace('Logger created')
|
return module
|
||||||
|
}
|
||||||
const module = new mType(this.client!, mLogger, conf, this.loadModule.bind(this))
|
|
||||||
this._modules!.set(name, module)
|
|
||||||
return module
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Start bot */
|
/** Start bot */
|
||||||
|
@ -77,8 +78,8 @@ export default class Cubbot {
|
||||||
|
|
||||||
this.logger.debug('Loading modules')
|
this.logger.debug('Loading modules')
|
||||||
this._modules = new Map<string, Module<{}>>()
|
this._modules = new Map<string, Module<{}>>()
|
||||||
this.loadModule<Client>(Client).setEngine(this)
|
this.getModule<Client>(Client, true)!.setEngine(this)
|
||||||
Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(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.client.on('error', error => {
|
||||||
this.logger.error({ msg: 'Error', value: error })
|
this.logger.error({ msg: 'Error', value: error })
|
||||||
})
|
})
|
||||||
|
// MAYBE: on 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getConf() {
|
protected getConf() {
|
||||||
|
|
|
@ -85,6 +85,7 @@ export default class Inventory extends Module<{}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _held = 0
|
private _held = 0
|
||||||
|
private _onHeld = new Array<(slot: number) => void>()
|
||||||
public get mainHand() {
|
public get mainHand() {
|
||||||
return this._held
|
return this._held
|
||||||
}
|
}
|
||||||
|
@ -108,6 +109,10 @@ export default class Inventory extends Module<{}> {
|
||||||
public setHeld(slotId: number) {
|
public setHeld(slotId: number) {
|
||||||
this.client.write('held_item_slot', { slotId })
|
this.client.write('held_item_slot', { slotId })
|
||||||
this._held = slotId
|
this._held = slotId
|
||||||
|
this._onHeld.forEach(cb => cb(slotId))
|
||||||
|
}
|
||||||
|
public registerHeld(cb: (slot: number) => void) {
|
||||||
|
this._onHeld.push(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
public swapOffItem() {
|
public swapOffItem() {
|
||||||
|
@ -164,7 +169,7 @@ export default class Inventory extends Module<{}> {
|
||||||
// TODO: action list
|
// TODO: action list
|
||||||
if (!packet.accepted) {
|
if (!packet.accepted) {
|
||||||
this.logger.warn('Conflict')
|
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 => {
|
this.client.on('set_slot', packet => {
|
||||||
|
|
|
@ -38,6 +38,12 @@ export default class Meal extends Module<IConf> {
|
||||||
|
|
||||||
const ready = () => {
|
const ready = () => {
|
||||||
this.life.events.on('update', this.eat.bind(this))
|
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()
|
this.eat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,12 +80,6 @@ export default class Meal extends Module<IConf> {
|
||||||
const food = this.inv.findFirst(this.conf.foodsIds!)
|
const food = this.inv.findFirst(this.conf.foodsIds!)
|
||||||
|
|
||||||
if (food.slot >= 0) {
|
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.logger.info('Eating %s', this.conf.foods[food.value])
|
||||||
this.inv.useItem(food.slot)
|
this.inv.useItem(food.slot)
|
||||||
this._eating = setTimeout(() => {
|
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 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'
|
import Spawn from './Spawn'
|
||||||
|
|
||||||
// MAYBE: split
|
// MAYBE: split
|
||||||
|
@ -15,11 +15,6 @@ interface IAbilities {
|
||||||
walkingSpeed: number
|
walkingSpeed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPositionPacket extends IPosition {
|
|
||||||
flags: number
|
|
||||||
teleportId: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPlayerInfo {
|
interface IPlayerInfo {
|
||||||
UUID: string
|
UUID: string
|
||||||
name: string
|
name: string
|
||||||
|
@ -57,7 +52,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public translate(delta: IDelta, onGround: boolean = true) {
|
public translate(delta: IDelta, onGround: boolean = true) {
|
||||||
this.client.write('position', {
|
this.client.write('position', { // TODO: optionally re-emit to proxy
|
||||||
onGround,
|
onGround,
|
||||||
x: this.position.x += delta.dX,
|
x: this.position.x += delta.dX,
|
||||||
y: this.position.y += delta.dY,
|
y: this.position.y += delta.dY,
|
||||||
|
@ -102,13 +97,15 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.conf.positionConfirm) {
|
if (this.conf.positionConfirm && data.teleportId) {
|
||||||
this.client.write('teleport_confirm', { teleportId: 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')
|
spawn.validateCheck('position')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: player movement
|
||||||
|
|
||||||
this.client.on('player_info', (packet: { action: number, data: IPlayerInfo[] }) => {
|
this.client.on('player_info', (packet: { action: number, data: IPlayerInfo[] }) => {
|
||||||
for (const player of packet.data) {
|
for (const player of packet.data) {
|
||||||
switch (packet.action) {
|
switch (packet.action) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Entities from './Entities'
|
||||||
import Inventory from './Inventory'
|
import Inventory from './Inventory'
|
||||||
import Life from './Life'
|
import Life from './Life'
|
||||||
import Meal from './Meal'
|
import Meal from './Meal'
|
||||||
|
import Proxy from './Proxy'
|
||||||
import Spawn from './Spawn'
|
import Spawn from './Spawn'
|
||||||
import State from './State'
|
import State from './State'
|
||||||
import Time from './Time'
|
import Time from './Time'
|
||||||
|
@ -22,4 +23,5 @@ export default [
|
||||||
Time,
|
Time,
|
||||||
Inventory,
|
Inventory,
|
||||||
Spawn,
|
Spawn,
|
||||||
|
Proxy,
|
||||||
] as IModuleType[]
|
] as IModuleType[]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Client } from 'minecraft-protocol'
|
import { Client } from 'minecraft-protocol'
|
||||||
import { Logger } from 'pino'
|
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 */
|
/** Typeof Module */
|
||||||
export interface IModuleType {
|
export interface IModuleType {
|
||||||
|
@ -37,8 +37,13 @@ export default abstract class Module<T extends object> {
|
||||||
/** Setup listener on client */
|
/** Setup listener on client */
|
||||||
protected abstract mount(): void
|
protected abstract mount(): void
|
||||||
|
|
||||||
/** Get reference to another module */
|
/** Load reference to another module */
|
||||||
protected load<M extends Module<{}>>(module: IModuleType): M {
|
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 IPosition extends ICoordinates, IRotation { }
|
||||||
export interface IMovable extends IPosition, IVelocity { }
|
export interface IMovable extends IPosition, IVelocity { }
|
||||||
|
|
||||||
|
export interface IPositionPacket extends IPosition {
|
||||||
|
flags: number
|
||||||
|
teleportId: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISlot {
|
export interface ISlot {
|
||||||
present: boolean
|
present: boolean
|
||||||
itemId: number
|
itemId: number
|
||||||
|
|
Loading…
Reference in New Issue