Good night
continuous-integration/drone/push Build is passing Details

This commit is contained in:
May B. 2020-04-20 23:07:23 +02:00
parent ea4101f8b6
commit 2dc150c38b
21 changed files with 2927 additions and 3650 deletions

1
.gitignore vendored
View File

@ -7,4 +7,5 @@ node_modules/
build/ build/
tmp/ tmp/
temp/ temp/
data/
server/ server/

View File

@ -1,8 +1,8 @@
FROM node FROM node
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN npm install RUN yarn install
RUN npm build RUN yarn run build
FROM node FROM node
WORKDIR /app WORKDIR /app

View File

@ -11,6 +11,7 @@ Scalliony API and UI
- [Usage](#usage) - [Usage](#usage)
- [Config](#config) - [Config](#config)
- [Run](#run) - [Run](#run)
- [Note](#note)
- [Tests](#tests) - [Tests](#tests)
- [License](#license) - [License](#license)
@ -19,7 +20,7 @@ Cubbot uses modules with could be enabled at compile time in `src/modules/index.
## Building ## Building
``` ```
npm run build yarn run build
``` ```
## Usage ## Usage
@ -30,12 +31,15 @@ npm run build
### Run ### Run
``` ```
npm run test yarn run test
``` ```
### Note
`minecraft-data` package isn't updated for 1.15+. As a workaround, data will be downloader for `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/{version}.json` and cached in `data` folder
## Tests ## Tests
``` ```
npm run test yarn run test
``` ```
## License ## License

0
data/.gitkeep Normal file
View File

View File

@ -6,10 +6,10 @@ steps:
image: node image: node
commands: commands:
- yarn install - yarn install
- npm run test:coverage - yarn run test:coverage
- name: build - name: build
image: node image: node
commands: commands:
- yarn install - yarn install
- npm run build - yarn run build

View File

@ -8,7 +8,11 @@
"level": "info" "level": "info"
}, },
"modules": { "modules": {
"Client": {}, "Connection": {
"reconnect": [
"idling"
]
},
"Combat": {}, "Combat": {},
"Chat": {}, "Chat": {},
"Time": { "Time": {

3485
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import * as mc from 'minecraft-protocol'
import { default as pino } from 'pino' import { default as pino } from 'pino'
import moduleList from './modules' import moduleList from './modules'
import Module, { IModuleType } from './utils/Module' import Module, { IModuleType } from './utils/Module'
import Client from './modules/Connection'
interface IConfig { interface IConfig {
/** Minecraft protocol options */ /** Minecraft protocol options */
@ -41,7 +42,7 @@ export default class Cubbot {
public registerModule(module: IModuleType, name?: string) { public registerModule(module: IModuleType, name?: string) {
const n: string = name || module.name const n: string = name || module.name
if (this._registeredModules.has(n)) { if (this._registeredModules.has(n)) {
this.logger.warn('Overriding module %s', n) this.logger.warn({ msg: 'Overriding module', value: n })
} }
this._registeredModules.set(n, module) this._registeredModules.set(n, module)
} }
@ -76,12 +77,13 @@ 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)
Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(this)) Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(this))
} }
} }
/** Stop bot */ /** Stop bot */
public umount() { public umount(exit: boolean = false) {
if (this.client) { if (this.client) {
this.logger.debug('Unloading modules') this.logger.debug('Unloading modules')
this.modules!.forEach(m => m.umount()) this.modules!.forEach(m => m.umount())
@ -91,8 +93,11 @@ export default class Cubbot {
//TODO: disconnect //TODO: disconnect
this._client = undefined this._client = undefined
this.logger.warn('Stopped') if (exit) {
this.logger.flush() this.logger.warn('Stopped')
this.logger.flush()
setTimeout(process.exit, 100)
}
} }
} }

View File

@ -7,7 +7,4 @@ const config = JSON.parse(stripJsonComments(readFileSync('./env.json').toString(
const app = new Cubbot(config) const app = new Cubbot(config)
app.mount() app.mount()
process.on('SIGINT', () => { process.on('SIGINT', () => app.umount(true))
app.umount()
setTimeout(process.exit, 100)
})

View File

@ -1,24 +1,23 @@
import md from 'minecraft-data'
import { vsprintf } from 'sprintf-js' import { vsprintf } from 'sprintf-js'
import Module from '../utils/Module' import Module from '../utils/Module'
import { IDict } from '../utils/types'
import Connection from './Connection'
/** Message packet payload */ /** Message packet payload */
interface IMessage { text?: string, extra?: Array<{text: string}>, translate?: string, with?: IMessage[] } export interface IMessage { text?: string, extra?: Array<{text: string}>, translate?: string, with?: IMessage[] }
/** Handle chat display and replies */ /** Handle chat display and replies */
export default class Chat extends Module<{ reply?: string }> { export default class Chat extends Module<{ reply?: string, bye?: string }> {
/** Translation dictionary (en only) */
private dict!: { [key: string]: string }
/** Convert payload to string */ /** Convert payload to string */
public parse(msg: IMessage) { public static parse(msg: IMessage, language: IDict) {
let text = '' let text = ''
if (msg.text) { if (msg.text) {
text += msg.text text += msg.text
} }
if (msg.translate && msg.translate in this.dict) { if (msg.translate && msg.translate in language) {
text += vsprintf(this.dict[msg.translate], (msg.with || []).map(this.parse.bind(this))) text += vsprintf(language[msg.translate], (msg.with || [])
.map(w => typeof w === 'string' ? w : Chat.parse(w, language)))
} }
if (msg.extra) { if (msg.extra) {
text += msg.extra.map(({text: t}) => t).join() text += msg.extra.map(({text: t}) => t).join()
@ -26,11 +25,23 @@ export default class Chat extends Module<{ reply?: string }> {
return text return text
} }
private connection!: Connection
public parse(msg: IMessage) {
return Chat.parse(msg, this.connection.oldData.language)
}
public write(message: string) {
this.client.write('chat', { message })
}
public umount() {
if (this.connection.connected && this.conf.bye) {
this.write(this.conf.bye)
}
}
protected mount() { protected mount() {
this.client.on('login', () => { this.connection = this.load<Connection>(Connection)
// FIXME: use this.client.version when 'minecraft-data' include 1.15 language
this.dict = md('1.14').language
})
this.client.on('chat', packet => { this.client.on('chat', packet => {
const message = JSON.parse(packet.message) const message = JSON.parse(packet.message)
@ -38,7 +49,6 @@ export default class Chat extends Module<{ reply?: string }> {
}) })
} }
//TODO: chat write
//TODO: reply //TODO: reply
protected getConf() { protected getConf() {

View File

@ -1,32 +0,0 @@
import Module from '../utils/Module'
/** Conection informations */
export default class Client extends Module<{ reconnect: boolean }> {
public umount() {
//TODO: proper disconnect
}
protected mount() {
this.client.on('connect', () => {
this.logger.trace('Connected')
})
this.client.on('disconnect', packet => {
this.logger.warn('Disconnected %s' + packet.reason)
})
this.client.on('kick_disconnect', packet => {
this.logger.warn('Kicked %s', packet.reason)
})
this.client.on('login', () => {
this.logger.trace('Logged')
})
this.client.on('error', error => {
this.logger.error('error %o', error)
})
}
protected getConf() {
return { reconnect: true }
}
}

View File

@ -2,25 +2,32 @@ import { ATTACK_DIST2, SERVER_TICK_RATE } from '../utils/constants'
import { dist2 } from '../utils/func' import { dist2 } from '../utils/func'
import Module from '../utils/Module' import Module from '../utils/Module'
import { IDelta } from '../utils/types' import { IDelta } from '../utils/types'
import Entities, { IAlive } from './Entities' import Connection from './Connection'
import Entities, { IAlive, ILiving, IPlayer } from './Entities'
import Life from './Life' import Life from './Life'
import State from './State' import State from './State'
interface IConf { interface IConf {
/** Respawn after death */ /** Respawn after death */
respawn: boolean, respawn: boolean
/** Attack dangerous entities */ /** Attack dangerous entities */
fight: boolean, fight: boolean
/** Must change weapon */ /** Must change weapon */
weapon: 'none' | 'needed' | 'always', weapon: 'none' | 'needed' | 'always'
/** Must react to arrows */ /** Must react to arrows */
arrows: 'ignore' | 'dodge' | 'shield' arrows: 'ignore' | 'dodge' | 'shield'
/** Time between attacks */ /** Time between attacks */
delay: number | 'item' | 'safe', delay: number | 'item' | 'safe'
/** Attacks multiple entities at ones */ /** Attacks multiple entities at ones */
multiaura: boolean, multiaura: boolean
/** Entity to target first */ /** Entity to target first */
priority: 'nearest' priority: 'nearest'
/** Players names */
friends: string[]
/** Targeted entities names */
mobs: string[]
/** Targeted entities ids (override mobs) */
mobsIds?: number[]
} }
interface IEnemy extends IAlive { interface IEnemy extends IAlive {
@ -33,7 +40,9 @@ const SAFE_DELAY = SERVER_TICK_RATE * 2
export default class Combat extends Module<IConf> { export default class Combat extends Module<IConf> {
private state!: State private state!: State
private life!: Life
private entities!: Entities private entities!: Entities
private connection!: Connection
public umount() { public umount() {
this.conf.fight = false this.conf.fight = false
@ -42,7 +51,9 @@ export default class Combat extends Module<IConf> {
protected mount() { protected mount() {
this.state = this.load<State>(State) this.state = this.load<State>(State)
this.life = this.load<Life>(Life)
this.entities = this.load<Entities>(Entities) this.entities = this.load<Entities>(Entities)
this.connection = this.load<Connection>(Connection)
this.fight() this.fight()
this.arrows() this.arrows()
@ -50,19 +61,21 @@ export default class Combat extends Module<IConf> {
this.client.on('combat_event', packet => { this.client.on('combat_event', packet => {
switch (packet.event) { switch (packet.event) {
case 0: case 0:
this.logger.warn('fighting') this.logger.warn({ msg: 'Fighting' })
break break
case 2: case 2:
if (packet.playerId === this.state.state.entityId) { if (packet.playerId === this.state.state.entityId) {
this.logger.error('%o killed me at %o', this.logger.error({
this.entities.getAlive(packet.entityId), this.state.position) msg: 'Killed', type: 'entity',
value: this.entities.getAlive(packet.entityId),
})
// MAYBE: chat packet.message // MAYBE: chat packet.message
if (this.conf.respawn) { if (this.conf.respawn) {
this.load<Life>(Life).respawn() this.life.respawn()
} }
} else { } else {
this.logger.info('killed %o', packet) this.logger.info({ msg: 'Fight end', packet })
} }
} }
}) })
@ -77,6 +90,14 @@ export default class Combat extends Module<IConf> {
delay: 'safe', delay: 'safe',
multiaura: false, multiaura: false,
priority: 'nearest', priority: 'nearest',
friends: [],
mobs: [ // TODO: find if aggressive
'blaze', 'cave_spider', 'creeper', 'drowned', 'elder_guardian', 'ender_dragon', 'enderman', 'endermite',
'evoker', 'evoker_fangs', 'ghast', 'giant', 'guardian', 'husk', 'illusioner', /*'iron_golem',*/
'magma_cube', 'phantom', 'pillager', 'player', 'ravager', 'shulker', 'silverfish', 'skeleton', 'slime',
/*'snow_golem',*/ 'spider', 'stray', 'vex', 'vindicator', 'witch', 'wither', 'wither_skeleton',
/*'wolf',*/ 'zombie', /*'zombie_pigman',*/ 'zombie_villager',
],
} }
} }
@ -84,11 +105,31 @@ export default class Combat extends Module<IConf> {
if (this.conf.fight) { if (this.conf.fight) {
//TODO: pick weapon if change wait //TODO: pick weapon if change wait
//TODO: pick shield //TODO: pick shield
if (!this.conf.mobsIds) {
if (this.connection.data?.ready) {
this.conf.mobsIds = this.conf.mobs
.map(name => this.connection.data!.entities![name].id)
} else {
this.logger.warn('Waiting data')
setTimeout(this.fight.bind(this), SAFE_DELAY)
}
}
// Find dangerous entities // Find dangerous entities
const dangers: IEnemy[] = ([...this.entities.players, ...this.entities.livings] as IAlive[]) const dangers: IEnemy[] = ([...this.entities.players, ...this.entities.livings] as IAlive[])
.filter(({ entityId }) => entityId !== this.state.state.entityId) .filter(e => {
//TODO: filter friends, creative, passives if ((e as ILiving).type !== undefined) { //TODO: add type to players
return this.conf.mobsIds!.includes((e as ILiving).type)
}
if ((e as IPlayer).playerUUID !== undefined && !(e as IPlayer).me) {
const data = this.state.players.get((e as IPlayer).playerUUID)
if (!data) {
return false
}
return (data.gamemode % 2 === 0) && !this.conf.friends.includes(data.name)
}
return false
})
.map(e => ({ reach2: dist2(this.state.position, e), ...e })) // TODO: include box .map(e => ({ reach2: dist2(this.state.position, e), ...e })) // TODO: include box
.filter(({ reach2 }) => reach2 <= ATTACK_DIST2) // MAYBE: as option .filter(({ reach2 }) => reach2 <= ATTACK_DIST2) // MAYBE: as option
.sort((a, b) => a.reach2 - b.reach2) .sort((a, b) => a.reach2 - b.reach2)
@ -96,10 +137,10 @@ export default class Combat extends Module<IConf> {
// MAYBE: include entity speed (for tiny zombie or phantom) // MAYBE: include entity speed (for tiny zombie or phantom)
if (dangers.length > 0) { if (dangers.length > 0) {
this.logger.info('fighting %s target(s)', dangers.length); this.logger.info({ msg: 'Fighting', type: 'count', value: dangers.length });
(this.conf.multiaura ? dangers : dangers.slice(0, 1)).forEach(({ entityId }) => { (this.conf.multiaura ? dangers : dangers.slice(0, 1)).forEach(({ entityId }) => {
this.logger.debug('attack %s', entityId) this.logger.debug({ msg: 'Attack', type: 'entityId', value: entityId })
this.entities.attack(entityId) this.entities.attack(entityId)
}) })
} }
@ -107,7 +148,7 @@ export default class Combat extends Module<IConf> {
const attackSpeed = 1.6 //TODO: get from inventory const attackSpeed = 1.6 //TODO: get from inventory
const delay = typeof this.conf.delay === 'number' ? this.conf.delay : ( const delay = typeof this.conf.delay === 'number' ? this.conf.delay : (
this.conf.delay === 'safe' && dangers.length == 0 ? SAFE_DELAY : // TODO: exponential back-off this.conf.delay === 'safe' && dangers.length === 0 ? SAFE_DELAY : // TODO: exponential back-off
1 / attackSpeed * 20 * SERVER_TICK_RATE) 1 / attackSpeed * 20 * SERVER_TICK_RATE)
setTimeout(this.fight.bind(this), delay) setTimeout(this.fight.bind(this), delay)
@ -120,7 +161,7 @@ export default class Combat extends Module<IConf> {
.filter(e => e.type === 2 && // arrow .filter(e => e.type === 2 && // arrow
dist2(this.state.position, e) <= 99) dist2(this.state.position, e) <= 99)
if (arrows.length > 0) { if (arrows.length > 0) {
this.logger.warn('%s arrows', arrows.length) this.logger.warn({ msg: 'Arrows', type: 'count', value: arrows.length })
this.state.translate(arrows.reduce((p, arrow) => this.state.translate(arrows.reduce((p, arrow) =>
Math.abs(arrow.velocityX) < Math.abs(arrow.velocityZ) ? Math.abs(arrow.velocityX) < Math.abs(arrow.velocityZ) ?

101
src/modules/Connection.ts Normal file
View File

@ -0,0 +1,101 @@
import md from 'minecraft-data'
import Cubbot from '../Cubbot'
import Data from '../utils/Data'
import Module from '../utils/Module'
import Chat, { IMessage } from './Chat'
type DisconnectReason = 'authservers_down' | 'banned' | 'banned.reason' | 'banned.expiration' | 'banned_ip.reason' |
'banned_ip.expiration' | 'duplicate_login' | 'flying' | 'generic' | 'idling' | 'illegal_characters' |
'invalid_entity_attacked' | 'invalid_player_movement' | 'invalid_vehicle_movement' | 'ip_banned' | 'kicked' |
'outdated_client' | 'outdated_server' | 'server_shutdown' | 'slow_login' | 'unverified_username' |
'not_whitelisted' | 'server_full' | 'name_taken' | 'unexpected_query_response' | 'genericReason' |
'disconnected' | 'lost' | 'kicked' | 'timeout' | 'closed' | 'loginFailed' | 'loginFailedInfo' |
'loginFailedInfo.serversUnavailable' | 'loginFailedInfo.invalidSession' | 'quitting' | 'endOfStream' |
'overflow' | 'spam'
const REASON_PREFIX = 'disconnect.'
interface IConfig {
/** Game data url */
dataSource: string
/** Game data cache path */
dataDir: string
reconnect: DisconnectReason[] | boolean
reconnectDelay: number
}
/** Connection informations */
export default class Connection extends Module<IConfig> {
private _connected = false
public get connected() {
return this._connected
}
private _oldData = md('1.14')
public get oldData() {
return this._oldData
}
private _data?: Data
public get data() {
return this._data
}
private _engine?: Cubbot
public setEngine(value: Cubbot) {
this._engine = value
}
protected mount() {
this.client.on('connect', () => {
this._connected = true
this.logger.trace('Connected')
})
this.client.on('disconnect', ({ reason }) => {
this._connected = false
const message: IMessage = JSON.parse(reason)
this.logger.warn({ msg: 'Disconnected', type: 'reason', value: Chat.parse(message, this.oldData.language) })
this.bye(message.translate)
})
this.client.on('kick_disconnect', ({ reason }) => {
this._connected = false
const message: IMessage = JSON.parse(reason)
this.logger.warn({ msg: 'Kicked', type: 'reason', value: Chat.parse(message, this.oldData.language) })
this.bye(message.translate)
})
this.client.on('login', () => {
this.logger.trace('Logged')
this._data = new Data(this.conf.dataSource, this.conf.dataDir, (this.client as any).version,
this.logger.child({ name: 'Data', level: 'info' }))
// FIXME: use 'minecraft-data' when it include 1.15+
})
this.client.on('error', error => {
this.logger.error({ msg: 'Error', value: error })
})
}
protected getConf() {
return {
dataDir: './data/',
dataSource: 'https://pokechu22.github.io/Burger/',
reconnect: false,
reconnectDelay: 5000,
}
}
private bye(reason?: string) {
if (this.conf.reconnect === true || (this.conf.reconnect !== false && reason !== undefined &&
this.conf.reconnect.some(r => reason.endsWith(REASON_PREFIX + r)))
) {
this._engine?.umount(false)
this.logger.info({ msg: 'Reconnecting', type: 'ms', value: this.conf.reconnectDelay })
// MAYBE: exponental backoff
setTimeout(() => this._engine?.mount(), this.conf.reconnectDelay)
} else {
this.logger.info('Bye')
this._engine?.umount(true)
}
}
}

View File

@ -68,7 +68,7 @@ export interface IAlive extends IMovableE, IEquipable, IAttributable, IMetadatab
headPitch: number headPitch: number
} }
interface ILiving extends IAlive { export interface ILiving extends IAlive {
entityUUID: string entityUUID: string
type: number type: number
} }
@ -76,7 +76,7 @@ interface ILiving extends IAlive {
interface IPlayerSpawn extends IPositioned { interface IPlayerSpawn extends IPositioned {
playerUUID: string playerUUID: string
} }
interface IPlayer extends IPlayerSpawn, IAlive { export interface IPlayer extends IPlayerSpawn, IAlive {
me?: boolean me?: boolean
} }
@ -172,117 +172,86 @@ export default class Entities extends Module<{}> {
// MAYBE: this.client.on('animation', packet => { }) // MAYBE: this.client.on('animation', packet => { })
// MAYBE: this.client.on('entity_status', packet => { }) // MAYBE: this.client.on('entity_status', packet => { })
this.client.on('rel_entity_move', (packet: IMove) => { this.client.on('rel_entity_move', (packet: IMove) =>
const entity = this.getEntity(packet.entityId) this.findOrLogger<IMovableE>('rel_move', packet, this.getEntity.bind(this), entity =>
if (entity) { applyDelta(entity, packet)))
applyDelta(entity, packet) this.client.on('entity_move_look', (packet: IMoveLook) =>
} else { this.findOrLogger<IMovableE>('move_look', packet, this.getEntity.bind(this), entity => {
this.logger.warn('rel_move of unknown entity %s', packet.entityId)
}
})
this.client.on('entity_move_look', (packet: IMoveLook) => {
const entity = this.getEntity(packet.entityId)
if (entity) {
applyDelta(entity, packet) applyDelta(entity, packet)
entity.pitch = packet.pitch entity.pitch = packet.pitch
entity.yaw = packet.yaw entity.yaw = packet.yaw
} else { }))
this.logger.warn('move_look of unknown entity %s', packet.entityId) this.client.on('entity_look', (packet: ILook) =>
} this.findOrLogger<IMovableE>('look', packet, this.getEntity.bind(this), entity => {
})
this.client.on('entity_look', (packet: ILook) => {
const entity = this.getEntity(packet.entityId)
if (entity) {
entity.pitch = packet.pitch entity.pitch = packet.pitch
entity.yaw = packet.yaw entity.yaw = packet.yaw
} else { }))
this.logger.warn('look of unknown entity %s', packet.entityId) this.client.on('entity_head_rotation', (packet: IHead) =>
} this.findOrLogger<IAlive>('head_rotation', packet, this.getAlive.bind(this), entity =>
}) entity.headPitch = packet.headPitch))
this.client.on('entity_head_rotation', (packet: IHead) => { this.client.on('entity_velocity', (packet: IVelocityE) =>
const entity = this.getAlive(packet.entityId) this.findOrLogger<IMovableE>('velocity', packet, this.getEntity.bind(this), entity => {
if (entity) {
entity.headPitch = packet.headPitch
} else {
this.logger.warn('head_rotation of unknown entity %s', packet.entityId)
}
})
this.client.on('entity_velocity', (packet: IVelocityE) => {
const entity = this.getEntity(packet.entityId)
if (entity) {
entity.velocityX = packet.velocityX entity.velocityX = packet.velocityX
entity.velocityY = packet.velocityY entity.velocityY = packet.velocityY
entity.velocityZ = packet.velocityZ entity.velocityZ = packet.velocityZ
} else { }))
this.logger.warn('velocity of unknown entity %s', packet.entityId) this.client.on('entity_teleport', (packet: IPositioned) =>
} this.findOrLogger<IMovableE>('teleport', packet, this.getEntity.bind(this), entity => {
})
this.client.on('entity_teleport', (packet: IPositioned) => {
const entity = this.getEntity(packet.entityId)
if (entity) {
entity.x = packet.x entity.x = packet.x
entity.y = packet.y entity.y = packet.y
entity.z = packet.z entity.z = packet.z
entity.pitch = packet.pitch entity.pitch = packet.pitch
entity.yaw = packet.yaw entity.yaw = packet.yaw
} else { }))
this.logger.warn('teleport of unknown entity %s', packet.entityId)
}
})
this.client.on('entity_destroy', (packet: { entityIds: number[] }) => { this.client.on('entity_destroy', (packet: { entityIds: number[] }) => {
packet.entityIds.forEach(id => { packet.entityIds.forEach(id => {
if (!this.deleteEntity(id)) { if (!this.deleteEntity(id)) {
this.logger.debug('can not delete entity %s', id) this.logger.debug({ msg: 'Can not delete entity', id })
} }
}) })
}) })
// TODO: identify metadatas // TODO: identify metadatas
this.client.on('entity_metadata', (packet: IMetadatas) => { this.client.on('entity_metadata', (packet: IMetadatas) =>
const entity = this.getMetadatable(packet.entityId) this.findOrLogger<IMetadatable>('metadata', packet, this.getMetadatable.bind(this), entity => {
if (entity) {
if (!entity.metadata) { if (!entity.metadata) {
entity.metadata = new Map() entity.metadata = new Map()
} }
for (const data of packet.metadata) { for (const data of packet.metadata) {
entity.metadata.set(data.key, data) entity.metadata.set(data.key, data)
} }
} else { }))
this.logger.warn('metadata of unknown entity %s (%s)', packet.entityId)
}
})
// MAYBE: this.client.on('entity', (packet: IId) => { }) // MAYBE: this.client.on('entity', (packet: IId) => { })
// TODO: entity_effect, remove_entity_effect // TODO: entity_effect, remove_entity_effect
this.client.on('entity_equipment', (packet: IEquipment) => { this.client.on('entity_equipment', (packet: IEquipment) =>
const entity = this.getAlive(packet.entityId) this.findOrLogger<IAlive>('equipment', packet, this.getAlive.bind(this), entity =>
if (entity) {
entity.equipment = entity.equipment ? entity.equipment = entity.equipment ?
entity.equipment.set(packet.slot, packet.item) : entity.equipment.set(packet.slot, packet.item) :
new Map([[packet.slot, packet.item]]) new Map([[packet.slot, packet.item]])))
} else {
this.logger.warn('equipment of unknown entity %s', packet.entityId)
}
})
// TODO: identify (with properties) // TODO: identify (with properties)
this.client.on('entity_update_attributes', (packet: IAttributes) => { this.client.on('entity_update_attributes', (packet: IAttributes) =>
const entity = this.getAlive(packet.entityId) this.findOrLogger<IAlive>('attributes', packet, this.getAlive.bind(this), entity =>
if (entity) { entity.attributes = packet.properties))
entity.attributes = packet.properties
} else {
this.logger.warn('attributes of unknown entity %s', packet.entityId)
}
})
} }
protected getConf() { protected getConf() {
return {} return {}
} }
private findOrLogger<T>(name: string, packet: { entityId: number }, query: (enitityId: number) => T | undefined,
then: (entity: T) => void) {
const entity = query(packet.entityId)
if (entity) {
then(entity)
} else {
this.logger.warn({ msg: 'Unknown entity', in: name, id: packet.entityId })
}
}
} }
// TODO: player info // TODO: player info

View File

@ -83,19 +83,19 @@ export default class Life extends Module<IConf> {
this._saturation = data.foodSaturation this._saturation = data.foodSaturation
if (this.health <= this.conf.lowHealth) { if (this.health <= this.conf.lowHealth) {
this.logger.warn({ type: 'health', status: this.alive ? 'low' : 'ko', health: this.health }) this.logger.warn({ msg: this.alive ? 'Low health' : 'Dead', type: 'health', value: this.health })
} else if (this.conf.showHealth) { } else if (this.conf.showHealth) {
this.logger.info({ type: 'health', status: 'ok', health: this.health }) this.logger.info({ msg: 'Healthy', type: 'health', value: this.health })
} }
if (this.food <= this.conf.lowFood) { if (this.food <= this.conf.lowFood) {
this.logger.warn({ type: 'food', status: 'low', food: this.food, saturation: this.saturation }) this.logger.warn({ msg: 'Hungry', type: 'food', value: this.food })
} else if (this.conf.showHealth) { } else if (this.conf.showHealth) {
this.logger.info({ type: 'food', status: 'ok', food: this.food, saturation: this.saturation }) this.logger.info({ msg: 'Replete', type: 'food', value: this.food })
} }
if (this.conf.eat && this.mustEat) { if (this.conf.eat && this.mustEat) {
this.logger.warn('TODO: must Eat') this.logger.warn({ msg: 'TODO: Must eat' })
// TODO: inventory.items.filter(food).orderBy(this.conf.preferSaturation) // TODO: inventory.items.filter(food).orderBy(this.conf.preferSaturation)
} }
}) })

View File

@ -1,5 +1,5 @@
import Module from '../utils/Module' import Module from '../utils/Module'
import { IDelta, IPosition, IState } from '../utils/types' import { IDelta, IPosition, IState, gameMode } from '../utils/types'
// MAYBE: split // MAYBE: split
@ -19,7 +19,13 @@ interface IPositionPacket extends IPosition {
teleportId: number teleportId: number
} }
// TODO: player_info interface IPlayerInfo {
UUID: string
name: string
properties: any[]
gamemode: gameMode
ping: number
}
/** Handle client state and position */ /** Handle client state and position */
export default class State extends Module<{ positionConfirm: boolean }> { export default class State extends Module<{ positionConfirm: boolean }> {
@ -34,7 +40,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
return this._difficulty return this._difficulty
} }
private _abilities!: IDifficulty private _abilities!: IAbilities
public get abilities() { public get abilities() {
return this._abilities return this._abilities
} }
@ -44,6 +50,11 @@ export default class State extends Module<{ positionConfirm: boolean }> {
return this._position return this._position
} }
private _players = new Map<string, IPlayerInfo>()
public get players() {
return this._players
}
public translate(delta: IDelta, onGround: boolean = true) { public translate(delta: IDelta, onGround: boolean = true) {
this.client.write('position', { this.client.write('position', {
onGround, onGround,
@ -63,7 +74,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
this.client.on('position', (data: IPositionPacket) => { this.client.on('position', (data: IPositionPacket) => {
if (this._position == null) { if (this._position == null) {
if (data.flags) { if (data.flags) {
this.logger.error('first position is relative') this.logger.error('First position is relative')
return return
} else { } else {
this._position = data this._position = data
@ -82,7 +93,29 @@ export default class State extends Module<{ positionConfirm: boolean }> {
if (this.conf.positionConfirm) { if (this.conf.positionConfirm) {
this.client.write('teleport_confirm', { teleportId: data.teleportId }) this.client.write('teleport_confirm', { teleportId: data.teleportId })
} }
this.logger.debug({ type: 'teleported', position: this.position }) this.logger.debug({ msg: 'Teleported', type: 'position', value: this.position })
})
this.client.on('player_info', (packet: { action: number, data: IPlayerInfo[] }) => {
for (const player of packet.data) {
switch (packet.action) {
case 0:
this._players.set(player.UUID, player)
break
case 1:
this._players.get(player.UUID)!.gamemode = player.gamemode
break
case 2:
this._players.get(player.UUID)!.ping = player.ping
break
case 3:
this._players.get(player.UUID)!.name = player.name
break
case 4:
this._players.delete(player.UUID)
break
}
}
}) })
} }

View File

@ -74,17 +74,17 @@ export default class Time extends Module<IConf> {
if (this._distAge) { if (this._distAge) {
this._tps = age - this._distAge this._tps = age - this._distAge
if (this.tps <= this.conf.minTps) { if (this.tps <= this.conf.minTps) {
this.logger.debug('Server Lag: %s / 20 tps', this.tps, SERVER_TPS) this.logger.debug({ msg: 'Server lag', type: 'tps', value: this.tps })
} }
this._spt = (now - this._at) / this.tps this._spt = (now - this._at) / this.tps
if (this.spt >= this.conf.maxSpt) { if (this.spt >= this.conf.maxSpt) {
this.logger.debug('Network Lag: %s / %s spt', this.spt, SERVER_TICK_RATE) this.logger.debug({ msg: 'Network lag', type: 'spt', value: this.spt })
} }
const offset = age - this._age const offset = age - this._age
if (Math.abs(offset) > this.conf.maxOffset) { if (Math.abs(offset) > this.conf.maxOffset) {
this.logger.debug('Client Off-sync %s ticks', offset) this.logger.debug({ msg: 'Client OffSync', type: 'ticks', value: offset })
} }
} }

View File

@ -1,6 +1,6 @@
import { IModuleType } from '../utils/Module' import { IModuleType } from '../utils/Module'
import Chat from './Chat' import Chat from './Chat'
import Client from './Client' import Client from './Connection'
import Combat from './Combat' import Combat from './Combat'
import Entities from './Entities' import Entities from './Entities'
import Life from './Life' import Life from './Life'

70
src/utils/Data.ts Normal file
View File

@ -0,0 +1,70 @@
import fs, { readFileSync } from 'fs'
import https from 'https'
import { Logger } from 'pino'
export interface IEntityData {
id: number
name: string
display_name: string
height: number
width: number
metadata: Array<{ entity?: string }>
}
/** Load game data */
export default class Data {
private _ready = false
public get ready() {
return this._ready
}
private _entities?: { [name: string]: IEntityData }
public get entities() {
return this._entities
}
constructor(source: string, directory: string, version: string, logger: Logger) {
logger.debug('Loading data file')
const path = directory + version + '.json'
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory)
}
if (!fs.existsSync(path)) {
logger.warn({ msg: 'Data file must be downloaded', type: 'version', value: version, note: 'will probably fail' })
const url = source + version + '.json'
const out = fs.createWriteStream(path)
https.get(url, res => {
res.on('data', data => out.write(data))
res.on('error', error => console.error({ msg: 'Downloading error', error }))
res.on('end', () => {
out.close()
setTimeout(() => this.load(path), 500)
})
}).on('error', error => console.error({ msg: 'Downloading error', error }))
} else {
this.load(path)
}
}
public getEntityById(entityId: number) {
if (this._entities) {
for (const name in (this._entities as object)) {
if (this._entities[name].id === entityId) {
return this._entities[name]
}
}
}
}
private load(path: string) {
try {
const data = JSON.parse(readFileSync(path).toString())[0] // NOTE: memory ???
this._entities = data.entities.entity
this._ready = true
} catch (error) {
console.error('/!\\ =( Please remove ' + path, error)
process.exit(1)
}
}
}

View File

@ -32,9 +32,10 @@ export interface ISlot {
//TODO: //TODO:
} }
export type gameMode = 0|1|2|3
export interface IState { export interface IState {
entityId: number, entityId: number,
gameMode: 0|1|2|3, gamemode: gameMode,
dimension: number, dimension: number,
hashedSeed: number[], hashedSeed: number[],
maxPlayers: number, maxPlayers: number,
@ -43,3 +44,5 @@ export interface IState {
reducedDebugInfo: boolean, reducedDebugInfo: boolean,
enableRespawnScreen: boolean enableRespawnScreen: boolean
} }
export interface IDict { [key: string]: string }

2556
yarn.lock Normal file

File diff suppressed because it is too large Load Diff