Atomic commit
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
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/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
.env
|
env.json
|
||||||
coverage/
|
coverage/
|
||||||
node_modules/
|
node_modules/
|
||||||
build/
|
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==",
|
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
|
||||||
"dev": true
|
"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": {
|
"@types/mocha": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz",
|
||||||
|
@ -365,6 +356,12 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
|
@ -995,11 +992,6 @@
|
||||||
"is-obj": "^2.0.0"
|
"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": {
|
"duplexer3": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||||
|
@ -1932,6 +1924,12 @@
|
||||||
"picomatch": "^2.0.4"
|
"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": {
|
"supports-color": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
|
||||||
|
@ -2651,6 +2649,14 @@
|
||||||
"ini": "~1.3.0",
|
"ini": "~1.3.0",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"strip-json-comments": "~2.0.1"
|
"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": {
|
"read-pkg": {
|
||||||
|
@ -2999,10 +3005,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strip-json-comments": {
|
"strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
|
||||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
"integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "6.1.0",
|
"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"
|
"test:coverage": "nyc -r lcov -e .ts -x \"*Test.ts\" mocha -r ts-node/register test/**/*Test.ts && nyc report"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^8.2.0",
|
|
||||||
"minecraft-protocol": "^1.11.0",
|
"minecraft-protocol": "^1.11.0",
|
||||||
"pino": "^6.2.0"
|
"pino": "^6.2.0",
|
||||||
|
"strip-json-comments": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.11",
|
"@types/chai": "^4.2.11",
|
||||||
"@types/dotenv": "^8.2.0",
|
|
||||||
"@types/mocha": "^7.0.2",
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/node": "^13.11.1",
|
"@types/node": "^13.11.1",
|
||||||
"@types/pino": "^6.0.0",
|
"@types/pino": "^6.0.0",
|
||||||
|
"@types/sprintf-js": "^1.1.2",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^5.1.0",
|
||||||
"mocha": "^7.1.1",
|
"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'
|
import { readFileSync } from 'fs'
|
||||||
dotenv.config()
|
import stripJsonComments from 'strip-json-comments'
|
||||||
|
import Cubbot from './Cubbot'
|
||||||
|
|
||||||
import app from './app'
|
const config = JSON.parse(stripJsonComments(readFileSync('./env.json').toString()))
|
||||||
app()
|
|
||||||
|
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 md from 'minecraft-data'
|
||||||
import Module from './Module'
|
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 => {
|
this.client.on('chat', packet => {
|
||||||
const message = JSON.parse(packet.message)
|
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: 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 { ATTACK_DIST2, SERVER_TICK_RATE } from '../utils/constants'
|
||||||
import Module from './Module'
|
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 => {
|
this.client.on('combat_event', packet => {
|
||||||
switch (packet.event) {
|
switch (packet.event) {
|
||||||
case 0:
|
case 0:
|
||||||
this.logger.warn('fighting')
|
this.logger.warn('fighting')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 1:
|
|
||||||
this.logger.info('fighting entity %s', packet.entityId)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
this.logger.info('dead player %s by %s', packet.playerId, packet.entityId)
|
if (packet.playerId === this.state.state.entityId) {
|
||||||
//TODO: is me ?
|
this.logger.error('%o killed me at %o',
|
||||||
//TODO: print chat message
|
this.entities.getAlive(packet.entityId), this.state.position)
|
||||||
if (this.respawn) {
|
// MAYBE: chat packet.message
|
||||||
//TODO: use Life.respawn()
|
if (this.conf.respawn) {
|
||||||
this.client.write('client_command', { action: 1 })
|
this.load<Life>(Life).respawn()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.info('killed %o', packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: respawn or disconnect
|
protected getConf(): IConf {
|
||||||
//TODO: aura
|
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
|
import { applyDelta, applyVelocity } from '../utils/func'
|
||||||
//TODO: entity_metadata
|
import Module from '../utils/Module'
|
||||||
//TODO: entity_update_attributes
|
import { IDelta, IMovable, IPosition, IRotation, ISlot, IState, IVelocity } from '../utils/types'
|
||||||
//TODO: entity_equipment
|
import Time from './Time'
|
||||||
//TODO: entity_move_look
|
|
||||||
//TODO: entity_head_rotation
|
interface IId {
|
||||||
//TODO: rel_entity_move
|
entityId: number
|
||||||
//TODO: entity_velocity
|
}
|
||||||
//TODO: entity_destroy
|
|
||||||
//TODO: entity_teleport
|
interface IPositioned extends IId, IPosition { }
|
||||||
//TODO: entity_look
|
interface IMovableE extends IId, IMovable { }
|
||||||
//TODO: set_passengers
|
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 { LOW_HEALTH, MAX_HEALTH, REGENERATE_FOOD, SPRINT_FOOD } from '../utils/constants'
|
||||||
import Module from './Module'
|
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() {
|
public get health() {
|
||||||
return this._health
|
return this._health
|
||||||
}
|
}
|
||||||
|
|
||||||
private _food!: number
|
|
||||||
public get food() {
|
public get food() {
|
||||||
return this._food
|
return this._food
|
||||||
}
|
}
|
||||||
|
|
||||||
private _saturation!: number
|
|
||||||
public get saturation() {
|
public get saturation() {
|
||||||
return this._saturation
|
return this._saturation
|
||||||
}
|
}
|
||||||
|
@ -22,35 +39,67 @@ export default class Life extends Module {
|
||||||
return this.health > 0
|
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() {
|
public get experienceBar() {
|
||||||
return this._experienceBar
|
return this._experienceBar
|
||||||
}
|
}
|
||||||
|
|
||||||
private _level!: number
|
|
||||||
public get level() {
|
public get level() {
|
||||||
return this._level
|
return this._level
|
||||||
}
|
}
|
||||||
|
|
||||||
private _totalExperience!: number
|
|
||||||
public get totalExperience() {
|
public get totalExperience() {
|
||||||
return this._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 => {
|
this.client.on('update_health', data => {
|
||||||
if (this._health == null) {
|
if (this.conf.spawn && this._health == null) {
|
||||||
setTimeout(this.respawn.bind(this), 500)
|
setTimeout(this.respawn.bind(this), 500)
|
||||||
}
|
}
|
||||||
this._health = data.health
|
|
||||||
|
this._health = round(data.health)
|
||||||
this._food = data.food
|
this._food = data.food
|
||||||
this._saturation = data.foodSaturation
|
this._saturation = data.foodSaturation
|
||||||
if (this.health > 5) {
|
|
||||||
this.logger.info('health: %s', this.health)
|
if (this.health <= this.conf.lowHealth) {
|
||||||
} else {
|
this.logger.warn({ type: 'health', status: this.alive ? 'low' : 'ko', health: this.health })
|
||||||
this.logger.warn('low health: %s', 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.client.on('experience', data => {
|
||||||
this._experienceBar = data.experienceBar
|
this._experienceBar = data.experienceBar
|
||||||
this._level = data.level
|
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
|
//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: 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 '../utils/Module'
|
||||||
import Module from './Module'
|
import { IDelta, IPosition, IState } from '../utils/types'
|
||||||
|
|
||||||
//MAYBE: split
|
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDifficulty {
|
interface IDifficulty {
|
||||||
level: number,
|
level: number,
|
||||||
|
@ -21,28 +9,20 @@ interface IDifficulty {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAbilities {
|
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,
|
flyingSpeed: number,
|
||||||
walkingSpeed: number
|
walkingSpeed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPosition {
|
interface IPositionPacket extends IPosition {
|
||||||
x: number,
|
flags: number
|
||||||
y: number,
|
|
||||||
z: number,
|
|
||||||
yaw: number,
|
|
||||||
pitch: number,
|
|
||||||
flags: number,
|
|
||||||
teleportId: number
|
teleportId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: entity_status {entityId, entityStatus} aka animations
|
// TODO: player_info
|
||||||
|
|
||||||
//TODO: player_info
|
/** Handle client state and position */
|
||||||
|
export default class State extends Module<{ positionConfirm: boolean }> {
|
||||||
export default class State extends Module {
|
|
||||||
|
|
||||||
public positionConfirm: boolean = true
|
|
||||||
|
|
||||||
private _state!: IState
|
private _state!: IState
|
||||||
public get state() {
|
public get state() {
|
||||||
|
@ -64,17 +44,26 @@ export default class State extends Module {
|
||||||
return this._position
|
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('login', data => this._state = data)
|
||||||
this.client.on('difficulty', data => this._difficulty = {
|
this.client.on('difficulty', data => this._difficulty = {
|
||||||
level: data.difficulty, locked: data.difficultyLocked,
|
level: data.difficulty, locked: data.difficultyLocked,
|
||||||
})
|
})
|
||||||
this.client.on('abilities', data => this._abilities = data)
|
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 (this._position == null) {
|
||||||
if (data.flags) {
|
if (data.flags) {
|
||||||
this.logger.error('first postion is relative')
|
this.logger.error('first position is relative')
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
this._position = data
|
this._position = data
|
||||||
|
@ -87,16 +76,20 @@ export default class State extends Module {
|
||||||
z: data.z + (data.flags & 0x04 ? this._position.z : 0),
|
z: data.z + (data.flags & 0x04 ? this._position.z : 0),
|
||||||
yaw: data.yaw + (data.flags & 0x08 ? this._position.yaw : 0),
|
yaw: data.yaw + (data.flags & 0x08 ? this._position.yaw : 0),
|
||||||
pitch: data.pitch + (data.flags & 0x10 ? this._position.pitch : 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.client.write('teleport_confirm', { teleportId: data.teleportId })
|
||||||
}
|
}
|
||||||
this.logger.debug({ type: 'teleported', position: this.position })
|
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 { EventEmitter } from 'events'
|
||||||
import { longToNumber } from '../core/utils'
|
import { SERVER_TICK_RATE, SERVER_TPS } from '../utils/constants'
|
||||||
import Module from './Module'
|
import { longToNumber } from '../utils/func'
|
||||||
|
import Module from '../utils/Module'
|
||||||
|
|
||||||
interface IUpdateTime {
|
interface IUpdateTime {
|
||||||
age: number[]
|
age: number[]
|
||||||
time: 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
|
private _age!: number
|
||||||
|
/** Ticks since world creation */
|
||||||
public get age() {
|
public get age() {
|
||||||
return this._age
|
return this._age
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _distAge!: number
|
||||||
|
|
||||||
private _time!: number
|
private _time!: number
|
||||||
|
/** Ticks of the day */
|
||||||
public get time() {
|
public get time() {
|
||||||
return this._time
|
return this._time
|
||||||
}
|
}
|
||||||
|
|
||||||
private _tps!: number
|
private _tps!: number
|
||||||
|
/** Processed Tick per seconds */
|
||||||
public get tps() {
|
public get tps() {
|
||||||
return this._tps
|
return this._tps
|
||||||
}
|
}
|
||||||
|
|
||||||
private _tpsLocal!: number
|
private _spt!: number
|
||||||
public get tpsLocal() {
|
/** Processed Tick per seconds */
|
||||||
return this._tpsLocal
|
public get spt() {
|
||||||
|
return this._spt
|
||||||
}
|
}
|
||||||
|
|
||||||
private _at: number = Date.now()
|
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) => {
|
this.client.on('update_time', (data: IUpdateTime) => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const age = longToNumber(data.age)
|
const age = longToNumber(data.age)
|
||||||
|
|
||||||
if (this._age) {
|
if (this._distAge) {
|
||||||
this._tps = age - this._age
|
this._tps = age - this._distAge
|
||||||
if (this.tps < 20) {
|
if (this.tps <= this.conf.minTps) {
|
||||||
this.logger.debug('Server Lag: %s', this.tps)
|
this.logger.debug('Server Lag: %s / 20 tps', this.tps, SERVER_TPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tpsLocal = (now - this._at) / 50
|
this._spt = (now - this._at) / this.tps
|
||||||
if (this._tpsLocal > 20 + .5) {
|
if (this.spt >= this.conf.maxSpt) {
|
||||||
this.logger.debug('Client Lag: %s', this.tpsLocal)
|
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._at = now
|
||||||
this._time = Math.abs(data.time[1]) % 24000
|
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 Chat from './Chat'
|
||||||
|
import Client from './Client'
|
||||||
import Combat from './Combat'
|
import Combat from './Combat'
|
||||||
|
import Entities from './Entities'
|
||||||
import Life from './Life'
|
import Life from './Life'
|
||||||
import State from './State'
|
import State from './State'
|
||||||
import Time from './Time'
|
import Time from './Time'
|
||||||
|
|
||||||
|
/** Building modules */
|
||||||
export default [
|
export default [
|
||||||
|
Chat,
|
||||||
|
Client,
|
||||||
|
Combat,
|
||||||
|
Entities,
|
||||||
|
Life,
|
||||||
State,
|
State,
|
||||||
Time,
|
Time,
|
||||||
Life,
|
] as IModuleType[]
|
||||||
Chat,
|
|
||||||
Combat,
|
|
||||||
]
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
export interface ICoordinates {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRotation {
|
||||||
|
yaw: number
|
||||||
|
pitch: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IVelocity {
|
||||||
|
velocityX: number
|
||||||
|
velocityY: number
|
||||||
|
velocityZ: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDelta {
|
||||||
|
dX: number
|
||||||
|
dY: number
|
||||||
|
dZ: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPosition extends ICoordinates, IRotation { }
|
||||||
|
export interface IMovable extends IPosition, IVelocity { }
|
||||||
|
|
||||||
|
export interface ISlot {
|
||||||
|
present: boolean
|
||||||
|
itemId: number
|
||||||
|
itemCount: number
|
||||||
|
nbtData: object
|
||||||
|
//TODO:
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IState {
|
||||||
|
entityId: number,
|
||||||
|
gameMode: 0|1|2|3,
|
||||||
|
dimension: number,
|
||||||
|
hashedSeed: number[],
|
||||||
|
maxPlayers: number,
|
||||||
|
levelType: string,
|
||||||
|
viewDistance: number,
|
||||||
|
reducedDebugInfo: boolean,
|
||||||
|
enableRespawnScreen: boolean
|
||||||
|
}
|
|
@ -13,7 +13,8 @@
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"downlevelIteration": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|
Loading…
Reference in New Issue