Good night
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
ea4101f8b6
commit
2dc150c38b
|
@ -7,4 +7,5 @@ node_modules/
|
|||
build/
|
||||
tmp/
|
||||
temp/
|
||||
data/
|
||||
server/
|
|
@ -1,8 +1,8 @@
|
|||
FROM node
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install
|
||||
RUN npm build
|
||||
RUN yarn install
|
||||
RUN yarn run build
|
||||
|
||||
FROM node
|
||||
WORKDIR /app
|
||||
|
|
10
README.md
10
README.md
|
@ -11,6 +11,7 @@ Scalliony API and UI
|
|||
- [Usage](#usage)
|
||||
- [Config](#config)
|
||||
- [Run](#run)
|
||||
- [Note](#note)
|
||||
- [Tests](#tests)
|
||||
- [License](#license)
|
||||
|
||||
|
@ -19,7 +20,7 @@ Cubbot uses modules with could be enabled at compile time in `src/modules/index.
|
|||
|
||||
## Building
|
||||
```
|
||||
npm run build
|
||||
yarn run build
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
@ -30,12 +31,15 @@ npm run build
|
|||
|
||||
### 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
|
||||
```
|
||||
npm run test
|
||||
yarn run test
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
|
@ -6,10 +6,10 @@ steps:
|
|||
image: node
|
||||
commands:
|
||||
- yarn install
|
||||
- npm run test:coverage
|
||||
- yarn run test:coverage
|
||||
|
||||
- name: build
|
||||
image: node
|
||||
commands:
|
||||
- yarn install
|
||||
- npm run build
|
||||
- yarn run build
|
|
@ -8,7 +8,11 @@
|
|||
"level": "info"
|
||||
},
|
||||
"modules": {
|
||||
"Client": {},
|
||||
"Connection": {
|
||||
"reconnect": [
|
||||
"idling"
|
||||
]
|
||||
},
|
||||
"Combat": {},
|
||||
"Chat": {},
|
||||
"Time": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,7 @@ import * as mc from 'minecraft-protocol'
|
|||
import { default as pino } from 'pino'
|
||||
import moduleList from './modules'
|
||||
import Module, { IModuleType } from './utils/Module'
|
||||
import Client from './modules/Connection'
|
||||
|
||||
interface IConfig {
|
||||
/** Minecraft protocol options */
|
||||
|
@ -41,7 +42,7 @@ export default class Cubbot {
|
|||
public registerModule(module: IModuleType, name?: string) {
|
||||
const n: string = name || module.name
|
||||
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)
|
||||
}
|
||||
|
@ -76,12 +77,13 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop bot */
|
||||
public umount() {
|
||||
public umount(exit: boolean = false) {
|
||||
if (this.client) {
|
||||
this.logger.debug('Unloading modules')
|
||||
this.modules!.forEach(m => m.umount())
|
||||
|
@ -91,8 +93,11 @@ export default class Cubbot {
|
|||
//TODO: disconnect
|
||||
this._client = undefined
|
||||
|
||||
this.logger.warn('Stopped')
|
||||
this.logger.flush()
|
||||
if (exit) {
|
||||
this.logger.warn('Stopped')
|
||||
this.logger.flush()
|
||||
setTimeout(process.exit, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,4 @@ const config = JSON.parse(stripJsonComments(readFileSync('./env.json').toString(
|
|||
const app = new Cubbot(config)
|
||||
app.mount()
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
app.umount()
|
||||
setTimeout(process.exit, 100)
|
||||
})
|
||||
process.on('SIGINT', () => app.umount(true))
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import md from 'minecraft-data'
|
||||
import { vsprintf } from 'sprintf-js'
|
||||
import Module from '../utils/Module'
|
||||
import { IDict } from '../utils/types'
|
||||
import Connection from './Connection'
|
||||
|
||||
/** 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 */
|
||||
export default class Chat extends Module<{ reply?: string }> {
|
||||
|
||||
/** Translation dictionary (en only) */
|
||||
private dict!: { [key: string]: string }
|
||||
export default class Chat extends Module<{ reply?: string, bye?: string }> {
|
||||
|
||||
/** Convert payload to string */
|
||||
public parse(msg: IMessage) {
|
||||
public static parse(msg: IMessage, language: IDict) {
|
||||
let text = ''
|
||||
if (msg.text) {
|
||||
text += msg.text
|
||||
}
|
||||
if (msg.translate && msg.translate in this.dict) {
|
||||
text += vsprintf(this.dict[msg.translate], (msg.with || []).map(this.parse.bind(this)))
|
||||
if (msg.translate && msg.translate in language) {
|
||||
text += vsprintf(language[msg.translate], (msg.with || [])
|
||||
.map(w => typeof w === 'string' ? w : Chat.parse(w, language)))
|
||||
}
|
||||
if (msg.extra) {
|
||||
text += msg.extra.map(({text: t}) => t).join()
|
||||
|
@ -26,11 +25,23 @@ export default class Chat extends Module<{ reply?: string }> {
|
|||
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() {
|
||||
this.client.on('login', () => {
|
||||
// FIXME: use this.client.version when 'minecraft-data' include 1.15 language
|
||||
this.dict = md('1.14').language
|
||||
})
|
||||
this.connection = this.load<Connection>(Connection)
|
||||
|
||||
this.client.on('chat', packet => {
|
||||
const message = JSON.parse(packet.message)
|
||||
|
@ -38,7 +49,6 @@ export default class Chat extends Module<{ reply?: string }> {
|
|||
})
|
||||
}
|
||||
|
||||
//TODO: chat write
|
||||
//TODO: reply
|
||||
|
||||
protected getConf() {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -2,25 +2,32 @@ import { ATTACK_DIST2, SERVER_TICK_RATE } from '../utils/constants'
|
|||
import { dist2 } from '../utils/func'
|
||||
import Module from '../utils/Module'
|
||||
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 State from './State'
|
||||
|
||||
interface IConf {
|
||||
/** Respawn after death */
|
||||
respawn: boolean,
|
||||
respawn: boolean
|
||||
/** Attack dangerous entities */
|
||||
fight: boolean,
|
||||
fight: boolean
|
||||
/** Must change weapon */
|
||||
weapon: 'none' | 'needed' | 'always',
|
||||
weapon: 'none' | 'needed' | 'always'
|
||||
/** Must react to arrows */
|
||||
arrows: 'ignore' | 'dodge' | 'shield'
|
||||
/** Time between attacks */
|
||||
delay: number | 'item' | 'safe',
|
||||
delay: number | 'item' | 'safe'
|
||||
/** Attacks multiple entities at ones */
|
||||
multiaura: boolean,
|
||||
multiaura: boolean
|
||||
/** Entity to target first */
|
||||
priority: 'nearest'
|
||||
/** Players names */
|
||||
friends: string[]
|
||||
/** Targeted entities names */
|
||||
mobs: string[]
|
||||
/** Targeted entities ids (override mobs) */
|
||||
mobsIds?: number[]
|
||||
}
|
||||
|
||||
interface IEnemy extends IAlive {
|
||||
|
@ -33,7 +40,9 @@ const SAFE_DELAY = SERVER_TICK_RATE * 2
|
|||
export default class Combat extends Module<IConf> {
|
||||
|
||||
private state!: State
|
||||
private life!: Life
|
||||
private entities!: Entities
|
||||
private connection!: Connection
|
||||
|
||||
public umount() {
|
||||
this.conf.fight = false
|
||||
|
@ -42,7 +51,9 @@ export default class Combat extends Module<IConf> {
|
|||
|
||||
protected mount() {
|
||||
this.state = this.load<State>(State)
|
||||
this.life = this.load<Life>(Life)
|
||||
this.entities = this.load<Entities>(Entities)
|
||||
this.connection = this.load<Connection>(Connection)
|
||||
|
||||
this.fight()
|
||||
this.arrows()
|
||||
|
@ -50,19 +61,21 @@ export default class Combat extends Module<IConf> {
|
|||
this.client.on('combat_event', packet => {
|
||||
switch (packet.event) {
|
||||
case 0:
|
||||
this.logger.warn('fighting')
|
||||
this.logger.warn({ msg: 'Fighting' })
|
||||
break
|
||||
|
||||
case 2:
|
||||
if (packet.playerId === this.state.state.entityId) {
|
||||
this.logger.error('%o killed me at %o',
|
||||
this.entities.getAlive(packet.entityId), this.state.position)
|
||||
this.logger.error({
|
||||
msg: 'Killed', type: 'entity',
|
||||
value: this.entities.getAlive(packet.entityId),
|
||||
})
|
||||
// MAYBE: chat packet.message
|
||||
if (this.conf.respawn) {
|
||||
this.load<Life>(Life).respawn()
|
||||
this.life.respawn()
|
||||
}
|
||||
} 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',
|
||||
multiaura: false,
|
||||
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) {
|
||||
//TODO: pick weapon if change wait
|
||||
//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
|
||||
const dangers: IEnemy[] = ([...this.entities.players, ...this.entities.livings] as IAlive[])
|
||||
.filter(({ entityId }) => entityId !== this.state.state.entityId)
|
||||
//TODO: filter friends, creative, passives
|
||||
.filter(e => {
|
||||
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
|
||||
.filter(({ reach2 }) => reach2 <= ATTACK_DIST2) // MAYBE: as option
|
||||
.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)
|
||||
|
||||
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.logger.debug('attack %s', entityId)
|
||||
this.logger.debug({ msg: 'Attack', type: 'entityId', value: 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 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)
|
||||
|
||||
setTimeout(this.fight.bind(this), delay)
|
||||
|
@ -120,7 +161,7 @@ export default class Combat extends Module<IConf> {
|
|||
.filter(e => e.type === 2 && // arrow
|
||||
dist2(this.state.position, e) <= 99)
|
||||
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) =>
|
||||
Math.abs(arrow.velocityX) < Math.abs(arrow.velocityZ) ?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -68,7 +68,7 @@ export interface IAlive extends IMovableE, IEquipable, IAttributable, IMetadatab
|
|||
headPitch: number
|
||||
}
|
||||
|
||||
interface ILiving extends IAlive {
|
||||
export interface ILiving extends IAlive {
|
||||
entityUUID: string
|
||||
type: number
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ interface ILiving extends IAlive {
|
|||
interface IPlayerSpawn extends IPositioned {
|
||||
playerUUID: string
|
||||
}
|
||||
interface IPlayer extends IPlayerSpawn, IAlive {
|
||||
export interface IPlayer extends IPlayerSpawn, IAlive {
|
||||
me?: boolean
|
||||
}
|
||||
|
||||
|
@ -172,117 +172,86 @@ export default class Entities extends Module<{}> {
|
|||
// MAYBE: this.client.on('animation', packet => { })
|
||||
// MAYBE: this.client.on('entity_status', packet => { })
|
||||
|
||||
this.client.on('rel_entity_move', (packet: IMove) => {
|
||||
const entity = this.getEntity(packet.entityId)
|
||||
if (entity) {
|
||||
applyDelta(entity, packet)
|
||||
} else {
|
||||
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) {
|
||||
this.client.on('rel_entity_move', (packet: IMove) =>
|
||||
this.findOrLogger<IMovableE>('rel_move', packet, this.getEntity.bind(this), entity =>
|
||||
applyDelta(entity, packet)))
|
||||
this.client.on('entity_move_look', (packet: IMoveLook) =>
|
||||
this.findOrLogger<IMovableE>('move_look', packet, this.getEntity.bind(this), entity => {
|
||||
applyDelta(entity, packet)
|
||||
entity.pitch = packet.pitch
|
||||
entity.yaw = packet.yaw
|
||||
} else {
|
||||
this.logger.warn('move_look of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
this.client.on('entity_look', (packet: ILook) => {
|
||||
const entity = this.getEntity(packet.entityId)
|
||||
if (entity) {
|
||||
}))
|
||||
this.client.on('entity_look', (packet: ILook) =>
|
||||
this.findOrLogger<IMovableE>('look', packet, this.getEntity.bind(this), entity => {
|
||||
entity.pitch = packet.pitch
|
||||
entity.yaw = packet.yaw
|
||||
} else {
|
||||
this.logger.warn('look of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
this.client.on('entity_head_rotation', (packet: IHead) => {
|
||||
const entity = this.getAlive(packet.entityId)
|
||||
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) {
|
||||
}))
|
||||
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_velocity', (packet: IVelocityE) =>
|
||||
this.findOrLogger<IMovableE>('velocity', packet, this.getEntity.bind(this), entity => {
|
||||
entity.velocityX = packet.velocityX
|
||||
entity.velocityY = packet.velocityY
|
||||
entity.velocityZ = packet.velocityZ
|
||||
} else {
|
||||
this.logger.warn('velocity of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
this.client.on('entity_teleport', (packet: IPositioned) => {
|
||||
const entity = this.getEntity(packet.entityId)
|
||||
if (entity) {
|
||||
}))
|
||||
this.client.on('entity_teleport', (packet: IPositioned) =>
|
||||
this.findOrLogger<IMovableE>('teleport', packet, this.getEntity.bind(this), entity => {
|
||||
entity.x = packet.x
|
||||
entity.y = packet.y
|
||||
entity.z = packet.z
|
||||
entity.pitch = packet.pitch
|
||||
entity.yaw = packet.yaw
|
||||
} else {
|
||||
this.logger.warn('teleport of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
this.client.on('entity_destroy', (packet: { entityIds: number[] }) => {
|
||||
packet.entityIds.forEach(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
|
||||
this.client.on('entity_metadata', (packet: IMetadatas) => {
|
||||
const entity = this.getMetadatable(packet.entityId)
|
||||
if (entity) {
|
||||
this.client.on('entity_metadata', (packet: IMetadatas) =>
|
||||
this.findOrLogger<IMetadatable>('metadata', packet, this.getMetadatable.bind(this), entity => {
|
||||
if (!entity.metadata) {
|
||||
entity.metadata = new Map()
|
||||
}
|
||||
for (const data of packet.metadata) {
|
||||
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) => { })
|
||||
|
||||
// TODO: entity_effect, remove_entity_effect
|
||||
this.client.on('entity_equipment', (packet: IEquipment) => {
|
||||
const entity = this.getAlive(packet.entityId)
|
||||
if (entity) {
|
||||
this.client.on('entity_equipment', (packet: IEquipment) =>
|
||||
this.findOrLogger<IAlive>('equipment', packet, this.getAlive.bind(this), entity =>
|
||||
entity.equipment = entity.equipment ?
|
||||
entity.equipment.set(packet.slot, packet.item) :
|
||||
new Map([[packet.slot, packet.item]])
|
||||
} else {
|
||||
this.logger.warn('equipment of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
new Map([[packet.slot, packet.item]])))
|
||||
|
||||
// TODO: identify (with properties)
|
||||
this.client.on('entity_update_attributes', (packet: IAttributes) => {
|
||||
const entity = this.getAlive(packet.entityId)
|
||||
if (entity) {
|
||||
entity.attributes = packet.properties
|
||||
} else {
|
||||
this.logger.warn('attributes of unknown entity %s', packet.entityId)
|
||||
}
|
||||
})
|
||||
|
||||
this.client.on('entity_update_attributes', (packet: IAttributes) =>
|
||||
this.findOrLogger<IAlive>('attributes', packet, this.getAlive.bind(this), entity =>
|
||||
entity.attributes = packet.properties))
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
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
|
||||
|
|
|
@ -83,19 +83,19 @@ export default class Life extends Module<IConf> {
|
|||
this._saturation = data.foodSaturation
|
||||
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
this.logger.warn('TODO: must Eat')
|
||||
this.logger.warn({ msg: 'TODO: Must eat' })
|
||||
// TODO: inventory.items.filter(food).orderBy(this.conf.preferSaturation)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Module from '../utils/Module'
|
||||
import { IDelta, IPosition, IState } from '../utils/types'
|
||||
import { IDelta, IPosition, IState, gameMode } from '../utils/types'
|
||||
|
||||
// MAYBE: split
|
||||
|
||||
|
@ -19,7 +19,13 @@ interface IPositionPacket extends IPosition {
|
|||
teleportId: number
|
||||
}
|
||||
|
||||
// TODO: player_info
|
||||
interface IPlayerInfo {
|
||||
UUID: string
|
||||
name: string
|
||||
properties: any[]
|
||||
gamemode: gameMode
|
||||
ping: number
|
||||
}
|
||||
|
||||
/** Handle client state and position */
|
||||
export default class State extends Module<{ positionConfirm: boolean }> {
|
||||
|
@ -34,7 +40,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
return this._difficulty
|
||||
}
|
||||
|
||||
private _abilities!: IDifficulty
|
||||
private _abilities!: IAbilities
|
||||
public get abilities() {
|
||||
return this._abilities
|
||||
}
|
||||
|
@ -44,6 +50,11 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
return this._position
|
||||
}
|
||||
|
||||
private _players = new Map<string, IPlayerInfo>()
|
||||
public get players() {
|
||||
return this._players
|
||||
}
|
||||
|
||||
public translate(delta: IDelta, onGround: boolean = true) {
|
||||
this.client.write('position', {
|
||||
onGround,
|
||||
|
@ -63,7 +74,7 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
this.client.on('position', (data: IPositionPacket) => {
|
||||
if (this._position == null) {
|
||||
if (data.flags) {
|
||||
this.logger.error('first position is relative')
|
||||
this.logger.error('First position is relative')
|
||||
return
|
||||
} else {
|
||||
this._position = data
|
||||
|
@ -82,7 +93,29 @@ export default class State extends Module<{ positionConfirm: boolean }> {
|
|||
if (this.conf.positionConfirm) {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -74,17 +74,17 @@ export default class Time extends Module<IConf> {
|
|||
if (this._distAge) {
|
||||
this._tps = age - this._distAge
|
||||
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
|
||||
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
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IModuleType } from '../utils/Module'
|
||||
import Chat from './Chat'
|
||||
import Client from './Client'
|
||||
import Client from './Connection'
|
||||
import Combat from './Combat'
|
||||
import Entities from './Entities'
|
||||
import Life from './Life'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,9 +32,10 @@ export interface ISlot {
|
|||
//TODO:
|
||||
}
|
||||
|
||||
export type gameMode = 0|1|2|3
|
||||
export interface IState {
|
||||
entityId: number,
|
||||
gameMode: 0|1|2|3,
|
||||
gamemode: gameMode,
|
||||
dimension: number,
|
||||
hashedSeed: number[],
|
||||
maxPlayers: number,
|
||||
|
@ -43,3 +44,5 @@ export interface IState {
|
|||
reducedDebugInfo: boolean,
|
||||
enableRespawnScreen: boolean
|
||||
}
|
||||
|
||||
export interface IDict { [key: string]: string }
|
||||
|
|
Loading…
Reference in New Issue