Atomic commit
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
parent
6ca0227fa4
commit
de29687a11
|
@ -1,8 +0,0 @@
|
|||
CORE_LOG=info
|
||||
CORE_HOST=""
|
||||
#CORE_PORT=
|
||||
CORE_USER=""
|
||||
CORE_PASS=""
|
||||
|
||||
CHAT_LOG=info
|
||||
COMBAT_LOG=info
|
|
@ -1,7 +1,7 @@
|
|||
.idea/
|
||||
.vscode/
|
||||
.nyc_output/
|
||||
.env
|
||||
env.json
|
||||
coverage/
|
||||
node_modules/
|
||||
build/
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"client": {
|
||||
"host": "server.tld",
|
||||
"username": "Bot",
|
||||
"password": "root"
|
||||
},
|
||||
"log": {
|
||||
"level": "info"
|
||||
},
|
||||
"modules": {
|
||||
"Client": {},
|
||||
"Combat": {},
|
||||
"Chat": {},
|
||||
"Time": {
|
||||
"log": "debug"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -318,15 +318,6 @@
|
|||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-ylSC9GhfRH7m1EUXBXofhgx4lUWmFeQDINW5oLuS+gxWdfUeW4zJdeVTYVkexEW+e2VUvlZR2kGnGGipAWR7kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dotenv": "*"
|
||||
}
|
||||
},
|
||||
"@types/mocha": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz",
|
||||
|
@ -365,6 +356,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-hkgzYF+qnIl8uTO8rmUSVSfQ8BIfMXC4yJAF4n8BE758YsKBZvFC4NumnAegj7KmylP0liEZNpb9RRGFMbFejA==",
|
||||
"dev": true
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
|
@ -995,11 +992,6 @@
|
|||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
|
@ -1932,6 +1924,12 @@
|
|||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
|
||||
|
@ -2651,6 +2649,14 @@
|
|||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
|
@ -2999,10 +3005,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||
"dev": true
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
|
||||
"integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "6.1.0",
|
||||
|
|
|
@ -16,16 +16,16 @@
|
|||
"test:coverage": "nyc -r lcov -e .ts -x \"*Test.ts\" mocha -r ts-node/register test/**/*Test.ts && nyc report"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^8.2.0",
|
||||
"minecraft-protocol": "^1.11.0",
|
||||
"pino": "^6.2.0"
|
||||
"pino": "^6.2.0",
|
||||
"strip-json-comments": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^13.11.1",
|
||||
"@types/pino": "^6.0.0",
|
||||
"@types/sprintf-js": "^1.1.2",
|
||||
"chai": "^4.2.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"mocha": "^7.1.1",
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import assert from 'assert'
|
||||
import * as mc from 'minecraft-protocol'
|
||||
import { default as pino } from 'pino'
|
||||
import moduleList from './modules'
|
||||
import Module, { IModuleType } from './utils/Module'
|
||||
|
||||
interface IConfig {
|
||||
/** Minecraft protocol options */
|
||||
client: mc.ClientOptions,
|
||||
/** Logger options */
|
||||
log: pino.LoggerOptions,
|
||||
/** Modules to load and options */
|
||||
modules: { [name: string]: object }
|
||||
}
|
||||
|
||||
/** Modules manager */
|
||||
export default class Cubbot {
|
||||
|
||||
private logger: pino.Logger
|
||||
|
||||
private _client?: mc.Client
|
||||
public get client() {
|
||||
return this._client
|
||||
}
|
||||
|
||||
private _registeredModules = new Map(moduleList.map(m => [m.name, m]))
|
||||
|
||||
private _modules?: Map<string, Module<{}>>
|
||||
public get modules() {
|
||||
return this._modules
|
||||
}
|
||||
|
||||
constructor(private readonly config: IConfig) {
|
||||
this.logger = pino(this.config.log)
|
||||
this.logger.warn('Cubbot')
|
||||
|
||||
assert.ok(this.config.client)
|
||||
}
|
||||
|
||||
/** Add addition module */
|
||||
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._registeredModules.set(n, module)
|
||||
}
|
||||
|
||||
public loadModule<T extends Module<{}>>(module: IModuleType): T {
|
||||
return this.loadModuleByName(module.name) as T
|
||||
}
|
||||
|
||||
public loadModuleByName(name: string) {
|
||||
if (this.modules!.has(name)) {
|
||||
return this.modules!.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 module = new mType(this.client!, mLogger, conf, this.loadModule.bind(this))
|
||||
this._modules!.set(name, module)
|
||||
return module
|
||||
}
|
||||
|
||||
/** Start bot */
|
||||
public mount() {
|
||||
if (!this.client) {
|
||||
this.logger.debug('Creating client')
|
||||
this._client = mc.createClient(this.config.client)
|
||||
|
||||
this.logger.debug('Loading modules')
|
||||
this._modules = new Map<string, Module<{}>>()
|
||||
Object.keys(this.config.modules).forEach(this.loadModuleByName.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop bot */
|
||||
public umount() {
|
||||
if (this.client) {
|
||||
this.logger.debug('Unloading modules')
|
||||
this.modules!.forEach(m => m.umount())
|
||||
this._modules = undefined
|
||||
|
||||
this.logger.debug('Removing client')
|
||||
//TODO: disconnect
|
||||
this._client = undefined
|
||||
|
||||
this.logger.warn('Stopped')
|
||||
this.logger.flush()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
26
src/app.ts
26
src/app.ts
|
@ -1,26 +0,0 @@
|
|||
import * as mc from 'minecraft-protocol'
|
||||
import Env from './core/Env'
|
||||
import { logger } from './core/logger'
|
||||
import modulesTypes from './modules'
|
||||
|
||||
export default () => {
|
||||
logger.warn('Cubbot start')
|
||||
const client = mc.createClient({
|
||||
host: Env.get('CORE_HOST'),
|
||||
password: Env.orFail('CORE_PASS'),
|
||||
port: Env.getAs('CORE_PORT', Number.parseInt),
|
||||
username: Env.orFail('CORE_USER'),
|
||||
})
|
||||
|
||||
client.on('connect', () => {
|
||||
logger.trace('Connected')
|
||||
})
|
||||
client.on('disconnect', packet => {
|
||||
logger.warn('Disconnected ' + packet.reason)
|
||||
})
|
||||
client.on('login', () => {
|
||||
logger.trace('Logged')
|
||||
})
|
||||
|
||||
const modules = modulesTypes.map(t => new t(client))
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
interface IStringMap { [key: string]: string }
|
||||
|
||||
export default class Env {
|
||||
|
||||
public static get(key: string) {
|
||||
return process.env[key]
|
||||
}
|
||||
public static getAs<T>(key: string, mapper: (val: string) => T): T | undefined {
|
||||
const val = this.get(key)
|
||||
return val ? mapper(val) : undefined
|
||||
}
|
||||
|
||||
public static orElse(key: string, fallback: string) {
|
||||
return this.get(key) || fallback
|
||||
}
|
||||
|
||||
public static orFail(key: string) {
|
||||
const val = this.get(key)
|
||||
if (val) {
|
||||
return val
|
||||
}
|
||||
throw Error(`Required config key: ${key}`)
|
||||
}
|
||||
|
||||
public static map(map: IStringMap) {
|
||||
const res = {} as IStringMap
|
||||
for (const [key, val] of Object.entries(map)) {
|
||||
res[key] = this.get(val)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { default as pino } from 'pino'
|
||||
import Env from './Env'
|
||||
|
||||
export const logger = pino({ level: Env.orFail('CORE_LOG') })
|
||||
logger.trace('Logger created')
|
||||
|
||||
export function child(name: string, level?: string) {
|
||||
const l = logger.child({ name, level: level || Env.get(name.toUpperCase() + '_LOG') })
|
||||
l.trace('Logger created')
|
||||
return l
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export function longToNumber(arr: number[]) {
|
||||
return arr[1] + 4294967296 * arr[0]
|
||||
}
|
16
src/index.ts
16
src/index.ts
|
@ -1,5 +1,13 @@
|
|||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import { readFileSync } from 'fs'
|
||||
import stripJsonComments from 'strip-json-comments'
|
||||
import Cubbot from './Cubbot'
|
||||
|
||||
import app from './app'
|
||||
app()
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -1,15 +1,48 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import Module from './Module'
|
||||
import md from 'minecraft-data'
|
||||
import { vsprintf } from 'sprintf-js'
|
||||
import Module from '../utils/Module'
|
||||
|
||||
export default class Chat extends Module {
|
||||
/** Message packet payload */
|
||||
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 }
|
||||
|
||||
/** Convert payload to string */
|
||||
public parse(msg: IMessage) {
|
||||
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.extra) {
|
||||
text += msg.extra.map(({text: t}) => t).join()
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
this.client.on('login', () => {
|
||||
// FIXME: use this.client.version when 'minecraft-data' include 1.15 language
|
||||
this.dict = md('1.14').language
|
||||
})
|
||||
|
||||
public mount() {
|
||||
this.client.on('chat', packet => {
|
||||
const message = JSON.parse(packet.message)
|
||||
this.logger.info(message.text || (message.extra ? message.extra.map((e: any) => e.text).join() : packet))
|
||||
this.logger.info(this.parse(message))
|
||||
})
|
||||
}
|
||||
|
||||
//TODO: chat write
|
||||
//TODO: reply
|
||||
|
||||
protected getConf() {
|
||||
return { }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +1,137 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import Module from './Module'
|
||||
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 Life from './Life'
|
||||
import State from './State'
|
||||
|
||||
export default class Combat extends Module {
|
||||
interface IConf {
|
||||
/** Respawn after death */
|
||||
respawn: boolean,
|
||||
/** Attack dangerous entities */
|
||||
fight: boolean,
|
||||
/** Must change weapon */
|
||||
weapon: 'none' | 'needed' | 'always',
|
||||
/** Must react to arrows */
|
||||
arrows: 'ignore' | 'dodge' | 'shield'
|
||||
/** Time between attacks */
|
||||
delay: number | 'item' | 'safe',
|
||||
/** Attacks multiple entities at ones */
|
||||
multiaura: boolean,
|
||||
/** Entity to target first */
|
||||
priority: 'nearest'
|
||||
}
|
||||
|
||||
private respawn: boolean = true
|
||||
interface IEnemy extends IAlive {
|
||||
reach2: number
|
||||
}
|
||||
|
||||
const SAFE_DELAY = SERVER_TICK_RATE * 2
|
||||
|
||||
/** Protect and fight */
|
||||
export default class Combat extends Module<IConf> {
|
||||
|
||||
private state!: State
|
||||
private entities!: Entities
|
||||
|
||||
public umount() {
|
||||
this.conf.fight = false
|
||||
this.conf.arrows = 'ignore'
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
this.state = this.load<State>(State)
|
||||
this.entities = this.load<Entities>(Entities)
|
||||
|
||||
this.fight()
|
||||
this.arrows()
|
||||
|
||||
public mount() {
|
||||
this.client.on('combat_event', packet => {
|
||||
switch (packet.event) {
|
||||
case 0:
|
||||
this.logger.warn('fighting')
|
||||
break
|
||||
|
||||
case 1:
|
||||
this.logger.info('fighting entity %s', packet.entityId)
|
||||
break
|
||||
|
||||
case 2:
|
||||
this.logger.info('dead player %s by %s', packet.playerId, packet.entityId)
|
||||
//TODO: is me ?
|
||||
//TODO: print chat message
|
||||
if (this.respawn) {
|
||||
//TODO: use Life.respawn()
|
||||
this.client.write('client_command', { action: 1 })
|
||||
if (packet.playerId === this.state.state.entityId) {
|
||||
this.logger.error('%o killed me at %o',
|
||||
this.entities.getAlive(packet.entityId), this.state.position)
|
||||
// MAYBE: chat packet.message
|
||||
if (this.conf.respawn) {
|
||||
this.load<Life>(Life).respawn()
|
||||
}
|
||||
} else {
|
||||
this.logger.info('killed %o', packet)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//TODO: respawn or disconnect
|
||||
//TODO: aura
|
||||
protected getConf(): IConf {
|
||||
return {
|
||||
respawn: true,
|
||||
fight: true,
|
||||
weapon: 'always',
|
||||
arrows: 'dodge',
|
||||
delay: 'safe',
|
||||
multiaura: false,
|
||||
priority: 'nearest',
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Interact Entity
|
||||
private fight() {
|
||||
if (this.conf.fight) {
|
||||
//TODO: pick weapon if change wait
|
||||
//TODO: pick shield
|
||||
|
||||
// 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
|
||||
.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)
|
||||
// MAYBE: switch this.conf.priority
|
||||
// MAYBE: include entity speed (for tiny zombie or phantom)
|
||||
|
||||
if (dangers.length > 0) {
|
||||
this.logger.info('fighting %s target(s)', dangers.length);
|
||||
|
||||
(this.conf.multiaura ? dangers : dangers.slice(0, 1)).forEach(({ entityId }) => {
|
||||
this.logger.debug('attack %s', entityId)
|
||||
this.entities.attack(entityId)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
1 / attackSpeed * 20 * SERVER_TICK_RATE)
|
||||
|
||||
setTimeout(this.fight.bind(this), delay)
|
||||
}
|
||||
}
|
||||
|
||||
private arrows() {
|
||||
if (this.conf.arrows !== 'ignore') {
|
||||
const arrows = [...this.entities.objects]
|
||||
.filter(e => e.type === 2 && // arrow
|
||||
dist2(this.state.position, e) <= 99)
|
||||
if (arrows.length > 0) {
|
||||
this.logger.warn('%s arrows', arrows.length)
|
||||
|
||||
this.state.translate(arrows.reduce((p, arrow) =>
|
||||
Math.abs(arrow.velocityX) < Math.abs(arrow.velocityZ) ?
|
||||
{ ...p, dX: p.dX + Math.sign(arrow.velocityX) } :
|
||||
{ ...p, dZ: p.dZ + Math.sign(arrow.velocityZ) },
|
||||
{ dX: 0, dY: 0, dZ: 0 } as IDelta)) // MAYBE: do some trigo
|
||||
}
|
||||
setTimeout(this.arrows.bind(this), SAFE_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: respawn or disconnect
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,293 @@
|
|||
//TODO: spawn_entity_living
|
||||
//TODO: entity_metadata
|
||||
//TODO: entity_update_attributes
|
||||
//TODO: entity_equipment
|
||||
//TODO: entity_move_look
|
||||
//TODO: entity_head_rotation
|
||||
//TODO: rel_entity_move
|
||||
//TODO: entity_velocity
|
||||
//TODO: entity_destroy
|
||||
//TODO: entity_teleport
|
||||
//TODO: entity_look
|
||||
//TODO: set_passengers
|
||||
import { applyDelta, applyVelocity } from '../utils/func'
|
||||
import Module from '../utils/Module'
|
||||
import { IDelta, IMovable, IPosition, IRotation, ISlot, IState, IVelocity } from '../utils/types'
|
||||
import Time from './Time'
|
||||
|
||||
interface IId {
|
||||
entityId: number
|
||||
}
|
||||
|
||||
interface IPositioned extends IId, IPosition { }
|
||||
interface IMovableE extends IId, IMovable { }
|
||||
interface IMove extends IId, IDelta { }
|
||||
interface ILook extends IId, IRotation { }
|
||||
interface IMoveLook extends IMove, ILook { }
|
||||
interface IHead extends IId {
|
||||
headPitch: number
|
||||
}
|
||||
interface IVelocityE extends IId, IVelocity { }
|
||||
|
||||
interface IObject extends IMovableE, IMetadatable {
|
||||
objectId: number
|
||||
type: number
|
||||
data: number
|
||||
}
|
||||
|
||||
interface IOrb extends IMovableE, IMetadatable {
|
||||
count: number
|
||||
}
|
||||
|
||||
interface IEquipment extends IId {
|
||||
slot: number,
|
||||
item: ISlot
|
||||
}
|
||||
interface IEquipable {
|
||||
equipment?: Map<number, ISlot>
|
||||
}
|
||||
|
||||
interface IAttribute {
|
||||
key: string
|
||||
value: number
|
||||
modifiers: IModifier[]
|
||||
}
|
||||
interface IModifier {
|
||||
uuid: string
|
||||
amount: number
|
||||
operation: number
|
||||
}
|
||||
interface IAttributes extends IId {
|
||||
properties: IAttribute[]
|
||||
}
|
||||
interface IAttributable {
|
||||
attributes?: IAttribute[]
|
||||
}
|
||||
|
||||
interface IMetadata {
|
||||
key: number
|
||||
type: number
|
||||
value: any
|
||||
}
|
||||
interface IMetadatas extends IId {
|
||||
metadata: IMetadata[]
|
||||
}
|
||||
interface IMetadatable {
|
||||
metadata?: Map<number, IMetadata>
|
||||
}
|
||||
|
||||
export interface IAlive extends IMovableE, IEquipable, IAttributable, IMetadatable {
|
||||
headPitch: number
|
||||
}
|
||||
|
||||
interface ILiving extends IAlive {
|
||||
entityUUID: string
|
||||
type: number
|
||||
}
|
||||
|
||||
interface IPlayerSpawn extends IPositioned {
|
||||
playerUUID: string
|
||||
}
|
||||
interface IPlayer extends IPlayerSpawn, IAlive {
|
||||
me?: boolean
|
||||
}
|
||||
|
||||
/** Track entities */
|
||||
export default class Entities extends Module<{}> {
|
||||
|
||||
private _objects = new Map<number, IObject>()
|
||||
public get objects() {
|
||||
return this._objects.values()
|
||||
}
|
||||
|
||||
private _orbs = new Map<number, IOrb>()
|
||||
public get orbs() {
|
||||
return this._orbs.values()
|
||||
}
|
||||
|
||||
private _livings = new Map<number, ILiving>()
|
||||
public get livings() {
|
||||
return this._livings.values()
|
||||
}
|
||||
|
||||
private _players = new Map<number, IPlayer>()
|
||||
public get players() {
|
||||
return this._players.values()
|
||||
}
|
||||
|
||||
public getEntity(entityId: number): IMovableE | undefined {
|
||||
return this._players.get(entityId) || this._livings.get(entityId) ||
|
||||
this._objects.get(entityId) || this._orbs.get(entityId)
|
||||
}
|
||||
|
||||
public getAlive(entityId: number): IAlive | undefined {
|
||||
return this._players.get(entityId) || this._livings.get(entityId)
|
||||
}
|
||||
|
||||
public getMetadatable(entityId: number): IMetadatable | undefined {
|
||||
return this._players.get(entityId) || this._livings.get(entityId) ||
|
||||
this._objects.get(entityId) || this._orbs.get(entityId)
|
||||
}
|
||||
|
||||
public deleteEntity(entityId: number) {
|
||||
return this._players.delete(entityId) || this._livings.delete(entityId) ||
|
||||
this._objects.delete(entityId) || this._orbs.delete(entityId)
|
||||
}
|
||||
|
||||
public attack(entityId: number, mainHand: boolean = true) {
|
||||
this.client.write('use_entity', { target: entityId, mouse: 1, hand: mainHand ? 0 : 1 })
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
|
||||
this.load<Time>(Time).events.on('tick', () => {
|
||||
this._objects.forEach(applyVelocity)
|
||||
// MAYBE: apply velocity to others
|
||||
})
|
||||
|
||||
this.client.on('spawn_entity', (packet: IObject) => {
|
||||
this._objects.set(packet.entityId, packet)
|
||||
})
|
||||
this.client.on('spawn_entity_experience_orb', (packet: IOrb) => {
|
||||
this._orbs.set(packet.entityId, packet)
|
||||
})
|
||||
this.client.on('spawn_entity_living', (packet: ILiving) => {
|
||||
this._livings.set(packet.entityId, packet)
|
||||
})
|
||||
this.client.on('named_entity_spawn', (packet: IPlayerSpawn) => {
|
||||
const player = {
|
||||
velocityX: 0,
|
||||
velocityY: 0,
|
||||
velocityZ: 0,
|
||||
headPitch: 0,
|
||||
...packet,
|
||||
}
|
||||
this._players.set(player.entityId, player)
|
||||
})
|
||||
this.client.on('login', (packet: IState) => {
|
||||
this._players.set(packet.entityId, { // MAYBE: link with State
|
||||
me: true,
|
||||
entityId: packet.entityId,
|
||||
playerUUID: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
pitch: 0,
|
||||
yaw: 0,
|
||||
velocityX: 0,
|
||||
velocityY: 0,
|
||||
velocityZ: 0,
|
||||
headPitch: 0,
|
||||
})
|
||||
})
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: identify metadatas
|
||||
this.client.on('entity_metadata', (packet: IMetadatas) => {
|
||||
const entity = this.getMetadatable(packet.entityId)
|
||||
if (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) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
return {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: player info
|
||||
|
||||
// MAYBE: animation
|
||||
// MAYBE: attach_entity
|
||||
// MAYBE: entity_sound_effect
|
||||
// MAYBE: set_passengers
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import Module from './Module'
|
||||
import { LOW_HEALTH, MAX_HEALTH, REGENERATE_FOOD, SPRINT_FOOD } from '../utils/constants'
|
||||
import { round } from '../utils/func'
|
||||
import Module from '../utils/Module'
|
||||
|
||||
export default class Life extends Module {
|
||||
interface IConf {
|
||||
/** Respawn on world join */
|
||||
spawn: boolean,
|
||||
/** Warn if health is lower */
|
||||
lowHealth: number,
|
||||
/** Life to regenerate */
|
||||
targetHealth: number,
|
||||
/** Log health info */
|
||||
showHealth: boolean,
|
||||
/** Use food if needed */
|
||||
eat: boolean,
|
||||
/** Prefer saturation over food power */
|
||||
preferSaturation: boolean,
|
||||
/** Warn and eat if food level is lower */
|
||||
lowFood: number,
|
||||
/** Food level needed to regenerate */
|
||||
regenerateFood: number,
|
||||
/** Log food level info */
|
||||
showFood: boolean,
|
||||
}
|
||||
|
||||
private _health!: number
|
||||
/** Monitor health, food and xp */
|
||||
export default class Life extends Module<IConf> {
|
||||
public get health() {
|
||||
return this._health
|
||||
}
|
||||
|
||||
private _food!: number
|
||||
public get food() {
|
||||
return this._food
|
||||
}
|
||||
|
||||
private _saturation!: number
|
||||
public get saturation() {
|
||||
return this._saturation
|
||||
}
|
||||
|
@ -22,35 +39,67 @@ export default class Life extends Module {
|
|||
return this.health > 0
|
||||
}
|
||||
|
||||
private _experienceBar!: number
|
||||
public get mustEat() {
|
||||
return this.food <= this.conf.lowFood ||
|
||||
(this.health <= this.conf.targetHealth && this.food <= this.conf.regenerateFood)
|
||||
}
|
||||
public get experienceBar() {
|
||||
return this._experienceBar
|
||||
}
|
||||
|
||||
private _level!: number
|
||||
public get level() {
|
||||
return this._level
|
||||
}
|
||||
|
||||
private _totalExperience!: number
|
||||
public get totalExperience() {
|
||||
return this._totalExperience
|
||||
}
|
||||
|
||||
public mount() {
|
||||
private _health!: number
|
||||
|
||||
private _food!: number
|
||||
|
||||
private _saturation!: number
|
||||
|
||||
private _experienceBar!: number
|
||||
|
||||
private _level!: number
|
||||
|
||||
private _totalExperience!: number
|
||||
|
||||
public respawn() {
|
||||
this.logger.info('respawn')
|
||||
this.client.write('client_command', { action: 1 })
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
//TODO: const inventory = this.conf.eat ? this.load('inventory') : undefined
|
||||
|
||||
this.client.on('update_health', data => {
|
||||
if (this._health == null) {
|
||||
if (this.conf.spawn && this._health == null) {
|
||||
setTimeout(this.respawn.bind(this), 500)
|
||||
}
|
||||
this._health = data.health
|
||||
|
||||
this._health = round(data.health)
|
||||
this._food = data.food
|
||||
this._saturation = data.foodSaturation
|
||||
if (this.health > 5) {
|
||||
this.logger.info('health: %s', this.health)
|
||||
} else {
|
||||
this.logger.warn('low health: %s', this.health)
|
||||
|
||||
if (this.health <= this.conf.lowHealth) {
|
||||
this.logger.warn({ type: 'health', status: this.alive ? 'low' : 'ko', health: this.health })
|
||||
} else if (this.conf.showHealth) {
|
||||
this.logger.info({ type: 'health', status: 'ok', health: this.health })
|
||||
}
|
||||
|
||||
if (this.food <= this.conf.lowFood) {
|
||||
this.logger.warn({ type: 'food', status: 'low', food: this.food, saturation: this.saturation })
|
||||
} else if (this.conf.showHealth) {
|
||||
this.logger.info({ type: 'food', status: 'ok', food: this.food, saturation: this.saturation })
|
||||
}
|
||||
|
||||
if (this.conf.eat && this.mustEat) {
|
||||
this.logger.warn('TODO: must Eat')
|
||||
// TODO: inventory.items.filter(food).orderBy(this.conf.preferSaturation)
|
||||
}
|
||||
})
|
||||
|
||||
this.client.on('experience', data => {
|
||||
this._experienceBar = data.experienceBar
|
||||
this._level = data.level
|
||||
|
@ -58,11 +107,20 @@ export default class Life extends Module {
|
|||
})
|
||||
}
|
||||
|
||||
public respawn() {
|
||||
this.logger.info('respawn')
|
||||
this.client.write('client_command', { action: 1 })
|
||||
}
|
||||
|
||||
//TODO: send client settings
|
||||
|
||||
protected getConf(): IConf {
|
||||
return {
|
||||
spawn: true,
|
||||
lowHealth: LOW_HEALTH,
|
||||
targetHealth: MAX_HEALTH,
|
||||
showHealth: false,
|
||||
eat: true,
|
||||
preferSaturation: true,
|
||||
lowFood: SPRINT_FOOD,
|
||||
regenerateFood: REGENERATE_FOOD,
|
||||
showFood: false,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
|
||||
//TODO: weather_entity aka thunderbolt
|
||||
|
||||
|
||||
//TODO: spawn_weather
|
||||
//TODO: spawn_painting
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import { Logger } from 'pino'
|
||||
import { child } from '../core/logger'
|
||||
|
||||
export default abstract class IModule {
|
||||
protected logger: Logger
|
||||
protected client: Client
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
this.logger = child(this.constructor.name)
|
||||
this.mount()
|
||||
}
|
||||
|
||||
protected abstract mount(): void
|
||||
}
|
|
@ -1,19 +1,7 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import Module from './Module'
|
||||
import Module from '../utils/Module'
|
||||
import { IDelta, IPosition, IState } from '../utils/types'
|
||||
|
||||
//MAYBE: split
|
||||
|
||||
interface IState {
|
||||
entityId: number,
|
||||
gameMode: 0|1|2|3,
|
||||
dimension: number,
|
||||
hashedSeed: number[],
|
||||
maxPlayers: number,
|
||||
levelType: string,
|
||||
viewDistance: number,
|
||||
reducedDebugInfo: boolean,
|
||||
enableRespawnScreen: boolean
|
||||
}
|
||||
// MAYBE: split
|
||||
|
||||
interface IDifficulty {
|
||||
level: number,
|
||||
|
@ -21,28 +9,20 @@ interface IDifficulty {
|
|||
}
|
||||
|
||||
interface IAbilities {
|
||||
//TODO: Flags Byte Bit mask. 0x08: damage disabled (god mode), 0x04: can fly, 0x02: is flying, 0x01: is Creative
|
||||
// TODO: Flags Byte Bit mask. 0x08: damage disabled (god mode), 0x04: can fly, 0x02: is flying, 0x01: is Creative
|
||||
flyingSpeed: number,
|
||||
walkingSpeed: number
|
||||
}
|
||||
|
||||
interface IPosition {
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
yaw: number,
|
||||
pitch: number,
|
||||
flags: number,
|
||||
interface IPositionPacket extends IPosition {
|
||||
flags: number
|
||||
teleportId: number
|
||||
}
|
||||
|
||||
//TODO: entity_status {entityId, entityStatus} aka animations
|
||||
// TODO: player_info
|
||||
|
||||
//TODO: player_info
|
||||
|
||||
export default class State extends Module {
|
||||
|
||||
public positionConfirm: boolean = true
|
||||
/** Handle client state and position */
|
||||
export default class State extends Module<{ positionConfirm: boolean }> {
|
||||
|
||||
private _state!: IState
|
||||
public get state() {
|
||||
|
@ -64,17 +44,26 @@ export default class State extends Module {
|
|||
return this._position
|
||||
}
|
||||
|
||||
public mount() {
|
||||
public translate(delta: IDelta, onGround: boolean = true) {
|
||||
this.client.write('position', {
|
||||
onGround,
|
||||
x: this.position.x += delta.dX,
|
||||
y: this.position.y += delta.dY,
|
||||
z: this.position.z += delta.dZ,
|
||||
})
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
this.client.on('login', data => this._state = data)
|
||||
this.client.on('difficulty', data => this._difficulty = {
|
||||
level: data.difficulty, locked: data.difficultyLocked,
|
||||
})
|
||||
this.client.on('abilities', data => this._abilities = data)
|
||||
|
||||
this.client.on('position', (data: IPosition) => {
|
||||
this.client.on('position', (data: IPositionPacket) => {
|
||||
if (this._position == null) {
|
||||
if (data.flags) {
|
||||
this.logger.error('first postion is relative')
|
||||
this.logger.error('first position is relative')
|
||||
return
|
||||
} else {
|
||||
this._position = data
|
||||
|
@ -87,16 +76,20 @@ export default class State extends Module {
|
|||
z: data.z + (data.flags & 0x04 ? this._position.z : 0),
|
||||
yaw: data.yaw + (data.flags & 0x08 ? this._position.yaw : 0),
|
||||
pitch: data.pitch + (data.flags & 0x10 ? this._position.pitch : 0),
|
||||
flags: 0,
|
||||
teleportId : -1,
|
||||
}
|
||||
}
|
||||
|
||||
if (this.positionConfirm) {
|
||||
if (this.conf.positionConfirm) {
|
||||
this.client.write('teleport_confirm', { teleportId: data.teleportId })
|
||||
}
|
||||
this.logger.debug({ type: 'teleported', position: this.position })
|
||||
})
|
||||
}
|
||||
|
||||
protected getConf() {
|
||||
return { positionConfirm: true }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: vehicule_move
|
||||
|
|
|
@ -1,57 +1,107 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import { longToNumber } from '../core/utils'
|
||||
import Module from './Module'
|
||||
import { EventEmitter } from 'events'
|
||||
import { SERVER_TICK_RATE, SERVER_TPS } from '../utils/constants'
|
||||
import { longToNumber } from '../utils/func'
|
||||
import Module from '../utils/Module'
|
||||
|
||||
interface IUpdateTime {
|
||||
age: number[]
|
||||
time: number[]
|
||||
}
|
||||
|
||||
export default class Time extends Module {
|
||||
interface IConf {
|
||||
/** Log if server lost ticks */
|
||||
minTps: number,
|
||||
/** Log if server lost tps */
|
||||
maxSpt: number,
|
||||
/** Log if client is not in sync */
|
||||
maxOffset: number
|
||||
}
|
||||
|
||||
interface IEmitter {
|
||||
on(event: 'tick', listener: (age: number) => void): this
|
||||
}
|
||||
|
||||
/** Monitor time and ticks */
|
||||
export default class Time extends Module<IConf> {
|
||||
|
||||
private _events = new EventEmitter()
|
||||
public get events(): IEmitter {
|
||||
return this._events
|
||||
}
|
||||
|
||||
private _age!: number
|
||||
/** Ticks since world creation */
|
||||
public get age() {
|
||||
return this._age
|
||||
}
|
||||
|
||||
private _distAge!: number
|
||||
|
||||
private _time!: number
|
||||
/** Ticks of the day */
|
||||
public get time() {
|
||||
return this._time
|
||||
}
|
||||
|
||||
private _tps!: number
|
||||
/** Processed Tick per seconds */
|
||||
public get tps() {
|
||||
return this._tps
|
||||
}
|
||||
|
||||
private _tpsLocal!: number
|
||||
public get tpsLocal() {
|
||||
return this._tpsLocal
|
||||
private _spt!: number
|
||||
/** Processed Tick per seconds */
|
||||
public get spt() {
|
||||
return this._spt
|
||||
}
|
||||
|
||||
private _at: number = Date.now()
|
||||
|
||||
public mount() {
|
||||
private _clock?: NodeJS.Timeout
|
||||
|
||||
public umount() {
|
||||
if (this._clock) {
|
||||
clearInterval(this._clock)
|
||||
this._clock = undefined
|
||||
}
|
||||
}
|
||||
|
||||
protected mount() {
|
||||
this.client.on('update_time', (data: IUpdateTime) => {
|
||||
const now = Date.now()
|
||||
const age = longToNumber(data.age)
|
||||
|
||||
if (this._age) {
|
||||
this._tps = age - this._age
|
||||
if (this.tps < 20) {
|
||||
this.logger.debug('Server Lag: %s', this.tps)
|
||||
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._tpsLocal = (now - this._at) / 50
|
||||
if (this._tpsLocal > 20 + .5) {
|
||||
this.logger.debug('Client Lag: %s', this.tpsLocal)
|
||||
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)
|
||||
}
|
||||
|
||||
const offset = age - this._age
|
||||
if (Math.abs(offset) > this.conf.maxOffset) {
|
||||
this.logger.debug('Client Off-sync %s ticks', offset)
|
||||
}
|
||||
}
|
||||
|
||||
this._age = age
|
||||
this._distAge = this._age = age
|
||||
this._at = now
|
||||
this._time = Math.abs(data.time[1]) % 24000
|
||||
})
|
||||
|
||||
this._clock = setInterval(() => this._events.emit('tick', ++this._age), SERVER_TICK_RATE)
|
||||
}
|
||||
|
||||
protected getConf(): IConf {
|
||||
return {
|
||||
minTps: SERVER_TPS - 1,
|
||||
maxSpt: SERVER_TICK_RATE + 10,
|
||||
maxOffset: 1,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { IModuleType } from '../utils/Module'
|
||||
import Chat from './Chat'
|
||||
import Client from './Client'
|
||||
import Combat from './Combat'
|
||||
import Entities from './Entities'
|
||||
import Life from './Life'
|
||||
import State from './State'
|
||||
import Time from './Time'
|
||||
|
||||
/** Building modules */
|
||||
export default [
|
||||
Chat,
|
||||
Client,
|
||||
Combat,
|
||||
Entities,
|
||||
Life,
|
||||
State,
|
||||
Time,
|
||||
Life,
|
||||
Chat,
|
||||
Combat,
|
||||
]
|
||||
] as IModuleType[]
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { Client } from 'minecraft-protocol'
|
||||
import { Logger } from 'pino'
|
||||
|
||||
type ModuleLoader = <T extends Module<{}>>(module: IModuleType) => T
|
||||
|
||||
/** Typeof Module */
|
||||
export interface IModuleType {
|
||||
name: string
|
||||
new(client: Client, logger: Logger, conf: object, getModule: ModuleLoader): Module<{}>
|
||||
}
|
||||
|
||||
/** Management unit */
|
||||
export default abstract class Module<T extends object> {
|
||||
|
||||
protected logger: Logger
|
||||
protected client: Client
|
||||
protected conf: T
|
||||
|
||||
private readonly getModule: ModuleLoader
|
||||
|
||||
constructor(client: Client, logger: Logger, conf: object, getModule: ModuleLoader) {
|
||||
this.client = client
|
||||
this.logger = logger
|
||||
this.conf = {...this.getConf(), ...conf}
|
||||
this.getModule = getModule
|
||||
this.mount()
|
||||
}
|
||||
|
||||
/** Mostly to clean timers */
|
||||
public umount() {
|
||||
// nothing
|
||||
}
|
||||
|
||||
/** Default values of config */
|
||||
protected abstract getConf(): T
|
||||
|
||||
/** Setup listener on client */
|
||||
protected abstract mount(): void
|
||||
|
||||
/** Get reference to another module */
|
||||
protected load<M extends Module<{}>>(module: IModuleType): M {
|
||||
return this.getModule<M>(module)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
export const SPRINT_FOOD = 6
|
||||
export const REGENERATE_FOOD = 18
|
||||
export const MAX_FOOD = 20
|
||||
export const MAX_HEALTH = 20
|
||||
export const LOW_HEALTH = 4
|
||||
|
||||
export const REACH_DIST = 6
|
||||
export const REACH_DIST2 = REACH_DIST * REACH_DIST
|
||||
|
||||
export const ATTACK_DIST = 4
|
||||
export const ATTACK_DIST2 = ATTACK_DIST * ATTACK_DIST
|
||||
|
||||
export const VELOCITY_RATIO = 8000
|
||||
export const DELTA_RATIO = 128 * 32
|
||||
|
||||
export const SERVER_TICK_RATE = 50
|
||||
export const SERVER_TPS = 1 / SERVER_TICK_RATE * 1000
|
|
@ -0,0 +1,30 @@
|
|||
import { DELTA_RATIO, VELOCITY_RATIO } from './constants'
|
||||
import { IDelta, IMovable, IPosition } from './types'
|
||||
|
||||
export function longToNumber(arr: number[]) {
|
||||
return arr[1] + 4294967296 * arr[0]
|
||||
}
|
||||
|
||||
export function round(val: number, precision: number = 1) {
|
||||
const pow = Math.pow(10, precision)
|
||||
return Math.round((val + Number.EPSILON) * pow) / pow
|
||||
}
|
||||
|
||||
export function dist2(from: IPosition, to: IPosition) {
|
||||
const x = from.x - to.x
|
||||
const y = from.y - to.y
|
||||
const z = from.z - to.z
|
||||
return (x * x) + (y * y) + (z * z)
|
||||
}
|
||||
|
||||
export function applyVelocity(movable: IMovable) {
|
||||
movable.x += (movable.velocityX / VELOCITY_RATIO)
|
||||
movable.y += (movable.velocityY / VELOCITY_RATIO)
|
||||
movable.z += (movable.velocityZ / VELOCITY_RATIO)
|
||||
}
|
||||
|
||||
export function applyDelta(movable: IMovable, delta: IDelta) {
|
||||
movable.x += (delta.dX / DELTA_RATIO)
|
||||
movable.y += (delta.dY / DELTA_RATIO)
|
||||
movable.z += (delta.dZ / DELTA_RATIO)
|
||||
}
|