diff --git a/auth/authlib.mjs b/auth/authlib.mjs index a6d41c4..a45daf1 100644 --- a/auth/authlib.mjs +++ b/auth/authlib.mjs @@ -43,7 +43,7 @@ export class Account { password: this.password, })).data; - return new SessionCache(auth_info.accessToken, auth_info.clientToken + return new YggdrasilSession(auth_info.accessToken, auth_info.clientToken , auth_info.selectedProfile, auth_info.availableProfiles); } @@ -78,7 +78,7 @@ export class Credentials { } async authProfile(profile) { - let sc = await SessionCache.load(profile); + let sc = await YggdrasilSession.load(profile); if (sc != null && !(await sc.validate(this.endpoint))) { sc = null; } if (sc == null) { @@ -92,7 +92,7 @@ export class Credentials { } }; -export class SessionCache { +export class YggdrasilSession { constructor(accessToken, clientToken, selectedProfile, availableProfiles) { this.accessToken = accessToken; this.clientToken = clientToken; @@ -120,7 +120,7 @@ export class SessionCache { static async load(profile) { let cache_data = await readJsonFile(`.cache/${profile}.json`); if (cache_data == null) { return null; } - return new SessionCache(cache_data.accessToken, cache_data.clientToken, cache_data.selectedProfile); + return new YggdrasilSession(cache_data.accessToken, cache_data.clientToken, cache_data.selectedProfile); } async validate(endpoint) { @@ -181,3 +181,25 @@ export class SessionCache { }; } }; + +export class OfflineSession { + constructor(username) { + this.username = username; + } + + name() { return this.username; } + session() { return null; } + async store() {} + static async load(profile) { return new OfflineSession(profile); } + async validate() { return true; } + async selectProfile(profile) { this.username = profile; } + + mineflayer() { + if (this.username == null) { throw new ProfileNotSelectedError(); } + + return { + username: this.name(), + auth: 'offline', + }; + } +} diff --git a/index.mjs b/index.mjs index a90f1ba..53de1ed 100644 --- a/index.mjs +++ b/index.mjs @@ -3,7 +3,7 @@ import mineflayer from 'mineflayer'; import yargs from 'yargs'; import { parseLogin, waitEvent } from 'compass-utils'; import repl from 'node:repl'; -import vm from 'node:vm'; +import debug from 'debug'; async function main() { const args = yargs((await import('yargs/helpers')).hideBin(process.argv)) @@ -22,37 +22,62 @@ async function main() { description: 'Credentials\' library file.', type: "string", default: "credentials.json", + }).option('offline', { + description: 'Login without credentials.', + type: 'boolean', }).usage('Uasge: profile@host:port').help().alias('help', 'h').argv; let login_info = args._[0]; if (login_info == null) { return; } const [name, host, port] = parseLogin(login_info); - const credential_info = await authlib.Credentials.fromFile(args.credentialsLib); - if (credential_info == null) { - throw new Error(`Cannot load credential ${args.credentialsLib}`); + + let session, endpoint = null; + if (args.offline) { + session = new authlib.OfflineSession(name); + } else { + const credential_info = await authlib.Credentials.fromFile(args.credentialsLib); + if (credential_info == null) { + throw new Error(`Cannot load credential ${args.credentialsLib}`); + } + session = await credential_info.authProfile(name); + endpoint = credential_info.endpoint; } - const session = await credential_info.authProfile(name); const bot = mineflayer.createBot({ host, port, version: args.protocal, - ...session.mineflayer(credential_info.endpoint) + ...session.mineflayer(endpoint) }); bot.on('error', console.error); bot.on('kicked', console.log); + bot.on('end', () => { + console.log('Disconnected. Exiting...'); + process.exit(0); + }); 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(); - context.bot = bot; - context.Vec3 = (await import('vec3')).Vec3; - context.mineflayer = mineflayer; - context.owner = () => { - if (!args.owner) { return null; } - return bot.players[args.owner]; - }; + async function loadReplContextModules(context) { + context.lib = { + utils: await import('compass-utils'), + control: await import('mineflayer-control'), + }; + context.bot = bot; + context.Vec3 = (await import('vec3')).Vec3; + context.mineflayer = mineflayer; + context.owner = () => { + if (!args.owner) { return null; } + return bot.players[args.owner]; + }; + + context.sc = {}; + context.sc.pos = () => bot.entity.position; + context.sc.debug_enable = (module) => debug.enable(module); + context.sc.debug_disable = (module) => debug.disable(module); + } + if (!args.noRepl) { let r = repl.start({ prompt: 'local > ', @@ -62,7 +87,7 @@ async function main() { terminal: true, ignoreUndefined: true, }); - r.context = context; + loadReplContextModules(r.context); } } diff --git a/plugin/control/index.mjs b/plugin/control/index.mjs index 08ec86c..c8fdc48 100644 --- a/plugin/control/index.mjs +++ b/plugin/control/index.mjs @@ -1,5 +1,5 @@ import debug from 'debug'; -import { Task } from 'compass-utils'; +import { Queue, Task } from 'compass-utils'; import { Vec3 } from 'vec3'; const logger = debug('mineflayer-control'); @@ -79,6 +79,10 @@ export class MoveInterferedError extends Error { constructor() { super('Move task has been interfered by an external force.'); } }; +export class MovePathBlockedError extends Error { + constructor() { super('Move path is possiblely blocked.'); } +}; + async function moveAxisTask(bot, task, axis_raw, target_raw, level) { const axis = AXIS[axis_raw]; const stable_axis = "xz"[axis % 2]; @@ -98,7 +102,8 @@ async function moveAxisTask(bot, task, axis_raw, target_raw, level) { 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) { + if (Math.abs(delta.y) > 0.5 + Number.EPSILON + || Math.abs(delta[stable_axis]) > Number.EPSILON) { throw new Error('Invalid Argument: target'); } @@ -120,7 +125,9 @@ async function moveAxisTask(bot, task, axis_raw, target_raw, level) { controls.apply(bot); logger('moveAxisTask() started.'); - let time_used = 0; + let time_used = 0, pos_queue = new Queue(); + const TRACK_TICKS = 5; + pos_queue.push(pos.clone()); do { await bot.waitForTicks(1); task._interuptableHere(); @@ -134,13 +141,33 @@ async function moveAxisTask(bot, task, axis_raw, target_raw, level) { logger(`moveAxisTask() pos.${stable_axis}: ${pos[stable_axis]}.`); throw new MoveInterferedError(); } + + if (Math.abs(pos.y - target.y) > 0.5 + Number.EPSILON) { + logger('moveAxisTask() y changed to much.'); + logger(`moveAxisTask() target.y=${target.y} vs. pos.y=${pos.y}`); + throw new MoveInterferedError(); + } + pos[stable_axis] = stable_axis_value; + pos_queue.push(pos.clone()); + if (pos_queue.size() > TRACK_TICKS) { pos_queue.popFront(); } + + if (pos_queue.size() == 5) { + let pos5t = pos_queue.front(); + if (pos.distanceSquared(pos5t) < Number.EPSILON) { + logger('moveAxisTask() position changed too little.'); + logger(`moveAxisTask() position 5 ticks ago: ${pos5t}.`); + logger(`moveAxisTask() position now: ${pos}.`); + throw new MovePathBlockedError(); + } + } 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); + pos.x = target.x; + pos.z = target.z; bot.entity.velocity.x = 0; bot.entity.velocity.z = 0; break; @@ -162,12 +189,14 @@ export default function inject(bot) { bot.control.moveAxis = (axis, target, level = MOVE_LEVEL.SPRINT) => { let task = new Task(); - queueMicrotask(() => { - task._start(); - moveAxisTask(bot, task, axis, target, level).catch(err => { + queueMicrotask(async () => { + try { + task._start(); + await moveAxisTask(bot, task, axis, target, level); + } catch(err) { bot.clearControlStates(); task._fail(err); - }); + }; }); return task; }; diff --git a/utils/task.mjs b/utils/task.mjs index da2c125..972c42d 100644 --- a/utils/task.mjs +++ b/utils/task.mjs @@ -2,12 +2,15 @@ export class TaskInteruptedError extends Error { constructor() { super('Task has been interupted.'); } }; +let task_id = 0; + export class Task { #promise; #resolve; #reject; constructor() { + this.id = ++task_id; this.status = Task.STATUS.pending; this.error = null; this.result = null; @@ -80,11 +83,16 @@ export class Task { valueOf() { return { + id: this.id, status: Task.STATUS_NAME[this.status], result: this.result, error: this.error, }; } + + toString() { + return `[Task ${this.id}: ${Task.STATUS_NAME[this.status]}]`; + } }; Task.STATUS_NAME = {