From e785aaa6a235e2e5566f014a9bb2e4e8003f198f Mon Sep 17 00:00:00 2001 From: szdytom Date: Mon, 30 Oct 2023 22:18:05 +0800 Subject: [PATCH] Add move control plugin --- auth/authlib.mjs | 11 +-- behavior-tree/index.mjs | 64 +++++++++++++ behavior-tree/package.json | 10 ++ index.mjs | 7 +- package.json | 2 + plugin/control/index.mjs | 180 ++++++++++++++++++++++++++++++++++++ plugin/control/package.json | 11 +++ utils/task.mjs | 7 ++ 8 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 behavior-tree/index.mjs create mode 100644 behavior-tree/package.json create mode 100644 plugin/control/index.mjs create mode 100644 plugin/control/package.json diff --git a/auth/authlib.mjs b/auth/authlib.mjs index e2c22a7..a6d41c4 100644 --- a/auth/authlib.mjs +++ b/auth/authlib.mjs @@ -1,5 +1,5 @@ import axios from 'axios'; -import { readJsonFile, writeJsonFile } from '../utils/index.mjs'; +import { readJsonFile, writeJsonFile } from 'compass-utils'; export class NoCredentialError extends Error { constructor(profile) { @@ -46,6 +46,10 @@ export class Account { return new SessionCache(auth_info.accessToken, auth_info.clientToken , auth_info.selectedProfile, auth_info.availableProfiles); } + + toString() { + return this.handle; + } }; export class Credentials { @@ -177,8 +181,3 @@ export class SessionCache { }; } }; - -export default { - NoCredentialError, ProfileNotSelectedError, ProfileAlreadySelectedError - , YggdrasilEndpoint, Account, Credentials, SessionCache -}; \ No newline at end of file diff --git a/behavior-tree/index.mjs b/behavior-tree/index.mjs new file mode 100644 index 0000000..5332014 --- /dev/null +++ b/behavior-tree/index.mjs @@ -0,0 +1,64 @@ + +export class BehaviorTree { + constructor(root) { + this.root = root; + } +}; + +export class Node { + constructor() {} +}; + +export class ExecutionNode extends Node { + constructor() { + super(); + } + + isLeaf() { return true; } +}; + +export class ControlNode extends Node { + constructor() { + super(); + this.children = []; + } + + isLeaf() { return false; } + appendChild(child) { + this.children.push(child); + return this; + } +}; + +export class SequenceNode extends ControlNode { + constructor() { + super(); + } + + async tick(blackboard) { + for (let child of this.children) { + await child.tick(blackboard); + } + } +}; + +export class FallbackNode extends ControlNode { + constructor() { super(); } + + async tick(blackboard) { + for (let i = 0; i < this.children.length; i += 1) { + try { + await child.tick(blackboard); + break; + } catch(err) { + if (i == this.children.length - 1) { throw err; } + } + } + } +}; + +export class ParallelNode extends ControlNode { + constructor() { super(); } + + async tick(blackboard) {} +} diff --git a/behavior-tree/package.json b/behavior-tree/package.json new file mode 100644 index 0000000..43d848d --- /dev/null +++ b/behavior-tree/package.json @@ -0,0 +1,10 @@ +{ + "name": "compass-behavior-tree", + "description": "Behavior Tree Library", + "type": "module", + "main": "index.mjs", + "dependencies": { + "debug": "^4.3.4", + "compass-utils": "file:../utils" + } +} diff --git a/index.mjs b/index.mjs index 7f48bcb..a90f1ba 100644 --- a/index.mjs +++ b/index.mjs @@ -1,7 +1,7 @@ -import authlib from './auth/authlib.mjs'; +import * as authlib from './auth/authlib.mjs'; import mineflayer from 'mineflayer'; import yargs from 'yargs'; -import { parseLogin, waitEvent } from './utils/index.mjs'; +import { parseLogin, waitEvent } from 'compass-utils'; import repl from 'node:repl'; import vm from 'node:vm'; @@ -37,9 +37,12 @@ async function main() { host, port, version: args.protocal, ...session.mineflayer(credential_info.endpoint) }); + bot.on('error', console.error); + bot.on('kicked', console.log); await waitEvent(bot, 'inject_allowed'); bot.loadPlugin((await import('mineflayer-event-promise')).default); + bot.loadPlugin((await import('mineflayer-control')).default); await bot.waitEvent('spawn'); let context = vm.createContext(); diff --git a/package.json b/package.json index e22ea24..45f3fc2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "debug": "^4.3.4", "mineflayer": "^4.14.0", "mineflayer-event-promise": "file:plugin/event-promise", + "compass-utils": "file:utils", + "mineflayer-control": "file:plugin/control", "mineflayer-pathfinder": "^2.4.5", "prismarine-viewer": "^1.25.0", "yargs": "^17.7.2" diff --git a/plugin/control/index.mjs b/plugin/control/index.mjs new file mode 100644 index 0000000..32d6d2a --- /dev/null +++ b/plugin/control/index.mjs @@ -0,0 +1,180 @@ +import debug from 'debug'; +import { Task } from 'compass-utils'; +import { Vec3 } from 'vec3'; +const logger = debug('mineflayer-control'); + +// yaw = axis * Math.PI / 2 +// name = "ZX"[axis % 2] +export const AXIS = { + '-Z': 0, + '-X': 1, + '+Z': 2, + '+X': 3, + NORTH: 0, + WEST: 1, + SOUTH: 2, + EAST: 3, + 0: 0, + 1: 1, + 2: 2, + 3: 3, +}; + +export const AXIS_UNIT = { + 0: new Vec3(0, 0, -1), + 1: new Vec3(-1, 0, 0), + 2: new Vec3(0, 0, 1), + 3: new Vec3(1, 0, 0), +}; + +AXIS_UNIT['-Z'] = AXIS_UNIT['NORTH'] = AXIS_UNIT[0]; +AXIS_UNIT['-X'] = AXIS_UNIT['WEST'] = AXIS_UNIT[1]; +AXIS_UNIT['+Z'] = AXIS_UNIT['SOUTH'] = AXIS_UNIT[2]; +AXIS_UNIT['+X'] = AXIS_UNIT['EAST'] = AXIS_UNIT[3]; + +export const MOVE_LEVEL = { + WALK: 1, + SPRINT: 2, + // TODO: SPRINT_JUMP +}; + +export class ControlState { + constructor() { + for (let key of ControlState.CONTROLS) { + this[key] = false; + } + } + + set(cs) { + for (let key of ControlState.CONTROLS) { + this[key] = cs[key] || false; + } + } + + static from(cs) { + let res = new ControlState(); + res.set(cs); + return res; + } + + apply(bot) { + for (let key of ControlState.CONTROLS) { + bot.setControlState(key, this[key]); + } + } +}; + +ControlState.CONTROLS = ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak']; + +function adjust05(x) { + return Math.floor(x) + .5; +} + +function adjustXZ(vec) { + vec.x = adjust05(vec.x); + vec.z = adjust05(vec.z); +} + +export class MoveInterferedError extends Error { + constructor() { super('Move task has been interfered by an external force.'); } +}; + +async function moveAxisTask(bot, task, axis_raw, target_raw, level) { + const axis = AXIS[axis_raw]; + const stable_axis = "xz"[axis % 2]; + const target = target_raw.clone(); + adjustXZ(target); + + bot.clearControlStates(); + bot.control.adjustXZ(); + + let pos = bot.entity.position; + const delta = target.minus(pos); + let remaining_dis = delta.dot(AXIS_UNIT[axis]); + + logger(`moveAxisTask() source: ${pos}.`); + logger(`moveAxisTask() target: ${target}.`); + logger(`moveAxisTask() delta: ${delta}.`); + logger(`moveAxisTask() distance: ${remaining_dis}.`); + logger(`moveAxisTask() stable_axis: ${stable_axis}.`); + logger(`moveAxisTask() Condition: ${delta[stable_axis]} ${delta.y}.`); + if (Math.abs(delta.y) > Number.EPSILON || Math.abs(delta[stable_axis]) > Number.EPSILON) { + throw new Error('Invalid Argument: target'); + } + + if (remaining_dis < 0) { + throw new Error('Invalid Argument: axis argument should reverse its sign.'); + } + + const stable_axis_value = target[stable_axis]; + + logger('moveAxisTask() pre adjust look angle'); + await bot.look(axis * Math.PI / 2, 0); + logger('moveAxisTask() post adjust look angle'); + task._interuptableHere(); + + const controls = new ControlState(); + controls.forward = true; + if (level >= MOVE_LEVEL.SPRINT) { controls.sprint = true; } + logger('moveAxisTask() control', controls); + controls.apply(bot); + logger('moveAxisTask() started.'); + + let time_used = 0; + do { + await bot.waitForTicks(1); + task._interuptableHere(); + + controls.apply(bot); + time_used += 1; + pos = bot.entity.position; + if (Math.abs(pos[stable_axis] - stable_axis_value) > 1.2) { + logger('moveAxisTask() stable axis changed.'); + logger(`moveAxisTask() target.${stable_axis}: ${stable_axis_value}.`); + logger(`moveAxisTask() pos.${stable_axis}: ${pos[stable_axis]}.`); + throw new MoveInterferedError(); + } + pos[stable_axis] = stable_axis_value; + + delta.update(target.minus(pos)); + remaining_dis = delta.dot(AXIS_UNIT[axis]); + if (Math.abs(remaining_dis) <= 0.5) { + logger('moveAxisTask() very close to target now.'); + pos.update(target); + bot.entity.velocity.x = 0; + bot.entity.velocity.z = 0; + break; + } + + if (remaining_dis < -0.5) { + logger('moveAxisTask() went past target.'); + throw new MoveInterferedError(); + } + } while (true); + bot.clearControlStates(); + task._ready(time_used); +} + +export default function inject(bot) { + bot.control = {}; + bot.control.getState = () => { return ControlState.from(bot.controlState); }; + bot.control.adjustXZ = () => { adjustXZ(bot.entity.position); }; + + bot.control.moveAxis = (axis, target, level = MOVE_LEVEL.SPRINT) => { + let task = new Task(); + queueMicrotask(() => { + task._start(); + moveAxisTask(bot, task, axis, target, level).catch(err => { + bot.clearControlStates(); + task._fail(err); + }); + }); + return task; + }; + + bot.control.jump = async () => { + bot.setControlState('jump', true); + await bot.waitForTicks(1); + bot.setControlState('jump', false); + }; +} diff --git a/plugin/control/package.json b/plugin/control/package.json new file mode 100644 index 0000000..58db414 --- /dev/null +++ b/plugin/control/package.json @@ -0,0 +1,11 @@ +{ + "name": "mineflayer-control", + "description": "High-level & handy API for mineflayer bot movement control.", + "type": "module", + "main": "index.mjs", + "dependencies": { + "debug": "^4.3.4", + "vec3": "^0.1.8", + "compass-utils": "file:../../utils" + } +} diff --git a/utils/task.mjs b/utils/task.mjs index f159b4b..aabfa2a 100644 --- a/utils/task.mjs +++ b/utils/task.mjs @@ -45,6 +45,13 @@ export class Task { this.#notifyFailure(); } + _interuptableHere() { + if (this._shouldInterupt()) { + this._confirmInterupt(); + throw this.error; + } + } + get() { if (this.status == Task.STATUS.ready) { return Promise.resolve(this.result); } if (this.status == Task.STATUS.failed || this.status == Task.STATUS.interupted) {