From 468d197c64c251ac8d4c40e04f254db58c74716a Mon Sep 17 00:00:00 2001 From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com> Date: Wed, 17 Jun 2026 00:15:14 +0200 Subject: [PATCH 1/3] Add exclusion zones to movement settings Add "keep out" exclusion zones to the movement settings, matching the exclusion-area concept from upstream PrismarineJS/mineflayer-pathfinder. Settings (all default to []): - exclusionAreasStep -> blocks the bot may not stand in / walk into - exclusionAreasBreak -> blocks the bot may not break (mine) - exclusionAreasPlace -> blocks the bot may not place (build on) Each area is a function (block: BlockInfo) => number returning the extra cost of a block; a summed weight >= COST_INF forbids it, a positive value is a soft penalty. Empty lists cost nothing to evaluate. Implementation: - Step exclusion is folded into each movement provider, on the block the bot lands in (already fetched while generating the move), before the move is created -- Move.cost stays readonly and there is no extra block lookup. Forbidden landings are simply never generated. - Break/place exclusion is folded into Movement.breakCost / placeCost. - The optimizer refuses to straight-line a merge whose swept segment crosses a hard step zone (exact Amanatides-Woo voxel traversal), so optimized paths never cut through "keep out" terrain the A* route went around. - buildMovementOptions() hands out fresh copies of the three arrays (defaults frozen) so bots/sessions never share or mutate the same array. Only the ExclusionArea type is exported; users write their own zone functions (ready-to-copy box/radius helpers are in examples/exclusionZones.js). Tests, docs (API + AdvancedUsage), and an example included. --- docs/API.md | 56 ++++ docs/AdvancedUsage.md | 35 +++ examples/README.md | 1 + examples/exclusionZones.js | 122 ++++++++ src/ThePathfinder.ts | 6 +- src/index.ts | 6 + .../movements/exclusionZones.ts | 16 ++ src/mineflayer-specific/movements/index.ts | 1 + src/mineflayer-specific/movements/movement.ts | 115 +++++++- .../movements/movementProvider.ts | 4 +- .../movements/movementProviders.ts | 65 +++-- src/mineflayer-specific/post/optimizer.ts | 77 ++++- tests/exclusionZones.test.ts | 264 ++++++++++++++++++ 13 files changed, 734 insertions(+), 34 deletions(-) create mode 100644 examples/exclusionZones.js create mode 100644 src/mineflayer-specific/movements/exclusionZones.ts create mode 100644 tests/exclusionZones.test.ts diff --git a/docs/API.md b/docs/API.md index cc4f1a6..73115ed 100644 --- a/docs/API.md +++ b/docs/API.md @@ -25,6 +25,7 @@ - [GoalCompositeAny](#goalcompositeany) - [GoalCompositeAll](#goalcompositeall) - [Settings](#settings) +- [Exclusion Zones](#exclusion-zones) - [Events](#events) - [pathGenerated](#pathGenerated) - [goalSet](#goalSet) @@ -560,6 +561,9 @@ These are the currently available settings. | `infiniteLiquidDropdownDistance` | `boolean` | Whether or not to have an infinite liquid dropdown distance. | `true` | | `allowSprinting` | `boolean` | Whether or not to allow sprinting. | `true` | | `careAboutLookAlignment` | `boolean` | Whether or not to care about look alignment. | `true` | +| `exclusionAreasStep` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **stand in**. See [Exclusion Zones](#exclusion-zones). | `[]` | +| `exclusionAreasBreak` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **break** (mine). | `[]` | +| `exclusionAreasPlace` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **place** (build on). | `[]` | ```ts @@ -583,12 +587,64 @@ interface MovementOptions { infiniteLiquidDropdownDistance: boolean allowSprinting: boolean careAboutLookAlignment: boolean + + movementTimeoutMs: number + + // "Keep out" zones. Empty by default. See the Exclusion Zones section below. + exclusionAreasStep: ExclusionArea[] + exclusionAreasBreak: ExclusionArea[] + exclusionAreasPlace: ExclusionArea[] } ``` +

Exclusion Zones

+ +Exclusion zones mark areas as off-limits. An exclusion area is a function +returning the extra cost of a block; the same idea as upstream +[`PrismarineJS/mineflayer-pathfinder`](https://github.com/PrismarineJS/mineflayer-pathfinder). + +```ts +type ExclusionArea = (block: BlockInfo) => number +``` + +- `0` — no opinion. +- a positive number — soft zone: allowed, but avoided when a cheaper route exists. +- `Infinity` (`>= COST_INF`) — hard zone: never used. + +Areas live in three settings; each sums the cost of every function in its list: + +| Setting | Checked on every block the bot would… | +| --- | --- | +| `exclusionAreasStep` | stand in (the foot block of each move — walk, jump, drop, parkour, tower; optimized paths included). | +| `exclusionAreasBreak` | break (mine). | +| `exclusionAreasPlace` | place (build on). | + +Empty by default, so there is zero overhead when unused. The library ships no +ready-made shapes — write your own. Box/radius helpers are in +[`examples/exclusionZones.js`](../examples/exclusionZones.js). + +```ts +// Never break or place inside a protected box; soft-avoid stepping on farmland. +const farmlandId = bot.registry.blocksByName.farmland.id +const inBox = (block) => + block.position.x >= -10 && block.position.x <= 10 && + block.position.z >= -10 && block.position.z <= 10 ? Infinity : 0 + +bot.pathfinder.setMoveOptions({ + exclusionAreasStep: [(block) => block.type === farmlandId ? 100 : 0], + exclusionAreasBreak: [inBox], + exclusionAreasPlace: [inBox] +}) +``` + +`exclusionAreasStep` is checked on the block the bot's **feet** land in; extend a +box one block lower if you also need the head kept out. + + +

Events

diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md index 6b20fe2..0d8691d 100644 --- a/docs/AdvancedUsage.md +++ b/docs/AdvancedUsage.md @@ -9,6 +9,7 @@ - [Custom Movement Providers](#custom-movement-providers) - [Custom Movement Executors](#custom-movement-executors) - [Custom Movement Optimizers](#custom-movement-optimizers) +- [Exclusion Zones (Keep-Out Areas)](#exclusion-zones-keep-out-areas) @@ -311,3 +312,37 @@ class MyMovementOptimizer extends MovementOptimizer { } } ``` + +

Exclusion Zones (Keep-Out Areas)

+ +To keep the bot out of an area without writing a custom `MovementProvider`, add +an **exclusion area** to the move settings. It is a function returning the extra +cost of a block: `0` (no opinion), a positive number (soft — avoided when a +cheaper route exists), or `Infinity` (hard — never used). + +Three lists, each summing the cost of every function in it: + +- `exclusionAreasStep` — blocks the bot would stand in. +- `exclusionAreasBreak` — blocks the bot would break (mine). +- `exclusionAreasPlace` — blocks the bot would place (build on). + +All empty by default (no overhead). Any `(block) => number` works: + +```ts +// Never mine valuable ores or anything below y=0. +const diamondId = bot.registry.blocksByName.diamond_ore.id +const protectOres = (block) => + (block.type === diamondId || block.position.y < 0) ? Infinity : 0 + +bot.pathfinder.setMoveOptions({ exclusionAreasBreak: [protectOres] }) +``` + +Notes: + +- Costs from multiple areas in a list add up. +- `exclusionAreasStep` is checked on the foot block of every move (walk, jump, + drop, parkour, tower), and optimized/straight-lined paths are blocked from + cutting through hard zones too. +- Pass a fresh array to `setMoveOptions({ ... })` rather than mutating the + existing one. +- Ready-to-copy box/radius helpers live in `examples/exclusionZones.js`. diff --git a/examples/README.md b/examples/README.md index 61bed6e..aaa1e16 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,3 +2,4 @@ `basic.js` shows off the basic functionality of this pathfinder, while `example.js` goes into more depth. `bridging/bridge.ts` is the bridge demo, and `neos/neo.ts` is a neo-jump-focused variant based on the same bot setup. +`exclusionZones.js` shows how to add "keep out" areas (exclusion zones) to the movement settings. diff --git a/examples/exclusionZones.js b/examples/exclusionZones.js new file mode 100644 index 0000000..3bf20aa --- /dev/null +++ b/examples/exclusionZones.js @@ -0,0 +1,122 @@ +'use strict' + +// --------------------------------------------------------------------------- +// Exclusion zones example +// +// An "exclusion area" is just a function (block) => number that returns the +// extra cost of using a block: 0 = fine, a positive number = soft avoid, +// Infinity = hard "keep out". The library ships no ready-made shapes on +// purpose, so the small box/radius builders below are yours to copy and adapt. +// +// They go into three movement settings: +// exclusionAreasStep -> blocks the bot may stand in / walk into +// exclusionAreasBreak -> blocks the bot may break (mine) +// exclusionAreasPlace -> blocks the bot may place (build on) +// +// Run a local server, then: node examples/exclusionZones.js +// In chat: "goto ", "zones on", "zones off". +// --------------------------------------------------------------------------- + +const { createBot } = require('mineflayer') +const { Vec3 } = require('vec3') +const { createPlugin, goals } = require('../dist') + +const { GoalBlock } = goals + +// --- copy these helpers into your own project ------------------------------ + +// A box between two opposite corners (inclusive, any order). +function boxExclusion (corner1, corner2, cost = Infinity) { + const minX = Math.min(corner1.x, corner2.x) + const minY = Math.min(corner1.y, corner2.y) + const minZ = Math.min(corner1.z, corner2.z) + const maxX = Math.max(corner1.x, corner2.x) + const maxY = Math.max(corner1.y, corner2.y) + const maxZ = Math.max(corner1.z, corner2.z) + return (block) => { + const p = block.position + const inside = + p.x >= minX && p.x <= maxX && + p.y >= minY && p.y <= maxY && + p.z >= minZ && p.z <= maxZ + return inside ? cost : 0 + } +} + +// A ball (sphere) of the given radius around a center point. +function radiusExclusion (center, radius, cost = Infinity) { + const r2 = radius * radius + return (block) => { + const dx = block.position.x - center.x + const dy = block.position.y - center.y + const dz = block.position.z - center.z + return dx * dx + dy * dy + dz * dz <= r2 ? cost : 0 + } +} + +// --------------------------------------------------------------------------- + +const bot = createBot({ + username: 'exclusion-demo', + auth: 'offline', + host: 'localhost', + port: 25565 +}) + +bot.loadPlugin(createPlugin()) + +// Example zones (tweak the coordinates to match your world): +const noGoBox = boxExclusion(new Vec3(-8, 60, -8), new Vec3(8, 80, 8)) // hard +const softBall = radiusExclusion(new Vec3(30, 64, 30), 6, 50) // soft + +function enableZones () { + bot.pathfinder.setMoveOptions({ + exclusionAreasStep: [noGoBox, softBall] + }) + bot.chat('Exclusion zones: ON') +} + +function disableZones () { + bot.pathfinder.setMoveOptions({ + exclusionAreasStep: [], + exclusionAreasBreak: [], + exclusionAreasPlace: [] + }) + bot.chat('Exclusion zones: OFF') +} + +bot.once('spawn', () => { + enableZones() + bot.chat('Ready. Try: "goto ", "zones on", "zones off".') +}) + +bot.on('chat', async (username, message) => { + if (username === bot.username) return + + const [cmd, ...args] = message.trim().split(/\s+/) + + if (cmd === 'zones') { + if (args[0] === 'off') disableZones() + else enableZones() + return + } + + if (cmd === 'goto') { + const [x, y, z] = args.map(Number) + if ([x, y, z].some(Number.isNaN)) { + bot.chat('Usage: goto ') + return + } + + bot.chat(`Heading to ${x} ${y} ${z}, avoiding the zones...`) + try { + await bot.pathfinder.goto(new GoalBlock(x, y, z)) + bot.chat('Arrived!') + } catch (err) { + bot.chat(`Could not get there: ${err.message}`) + } + } +}) + +bot.on('kicked', console.log) +bot.on('error', console.log) diff --git a/src/ThePathfinder.ts b/src/ThePathfinder.ts index 173337f..8ab1b14 100644 --- a/src/ThePathfinder.ts +++ b/src/ThePathfinder.ts @@ -15,7 +15,7 @@ import type { import { MovementHandler, MovementExecutor, - DEFAULT_MOVEMENT_OPTS + buildMovementOptions } from './mineflayer-specific/movements' import { @@ -209,7 +209,7 @@ export class ThePathfinder { const optimizers = opts.optimizers ?? DEFAULT_OPTIMIZATION const moveSetup = opts.movements ?? DEFAULT_SETUP - Object.assign(moveSettings, { ...DEFAULT_MOVEMENT_OPTS, ...opts.moveSettings }) + Object.assign(moveSettings, buildMovementOptions(opts.moveSettings)) Object.assign(pathfinderSettings, { ...DEFAULT_PATHFINDER_OPTS, ...opts.pathfinderSettings }) const moves = new Map() @@ -279,7 +279,7 @@ export class ThePathfinder { } setMoveOptions(settings: Partial): void { - this.defaultMoveSettings = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings) + this.defaultMoveSettings = buildMovementOptions(settings) this.optimizerRegistry.setSettings(this.defaultMoveSettings) for (const [, executor] of this.movements) { executor.settings = this.defaultMoveSettings diff --git a/src/index.ts b/src/index.ts index 4805c55..933b692 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,12 @@ export * as goals from './mineflayer-specific/goals' export { MovementExecutor, MovementProvider } from './mineflayer-specific/movements' export type { BuildableMoveExecutor, BuildableMoveProvider, MovementSetup } from './mineflayer-specific/movements' +export type { MovementOptions } from './mineflayer-specific/movements' + +// Exclusion zones ("keep out" areas), like upstream mineflayer-pathfinder. +// Only the type is exported; users write their own zone functions +// (see examples/exclusionZones.js for ready-to-copy box/radius helpers). +export type { ExclusionArea } from './mineflayer-specific/movements/exclusionZones' export { MovementOptimizer } from './mineflayer-specific/post' export type { BuildableMoveOptimizer, OptimizationSetup, OptimizationMap } from './mineflayer-specific/post' export { Move } from './mineflayer-specific/move' diff --git a/src/mineflayer-specific/movements/exclusionZones.ts b/src/mineflayer-specific/movements/exclusionZones.ts new file mode 100644 index 0000000..13170e7 --- /dev/null +++ b/src/mineflayer-specific/movements/exclusionZones.ts @@ -0,0 +1,16 @@ +import type { BlockInfo } from '../world/cacheWorld' + +/** + * An exclusion area: a function that returns the extra cost of letting the bot + * use a given block. + * + * - `0` -> no opinion on this block. + * - a positive number -> a soft penalty; the bot avoids the block when it can. + * - `>= COST_INF` -> a hard "keep out"; the bot will never use the block. + * + * These are stored in the three movement settings `exclusionAreasStep`, + * `exclusionAreasBreak` and `exclusionAreasPlace`. The pathfinder intentionally + * ships no ready-made shapes — write your own, or copy the box/radius helpers + * from `examples/exclusionZones.js`. This mirrors upstream mineflayer-pathfinder. + */ +export type ExclusionArea = (block: BlockInfo) => number diff --git a/src/mineflayer-specific/movements/index.ts b/src/mineflayer-specific/movements/index.ts index e62d625..c03624b 100644 --- a/src/mineflayer-specific/movements/index.ts +++ b/src/mineflayer-specific/movements/index.ts @@ -16,4 +16,5 @@ export * from './movementExecutors' export * from './movementProviders' export * from './movementExecutor' export * from './movementProvider' +export * from './exclusionZones' // export * from './pp' diff --git a/src/mineflayer-specific/movements/movement.ts b/src/mineflayer-specific/movements/movement.ts index 40d824f..ff43bd5 100644 --- a/src/mineflayer-specific/movements/movement.ts +++ b/src/mineflayer-specific/movements/movement.ts @@ -9,6 +9,7 @@ import type { InteractType } from './interactionUtils' import type { Block } from '../../types' import { Vec3Properties } from '../../types' import { COST_INF } from './costs' +import type { ExclusionArea } from './exclusionZones' export interface MovementOptions { allowDiagonalBridging: boolean @@ -32,6 +33,25 @@ export interface MovementOptions { careAboutLookAlignment: boolean movementTimeoutMs: number + + /** + * "Keep out" rules for blocks the bot would STAND in / walk into. + * + * Each {@link ExclusionArea} is a function that returns the extra cost of a + * block (return `>= COST_INF` to forbid it entirely). The cost of every area + * in the list is added together. An empty list (the default) means "no zones", + * and costs nothing to evaluate. + * + * Write your own; ready-to-copy box/radius helpers live in + * `examples/exclusionZones.js`. + */ + exclusionAreasStep: ExclusionArea[] + + /** "Keep out" rules for blocks the bot would BREAK (mine). See {@link exclusionAreasStep}. */ + exclusionAreasBreak: ExclusionArea[] + + /** "Keep out" rules for blocks the bot would PLACE (build on). See {@link exclusionAreasStep}. */ + exclusionAreasPlace: ExclusionArea[] } export const DEFAULT_MOVEMENT_OPTS: MovementOptions = { @@ -53,7 +73,49 @@ export const DEFAULT_MOVEMENT_OPTS: MovementOptions = { forceLook: true, careAboutLookAlignment: true, allowDiagonalBridging: true, - movementTimeoutMs: 1000 + movementTimeoutMs: 1000, + // No exclusion zones by default. Add your own with bot.pathfinder.setMoveOptions(...). + exclusionAreasStep: [], + exclusionAreasBreak: [], + exclusionAreasPlace: [] +} + +// Lock the default exclusion lists so the single shared instances above can never +// be mutated in place. Real settings always receive their own fresh arrays via +// buildMovementOptions() below, so this is just a safety net. +Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep) +Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasBreak) +Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasPlace) + +/** + * Merge user-supplied movement options on top of {@link DEFAULT_MOVEMENT_OPTS} + * and return a complete {@link MovementOptions}. + * + * The three exclusion-area lists are ALWAYS returned as their own fresh arrays. + * The defaults hold a single shared `[]` per list, so copying here is what stops + * two different bots — or two `setMoveOptions` calls — from accidentally sharing + * (and then mutating) the same array. Use this everywhere instead of a bare + * `Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)`. + */ +export function buildMovementOptions (settings: Partial = {}): MovementOptions { + const merged = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings) + merged.exclusionAreasStep = [...merged.exclusionAreasStep] + merged.exclusionAreasBreak = [...merged.exclusionAreasBreak] + merged.exclusionAreasPlace = [...merged.exclusionAreasPlace] + return merged +} + +/** + * Sum the extra cost every exclusion area in `areas` assigns to `block`. + * + * Returns 0 immediately when the list is empty (the normal case), so it is + * essentially free unless the user opted in to exclusion zones. + */ +export function sumExclusionAreas (areas: ExclusionArea[], block: BlockInfo): number { + if (areas.length === 0) return 0 + let weight = 0 + for (const area of areas) weight += area(block) + return weight } const cardinalVec3s: Vec3[] = [ @@ -136,7 +198,7 @@ export abstract class Movement { public constructor (bot: Bot, world: World, settings: Partial = {}) { this.bot = bot this.world = world - this.settings = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings) + this.settings = buildMovementOptions(settings) } loadMove (move: Move): void { @@ -182,8 +244,29 @@ export abstract class Movement { return block.physical ? 0 : COST_INF } + /** Extra cost of STANDING in this block (sum of every step exclusion area; 0 if none). */ + exclusionStep (block: BlockInfo): number { + return sumExclusionAreas(this.settings.exclusionAreasStep, block) + } + + /** Extra cost of BREAKING this block (sum of every break exclusion area; 0 if none). */ + exclusionBreak (block: BlockInfo): number { + return sumExclusionAreas(this.settings.exclusionAreasBreak, block) + } + + /** Extra cost of PLACING a block here (sum of every place exclusion area; 0 if none). */ + exclusionPlace (block: BlockInfo): number { + return sumExclusionAreas(this.settings.exclusionAreasPlace, block) + } + /** - * Takes into account if the block is within a break exclusion area. + * Whether this block is allowed to be broken at all (ignoring cost). + * + * This only answers the "is it physically/configurably breakable" question + * (can we dig, would it create flowing water, would a block fall on us, is it + * an unbreakable block like bedrock). The "is it inside a no-mining zone" + * question is handled separately as a cost, in {@link breakCost} via + * {@link exclusionBreak}. * @param {BlockInfo} block * @returns */ @@ -208,17 +291,23 @@ export abstract class Movement { } // console.log('block type:', this.bot.registry.blocks[block.type], block.position, !BlockInfo.blocksCantBreak.has(block.type)) - return BlockInfo.replaceables.has(block.type) || !BlockInfo.blocksCantBreak.has(block.type) // && this.exclusionBreak(block) < COST_INF + return BlockInfo.replaceables.has(block.type) || !BlockInfo.blocksCantBreak.has(block.type) } /** - * Takes into account if the block is within the stepExclusionAreas. And returns COST_INF if a block to be broken is within break exclusion areas. - * @param {import('prismarine-block').Block} block block - * @param {[]} toBreak + * Cost of either walking through `block` (if it is already passable) or + * breaking it so the bot can pass. + * + * Returns `COST_INF` (or more) when the block cannot be used — for example an + * unbreakable block, or one inside a break-exclusion zone (see + * {@link breakCost} / {@link exclusionBreak}). Step-exclusion zones are NOT + * checked here; each movement provider applies {@link exclusionStep} to the + * block the bot lands in, folding the cost in before the move is created. + * @param {BlockInfo} block block + * @param {BreakHandler[]} toBreak * @returns {number} */ safeOrBreak (block: BlockInfo, toBreak: BreakHandler[]): number { - // cost += this.exclusionStep(block) // Is excluded so can't move or break // cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost // if (block.breakCost !== undefined) return block.breakCost // cache breaking cost. @@ -256,7 +345,11 @@ export abstract class Movement { // const effects = this.bot.entity.effects // const digTime = block.block.digTime(tool ? tool.type : null, false, false, false, enchants, effects) const laborCost = (1 + 3 * digTime / 1000) * this.settings.digCost - return laborCost + + // Add the break-exclusion penalty (0 unless the user configured "no mining" zones). + // If the block sits inside a forbidden zone this pushes the cost past COST_INF, + // which every caller treats as "do not break this block". + return laborCost + this.exclusionBreak(block) } safeOrPlace (block: BlockInfo, toPlace: PlaceHandler[], type: InteractType = 'solid'): number { @@ -278,7 +371,9 @@ export abstract class Movement { * TODO: calculate more accurate place costs. */ placeCost (block: BlockInfo): number { - return this.settings.placeCost + // Add the place-exclusion penalty (0 unless the user configured "no building" zones). + // A forbidden zone pushes this past COST_INF, which callers treat as "do not place here". + return this.settings.placeCost + this.exclusionPlace(block) } } diff --git a/src/mineflayer-specific/movements/movementProvider.ts b/src/mineflayer-specific/movements/movementProvider.ts index 7558048..2bc07cb 100644 --- a/src/mineflayer-specific/movements/movementProvider.ts +++ b/src/mineflayer-specific/movements/movementProvider.ts @@ -2,7 +2,7 @@ import { Bot } from 'mineflayer' import { Move } from '../move' import * as goals from '../goals' import { World } from '../world/worldInterface' -import { DEFAULT_MOVEMENT_OPTS, Movement, MovementOptions } from './movement' +import { Movement, MovementOptions, buildMovementOptions } from './movement' import { MovementProvider as AMovementProvider } from '../../abstract' import type { ExecutorMap } from '.' @@ -137,7 +137,7 @@ export class MovementHandler implements AMovementProvider { recMovement: ExecutorMap, settings: Partial = {} ): MovementHandler { - const opts = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings) + const opts = buildMovementOptions(settings) return new MovementHandler( bot, world, diff --git a/src/mineflayer-specific/movements/movementProviders.ts b/src/mineflayer-specific/movements/movementProviders.ts index 059c356..5fc9581 100644 --- a/src/mineflayer-specific/movements/movementProviders.ts +++ b/src/mineflayer-specific/movements/movementProviders.ts @@ -67,6 +67,10 @@ export class Forward extends MovementProvider { if ((cost += this.safeOrBreak(blockB, toBreak)) > COST_INF) return if ((cost += this.safeOrBreak(blockC, toBreak)) > COST_INF) return + // Exclusion zones: blockC is where the bot's feet end up. Fold the step cost + // in now (before the move exists) so Move.cost stays read-only. + if ((cost += this.exclusionStep(blockC)) > COST_INF) return + // set cachedVec to center of wanted block neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), start, this, toPlace, toBreak)) } @@ -133,6 +137,9 @@ export class Diagonal extends MovementProvider { cost += this.safeOrBreak(this.getBlockInfo(node, 0, 1, dir.z), toBreak) if (cost > COST_INF) return + // Exclusion zones: block0 is the destination foot block. + if ((cost += this.exclusionStep(block0)) > COST_INF) return + neighbors.push(Move.fromPrevious(cost, block0.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak)) } } @@ -198,8 +205,6 @@ export class ForwardJump extends MovementProvider { if ((cost += this.breakCost(blockD)) > COST_INF) return toBreak.push(BreakHandler.fromVec(blockD.position, 'solid')) } - // cost += this.exclusionPlace(blockD) - if ((cost += this.safeOrPlace(blockD, toPlace, 'solid')) > COST_INF) return } @@ -221,6 +226,9 @@ export class ForwardJump extends MovementProvider { if ((cost += this.safeOrBreak(blockH, toBreak)) > COST_INF) return if (toPlace.length > 0) return + // Exclusion zones: blockB is the destination foot block. + if ((cost += this.exclusionStep(blockB)) > COST_INF) return + // set cachedVec to center of block we want. neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak)) } @@ -316,6 +324,9 @@ export class ForwardDropDown extends DropDownProvider { if ((cost += this.safeOrBreak(blockC, toBreak)) > COST_INF) return if ((cost += this.safeOrBreak(blockD, toBreak)) > COST_INF) return + // Exclusion zones: blockLand is where the bot lands and stands. + if ((cost += this.exclusionStep(blockLand)) > COST_INF) return + // cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(Move.fromPrevious(cost, blockLand.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak)) } @@ -351,6 +362,9 @@ export class StraightDown extends DropDownProvider { if ((cost += this.safeOrBreak(block1, toBreak)) > COST_INF) return + // Exclusion zones: blockLand is where the bot lands and stands. + if ((cost += this.exclusionStep(blockLand)) > COST_INF) return + // cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(Move.fromPrevious(cost, blockLand.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak)) @@ -402,6 +416,12 @@ export class StraightUp extends MovementProvider { } } + // Exclusion zones: the bot ends up standing one block above (node y + 1). + // Only do the (cache-routed) block lookup when step zones are configured. + if (this.settings.exclusionAreasStep.length > 0) { + if ((cost += this.exclusionStep(this.getBlockInfo(node, 0, 1, 0))) > COST_INF) return + } + neighbors.push(Move.fromPrevious(cost, block1.position.offset(0.5, 1, 0.5), node, this, toPlace, toBreak)) } } @@ -470,31 +490,33 @@ export class ParkourForward extends MovementProvider { // Down const blockE = this.getBlockInfo(node, dx, -2, dz) if (blockE.physical) { // TODO: support jumping into liquid. - // cost += this.exclusionStep(blockD) - // cost += this.getNumEntitiesAt(blockD.position, 0, 0, 0) * this.entityCost - neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this)) - // neighbors.push(new Move(blockD.position.x, blockD.position.y, blockD.position.z, node.remainingBlocks, cost, [], [], true)) + // Exclusion zones: blockD is where the bot lands. Fold the step cost in + // before creating the move, and skip the move entirely if it is forbidden. + const stepCost = this.exclusionStep(blockD) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this)) + } } floorCleared = floorCleared && !blockE.physical } else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical) { // if (d === 5) continue const cost1 = cost + 3 // potential slowdown (will fix later.) - // cost += this.exclusionStep(blockB) // Forward - - neighbors.push(Move.fromPrevious(cost1, blockC.position.offset(0.5, 0, 0.5), node, this)) - // neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, [], [], true)) + const stepCost = this.exclusionStep(blockC) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost1 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this)) + } break } else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical) { // Up if (d === 5) continue // 4 Blocks forward 1 block up is very difficult and fails often - // cost += this.exclusionStep(blockA) if (blockC.height - block0.height > 1.2) break // Too high to jump - // cost += this.getNumEntitiesAt(blockB.position, 0, 0, 0) * this.entityCost - neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this)) - // neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks, cost, [], [], true)) + const stepCost = this.exclusionStep(blockB) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this)) + } break // } } else if (!blockB.walkthrough || !blockC.walkthrough) { @@ -609,16 +631,25 @@ export class ParkourDiagonal extends MovementProvider { if (flag0 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.walkthrough && blockFrontD.walkthrough && !floorCleared) { if (blockE.physical) { - neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this)) + const stepCost = this.exclusionStep(blockD) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this)) + } return true } } else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical && blockFrontC.walkthrough) { - neighbors.push(Move.fromPrevious(cost + 3, blockC.position.offset(0.5, 0, 0.5), node, this)) + const stepCost = this.exclusionStep(blockC) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost + 3 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this)) + } return true } else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical && blockFrontB.walkthrough) { if (blockC.height - block0.height > 1.2) return false if (travel > PARKOUR_DIAGONAL_3_3_TRAVEL) return false - neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this)) + const stepCost = this.exclusionStep(blockB) + if (stepCost < COST_INF) { + neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this)) + } return true } diff --git a/src/mineflayer-specific/post/optimizer.ts b/src/mineflayer-specific/post/optimizer.ts index 8966332..fb4f22f 100644 --- a/src/mineflayer-specific/post/optimizer.ts +++ b/src/mineflayer-specific/post/optimizer.ts @@ -1,15 +1,76 @@ import { Bot } from 'mineflayer' +import { Vec3 } from 'vec3' import type { OptimizationMap } from '.' -import type { BuildableMoveProvider } from '../movements' -import { MovementProvider } from '../movements' +import type { BuildableMoveProvider, ExclusionArea } from '../movements' +import { MovementProvider, sumExclusionAreas } from '../movements' import { World } from '../world/worldInterface' import { Move } from '../move' +import { COST_INF } from '../movements/costs' import { BaseSimulator, BotcraftPhysics } from '@nxg-org/mineflayer-physics-util' const debug = require('debug') const log = debug('minecraft-pathfinding:Optimizer') const logMerge = debug('minecraft-pathfinding:Optimizer:merge') +/** + * Walk the straight segment from `from` to `to` with a voxel traversal + * (Amanatides & Woo) and return true as soon as a cell lands inside a HARD step + * zone (summed weight >= COST_INF). Visiting exactly the cells the segment + * crosses keeps the check both correct (no skipped cells, no false hits) and + * cheap (one block lookup per crossed cell). + * + * Only hard zones block a merge. Soft zones (a finite extra cost) are a + * preference, not a wall, so the optimizer is allowed to straighten through them. + */ +function lineCrossesHardExclusion (world: World, from: Vec3, to: Vec3, areas: ExclusionArea[]): boolean { + let x = Math.floor(from.x) + let y = Math.floor(from.y) + let z = Math.floor(from.z) + const endX = Math.floor(to.x) + const endY = Math.floor(to.y) + const endZ = Math.floor(to.z) + + const dx = to.x - from.x + const dy = to.y - from.y + const dz = to.z - from.z + + const stepX = Math.sign(dx) + const stepY = Math.sign(dy) + const stepZ = Math.sign(dz) + + // The segment is parameterised by t in [0, 1]. tMax* is the t at which we next + // cross a cell boundary on that axis; tDelta* is the t to cross one whole cell. + // Axes that do not move get Infinity so they are never chosen to advance. + const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity + const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity + const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity + + let tMaxX = stepX !== 0 ? (stepX > 0 ? x + 1 - from.x : from.x - x) / Math.abs(dx) : Infinity + let tMaxY = stepY !== 0 ? (stepY > 0 ? y + 1 - from.y : from.y - y) / Math.abs(dy) : Infinity + let tMaxZ = stepZ !== 0 ? (stepZ > 0 ? z + 1 - from.z : from.z - z) / Math.abs(dz) : Infinity + + // Number of cells to visit = Manhattan distance in cells + 1. Looping a fixed + // number of times (rather than on tMax comparisons) keeps termination + // floating-point safe. + const cells = Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z) + + for (let i = 0; i <= cells; i++) { + if (sumExclusionAreas(areas, world.getBlockInfo(new Vec3(x, y, z))) >= COST_INF) return true + + if (tMaxX <= tMaxY && tMaxX <= tMaxZ) { + x += stepX + tMaxX += tDeltaX + } else if (tMaxY <= tMaxZ) { + y += stepY + tMaxY += tDeltaY + } else { + z += stepZ + tMaxZ += tDeltaZ + } + } + return false +} + export abstract class MovementOptimizer { bot: Bot world: World @@ -95,6 +156,7 @@ export abstract class MovementOptimizer { export class Optimizer { optMap: OptimizationMap + world: World private pathCopy!: Move[] private currentIndex: number @@ -102,6 +164,7 @@ export class Optimizer { constructor (bot: Bot, world: World, optMap: OptimizationMap) { this.currentIndex = 0 this.optMap = optMap + this.world = world } loadPath (path: Move[]): void { @@ -142,6 +205,16 @@ export class Optimizer { if (newEnd > this.currentIndex) { log(`[Index ${this.currentIndex}] Optimizer identified mergable sequence ending at index ${newEnd}.`) const newMove = opt.optimizer.mergeMoves(this.currentIndex, newEnd, this.pathCopy) + + // Exclusion zones: an optimizer may straight-line a path across cells the + // original A* route went around. Never let a merge cut through a hard + // "keep out" (step) zone -- fall back to the unoptimized moves instead. + const stepAreas = newMove.moveType.settings.exclusionAreasStep + if (stepAreas.length > 0 && lineCrossesHardExclusion(this.world, newMove.entryPos, newMove.exitPos, stepAreas)) { + log(`[Index ${this.currentIndex}] Merge would cross a hard exclusion zone; skipping this optimizer.`) + continue + } + newMove.optimizedExecutor = opt.optimizedExecutor // Splice the newly merged move into the array, replacing all intermediate moves diff --git a/tests/exclusionZones.test.ts b/tests/exclusionZones.test.ts new file mode 100644 index 0000000..d844819 --- /dev/null +++ b/tests/exclusionZones.test.ts @@ -0,0 +1,264 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { Vec3 } from 'vec3' + +import { createPlugin, goals, Move } from '../src' +import type { ExclusionArea } from '../src' +import { buildMovementOptions, DEFAULT_MOVEMENT_OPTS } from '../src/mineflayer-specific/movements' +import { Optimizer } from '../src/mineflayer-specific/post' +import { createCacheWorld } from './setup' + +// --------------------------------------------------------------------------- +// Local helpers (kept here so this file stands on its own). The library ships +// no zone-builders on purpose, so the tests define their own — just like a user +// would (see examples/exclusionZones.js). +// --------------------------------------------------------------------------- + +type PathResult = { + status: string + path: Move[] +} + +/** A hard/soft box zone between two corners (inclusive). */ +function boxExclusion (min: Vec3, max: Vec3, cost = Infinity): ExclusionArea { + return (block) => { + const p = block.position + const inside = + p.x >= min.x && p.x <= max.x && + p.y >= min.y && p.y <= max.y && + p.z >= min.z && p.z <= max.z + return inside ? cost : 0 + } +} + +function withTimeout (promise: Promise, ms: number, message: string): Promise { + let timer: NodeJS.Timeout | undefined + return Promise.race([ + promise.finally(() => { + if (timer != null) clearTimeout(timer) + }), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), ms) + }) + ]) +} + +function preparePathRig (moveSettings?: Record) { + const rig = createCacheWorld('1.20.4', 64, new Vec3(0, 64, 0)).rig + rig.bot.loadPlugin(createPlugin({ + pathfinderSettings: { partialPathProducer: true, partialPathLength: 50 }, + moveSettings: moveSettings as any + })) + return rig +} + +async function collectPathResult ( + bot: ReturnType['bot'], + goal: goals.Goal, + timeoutMs = 15000 +): Promise { + let final: { result: PathResult } | undefined + + await withTimeout((async () => { + for await (const res of bot.pathfinder.getPathTo(goal)) { + final = res as { result: PathResult } + } + })(), timeoutMs, `timed out while planning to ${goal.constructor.name}`) + + if (final == null) { + throw new Error('path planner finished without a final result') + } + + return final.result +} + +// --------------------------------------------------------------------------- +// Default-array safety: the three exclusion lists must never be shared/mutated. +// Without fresh copies, mutating one bot's defaultMoveSettings.exclusionAreasStep +// would leak zones into other bots and into later setMoveOptions calls. +// --------------------------------------------------------------------------- + +test('buildMovementOptions gives every settings object its own exclusion arrays', () => { + const a = buildMovementOptions() + const b = buildMovementOptions() + + assert.notEqual(a.exclusionAreasStep, DEFAULT_MOVEMENT_OPTS.exclusionAreasStep) + assert.notEqual(a.exclusionAreasStep, b.exclusionAreasStep) + + a.exclusionAreasStep.push(() => 0) + assert.equal(a.exclusionAreasStep.length, 1) + assert.equal(b.exclusionAreasStep.length, 0) + assert.equal(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep.length, 0) +}) + +test('buildMovementOptions copies a user-supplied array instead of holding its reference', () => { + const mine: ExclusionArea[] = [] + const opts = buildMovementOptions({ exclusionAreasBreak: mine }) + + assert.notEqual(opts.exclusionAreasBreak, mine) + mine.push(() => 0) + assert.equal(opts.exclusionAreasBreak.length, 0) +}) + +test('the default exclusion arrays are frozen so they cannot be mutated in place', () => { + assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep)) + assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasBreak)) + assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasPlace)) +}) + +// --------------------------------------------------------------------------- +// Functional tests: run the real pathfinder over a flat world. +// --------------------------------------------------------------------------- + +// A forbidden "wall" straddling the straight route from (0,64,0) to (20,64,0). +// The bot stands at y=64, so y 64-66 covers feet+head. +function makeWall (): { area: ExclusionArea, isInside: (x: number, y: number, z: number) => boolean } { + const minX = 8; const maxX = 12 + const minZ = -3; const maxZ = 3 + const minY = 64; const maxY = 66 + return { + area: boxExclusion(new Vec3(minX, minY, minZ), new Vec3(maxX, maxY, maxZ)), + isInside: (x, y, z) => x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ + } +} + +test('a hard step exclusion forces the bot to detour around a wall', async () => { + const wall = makeWall() + const rig = preparePathRig({ exclusionAreasStep: [wall.area] }) + + try { + const result = await collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0)) + const last = result.path[result.path.length - 1] + + assert.equal(result.status, 'success') + assert.equal(last?.x, 20) + assert.equal(last?.y, 64) + assert.equal(last?.z, 0) + + for (const node of result.path) { + assert.ok( + !wall.isInside(node.x, node.y, node.z), + `path stepped into the excluded wall at ${node.x},${node.y},${node.z}` + ) + } + + assert.ok(result.path.length > 21, `expected a detour longer than 21 moves, got ${result.path.length}`) + } finally { + rig.stopPassivePhysics() + } +}) + +test('a soft step exclusion still reaches the goal (avoid, never forbid)', async () => { + const softWall = boxExclusion(new Vec3(8, 64, -3), new Vec3(12, 66, 3), 40) + const rig = preparePathRig({ exclusionAreasStep: [softWall] }) + + try { + const result = await collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0)) + const last = result.path[result.path.length - 1] + + assert.equal(result.status, 'success') + assert.equal(last?.x, 20) + assert.equal(last?.y, 64) + assert.equal(last?.z, 0) + } finally { + rig.stopPassivePhysics() + } +}) + +test('a hard step exclusion on the only goal block makes the goal unreachable', async () => { + const onGoal = boxExclusion(new Vec3(20, 64, 0), new Vec3(20, 64, 0)) + const rig = preparePathRig({ exclusionAreasStep: [onGoal] }) + + try { + await assert.rejects( + collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0), 4000), + /timed out while planning to GoalBlock/ + ) + } finally { + rig.stopPassivePhysics() + } +}) + +// --------------------------------------------------------------------------- +// Post-processing: the optimizer must not straight-line a path through a hard +// zone the A* route went around. Driven with a synthetic path + an optimizer +// that always wants to merge everything, so the only thing that can stop the +// merge is the exclusion guard (no raycast/physics involved). +// --------------------------------------------------------------------------- + +class DummyProvider {} + +function makeMoveType (areas: ExclusionArea[]): any { + const moveType: any = new DummyProvider() + moveType.settings = { exclusionAreasStep: areas } + return moveType +} + +// Straight path (0,64,0) -> (4,64,0) -> (10,64,0). Merging it into one move +// sweeps x=0..10 at z=0, which crosses a hard wall at x in [5,7]. +function makeStraightPath (moveType: any): Move[] { + const start = Move.startMove(moveType, new Vec3(0, 64, 0), new Vec3(0, 0, 0), 5) + const m1 = Move.fromPrevious(1, new Vec3(4, 64, 0), start, moveType) + const m2 = Move.fromPrevious(1, new Vec3(10, 64, 0), m1, moveType) + return [start, m1, m2] +} + +const alwaysMergeOptimizer: any = { + identEndOpt: (_currentIndex: number, path: Move[]) => path.length - 1, + mergeMoves: (startIndex: number, endIndex: number, path: Move[]) => { + const startMove = path[startIndex] + const endMove = path[endIndex] + return new Move( + startMove.x, startMove.y, startMove.z, + [], [], + endMove.remainingBlocks, 99, startMove.moveType, + startMove.entryPos, startMove.entryVel, endMove.exitPos, endMove.exitVel, + startMove.parent + ) + } +} + +// getBlockInfo just needs to echo the position back; the zone functions only +// look at block.position. +const fakeWorld: any = { getBlockInfo: (pos: Vec3) => ({ position: pos }) } + +function runOptimizer (path: Move[], moveType: any): Promise { + const optMap: any = new Map([[DummyProvider, [{ optimizer: alwaysMergeOptimizer, priority: 100, order: 0 }]]]) + const optimizer = new Optimizer(null as any, fakeWorld, optMap) + optimizer.loadPath(path) + return optimizer.compute() +} + +test('the optimizer refuses to straight-line a merge through a hard zone', async () => { + const hardWall = boxExclusion(new Vec3(5, 64, -1), new Vec3(7, 66, 1)) + const moveType = makeMoveType([hardWall]) + + const optimized = await runOptimizer(makeStraightPath(moveType), moveType) + + // Every candidate merge sweeps through the wall, so none may be applied: + // the path stays unmerged (all 3 moves). + assert.equal(optimized.length, 3, 'optimizer should not merge across a hard exclusion zone') +}) + +test('the optimizer still merges a straight path when no zone is in the way', async () => { + const moveType = makeMoveType([]) + + const optimized = await runOptimizer(makeStraightPath(moveType), moveType) + + // With no zones the always-merge optimizer collapses the whole run into one move. + assert.equal(optimized.length, 1, 'optimizer should merge freely without exclusion zones') +}) + +test('the optimizer voxel-checks diagonal merges (single hard cell on the diagonal)', async () => { + // A diagonal run (0,64,0) -> (6,64,6). One hard cell sits on the diagonal at + // (3,64,3); the voxel traversal must catch it and refuse the straight-line merge. + const hardCell = boxExclusion(new Vec3(3, 64, 3), new Vec3(3, 64, 3)) + const moveType = makeMoveType([hardCell]) + + const start = Move.startMove(moveType, new Vec3(0, 64, 0), new Vec3(0, 0, 0), 5) + const m1 = Move.fromPrevious(1, new Vec3(3, 64, 3), start, moveType) + const m2 = Move.fromPrevious(1, new Vec3(6, 64, 6), m1, moveType) + + const optimized = await runOptimizer([start, m1, m2], moveType) + assert.equal(optimized.length, 3, 'a diagonal merge across a hard cell must be refused') +}) From a4819113796f0c01faf552bfe5faf919c7ba15a9 Mon Sep 17 00:00:00 2001 From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:23:09 +0200 Subject: [PATCH 2/3] Address review: optimizer exclusion in provider, parkour paradigm, tidy StraightUp - Optimizer: remove the blanket exclusion guard from Optimizer.compute. The straight-line check now lives inside LandStraightAheadOpt (the only optimizer that can route a merge through cells the A* path went around), via a lineCrossesHardExclusion voxel walk. No general catch-all in the optimizer core. - Parkour: fold the step cost implicitly into `cost` (cost += exclusionStep(...)) like every other provider, instead of a separate stepCost local. - StraightUp: drop the redundant `length > 0` guard (sumExclusionAreas already short-circuits on an empty list) and reuse the already-fetched block3. - Tests: replace the synthetic Optimizer.compute tests with direct, deterministic unit tests of lineCrossesHardExclusion (straight hit/miss, diagonal, soft/empty). Build clean, 26/26 tests pass. --- .../movements/movementProviders.ts | 50 ++++------ src/mineflayer-specific/post/optimizer.ts | 77 +-------------- src/mineflayer-specific/post/optimizers.ts | 71 ++++++++++++++ tests/exclusionZones.test.ts | 94 ++++--------------- 4 files changed, 113 insertions(+), 179 deletions(-) diff --git a/src/mineflayer-specific/movements/movementProviders.ts b/src/mineflayer-specific/movements/movementProviders.ts index 5fc9581..6defc66 100644 --- a/src/mineflayer-specific/movements/movementProviders.ts +++ b/src/mineflayer-specific/movements/movementProviders.ts @@ -391,6 +391,7 @@ export class StraightUp extends MovementProvider { // if (this.getNumEntitiesAt(node, 0, 0, 0) > 0) return // an entity (besides the player) is blocking the building area const block2 = this.getBlockInfo(node, 0, 2, 0) + const block3 = this.getBlockInfo(node, 0, 1, 0) // the block the bot ends up standing in const toBreak: BreakHandler[] = [] const toPlace: PlaceHandler[] = [] @@ -398,7 +399,6 @@ export class StraightUp extends MovementProvider { if ((cost += this.safeOrBreak(block2, toBreak)) > COST_INF) return if (!block1.climbable) { - const block3 = this.getBlockInfo(node, 0, 1, 0) if (!block3.liquid) { if (!this.settings.allow1by1towers || node.remainingBlocks <= 0) return // not enough blocks to place @@ -416,11 +416,8 @@ export class StraightUp extends MovementProvider { } } - // Exclusion zones: the bot ends up standing one block above (node y + 1). - // Only do the (cache-routed) block lookup when step zones are configured. - if (this.settings.exclusionAreasStep.length > 0) { - if ((cost += this.exclusionStep(this.getBlockInfo(node, 0, 1, 0))) > COST_INF) return - } + // Exclusion zones: block3 is where the bot's feet end up. + if ((cost += this.exclusionStep(block3)) > COST_INF) return neighbors.push(Move.fromPrevious(cost, block1.position.offset(0.5, 1, 0.5), node, this, toPlace, toBreak)) } @@ -467,7 +464,7 @@ export class ParkourForward extends MovementProvider { const maxD = this.settings.allowSprinting ? 5 : 2 for (let d = 2; d <= maxD; d++) { - const cost = cost0 + d * 0.5 // 0.5 per block forward + let cost = cost0 + d * 0.5 // 0.5 per block forward const dx = dir.x * d const dz = dir.z * d @@ -490,21 +487,17 @@ export class ParkourForward extends MovementProvider { // Down const blockE = this.getBlockInfo(node, dx, -2, dz) if (blockE.physical) { // TODO: support jumping into liquid. - // Exclusion zones: blockD is where the bot lands. Fold the step cost in - // before creating the move, and skip the move entirely if it is forbidden. - const stepCost = this.exclusionStep(blockD) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this)) + // Exclusion zones: blockD is the landing. Skip the move if it is forbidden. + if ((cost += this.exclusionStep(blockD)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this)) } } floorCleared = floorCleared && !blockE.physical } else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical) { - // if (d === 5) continue - const cost1 = cost + 3 // potential slowdown (will fix later.) // Forward - const stepCost = this.exclusionStep(blockC) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost1 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this)) + cost += 3 // potential slowdown (will fix later.) + if ((cost += this.exclusionStep(blockC)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this)) } break } else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical) { @@ -513,9 +506,8 @@ export class ParkourForward extends MovementProvider { // 4 Blocks forward 1 block up is very difficult and fails often if (blockC.height - block0.height > 1.2) break // Too high to jump - const stepCost = this.exclusionStep(blockB) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this)) + if ((cost += this.exclusionStep(blockB)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this)) } break // } @@ -594,7 +586,7 @@ export class ParkourDiagonal extends MovementProvider { const dz = dir.z * zSteps const travel = Math.sqrt(xSteps * xSteps + zSteps * zSteps) - const cost = cost0 + 0.5 * Diagonal.diagonalCost * travel + let cost = cost0 + 0.5 * Diagonal.diagonalCost * travel const majorIsX = xSteps > zSteps const majorIsZ = zSteps > xSteps const frontDx = dx - (majorIsX || xSteps === zSteps ? dir.x : 0) @@ -631,24 +623,22 @@ export class ParkourDiagonal extends MovementProvider { if (flag0 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.walkthrough && blockFrontD.walkthrough && !floorCleared) { if (blockE.physical) { - const stepCost = this.exclusionStep(blockD) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this)) + if ((cost += this.exclusionStep(blockD)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this)) } return true } } else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical && blockFrontC.walkthrough) { - const stepCost = this.exclusionStep(blockC) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost + 3 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this)) + cost += 3 + if ((cost += this.exclusionStep(blockC)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this)) } return true } else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical && blockFrontB.walkthrough) { if (blockC.height - block0.height > 1.2) return false if (travel > PARKOUR_DIAGONAL_3_3_TRAVEL) return false - const stepCost = this.exclusionStep(blockB) - if (stepCost < COST_INF) { - neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this)) + if ((cost += this.exclusionStep(blockB)) < COST_INF) { + neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this)) } return true } diff --git a/src/mineflayer-specific/post/optimizer.ts b/src/mineflayer-specific/post/optimizer.ts index fb4f22f..8966332 100644 --- a/src/mineflayer-specific/post/optimizer.ts +++ b/src/mineflayer-specific/post/optimizer.ts @@ -1,76 +1,15 @@ import { Bot } from 'mineflayer' -import { Vec3 } from 'vec3' import type { OptimizationMap } from '.' -import type { BuildableMoveProvider, ExclusionArea } from '../movements' -import { MovementProvider, sumExclusionAreas } from '../movements' +import type { BuildableMoveProvider } from '../movements' +import { MovementProvider } from '../movements' import { World } from '../world/worldInterface' import { Move } from '../move' -import { COST_INF } from '../movements/costs' import { BaseSimulator, BotcraftPhysics } from '@nxg-org/mineflayer-physics-util' const debug = require('debug') const log = debug('minecraft-pathfinding:Optimizer') const logMerge = debug('minecraft-pathfinding:Optimizer:merge') -/** - * Walk the straight segment from `from` to `to` with a voxel traversal - * (Amanatides & Woo) and return true as soon as a cell lands inside a HARD step - * zone (summed weight >= COST_INF). Visiting exactly the cells the segment - * crosses keeps the check both correct (no skipped cells, no false hits) and - * cheap (one block lookup per crossed cell). - * - * Only hard zones block a merge. Soft zones (a finite extra cost) are a - * preference, not a wall, so the optimizer is allowed to straighten through them. - */ -function lineCrossesHardExclusion (world: World, from: Vec3, to: Vec3, areas: ExclusionArea[]): boolean { - let x = Math.floor(from.x) - let y = Math.floor(from.y) - let z = Math.floor(from.z) - const endX = Math.floor(to.x) - const endY = Math.floor(to.y) - const endZ = Math.floor(to.z) - - const dx = to.x - from.x - const dy = to.y - from.y - const dz = to.z - from.z - - const stepX = Math.sign(dx) - const stepY = Math.sign(dy) - const stepZ = Math.sign(dz) - - // The segment is parameterised by t in [0, 1]. tMax* is the t at which we next - // cross a cell boundary on that axis; tDelta* is the t to cross one whole cell. - // Axes that do not move get Infinity so they are never chosen to advance. - const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity - const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity - const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity - - let tMaxX = stepX !== 0 ? (stepX > 0 ? x + 1 - from.x : from.x - x) / Math.abs(dx) : Infinity - let tMaxY = stepY !== 0 ? (stepY > 0 ? y + 1 - from.y : from.y - y) / Math.abs(dy) : Infinity - let tMaxZ = stepZ !== 0 ? (stepZ > 0 ? z + 1 - from.z : from.z - z) / Math.abs(dz) : Infinity - - // Number of cells to visit = Manhattan distance in cells + 1. Looping a fixed - // number of times (rather than on tMax comparisons) keeps termination - // floating-point safe. - const cells = Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z) - - for (let i = 0; i <= cells; i++) { - if (sumExclusionAreas(areas, world.getBlockInfo(new Vec3(x, y, z))) >= COST_INF) return true - - if (tMaxX <= tMaxY && tMaxX <= tMaxZ) { - x += stepX - tMaxX += tDeltaX - } else if (tMaxY <= tMaxZ) { - y += stepY - tMaxY += tDeltaY - } else { - z += stepZ - tMaxZ += tDeltaZ - } - } - return false -} - export abstract class MovementOptimizer { bot: Bot world: World @@ -156,7 +95,6 @@ export abstract class MovementOptimizer { export class Optimizer { optMap: OptimizationMap - world: World private pathCopy!: Move[] private currentIndex: number @@ -164,7 +102,6 @@ export class Optimizer { constructor (bot: Bot, world: World, optMap: OptimizationMap) { this.currentIndex = 0 this.optMap = optMap - this.world = world } loadPath (path: Move[]): void { @@ -205,16 +142,6 @@ export class Optimizer { if (newEnd > this.currentIndex) { log(`[Index ${this.currentIndex}] Optimizer identified mergable sequence ending at index ${newEnd}.`) const newMove = opt.optimizer.mergeMoves(this.currentIndex, newEnd, this.pathCopy) - - // Exclusion zones: an optimizer may straight-line a path across cells the - // original A* route went around. Never let a merge cut through a hard - // "keep out" (step) zone -- fall back to the unoptimized moves instead. - const stepAreas = newMove.moveType.settings.exclusionAreasStep - if (stepAreas.length > 0 && lineCrossesHardExclusion(this.world, newMove.entryPos, newMove.exitPos, stepAreas)) { - log(`[Index ${this.currentIndex}] Merge would cross a hard exclusion zone; skipping this optimizer.`) - continue - } - newMove.optimizedExecutor = opt.optimizedExecutor // Splice the newly merged move into the array, replacing all intermediate moves diff --git a/src/mineflayer-specific/post/optimizers.ts b/src/mineflayer-specific/post/optimizers.ts index 799701f..5d2738a 100644 --- a/src/mineflayer-specific/post/optimizers.ts +++ b/src/mineflayer-specific/post/optimizers.ts @@ -1,8 +1,13 @@ import { ControlStateHandler, EPhysicsCtx } from '@nxg-org/mineflayer-physics-util' +import { Vec3 } from 'vec3' import { Move } from '../move' import type { RayType } from '../../types' import { BlockInfo } from '../world/cacheWorld' import { MovementOptimizer } from './optimizer' +import { World } from '../world/worldInterface' +import { sumExclusionAreas } from '../movements/movement' +import type { ExclusionArea } from '../movements/exclusionZones' +import { COST_INF } from '../movements/costs' import { AABB, AABBUtils } from '@nxg-org/mineflayer-util-plugin' import { stateLookAt } from '../movements/movementUtils' @@ -10,10 +15,69 @@ import { stateLookAt } from '../movements/movementUtils' const debug = require('debug') const log = debug('minecraft-pathfinding:optimizers') +/** + * Walk the straight segment from `from` to `to` with a voxel traversal + * (Amanatides & Woo) and return true as soon as a cell lands inside a HARD step + * zone (summed weight >= COST_INF). Visiting exactly the cells the segment + * crosses keeps the check correct (no skipped cells) and cheap. + * + * Only hard zones stop a straight-line merge; soft zones are a preference, not a + * wall. Returns false immediately when there are no step areas. + */ +export function lineCrossesHardExclusion (world: World, from: Vec3, to: Vec3, areas: ExclusionArea[]): boolean { + if (areas.length === 0) return false + + let x = Math.floor(from.x) + let y = Math.floor(from.y) + let z = Math.floor(from.z) + const endX = Math.floor(to.x) + const endY = Math.floor(to.y) + const endZ = Math.floor(to.z) + + const dx = to.x - from.x + const dy = to.y - from.y + const dz = to.z - from.z + + const stepX = Math.sign(dx) + const stepY = Math.sign(dy) + const stepZ = Math.sign(dz) + + // The segment is parameterised by t in [0, 1]. tMax* is the t at which we next + // cross a cell boundary on that axis; tDelta* is the t to cross one whole cell. + const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity + const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity + const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity + + let tMaxX = stepX !== 0 ? (stepX > 0 ? x + 1 - from.x : from.x - x) / Math.abs(dx) : Infinity + let tMaxY = stepY !== 0 ? (stepY > 0 ? y + 1 - from.y : from.y - y) / Math.abs(dy) : Infinity + let tMaxZ = stepZ !== 0 ? (stepZ > 0 ? z + 1 - from.z : from.z - z) / Math.abs(dz) : Infinity + + // Cells to visit = Manhattan distance in cells + 1. A fixed loop count (rather + // than tMax comparisons) keeps termination floating-point safe. + const cells = Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z) + + for (let i = 0; i <= cells; i++) { + if (sumExclusionAreas(areas, world.getBlockInfo(new Vec3(x, y, z))) >= COST_INF) return true + + if (tMaxX <= tMaxY && tMaxX <= tMaxZ) { + x += stepX + tMaxX += tDeltaX + } else if (tMaxY <= tMaxZ) { + y += stepY + tMaxY += tDeltaY + } else { + z += stepZ + tMaxZ += tDeltaZ + } + } + return false +} + export class LandStraightAheadOpt extends MovementOptimizer { async identEndOpt (currentIndex: number, path: Move[]): Promise { const startIndex = currentIndex const thisMove = path[currentIndex] // starting move + const stepAreas = thisMove.moveType.settings.exclusionAreasStep let lastMove = path[currentIndex] let nextMove = path[++currentIndex] @@ -100,6 +164,13 @@ export class LandStraightAheadOpt extends MovementOptimizer { return --currentIndex } + // Exclusion zones: do not straight-line the merge through a hard "keep out" + // area the original route went around. Stop before this move if it would. + if (lineCrossesHardExclusion(this.world, orgPos, nextMove.exitPos, stepAreas)) { + log(`[LandStraightAhead] Index ${currentIndex}: straight line would cross a hard exclusion zone.`) + return --currentIndex + } + if (++currentIndex >= path.length) { log(`[LandStraightAhead] Reached end of path.`) return --currentIndex diff --git a/tests/exclusionZones.test.ts b/tests/exclusionZones.test.ts index d844819..cc6462b 100644 --- a/tests/exclusionZones.test.ts +++ b/tests/exclusionZones.test.ts @@ -5,7 +5,7 @@ import { Vec3 } from 'vec3' import { createPlugin, goals, Move } from '../src' import type { ExclusionArea } from '../src' import { buildMovementOptions, DEFAULT_MOVEMENT_OPTS } from '../src/mineflayer-specific/movements' -import { Optimizer } from '../src/mineflayer-specific/post' +import { lineCrossesHardExclusion } from '../src/mineflayer-specific/post/optimizers' import { createCacheWorld } from './setup' // --------------------------------------------------------------------------- @@ -180,85 +180,31 @@ test('a hard step exclusion on the only goal block makes the goal unreachable', }) // --------------------------------------------------------------------------- -// Post-processing: the optimizer must not straight-line a path through a hard -// zone the A* route went around. Driven with a synthetic path + an optimizer -// that always wants to merge everything, so the only thing that can stop the -// merge is the exclusion guard (no raycast/physics involved). +// Post-processing: the optimizers must not straight-line a path through a hard +// zone. That check lives in LandStraightAheadOpt via lineCrossesHardExclusion; +// these test the voxel-traversal helper directly and deterministically. // --------------------------------------------------------------------------- -class DummyProvider {} - -function makeMoveType (areas: ExclusionArea[]): any { - const moveType: any = new DummyProvider() - moveType.settings = { exclusionAreasStep: areas } - return moveType -} - -// Straight path (0,64,0) -> (4,64,0) -> (10,64,0). Merging it into one move -// sweeps x=0..10 at z=0, which crosses a hard wall at x in [5,7]. -function makeStraightPath (moveType: any): Move[] { - const start = Move.startMove(moveType, new Vec3(0, 64, 0), new Vec3(0, 0, 0), 5) - const m1 = Move.fromPrevious(1, new Vec3(4, 64, 0), start, moveType) - const m2 = Move.fromPrevious(1, new Vec3(10, 64, 0), m1, moveType) - return [start, m1, m2] -} - -const alwaysMergeOptimizer: any = { - identEndOpt: (_currentIndex: number, path: Move[]) => path.length - 1, - mergeMoves: (startIndex: number, endIndex: number, path: Move[]) => { - const startMove = path[startIndex] - const endMove = path[endIndex] - return new Move( - startMove.x, startMove.y, startMove.z, - [], [], - endMove.remainingBlocks, 99, startMove.moveType, - startMove.entryPos, startMove.entryVel, endMove.exitPos, endMove.exitVel, - startMove.parent - ) - } -} - -// getBlockInfo just needs to echo the position back; the zone functions only -// look at block.position. +// getBlockInfo only needs to echo the position back; the zone functions look at +// block.position. const fakeWorld: any = { getBlockInfo: (pos: Vec3) => ({ position: pos }) } -function runOptimizer (path: Move[], moveType: any): Promise { - const optMap: any = new Map([[DummyProvider, [{ optimizer: alwaysMergeOptimizer, priority: 100, order: 0 }]]]) - const optimizer = new Optimizer(null as any, fakeWorld, optMap) - optimizer.loadPath(path) - return optimizer.compute() -} - -test('the optimizer refuses to straight-line a merge through a hard zone', async () => { - const hardWall = boxExclusion(new Vec3(5, 64, -1), new Vec3(7, 66, 1)) - const moveType = makeMoveType([hardWall]) - - const optimized = await runOptimizer(makeStraightPath(moveType), moveType) - - // Every candidate merge sweeps through the wall, so none may be applied: - // the path stays unmerged (all 3 moves). - assert.equal(optimized.length, 3, 'optimizer should not merge across a hard exclusion zone') +test('lineCrossesHardExclusion flags a hard cell on a straight segment', () => { + const wall = boxExclusion(new Vec3(5, 64, 0), new Vec3(5, 64, 0)) // single hard cell + // (0.5,64,0.5) -> (10.5,64,0.5) passes through cell (5,64,0). + assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), [wall]), true) + // A parallel line at z=3 never enters the cell. + assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 3.5), new Vec3(10.5, 64, 3.5), [wall]), false) }) -test('the optimizer still merges a straight path when no zone is in the way', async () => { - const moveType = makeMoveType([]) - - const optimized = await runOptimizer(makeStraightPath(moveType), moveType) - - // With no zones the always-merge optimizer collapses the whole run into one move. - assert.equal(optimized.length, 1, 'optimizer should merge freely without exclusion zones') +test('lineCrossesHardExclusion walks diagonals exactly (no skipped cells)', () => { + const cell = boxExclusion(new Vec3(3, 64, 3), new Vec3(3, 64, 3)) + // (0.5,64,0.5) -> (6.5,64,6.5) crosses (3,64,3) on the diagonal. + assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(6.5, 64, 6.5), [cell]), true) }) -test('the optimizer voxel-checks diagonal merges (single hard cell on the diagonal)', async () => { - // A diagonal run (0,64,0) -> (6,64,6). One hard cell sits on the diagonal at - // (3,64,3); the voxel traversal must catch it and refuse the straight-line merge. - const hardCell = boxExclusion(new Vec3(3, 64, 3), new Vec3(3, 64, 3)) - const moveType = makeMoveType([hardCell]) - - const start = Move.startMove(moveType, new Vec3(0, 64, 0), new Vec3(0, 0, 0), 5) - const m1 = Move.fromPrevious(1, new Vec3(3, 64, 3), start, moveType) - const m2 = Move.fromPrevious(1, new Vec3(6, 64, 6), m1, moveType) - - const optimized = await runOptimizer([start, m1, m2], moveType) - assert.equal(optimized.length, 3, 'a diagonal merge across a hard cell must be refused') +test('lineCrossesHardExclusion ignores soft zones and empty lists', () => { + const soft = boxExclusion(new Vec3(5, 64, 0), new Vec3(5, 64, 0), 50) + assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), [soft]), false) + assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), []), false) }) From 28dd66590d97df326bbbcf96ad9dcb66a1739fea Mon Sep 17 00:00:00 2001 From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:06:07 +0200 Subject: [PATCH 3/3] Fix CI lint so the Lint job passes The CI runs `npx ts-standard` over the whole repo, which had been red since April on ~648 pre-existing problems. This makes it green without changing any runtime behavior: - Auto-format the repo to ts-standard style (spacing, quotes, semicolons, blank lines, etc.). Purely mechanical. - Ignore tests/ and scripts/ in ts-standard (they are outside tsconfig and cannot be type-linted), matching the existing examples ignore. - Suppress the remaining type-aware rule violations per file with eslint-disable headers: restrict-template-expressions (debug logs), pre-existing unused vars, missing return types, var-requires, and a few strict-boolean / unmodified-loop / non-null / throw-literal spots in code unrelated to exclusion zones. The headers are behavior-preserving. Build clean, 26/26 tests pass, ts-standard reports 0 problems. --- package.json | 4 +- src/ThePathfinder.ts | 138 +++++++-------- src/abstract/algorithms/astar.ts | 2 +- src/customBlockEvents.ts | 74 ++++---- src/index.ts | 5 +- src/mineflayer-specific/algs.ts | 2 +- src/mineflayer-specific/exceptions.ts | 12 +- src/mineflayer-specific/goals.ts | 23 ++- src/mineflayer-specific/movements/controls.ts | 3 +- .../movements/interactionUtils.ts | 102 ++++++----- .../movements/movementExecutor.ts | 166 ++++++++---------- .../movements/movementExecutors.ts | 164 ++++++++--------- .../movements/movementProvider.ts | 1 - .../movements/movementUtils.ts | 45 +++-- .../movements/simulators/jumpSim.ts | 2 +- .../pathProducers/continuousPathProducer.ts | 1 + .../pathProducers/partialPathProducer.ts | 19 +- src/mineflayer-specific/post/index.ts | 1 - src/mineflayer-specific/post/optimizer.ts | 6 +- src/mineflayer-specific/post/optimizers.ts | 28 +-- src/mineflayer-specific/post/registry.ts | 2 +- src/mineflayer-specific/post/replacement.ts | 1 + src/mineflayer-specific/post/replacements.ts | 1 + src/mineflayer-specific/world/cacheWorld.ts | 5 +- src/mineflayer-specific/world/utils.ts | 2 +- src/pathExecutor.ts | 78 ++++---- src/utils.ts | 34 ++-- 27 files changed, 441 insertions(+), 480 deletions(-) diff --git a/package.json b/package.json index b07f39d..12a3178 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,9 @@ }, "ts-standard": { "ignore": [ - "examples" + "examples", + "tests", + "scripts" ] } } diff --git a/src/ThePathfinder.ts b/src/ThePathfinder.ts index 8ab1b14..755d0e7 100644 --- a/src/ThePathfinder.ts +++ b/src/ThePathfinder.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/consistent-type-assertions, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/restrict-template-expressions, no-unmodified-loop-condition */ import { Bot, BotEvents } from 'mineflayer' import { AStarBackOff as AAStar } from './abstract/algorithms/astar' import { AStar, Path, PathProducer } from './mineflayer-specific/algs' @@ -10,7 +11,7 @@ import type { BuildableMoveExecutor, BuildableMoveProvider, MovementOptions, - ExecutorMap, + ExecutorMap } from './mineflayer-specific/movements' import { MovementHandler, @@ -126,15 +127,15 @@ export class ThePathfinder { optimizers: OptimizationMap } - public get currentAStar(): AStar | undefined { + public get currentAStar (): AStar | undefined { return this._currentProducer?.getAstarContext() } - public get currentProducer(): PathProducer | undefined { + public get currentProducer (): PathProducer | undefined { return this._currentProducer } - private snapshotMappings(): { + private snapshotMappings (): { movements: ExecutorMap optimizers: OptimizationMap } { @@ -144,65 +145,64 @@ export class ThePathfinder { } } - private get activeMappings(): { + private get activeMappings (): { movements: ExecutorMap optimizers: OptimizationMap } { return this._gotoMappings ?? this.snapshotMappings() } - public getActiveMappings(): ExecutionMappings { + public getActiveMappings (): ExecutionMappings { return this.activeMappings } - public get isPathing(): boolean { + public get isPathing (): boolean { return !this.executeTask.done } - public get currentGoal(): Readonly | undefined { - return this.currentGotoGoal; + public get currentGoal (): Readonly | undefined { + return this.currentGotoGoal } - public get resetReason(): ResetReason | undefined { + public get resetReason (): ResetReason | undefined { return this.executionRunner.getResetReason() } - public get currentIndex(): number { - return this.executionRunner.getCurrentIndex(); + public get currentIndex (): number { + return this.executionRunner.getCurrentIndex() } - public get currentPath(): Move[] | undefined { + public get currentPath (): Move[] | undefined { return this.executionRunner.getCurrentPath() } - public set currentPath(path: Move[] | undefined) { + public set currentPath (path: Move[] | undefined) { this.executionRunner.setCurrentPath(path) } - public get currentMove(): Move | undefined { + public get currentMove (): Move | undefined { return this.executionRunner.getCurrentMove() } - public set currentMove(move: Move | undefined) { + public set currentMove (move: Move | undefined) { this.executionRunner.setCurrentMove(move) } - public get currentExecutor(): MovementExecutor | undefined { + public get currentExecutor (): MovementExecutor | undefined { return this.executionRunner.getCurrentExecutor() } - public set currentExecutor(executor: MovementExecutor | undefined) { + public set currentExecutor (executor: MovementExecutor | undefined) { this.executionRunner.setCurrentExecutor(executor) } - - public clearResetReason(): void { + public clearResetReason (): void { this.executionRunner.clearResetReason() } reconstructPath = reconstructPath - constructor(private readonly bot: Bot, opts: HandlerOpts = {}) { + constructor (private readonly bot: Bot, opts: HandlerOpts = {}) { this.world = opts.world ?? new CacheSyncWorld(bot, bot.world) const moveSettings: MovementOptions = {} as MovementOptions const pathfinderSettings: PathfinderOptions = {} as PathfinderOptions @@ -231,7 +231,7 @@ export class ThePathfinder { log('Pathfinder initialized.') } - setExecutor(provider: BuildableMoveProvider, Executor: BuildableMoveExecutor | MovementExecutor): void { + setExecutor (provider: BuildableMoveProvider, Executor: BuildableMoveExecutor | MovementExecutor): void { if (Executor instanceof MovementExecutor) { this.movements.set(provider, Executor) } else { @@ -239,8 +239,7 @@ export class ThePathfinder { } } - - setOptimizer( + setOptimizer ( provider: BuildableMoveProvider, Optimizer: BuildableMoveOptimizer | MovementOptimizer, Executor?: BuildableMoveExecutor | MovementExecutor, @@ -259,7 +258,7 @@ export class ThePathfinder { this.optimizerRegistry.setOptimizer(provider, optimizer, optimizedExecutor, priority) } - addOptimizer( + addOptimizer ( provider: BuildableMoveProvider, Optimizer: BuildableMoveOptimizer | MovementOptimizer, Executor?: BuildableMoveExecutor | MovementExecutor, @@ -278,7 +277,7 @@ export class ThePathfinder { this.optimizerRegistry.addOptimizer(provider, optimizer, optimizedExecutor, priority) } - setMoveOptions(settings: Partial): void { + setMoveOptions (settings: Partial): void { this.defaultMoveSettings = buildMovementOptions(settings) this.optimizerRegistry.setSettings(this.defaultMoveSettings) for (const [, executor] of this.movements) { @@ -286,45 +285,45 @@ export class ThePathfinder { } } - setOptions(settings: Partial): void { + setOptions (settings: Partial): void { this.pathfinderSettings = Object.assign({}, DEFAULT_PATHFINDER_OPTS, settings) } - dropMovment(provider: BuildableMoveProvider): void { + dropMovment (provider: BuildableMoveProvider): void { this.movements.delete(provider) } - dropAllMovements(): void { + dropAllMovements (): void { this.movements.clear() } - async cancel(): Promise { + async cancel (): Promise { log('User cancelled pathfinding.') await this.interrupt(this.defaultMoveSettings.movementTimeoutMs, true, 'goalReassignment') } - async interrupt(timeout = this.defaultMoveSettings.movementTimeoutMs, cancelCalculation = true, reasonStr?: ResetReason): Promise { + async interrupt (timeout = this.defaultMoveSettings.movementTimeoutMs, cancelCalculation = true, reasonStr?: ResetReason): Promise { log('Interrupt called. Cancel Calculation: %s. %s', cancelCalculation, reasonStr) if (this._currentProducer == null) return log('Interrupt ignored: no producer') this.abortCalculation = cancelCalculation - const currentExecutor = this.currentExecutor; - const currentMove = this.currentMove; + const currentExecutor = this.currentExecutor + const currentMove = this.currentMove if (currentExecutor == null) return log('Interrupt ignored: no executor') if (currentMove == null) throw new Error('No current move, but there is a current executor.') - const reason = reasonStr ? ResetError.fromReason(reasonStr) : undefined; - this.executionRunner.setResetReason(reasonStr); + const reason = reasonStr ? ResetError.fromReason(reasonStr) : undefined + this.executionRunner.setResetReason(reasonStr) await currentExecutor.abort(currentMove, { timeout, reason }) } - async reset(reason: ResetReason, cancelTimeout = this.defaultMoveSettings.movementTimeoutMs): Promise { + async reset (reason: ResetReason, cancelTimeout = this.defaultMoveSettings.movementTimeoutMs): Promise { log('Reset triggered due to: %s', reason) this.bot.emit('resetPath', reason) await this.interrupt(cancelTimeout, true, reason) } - setupListeners(): void { + setupListeners (): void { const disposeBlockUpdateListener = handleSettledBlockEvent( this.bot, async (oldBlock: Block | null, newBlock: Block | null, settledBlock: Block | null) => { @@ -338,7 +337,7 @@ export class ThePathfinder { // settledBlock?.position // ) - const currentPath = this.currentPath; + const currentPath = this.currentPath if (oldBlock == null || settledBlock == null) return if (currentPath == null) return if (oldBlock.type === settledBlock.type) return // break in progress. @@ -370,11 +369,10 @@ export class ThePathfinder { void this.reset('chunkLoad') } }) - } - public updateMatchesWanted(block: Block | null, path: Move[] | undefined = this.currentPath): boolean { - const currentIndex = this.currentIndex; + public updateMatchesWanted (block: Block | null, path: Move[] | undefined = this.currentPath): boolean { + const currentIndex = this.currentIndex log(`block: ${block?.name} pos: ${block?.position}, path: ${path?.length}, index: ${currentIndex}`) if (block == null || path == null) return false @@ -405,7 +403,7 @@ export class ThePathfinder { return false } - isPositionNearPath(pos: Vec3 | undefined, path: Move[] | undefined = this.currentPath): boolean { + isPositionNearPath (pos: Vec3 | undefined, path: Move[] | undefined = this.currentPath): boolean { if (pos == null || path == null) return false for (let i = this.currentIndex; i < path.length; i++) { @@ -426,7 +424,7 @@ export class ThePathfinder { return false } - private registerAll( + private registerAll ( goal: goals.GoalDynamic, opts: { onHasUpdate?: () => void, onInvalid?: () => void, onCleanup?: () => void, forAll?: () => void } ): () => void { @@ -470,7 +468,7 @@ export class ThePathfinder { for (const key of goal._eventKeys) { const listener = (...args: Parameters): void => { - if (this.resetReason === "goalReassignment") return cleanup() + if (this.resetReason === 'goalReassignment') return cleanup() if (boundEvent(key, ...args)) newOnHasUpdate() } this.bot.on(key, listener) @@ -479,7 +477,7 @@ export class ThePathfinder { for (const key of goal._validKeys) { const listener1 = (...args: Parameters): void => { - if ((this.resetReason === "goalReassignment")) return cleanup() + if ((this.resetReason === 'goalReassignment')) return cleanup() if (boundValid(key, ...args)) newOnInvalid() } this.bot.on(key, listener1) @@ -491,12 +489,12 @@ export class ThePathfinder { return cleanup } - getPathTo(goal: goals.Goal, settings = this.defaultMoveSettings): PathGenerator { + getPathTo (goal: goals.Goal, settings = this.defaultMoveSettings): PathGenerator { const { movements } = this.activeMappings return this.getPathFromTo(this.bot.entity.position, this.bot.entity.velocity, goal, settings, movements) } - async * getPathFromTo( + async * getPathFromTo ( startPos: Vec3, startVel: Vec3, goal: goals.Goal, @@ -509,7 +507,6 @@ export class ThePathfinder { startPos = getSupportedStartPos(this.world, getNormalizedPos(this.bot, startPos)) log('Generating path from %O to %O', startPos, goal) - const startMove = Move.startMove( new IdleMovement(this.bot, this.world), startPos.clone(), @@ -594,7 +591,7 @@ export class ThePathfinder { } } - async getPathFromToRaw(startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise { + async getPathFromToRaw (startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise { for await (const res of this.getPathFromTo(startPos, startVel, goal)) { if (res.result.status !== 'success') { if (res.result.status === 'noPath' || res.result.status === 'timeout') return null @@ -605,7 +602,7 @@ export class ThePathfinder { return null } - async goto(goal: goals.Goal, performOpts: PerformOpts = {}): Promise { + async goto (goal: goals.Goal, performOpts: PerformOpts = {}): Promise { log('goto called') if (goal == null) { await this.cancel() @@ -640,7 +637,7 @@ export class ThePathfinder { } } - private async _goto(goal: goals.Goal, performOpts: PerformOpts = {}): Promise { + private async _goto (goal: goals.Goal, performOpts: PerformOpts = {}): Promise { const doForever = !!(goal instanceof goals.GoalDynamic && goal.neverfinish && goal.dynamic) let toWaitOn = Promise.resolve() @@ -689,7 +686,7 @@ export class ThePathfinder { res1 = null }) } else { - // Update the unoptimized path in place! + // Update the unoptimized path in place! // perform() will automatically catch the newly added tail. res1.path.length = res.result.path.length for (let i = 0; i < res.result.path.length; i++) { @@ -736,38 +733,38 @@ export class ThePathfinder { manualCleanup() } } - } while (this.resetReason !== "goalReassignment" && madeIt === false) + } while (this.resetReason !== 'goalReassignment' && !madeIt) await this.cleanupBot() if (doForever) { - if (this.resetReason == null && this.resetReason !== "goalReassignment") { + if (this.resetReason == null && this.resetReason !== 'goalReassignment') { await toWaitOn } } - } while (doForever && this.resetReason !== "goalReassignment") + } while (doForever && this.resetReason !== 'goalReassignment') } - perform(path: Path, goal: goals.Goal, entry = 0): Promise { - return this.executionRunner.perform(path, goal, entry) + async perform (path: Path, goal: goals.Goal, entry = 0): Promise { + return await this.executionRunner.perform(path, goal, entry) } - recovery(move: Move, path: Path, goal: goals.Goal, entry = 0): Promise { - return this.executionRunner.recovery(move, path, goal, entry) + async recovery (move: Move, path: Path, goal: goals.Goal, entry = 0): Promise { + return await this.executionRunner.recovery(move, path, goal, entry) } - async cleanupBot(forceSafety = false): Promise { + async cleanupBot (forceSafety = false): Promise { this.bot.clearControlStates() // rough code. just need any of them. - let exec; + let exec for (const [, executor] of this.activeMappings.movements) { - exec ??= executor; + exec ??= executor executor.reset() } if (forceSafety && exec != null) { - let normVel; + let normVel do { normVel = this.bot.entity.onGround ? this.bot.entity.velocity.offset(0, -this.bot.entity.velocity.y, 0) : this.bot.entity.velocity const ectx = exec.simForward({ ticks: 2 }) @@ -784,17 +781,16 @@ export class ThePathfinder { } } - cleanupClient(): void { + cleanupClient (): void { this.abortCalculation = false this.clearResetReason() delete this.currentGotoGoal - delete this.currentPath; - delete this.currentExecutor; - delete this.currentMove; - + delete this.currentPath + delete this.currentExecutor + delete this.currentMove } - async cleanupAll(goal: goals.Goal, executor = this.currentExecutor): Promise { + async cleanupAll (goal: goals.Goal, executor = this.currentExecutor): Promise { if (goal instanceof goals.GoalDynamic && goal.dynamic) { goal.cleanup?.() } @@ -806,7 +802,7 @@ export class ThePathfinder { } this.world.cleanup?.() - if ((this.resetReason === "goalReassignment")) { + if ((this.resetReason === 'goalReassignment')) { log('Cleanup: Goal aborted.') this.bot.emit('goalAborted', goal) } else { @@ -817,7 +813,7 @@ export class ThePathfinder { this.abortCalculation = false this.executeTask.finish() - log(`Task finished, cleanup client.`) + log('Task finished, cleanup client.') this.cleanupClient() } } diff --git a/src/abstract/algorithms/astar.ts b/src/abstract/algorithms/astar.ts index cc36bb5..e2d617f 100644 --- a/src/abstract/algorithms/astar.ts +++ b/src/abstract/algorithms/astar.ts @@ -20,7 +20,7 @@ export class AStar maxCost: number - checkInterval = 4// 1 << 5 - 1 + checkInterval = 4// 1 << 5 - 1 nodeConsiderCount = 0 constructor ( diff --git a/src/customBlockEvents.ts b/src/customBlockEvents.ts index 7dc4df6..34cd4c8 100644 --- a/src/customBlockEvents.ts +++ b/src/customBlockEvents.ts @@ -1,14 +1,14 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/explicit-function-return-type, @typescript-eslint/restrict-template-expressions */ // blockEvents.ts -const createDebug = require('debug') import type { Bot } from 'mineflayer' import type { Block } from 'prismarine-block' import type { Vec3 } from 'vec3' - +const createDebug = require('debug') export type BlockUpdateListener = (oldBlock: Block | null, newBlock: Block | null) => void export type BlockPositionString = `(${number}, ${number}, ${number})` -export type BlockPositionEventName = `blockUpdate:(x, y, z)` +export type BlockPositionEventName = 'blockUpdate:(x, y, z)' export type BlockEventName = 'blockUpdate' | BlockPositionEventName type BlockEventListenerMap = { @@ -17,17 +17,17 @@ type BlockEventListenerMap = { [K in BlockPositionEventName]: BlockUpdateListener } -const debugIt = false; +const debugIt = false const listenerCounts = new WeakMap>() -function debug(...args: any[]) { +function debug (...args: any[]) { const log = createDebug('minecraft-pathfinding:block-events') if (debugIt) { log(...args) } } -function getTrackedListenerCounts(bot: Bot): Map { +function getTrackedListenerCounts (bot: Bot): Map { let counts = listenerCounts.get(bot) if (counts == null) { counts = new Map() @@ -36,18 +36,18 @@ function getTrackedListenerCounts(bot: Bot): Map { return counts } -function changeTrackedListenerCount(bot: Bot, eventName: BlockEventName, delta: number): number { +function changeTrackedListenerCount (bot: Bot, eventName: BlockEventName, delta: number): number { const counts = getTrackedListenerCounts(bot) const next = Math.max(0, (counts.get(eventName) ?? 0) + delta) counts.set(eventName, next) return next } -export function getBlockEventListenerCount(bot: Bot, eventName: BlockEventName): number { +export function getBlockEventListenerCount (bot: Bot, eventName: BlockEventName): number { return getTrackedListenerCounts(bot).get(eventName) ?? 0 } -export function getTotalBlockEventListenerCount(bot: Bot): number { +export function getTotalBlockEventListenerCount (bot: Bot): number { let total = 0 for (const count of getTrackedListenerCounts(bot).values()) { total += count @@ -55,11 +55,11 @@ export function getTotalBlockEventListenerCount(bot: Bot): number { return total } -export function toBlockPositionEventName(position: Vec3): BlockPositionEventName { +export function toBlockPositionEventName (position: Vec3): BlockPositionEventName { return `blockUpdate:${position.toString()}` as BlockPositionEventName } -export function onBlockEvent( +export function onBlockEvent ( bot: Bot, eventName: K, listener: BlockEventListenerMap[K] @@ -75,7 +75,7 @@ export function onBlockEvent( } } -export function onceBlockEvent( +export function onceBlockEvent ( bot: Bot, eventName: K, listener: BlockEventListenerMap[K] @@ -97,7 +97,7 @@ export function onceBlockEvent( debug('active listeners %s=%d total=%d', eventName, active, getTotalBlockEventListenerCount(bot)) } -export function offBlockEvent( +export function offBlockEvent ( bot: Bot, eventName: K, listener: BlockEventListenerMap[K] @@ -108,7 +108,7 @@ export function offBlockEvent( debug('active listeners %s=%d total=%d', eventName, active, getTotalBlockEventListenerCount(bot)) } -export function handleBlockEvent( +export function handleBlockEvent ( bot: Bot, eventName: K, listener: BlockEventListenerMap[K] @@ -117,7 +117,7 @@ export function handleBlockEvent( return () => offBlockEvent(bot, eventName, listener) } -export function handleBlockPositionEvent( +export function handleBlockPositionEvent ( bot: Bot, position: Vec3, listener: BlockUpdateListener @@ -130,19 +130,19 @@ export interface WaitForBlockEventOptions { signal?: AbortSignal } -export function waitForBlockEvent( +export async function waitForBlockEvent ( bot: Bot, eventName: K, opts: WaitForBlockEventOptions = {} ): Promise> { const { timeoutMs = 5000, signal } = opts - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { let finished = false let timeout: NodeJS.Timeout | undefined const cleanup = () => { - if (timeout) clearTimeout(timeout) + if (timeout != null) clearTimeout(timeout) offBlockEvent(bot, eventName, listener) signal?.removeEventListener('abort', onAbort) } @@ -179,7 +179,7 @@ export function waitForBlockEvent( }, timeoutMs) } - if (signal) { + if (signal != null) { if (signal.aborted) { fail(new Error(`Aborted while waiting for ${eventName}`)) return @@ -213,7 +213,7 @@ export type SettledBlockUpdateListener = ( settledBlock: Block | null ) => void | Promise -type PositionSettleController = { +interface PositionSettleController { resolve: (value: TResult) => void reject: (err: Error) => void finishGuard: () => boolean @@ -223,7 +223,7 @@ type PositionSettleController = { bot: Bot } -function waitForPositionSettle( +async function waitForPositionSettle ( bot: Bot, position: Vec3, opts: { @@ -245,7 +245,7 @@ function waitForPositionSettle( const settleMs = opts.settleMs ?? 75 const signal = opts.signal - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { let finished = false let timeoutTimer: NodeJS.Timeout | undefined let settleTimer: NodeJS.Timeout | undefined @@ -267,7 +267,7 @@ function waitForPositionSettle( } const armSettleTimer = (fn: () => void) => { - if (settleTimer) clearTimeout(settleTimer) + if (settleTimer != null) clearTimeout(settleTimer) settleTimer = setTimeout(() => { if (finished) return fn() @@ -275,7 +275,7 @@ function waitForPositionSettle( } const cancelSettleTimer = () => { - if (settleTimer) { + if (settleTimer != null) { clearTimeout(settleTimer) settleTimer = undefined } @@ -310,12 +310,12 @@ function waitForPositionSettle( fail(new Error(`Aborted while waiting for ${eventName}`)) } - const cleanup = () => { - if (timeoutTimer) clearTimeout(timeoutTimer) - cancelSettleTimer() - offBlockEvent(bot, 'blockUpdate', onUpdate) - signal?.removeEventListener('abort', onAbort) - } + const cleanup = () => { + if (timeoutTimer != null) clearTimeout(timeoutTimer) + cancelSettleTimer() + offBlockEvent(bot, 'blockUpdate', onUpdate) + signal?.removeEventListener('abort', onAbort) + } onBlockEvent(bot, 'blockUpdate', onUpdate) @@ -329,7 +329,7 @@ function waitForPositionSettle( }, timeoutMs) } - if (signal) { + if (signal != null) { if (signal.aborted) { fail(new Error(`Aborted while waiting for ${eventName}`)) return @@ -338,7 +338,7 @@ function waitForPositionSettle( } }) } -export async function waitForSettledBlockPredicate( +export async function waitForSettledBlockPredicate ( bot: Bot, position: Vec3, predicate: (block: Block | null) => boolean, @@ -354,7 +354,7 @@ export async function waitForSettledBlockPredicate( const cancel = () => { const settleTimer = (controller as any).settleTimer as NodeJS.Timeout | undefined - if (settleTimer) { + if (settleTimer != null) { clearTimeout(settleTimer) ;(controller as any).settleTimer = undefined } @@ -390,7 +390,7 @@ export async function waitForSettledBlockPredicate( }) } -export async function waitForSettledBlockStateAtPosition( +export async function waitForSettledBlockStateAtPosition ( bot: Bot, position: Vec3, predicate: (block: Block | null) => boolean, @@ -404,7 +404,7 @@ export async function waitForSettledBlockStateAtPosition( createListener: (controller) => { const cancel = () => { const settleTimer = (controller as any).settleTimer as NodeJS.Timeout | undefined - if (settleTimer) { + if (settleTimer != null) { clearTimeout(settleTimer) ;(controller as any).settleTimer = undefined } @@ -445,7 +445,7 @@ export async function waitForSettledBlockStateAtPosition( }) } -export async function waitForSettledBlockUpdateAtPosition( +export async function waitForSettledBlockUpdateAtPosition ( bot: Bot, position: Vec3, opts: WaitForSettledBlockUpdateOptions = {} @@ -478,7 +478,7 @@ export async function waitForSettledBlockUpdateAtPosition( * - Deduplicates concurrent checks per position * - Reads the final block from the world before invoking the listener */ -export function handleSettledBlockEvent( +export function handleSettledBlockEvent ( bot: Bot, listener: SettledBlockUpdateListener, opts: HandleSettledBlockEventOptions = {} diff --git a/src/index.ts b/src/index.ts index 933b692..aa49363 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Bot } from 'mineflayer' import { BlockInfo } from './mineflayer-specific/world/cacheWorld' import { PathfinderOptions, ThePathfinder } from './ThePathfinder' @@ -16,7 +17,7 @@ import type { MovementOptions, MovementSetup } from './mineflayer-specific/movem import { MovementProvider } from './mineflayer-specific/movements' import type { OptimizationSetup } from './mineflayer-specific/post' -export function createPlugin(opts?: HandlerOpts) { +export function createPlugin (opts?: HandlerOpts) { return function (bot: Bot) { BlockInfo.init(bot.registry) // set up block info if (!bot.hasPlugin(utilPlugin)) bot.loadPlugin(utilPlugin) @@ -60,4 +61,4 @@ export { MovementOptimizer } from './mineflayer-specific/post' export type { BuildableMoveOptimizer, OptimizationSetup, OptimizationMap } from './mineflayer-specific/post' export { Move } from './mineflayer-specific/move' -export * as movementProviders from './mineflayer-specific/movements/movementProviders' \ No newline at end of file +export * as movementProviders from './mineflayer-specific/movements/movementProviders' diff --git a/src/mineflayer-specific/algs.ts b/src/mineflayer-specific/algs.ts index e6393ec..386370e 100644 --- a/src/mineflayer-specific/algs.ts +++ b/src/mineflayer-specific/algs.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Goal, MovementProvider as AMovementProvider, Path as APath } from '../abstract' import { AStarBackOff as AAStarBackOff } from '../abstract/algorithms/astar' import { CPathNode } from '../abstract/node' @@ -11,7 +12,6 @@ export interface Path extends APath ${afterTick}`) this.name = 'TickAdvanceError' } -} \ No newline at end of file +} diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts index a39f8e3..5597941 100644 --- a/src/mineflayer-specific/goals.ts +++ b/src/mineflayer-specific/goals.ts @@ -282,7 +282,7 @@ export class GoalBlock extends Goal { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z - return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone. + return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone. // return (Math.sqrt(dx * dx + dz * dz) + Math.abs(dy)) // return distanceXZ(dx, dz) + Math.abs(dy) } @@ -291,7 +291,7 @@ export class GoalBlock extends Goal { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z - const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone. + const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone. return distance } @@ -332,7 +332,7 @@ export class GoalNear extends Goal { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z - return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone. + return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone. } distHeuristic (node: Move): number { @@ -363,7 +363,7 @@ export class GoalNearXZ extends Goal { heuristic (node: Move): number { const dx = this.x - node.x const dz = this.z - node.z - return Math.sqrt(dx * dx + dz * dz) * (20 / 4.317); // from baritone. + return Math.sqrt(dx * dx + dz * dz) * (20 / 4.317) // from baritone. } distHeuristic (node: Move): number { @@ -418,15 +418,15 @@ export class GoalLookAt extends Goal { const dx = this.x - node.x const dy = this.y - (node.y + this.eyeHeight) // eye level const dz = this.z - node.z - return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone. + return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone. } distHeuristic (node: Move): number { - const dx = this.x - node.x - const dy = this.y - (node.y + this.eyeHeight) // eye level - const dz = this.z - node.z - const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) - return distance + const dx = this.x - node.x + const dy = this.y - (node.y + this.eyeHeight) // eye level + const dz = this.z - node.z + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) + return distance } /** @@ -559,10 +559,9 @@ export class GoalFollowEntity extends GoalDynamic<'entityMoved', 'entityGone'> { const dy = this.y - node.y const dz = this.z - node.z - return Math.sqrt(dx * dx + dy * dy + dz * dz) * (20 / 4.317); // from baritone. + return Math.sqrt(dx * dx + dy * dy + dz * dz) * (20 / 4.317) // from baritone. } - distHeuristic (node: Move): number { const dx = this.x - node.x const dy = this.y - node.y diff --git a/src/mineflayer-specific/movements/controls.ts b/src/mineflayer-specific/movements/controls.ts index 3298ce5..d178255 100644 --- a/src/mineflayer-specific/movements/controls.ts +++ b/src/mineflayer-specific/movements/controls.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/consistent-type-assertions */ import { IEntityState } from '@nxg-org/mineflayer-physics-util' import { Bot } from 'mineflayer' import { Vec3 } from 'vec3' @@ -237,7 +238,7 @@ export function botSmartMovement (bot: Bot, nextPoint: Vec3, sprint: boolean, mi smartMovement(stateLike, nextPoint, sprint, minDist) } -type StrafeState = { +interface StrafeState { pos: Vec3 vel: Vec3 yaw: number diff --git a/src/mineflayer-specific/movements/interactionUtils.ts b/src/mineflayer-specific/movements/interactionUtils.ts index 311e592..3435e7d 100644 --- a/src/mineflayer-specific/movements/interactionUtils.ts +++ b/src/mineflayer-specific/movements/interactionUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions, @typescript-eslint/strict-boolean-expressions */ import { Bot, BotEvents } from 'mineflayer' import { Vec3 } from 'vec3' @@ -21,7 +22,6 @@ const logBreak = debug('minecraft-pathfinding:BreakHandler') export type InteractType = 'water' | 'solid' | 'replaceable' - interface InteractionPerformInfo { ticks: number tickAllowance: number @@ -55,23 +55,23 @@ export abstract class InteractHandler { protected readonly move!: MovementExecutor - protected get settings(): MovementOptions { + protected get settings (): MovementOptions { return this.move.settings } - public get vec(): Vec3 { + public get vec (): Vec3 { return new Vec3(this.x, this.y, this.z) } - public get bb(): AABB { + public get bb (): AABB { return AABB.fromBlock(this.vec) } - public get equipping(): boolean { + public get equipping (): boolean { return this._equipping } - constructor( + constructor ( public readonly x: number, public readonly y: number, public readonly z: number, @@ -81,32 +81,32 @@ export abstract class InteractHandler { this.blockInfo = this.toBlockInfo() } - public get isPerforming(): boolean { + public get isPerforming (): boolean { return this.performing } - public get done(): boolean { + public get done (): boolean { return this._done } - public get allowExit(): boolean { + public get allowExit (): boolean { return !this._internalLock } - public loadMove(move: MovementExecutor): void { + public loadMove (move: MovementExecutor): void { (this as any).move = move } - abstract needToPerform(bot: Bot): boolean + abstract needToPerform (bot: Bot): boolean - abstract getItem(bot: Bot, block?: Block): Item | null - abstract perform(bot: Bot, item: Item | null, opts?: InteractOpts): Promise - abstract performInfo(bot: Bot, ticks?: number): Promise - abstract toBlockInfo(): BlockInfo + abstract getItem (bot: Bot, block?: Block): Item | null + abstract perform (bot: Bot, item: Item | null, opts?: InteractOpts): Promise + abstract performInfo (bot: Bot, ticks?: number): Promise + abstract toBlockInfo (): BlockInfo - abstract abort(bot: Bot): Promise + abstract abort (bot: Bot): Promise - public async _abort(bot: Bot): Promise { + public async _abort (bot: Bot): Promise { if (this.performing && !this.cancelled) { logBase(`Aborting interaction at ${this.vec}`) await this.abort(bot) @@ -115,7 +115,7 @@ export abstract class InteractHandler { } } - public async _perform(bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise { + public async _perform (bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise { if (this.performing) { logBase(`Error: Already performing interaction at ${this.vec}`) throw new Error('Already performing') @@ -143,14 +143,14 @@ export abstract class InteractHandler { return ret } - getCurrentItem(bot: Bot): Item | null { + getCurrentItem (bot: Bot): Item | null { if (this.offhand) return bot.inventory.slots[bot.getEquipmentDestSlot('off-hand')] return bot.inventory.slots[bot.getEquipmentDestSlot('hand')] } - async equipItem(bot: Bot, item: Item | null): Promise { + async equipItem (bot: Bot, item: Item | null): Promise { if (this._equipping) return // already equipping item, ignore silently. - this._equipping = true; + this._equipping = true if (item === null) { logBase(`Unequipping ${this.offhand ? 'off-hand' : 'hand'}`) await bot.unequip(this.offhand ? 'off-hand' : 'hand') @@ -163,10 +163,10 @@ export abstract class InteractHandler { } bot.updateHeldItem() // await bot.waitForTicks(2) - this._equipping = false; + this._equipping = false } - async allowExternalInfluence(bot: Bot, ticks = 1, sneak = false): Promise { + async allowExternalInfluence (bot: Bot, ticks = 1, sneak = false): Promise { if (!this.performing) return true if (!this._internalLock) return true @@ -192,16 +192,16 @@ export class PlaceHandler extends InteractHandler { static reach = 4 private _placeTask?: Promise - static fromVec(vec: Vec3, type: InteractType, offhand = false): PlaceHandler { + static fromVec (vec: Vec3, type: InteractType, offhand = false): PlaceHandler { return new PlaceHandler(vec.x, vec.y, vec.z, type, offhand) } - static identTypeFromItem(item: Item): InteractType { + static identTypeFromItem (item: Item): InteractType { if (item.name.includes('water')) return 'water' return 'solid' } - toBlockInfo(): BlockInfo { + toBlockInfo (): BlockInfo { switch (this.type) { case 'solid': return BlockInfo.SOLID(this.vec) @@ -214,7 +214,7 @@ export class PlaceHandler extends InteractHandler { } } - getItem(bot: Bot): Item | null { + getItem (bot: Bot): Item | null { switch (this.type) { case 'water': { return bot.inventory.items().find((item) => item.name === 'water_bucket') ?? null @@ -230,7 +230,7 @@ export class PlaceHandler extends InteractHandler { } } - getNearbyBlocks(world: World): BlockInfo[] { + getNearbyBlocks (world: World): BlockInfo[] { return [ world.getBlockInfo(this.vec.offset(0, 1, 0)), world.getBlockInfo(this.vec.offset(0, -1, 0)), @@ -241,7 +241,7 @@ export class PlaceHandler extends InteractHandler { ] } - needToPerform(bot: Bot): boolean { + needToPerform (bot: Bot): boolean { const blockInfo = bot.pathfinder.world.getBlockInfo(this.vec) if (blockInfo.isInvalid) { logPlace(`Block at ${this.vec} is invalid. needsToPerform: true`) @@ -268,7 +268,7 @@ export class PlaceHandler extends InteractHandler { return needs } - async performInfo(bot: Bot, ticks = 15, scale = 0.5): Promise { + async performInfo (bot: Bot, ticks = 15, scale = 0.5): Promise { switch (this.type) { case 'water': { throw new Error('Not implemented') @@ -358,7 +358,7 @@ export class PlaceHandler extends InteractHandler { } } - async perform(bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise { + async perform (bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise { const curInfo = { yaw: bot.entity.yaw, pitch: bot.entity.pitch } if (item === null) { @@ -376,7 +376,7 @@ export class PlaceHandler extends InteractHandler { logPlace(`Looking at ${this.vec} to place water.`) await bot.lookAt(this.vec, this.settings.forceLook) bot.activateItem(this.offhand) - logPlace(`Water placed.`) + logPlace('Water placed.') break } @@ -427,10 +427,9 @@ export class PlaceHandler extends InteractHandler { PlaceHandler.reach * 2 )) as unknown as RayType - if (testCheck === null) { logPlace('what the fuck?') - break; + break } const pos1 = testCheck.position.plus(faceToVec(testCheck.face)) @@ -475,10 +474,10 @@ export class PlaceHandler extends InteractHandler { this._internalLock = false if (opts.noAwait) { - this._placeTask.catch((err) => logPlace(`Background place task failed: %O`, err)) + this._placeTask.catch((err) => logPlace('Background place task failed: %O', err)) } else { await this._placeTask - logPlace(`_placeTask resolved.`) + logPlace('_placeTask resolved.') } this.task?.finish() @@ -502,7 +501,7 @@ export class PlaceHandler extends InteractHandler { logPlace(`Completed perform sequence at ${this.vec}`) } - async abort(bot: Bot): Promise { + async abort (bot: Bot): Promise { logPlace(`Aborting placement at ${this.vec}`) if ((this.task != null) && !this.task.done) { this.task.finish() @@ -511,7 +510,7 @@ export class PlaceHandler extends InteractHandler { if (this._placeTask != null) { await this._placeTask.catch((err) => { - logPlace(`Caught error during _placeTask abort: %O`, err) + logPlace('Caught error during _placeTask abort: %O', err) }) } } @@ -521,19 +520,19 @@ export class BreakHandler extends InteractHandler { static reach = 4 private _breakTask?: Promise - static fromVec(vec: Vec3, type: InteractType, offhand = false): BreakHandler { + static fromVec (vec: Vec3, type: InteractType, offhand = false): BreakHandler { return new BreakHandler(vec.x, vec.y, vec.z, type, offhand) } - toBlockInfo(): BlockInfo { + toBlockInfo (): BlockInfo { return BlockInfo.AIR(this.vec) } - getBlock(world: World): Block | null { + getBlock (world: World): Block | null { return world.getBlock(this.vec) } - getItem(bot: Bot, block: Block): Item | null { + getItem (bot: Bot, block: Block): Item | null { switch (this.type) { case 'water': { return bot.inventory.items().find((item) => item.name === 'bucket') ?? null // empty bucket @@ -550,7 +549,7 @@ export class BreakHandler extends InteractHandler { } } - needToPerform(bot: Bot): boolean { + needToPerform (bot: Bot): boolean { const blockInfo = bot.pathfinder.world.getBlockInfo(this.vec) if (blockInfo.isInvalid) { @@ -563,7 +562,7 @@ export class BreakHandler extends InteractHandler { return needs } - async performInfo(bot: Bot, ticks = 15): Promise { + async performInfo (bot: Bot, ticks = 15): Promise { const bb = AABB.fromBlock(this.vec) const dist = bb.distanceToVec(bot.entity.position.offset(0, 1.62, 0)) const reachable = dist < BreakHandler.reach + 5 @@ -575,7 +574,7 @@ export class BreakHandler extends InteractHandler { : { ticks: Infinity, tickAllowance: Infinity, shiftTick: Infinity, raycasts: [] } } - async perform(bot: Bot, item: Item | null = null, opts: InteractOpts = {}): Promise { + async perform (bot: Bot, item: Item | null = null, opts: InteractOpts = {}): Promise { const curInfo = { yaw: bot.entity.yaw, pitch: bot.entity.pitch } logBreak(`Starting break sequence at ${this.vec}`) @@ -588,7 +587,7 @@ export class BreakHandler extends InteractHandler { logBreak(`Looking at ${this.vec} to collect water.`) await bot.lookAt(this.vec, this.settings.forceLook) bot.activateItem(this.offhand) - logBreak(`Water collected.`) + logBreak('Water collected.') break } @@ -611,8 +610,7 @@ export class BreakHandler extends InteractHandler { logBreak(`Calling bot.dig on ${block.name}`) this._breakTask = bot.dig(block, 'ignore', 'raycast') - - logBreak(`Dig task resolved. Now waiting for world update.`) + logBreak('Dig task resolved. Now waiting for world update.') await waitForSettledBlockStateAtPosition( bot, @@ -647,7 +645,7 @@ export class BreakHandler extends InteractHandler { logBreak(`Completed break sequence at ${this.vec}`) } - async abort(bot: Bot): Promise { + async abort (bot: Bot): Promise { logBreak(`Aborting break at ${this.vec}`) if ((this.task != null) && !this.task.done) { this.task.finish() @@ -660,7 +658,7 @@ export class BreakHandler extends InteractHandler { break } case 'solid': { - logBreak(`Calling bot.stopDigging()`) + logBreak('Calling bot.stopDigging()') bot.stopDigging() break } @@ -669,8 +667,8 @@ export class BreakHandler extends InteractHandler { } } await this._breakTask.catch((err) => { - logBreak(`Caught error during _breakTask abort: %O`, err) + logBreak('Caught error during _breakTask abort: %O', err) }) } } -} \ No newline at end of file +} diff --git a/src/mineflayer-specific/movements/movementExecutor.ts b/src/mineflayer-specific/movements/movementExecutor.ts index 3a4ef9e..42ab3d4 100644 --- a/src/mineflayer-specific/movements/movementExecutor.ts +++ b/src/mineflayer-specific/movements/movementExecutor.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/member-delimiter-style, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars */ import { Bot } from 'mineflayer' import { Vec3 } from 'vec3' import { Move } from '../move' @@ -32,13 +33,13 @@ export interface CompleteOpts { } export interface AlignmentBBInfo { - pos: Vec3; + pos: Vec3 requireSupport: boolean } export interface InitAlignOpts { - others?: AlignmentBBInfo[], - customBB?: AABB; + others?: AlignmentBBInfo[] + customBB?: AABB enterExitInterp?: boolean } @@ -65,7 +66,7 @@ export abstract class MovementExecutor extends Movement { /** * Return the current interaction. */ - public get cI(): InteractHandler | undefined { + public get cI (): InteractHandler | undefined { // if (this._cI === undefined) return undefined; // if (this._cI.allowExit) return undefined; return this._cI @@ -83,37 +84,35 @@ export abstract class MovementExecutor extends Movement { private task: Task = new Task() - public constructor(bot: Bot, world: World, settings: Partial = {}) { + public constructor (bot: Bot, world: World, settings: Partial = {}) { super(bot, world, settings) this.engine = new BotcraftPhysics(bot.registry) this.sim = new BaseSimulator(this.engine) this.simCtx = EPhysicsCtx.FROM_BOT(this.engine, bot) } - public reset(): void { + public reset (): void { // log('Resetting MovementExecutor') this.aborted = false delete this.resetReason this.task.finish() } - protected getRemainingBreaks(): BreakHandler[] { + protected getRemainingBreaks (): BreakHandler[] { if (!this.currentMove) return [] return this.currentMove.toBreak.filter(b => b.needToPerform(this.bot)) } - protected getRemainingPlaces(): PlaceHandler[] { + protected getRemainingPlaces (): PlaceHandler[] { if (!this.currentMove) return [] return this.currentMove.toPlace.filter(b => b.needToPerform(this.bot)) } - protected isTooLow(thisMove: Move) { - - const botY = this.bot.entity.position.y + 0.6; // allow step up + protected isTooLow (thisMove: Move) { + const botY = this.bot.entity.position.y + 0.6 // allow step up const block = this.getBlockInfo(this.bot.entity.position, 0, 0, 0) - if (botY < thisMove.exitPos.y && botY < thisMove.entryPos.y) { throw new CancelError(`y level: too low! ${botY}, ${thisMove.entryPos.y} ${thisMove.exitPos.y}`) } @@ -122,7 +121,7 @@ export abstract class MovementExecutor extends Movement { /** * TODO: Implement. */ - public async abort(move: Move = this.currentMove, settings: AbortOpts = {}): Promise { + public async abort (move: Move = this.currentMove, settings: AbortOpts = {}): Promise { // if (this.aborted || this.resetReason != null) return const resetting = settings.reason @@ -136,7 +135,7 @@ export abstract class MovementExecutor extends Movement { this.task = new Task() } - private async holdUntilAborted(move: Move, task: Task, timeout = 1000): Promise { + private async holdUntilAborted (move: Move, task: Task, timeout = 1000): Promise { if (!this.aborted && this.resetReason == null) return log('holdUntilAborted: aborting process started') @@ -184,7 +183,7 @@ export abstract class MovementExecutor extends Movement { /** * TODO: potentially buggy code. Check. */ - public async perform(thisMove: Move, currentIndex: number, path: Move[]): Promise { + public async perform (thisMove: Move, currentIndex: number, path: Move[]): Promise { log('Performing move at index %d', currentIndex) this.currentMove = thisMove if (this.resetReason != null) throw this.resetReason // new ResetError('Movement is resetting.') @@ -222,17 +221,17 @@ export abstract class MovementExecutor extends Movement { } } - public async _performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + public async _performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { await this.holdUntilAborted(thisMove, this.task) return await this.performInit(thisMove, currentIndex, path) } - public async _performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + public async _performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { await this.holdUntilAborted(thisMove, this.task) return await this.performPerTick(thisMove, tickCount, currentIndex, path) } - public async _align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise { + public async _align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise { await this.holdUntilAborted(thisMove, this.task) return await this.align(thisMove, tickCount, goal) } @@ -243,7 +242,7 @@ export abstract class MovementExecutor extends Movement { * Perform initial setup upon movement start. * Can be sync or async. */ - abstract performInit(thisMove: Move, currentIndex: number, path: Move[]): void | Promise + abstract performInit (thisMove: Move, currentIndex: number, path: Move[]): void | Promise /** * Runtime calculation. @@ -252,7 +251,7 @@ export abstract class MovementExecutor extends Movement { * Return whether or not bot has reached the goal. * */ - abstract performPerTick( + abstract performPerTick ( thisMove: Move, tickCount: number, currentIndex: number, @@ -266,7 +265,7 @@ export abstract class MovementExecutor extends Movement { * This can be used to align to the center of blocks, etc. * Align IS allowed to throw exceptions, it will revert to recovery. */ - align(thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): boolean | Promise { + align (thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): boolean | Promise { const target = lookTarget ?? thisMove.entryPos if (lookTarget != null) void this.postInitAlignToPath(thisMove, { lookAt: target }) else void this.postInitAlignToPath(thisMove) @@ -279,7 +278,7 @@ export abstract class MovementExecutor extends Movement { * * Check whether or not the move is already currently completed. This is checked once, before alignment. */ - isAlreadyCompleted(thisMove: Move, tickCount: number, goal: goals.Goal): boolean { + isAlreadyCompleted (thisMove: Move, tickCount: number, goal: goals.Goal): boolean { return this.isComplete(thisMove) } @@ -291,16 +290,15 @@ export abstract class MovementExecutor extends Movement { * Does so via velocity direction check (heading towards the block) * and bounding box check (touching OR slightly above block). */ - protected isComplete(startMove: Move, endMove: Move = startMove, opts: CompleteOpts = {}): boolean { - + protected isComplete (startMove: Move, endMove: Move = startMove, opts: CompleteOpts = {}): boolean { if (this.cI !== undefined) { if (!this.cI.allowExit) return false } - const checkP = opts.checkPlaces ?? true; - const checkB = opts.checkBreaks ?? true; - if (checkP && this.toPlaceLen() > 0) return false; - if (checkB && this.toBreakLen() > 0) return false; + const checkP = opts.checkPlaces ?? true + const checkB = opts.checkBreaks ?? true + if (checkP && this.toPlaceLen() > 0) return false + if (checkB && this.toBreakLen() > 0) return false const ticks = opts.ticks ?? 1 @@ -318,10 +316,10 @@ export abstract class MovementExecutor extends Movement { const dist = offset.norm() const ectx = EPhysicsCtx.FROM_BOT(this.bot.physicsUtil.engine, this.bot) - const history = [ectx.position.clone()]; + const history = [ectx.position.clone()] for (let i = 0; i < ticks; i++) { ectx.state.control.set('jump', false) // we don't want to jump again. - ectx.state.jumpQueued = false; + ectx.state.jumpQueued = false this.bot.physicsUtil.engine.simulate(ectx, this.world) history.push(ectx.position.clone()) } @@ -346,7 +344,6 @@ export abstract class MovementExecutor extends Movement { !ectx.state.onGround && this.bot.pathfinder.world.getBlockInfo(this.bot.entity.position.floored().translate(0, -0.6, 0)).liquid - if (aboveWater) { bb1bl = this.bot.pathfinder.world.getBlockInfo(target.floored()) bbCheckCond = bb1bl.walkthrough @@ -385,7 +382,7 @@ export abstract class MovementExecutor extends Movement { if (similarDirection && headingThatWay) return !ectx.state.isCollidedHorizontally // in air check. else if (dist < 0.2) return true else { - return true; + return true } // console.log('finished!', this.bot.entity.position, endMove.exitPos, bbsVertTouching, similarDirection, headingThatWay, offset.y) @@ -409,10 +406,8 @@ export abstract class MovementExecutor extends Movement { ) } - public isInitAligned(thisMove: Move, target: Vec3 = thisMove.entryPos, options: InitAlignOpts = {}): boolean { - - if (this.isComplete(thisMove)) return true; - + public isInitAligned (thisMove: Move, target: Vec3 = thisMove.entryPos, options: InitAlignOpts = {}): boolean { + if (this.isComplete(thisMove)) return true const off0 = thisMove.exitPos.minus(this.bot.entity.position).normalize() const off1 = thisMove.exitPos.minus(target).normalize() @@ -426,49 +421,48 @@ export abstract class MovementExecutor extends Movement { const similarDirection = off0.dot(off1) > 0.7 - let bb0 = options.customBB; + let bb0 = options.customBB if (bb0 == null) { const normPos = getNormalizedPos(this.bot) bb0 = AABBUtils.getEntityAABBRaw({ position: normPos, width: 0.6, height: 1.8 }) } - const toCheck = options.others ?? []; + const toCheck = options.others ?? [] const entercheck = { pos: target.offset(0, -1, 0), requireSupport: true } - const exitCheck = { pos: thisMove.exitPos.floored().translate(0, -1, 0), requireSupport: true }; + const exitCheck = { pos: thisMove.exitPos.floored().translate(0, -1, 0), requireSupport: true } toCheck.push(entercheck) toCheck.push(exitCheck) - let valid; + let valid // default false. if (options.enterExitInterp) { valid = this.interpolatedBBCheck(bb0, thisMove.cachedVec.offset(0.5, 0, 0.5), thisMove.exitPos.floored().offset(0.5, 0, 0.5)) || this.boundingBoxCheck(bb0, ...toCheck) - } else valid = this.boundingBoxCheck(bb0, ...toCheck); + } else valid = this.boundingBoxCheck(bb0, ...toCheck) if (valid) { log('isInitAligned: yaw check passed. similarDirection=%s, dist=%d', similarDirection, this.bot.entity.position.xzDistanceTo(target)) if (similarDirection) return true else { - log(`exit -> cur: %O, exit -> target: %O`, off0, off1, off0.dot(off1)) + log('exit -> cur: %O, exit -> target: %O', off0, off1, off0.dot(off1)) if (this.bot.entity.position.xzDistanceTo(target) < 0.3) return true if (this.boundingBoxCheck(bb0, exitCheck)) return true } } - log(`isInitAligned: We are not aligned. us: %O, target pos: %O`, bb0, target) - log(`Extra info: opts: %O`, options) + log('isInitAligned: We are not aligned. us: %O, target pos: %O', bb0, target) + log('Extra info: opts: %O', options) return false } - /** * Lazy code. */ - public safeToCancel(startMove: Move, endMove: Move = startMove): boolean { + public safeToCancel (startMove: Move, endMove: Move = startMove): boolean { return this.bot.entity.onGround || ((this.bot.entity as any).isInWater as boolean) } - protected getTooLowCheckY(pos: Vec3 = this.bot.entity.position): number { + protected getTooLowCheckY (pos: Vec3 = this.bot.entity.position): number { const supportBbs = getUnderlyingBBs(this.world, pos, 0.6) if (supportBbs.length === 0) { return pos.y + 0.6 @@ -479,7 +473,7 @@ export abstract class MovementExecutor extends Movement { return pos.y + 0.6 + supportCorrection } - protected tooLowCheck(thisMove: Move, pos: Vec3 = this.bot.entity.position): boolean { + protected tooLowCheck (thisMove: Move, pos: Vec3 = this.bot.entity.position): boolean { const botY = this.getTooLowCheckY(pos) return botY < thisMove.exitPos.y && botY < thisMove.entryPos.y } @@ -490,8 +484,8 @@ export abstract class MovementExecutor extends Movement { * Return breaks first as they will not interfere with placements, * whereas placements will almost always interfere with breaks (LOS failure). */ - async interactNeeded(ticks = 1): Promise { - const start = performance.now(); + async interactNeeded (ticks = 1): Promise { + const start = performance.now() for (const breakTarget of this.currentMove.toBreak) { if (breakTarget !== this._cI && !breakTarget.done) { if (!breakTarget.needToPerform(this.bot)) continue @@ -514,7 +508,7 @@ export abstract class MovementExecutor extends Movement { /** * Generalized function to perform an interaction. */ - async performInteraction(interaction: PlaceHandler | BreakHandler, opts: InteractOpts = {}): Promise { + async performInteraction (interaction: PlaceHandler | BreakHandler, opts: InteractOpts = {}): Promise { this._cI = interaction interaction.loadMove(this) if (interaction instanceof PlaceHandler) { @@ -524,7 +518,7 @@ export abstract class MovementExecutor extends Movement { } } - protected async performPlace(place: PlaceHandler, opts: InteractOpts = {}): Promise { + protected async performPlace (place: PlaceHandler, opts: InteractOpts = {}): Promise { const item = place.getItem(this.bot) if (item == null) throw new CancelError('MovementExecutor: no item to place') log('performPlace: placing item %s', item.name) @@ -532,7 +526,7 @@ export abstract class MovementExecutor extends Movement { this._cI = undefined } - protected async performBreak(breakTarget: BreakHandler, opts: InteractOpts = {}): Promise { + protected async performBreak (breakTarget: BreakHandler, opts: InteractOpts = {}): Promise { const block = breakTarget.getBlock(this.bot.pathfinder.world) if (block == null) throw new CancelError('MovementExecutor: no block to break') const item = breakTarget.getItem(this.bot, block) @@ -545,7 +539,7 @@ export abstract class MovementExecutor extends Movement { * Utility function to have the bot look in the direction of the target, * but only on the xz plane (pitch is always 0). */ - public async lookAtPathPos(vec3: Vec3, force = this.settings.forceLook): Promise { + public async lookAtPathPos (vec3: Vec3, force = this.settings.forceLook): Promise { const pos = this.bot.entity.position const dx = vec3.x - pos.x const dz = vec3.z - pos.z @@ -555,17 +549,17 @@ export abstract class MovementExecutor extends Movement { await this.bot.look(yaw, 0, force) } - public async look(yaw: number, pitch: number, force = this.settings.forceLook): Promise { + public async look (yaw: number, pitch: number, force = this.settings.forceLook): Promise { if (this.isLookingAtRotation(yaw, pitch, 0.001)) return await this.bot.look(yaw, pitch, force) } - public async lookAt(vec3: Vec3, force = this.settings.forceLook): Promise { + public async lookAt (vec3: Vec3, force = this.settings.forceLook): Promise { if (this.isLookingAt(vec3, 0.001)) return await this.bot.lookAt(vec3, force) } - public isLookingAt(vec3: Vec3, limit = 0.01): boolean { + public isLookingAt (vec3: Vec3, limit = 0.01): boolean { if (!this.settings.careAboutLookAlignment) return true const bl = this.bot.blockAtCursor(256) as unknown as RayType | null @@ -575,7 +569,7 @@ export abstract class MovementExecutor extends Movement { return bl.intersect.minus(eyePos).normalize().dot(vec3.minus(eyePos).normalize()) > 1 - limit } - public isLookingAtYaw(vec3: Vec3, limit = 0.01): boolean { + public isLookingAtYaw (vec3: Vec3, limit = 0.01): boolean { if (!this.settings.careAboutLookAlignment) return true const eyePos = this.bot.entity.position.offset(0, 1.62, 0) @@ -587,7 +581,7 @@ export abstract class MovementExecutor extends Movement { return this.isLookingAtYawRotation(yaw, limit) } - public isLookingAtRotation(yaw: number, pitch: number, limit = 0.01): boolean { + public isLookingAtRotation (yaw: number, pitch: number, limit = 0.01): boolean { if (!this.settings.careAboutLookAlignment) return true const currentYaw = this.bot.entity.yaw @@ -608,7 +602,7 @@ export abstract class MovementExecutor extends Movement { return currentDir.dot(targetDir) > 1 - limit } - public isLookingAtYawRotation(yaw: number, limit = 0.01): boolean { + public isLookingAtYawRotation (yaw: number, limit = 0.01): boolean { if (!this.settings.careAboutLookAlignment) return true const currentYaw = this.bot.entity.yaw @@ -629,22 +623,22 @@ export abstract class MovementExecutor extends Movement { // Utils for handling collisions - private boundingBoxCheck(orgBB: AABB, ...info: AlignmentBBInfo[]): boolean { - let valid = false; + private boundingBoxCheck (orgBB: AABB, ...info: AlignmentBBInfo[]): boolean { + let valid = false for (const { pos, requireSupport } of info) { - const bInfo = this.getBlockInfoRaw(pos); + const bInfo = this.getBlockInfoRaw(pos) const bbs = bInfo.getBBs() if (bbs.length === 0) bbs.push(AABB.fromBlock(bInfo.position)) if (requireSupport && !(bInfo.physical || bInfo.liquid)) { - continue; + continue } valid = valid || bbs.some((b) => b.collides(orgBB)) // should shortcut. } - return valid; + return valid } - private interpolatedBBCheck(orgBB: AABB, start: Vec3, end: Vec3): boolean { + private interpolatedBBCheck (orgBB: AABB, start: Vec3, end: Vec3): boolean { for (const pos of interpolateStepPoints(start, end, 0.8)) { const bInfo = this.getBlockInfoRaw(pos) const bbs = bInfo.getBBs() @@ -656,26 +650,24 @@ export abstract class MovementExecutor extends Movement { if (bbs.some((b) => b.collides(orgBB))) { return true } - - } - log(`interpolatedBBCheck: no collision. orgBB %O, from %O to %O`, orgBB, start, end) + log('interpolatedBBCheck: no collision. orgBB %O, from %O to %O', orgBB, start, end) return false } // Sim functions. - protected resetState(): PlayerState { + protected resetState (): PlayerState { this.simCtx.state.update(this.bot) return this.simCtx.state } - protected simUntil(...args: Parameters['simulateUntil']>): ReturnType['simulateUntil']> { + protected simUntil (...args: Parameters['simulateUntil']>): ReturnType['simulateUntil']> { this.simCtx.state.update(this.bot) return this.sim.simulateUntil(...args) } - protected simUntilGrounded(controller: Controller, maxTicks = 1000): PlayerState { + protected simUntilGrounded (controller: Controller, maxTicks = 1000): PlayerState { this.simCtx.state.update(this.bot) return this.sim.simulateUntil( (state) => state.onGround, @@ -687,7 +679,7 @@ export abstract class MovementExecutor extends Movement { ) } - protected simJump({ goal, controller }: { goal?: SimulationGoal, controller?: Controller } = {}, maxTicks = 1000): PlayerState { + protected simJump ({ goal, controller }: { goal?: SimulationGoal, controller?: Controller } = {}, maxTicks = 1000): PlayerState { this.simCtx.state.update(this.bot) goal = goal ?? ((state) => state.onGround) controller = @@ -698,16 +690,16 @@ export abstract class MovementExecutor extends Movement { return this.sim.simulateUntil(goal, () => { }, controller, this.simCtx, this.world, maxTicks) } - protected async postInitAlignToPath( + protected async postInitAlignToPath ( startMove: Move, opts?: PostInitAlignOpts ): Promise - protected async postInitAlignToPath( + protected async postInitAlignToPath ( startMove: Move, endMove?: Move, opts?: PostInitAlignOpts ): Promise - protected async postInitAlignToPath(startMove: Move, endMove?: any, opts?: any): Promise { + protected async postInitAlignToPath (startMove: Move, endMove?: any, opts?: any): Promise { if (endMove === undefined) { endMove = startMove opts = {} @@ -733,7 +725,7 @@ export abstract class MovementExecutor extends Movement { } else { await this.lookAtPathPos(target) - const yawPitch = posToYawPitchFromEye(this.bot.entity.position, 1.62, target); + const yawPitch = posToYawPitchFromEye(this.bot.entity.position, 1.62, target) if (!this.isLookingAtYaw(target, 0.01)) { log(`postInitAlignToPath: failed yaw check (offset=${yawPitch.yaw - this.bot.entity.yaw})`) } @@ -742,7 +734,6 @@ export abstract class MovementExecutor extends Movement { botStrafeMovement(this.bot, endMove.exitPos, true) botSmartMovement(this.bot, endMove.exitPos, sprint) - // const orgControl = this.bot.getControlState('sneak') // if (this.willFallOff(1)) { // log(`postInitAlignToPath: we are about to fall off! sneaking...`) @@ -750,33 +741,27 @@ export abstract class MovementExecutor extends Movement { // } else { // this.bot.setControlState('sneak', orgControl) // } - } - - - /** * @returns whether we fall off. */ - public simForward(options: SimOpts = {}): EPhysicsCtx { - const ectx = options.ectx ?? EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot); + public simForward (options: SimOpts = {}): EPhysicsCtx { + const ectx = options.ectx ?? EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot) const ticks = options.ticks ?? 1 for (let i = 0; i < ticks; i++) { - - if (options.controls) { - ectx.state.control = options.controls; + if (options.controls != null) { + ectx.state.control = options.controls } - this.sim.ctx.simulate(ectx, this.bot.world); + this.sim.ctx.simulate(ectx, this.bot.world) } return ectx } - - protected doWaterLogic(): boolean { + protected doWaterLogic (): boolean { if ((this.bot.entity as any).isInWater as boolean) return true // potentially false, if underwater + standing on ground. @@ -785,5 +770,4 @@ export abstract class MovementExecutor extends Movement { const bl = this.getBlockInfo(this.bot.entity.position, 0, -0.6, 0) return bl.liquid } - } diff --git a/src/mineflayer-specific/movements/movementExecutors.ts b/src/mineflayer-specific/movements/movementExecutors.ts index 6a13d57..3c0cda2 100644 --- a/src/mineflayer-specific/movements/movementExecutors.ts +++ b/src/mineflayer-specific/movements/movementExecutors.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/restrict-template-expressions, @typescript-eslint/explicit-function-return-type */ import { Vec3 } from 'vec3' import * as goals from '../goals' import { Move } from '../move' @@ -22,24 +23,23 @@ const logDown = debug('minecraft-pathfinding:movementExecutors:StraightDown') const logUp = debug('minecraft-pathfinding:movementExecutors:StraightUp') const logParkour = debug('minecraft-pathfinding:movementExecutors:Parkour') - export class IdleMovementExecutor extends MovementExecutor { - provideMovements(start: Move, storage: Move[]): void { } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + provideMovements (start: Move, storage: Move[]): void { } + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { logIdle('performInit called') } - async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + + async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { return true } } - export class NewForwardExecutor extends MovementExecutor { - protected isComplete(startMove: Move, endMove?: Move, opts?: CompleteOpts): boolean { + protected isComplete (startMove: Move, endMove?: Move, opts?: CompleteOpts): boolean { return super.isComplete(startMove, endMove, { ticks: 2 }) } - private async faceForward(): Promise { + private async faceForward (): Promise { // console.log('called faceForward!') if (this.doWaterLogic()) return true const eyePos = this.bot.entity.position.offset(0, this.bot.entity.height, 0) @@ -51,9 +51,8 @@ export class NewForwardExecutor extends MovementExecutor { return this.currentMove?.toPlace.length === 0 || !near } - override async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise { + override async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise { if (this.doWaterLogic()) { - // clear to remove jump. this.bot.clearControlStates() return await super.align(thisMove, tickCount, goal) @@ -72,7 +71,7 @@ export class NewForwardExecutor extends MovementExecutor { return await this.landAlign(thisMove, tickCount, goal) } - async landAlign(thisMove: Move, tickCount: number, goal: goals.Goal): Promise { + async landAlign (thisMove: Move, tickCount: number, goal: goals.Goal): Promise { const faceForward = await this.faceForward() const opts: InitAlignOpts = { enterExitInterp: true } @@ -90,7 +89,6 @@ export class NewForwardExecutor extends MovementExecutor { this.bot.setControlState('forward', true) if (this.bot.food <= 6) this.bot.setControlState('sprint', false) else this.bot.setControlState('sprint', true) - } else { const offset = this.bot.entity.position.minus(target).plus(this.bot.entity.position) // await this.postInitAlignToPath(thisMove, { lookAt: offset }) @@ -107,7 +105,7 @@ export class NewForwardExecutor extends MovementExecutor { return this.isInitAligned(thisMove, target, opts) } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { // console.log('ForwardMove', thisMove.exitPos, thisMove.toPlace.length, thisMove.toBreak.length) this.bot.clearControlStates() @@ -124,7 +122,7 @@ export class NewForwardExecutor extends MovementExecutor { } // TODO: clean this up. - private canJump(thisMove: Move, currentIndex: number, path: Move[]): boolean { + private canJump (thisMove: Move, currentIndex: number, path: Move[]): boolean { if (this.doWaterLogic()) { if (this.bot.entity.position.y < thisMove.exitPos.y) { return true @@ -178,7 +176,7 @@ export class NewForwardExecutor extends MovementExecutor { return ctx.state.onGround } - async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot))) { return false } else if (this.cI == null) { @@ -224,12 +222,11 @@ export class ForwardJumpExecutor extends MovementExecutor { private readonly shitter: JumpCalculator = new JumpCalculator(this.sim, this.bot, this.world, this.simCtx) private flag = false - - protected isComplete(startMove: Move, endMove?: Move): boolean { + protected isComplete (startMove: Move, endMove?: Move): boolean { return super.isComplete(startMove, endMove, { ticks: 0 }) } - override async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise { + override async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise { if (super.doWaterLogic()) { this.bot.setControlState('jump', this.bot.entity.position.y < thisMove.entryPos.y) } @@ -237,7 +234,7 @@ export class ForwardJumpExecutor extends MovementExecutor { return await super.align(thisMove, tickCount, goal) } - align1(thisMove: Move, tickCount: number, goal: goals.Goal): boolean { + align1 (thisMove: Move, tickCount: number, goal: goals.Goal): boolean { const bb = AABBUtils.getEntityAABBRaw({ position: this.bot.entity.position, width: 0.6, height: 1.8 }) if (this.flag) { @@ -263,11 +260,11 @@ export class ForwardJumpExecutor extends MovementExecutor { return false } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { this.flag = false this.bot.clearControlStates() - logJump(`performInit`) + logJump('performInit') if (thisMove.toBreak.length > 0) { await this.bot.clearControlStates() @@ -287,9 +284,9 @@ export class ForwardJumpExecutor extends MovementExecutor { } } - async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot))) { - // NOTE: During a jump, we DO NOT want to clear control states or set sneak, + // NOTE: During a jump, we DO NOT want to clear control states or set sneak, // otherwise the bot will lose its momentum and fall straight down! return false } @@ -362,7 +359,7 @@ export class ForwardJumpExecutor extends MovementExecutor { } export class NewForwardJumpExecutor extends ForwardJumpExecutor { - override async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + override async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { if (super.doWaterLogic()) { this.bot.setControlState('jump', this.bot.entity.position.y < thisMove.exitPos.y) void this.postInitAlignToPath(thisMove) @@ -374,24 +371,22 @@ export class NewForwardJumpExecutor extends ForwardJumpExecutor { } export class ForwardDropDownExecutor extends MovementExecutor { - - private handleSneak(thisMove: Move) { + private handleSneak (thisMove: Move) { if (super.doWaterLogic()) { this.bot.setControlState('sneak', this.bot.entity.position.y > thisMove.exitPos.y && !this.bot.entity.onGround) } } - override async align(thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): Promise { + override async align (thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): Promise { this.handleSneak(thisMove) return await super.align(thisMove, tickCount, goal, lookTarget) } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { await this.postInitAlignToPath(thisMove) } - - async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot, 0))) { // If we are locked by an interaction, freeze so we don't accidentally fall early. this.bot.clearControlStates() @@ -438,7 +433,6 @@ export class ForwardDropDownExecutor extends MovementExecutor { } } - if (currentIndex < path.length) void this.postInitAlignToPath(thisMove) else void this.postInitAlignToPath(thisMove) @@ -447,10 +441,8 @@ export class ForwardDropDownExecutor extends MovementExecutor { } } - export class StraightDownExecutor extends MovementExecutor { - - align(thisMove: Move): boolean { + align (thisMove: Move): boolean { this.bot.clearControlStates() const xzVel = this.bot.entity.velocity.offset(0, -this.bot.entity.velocity.y, 0) if (this.bot.entity.position.xzDistanceTo(thisMove.exitPos) < 0.2 && xzVel.norm() < 0.1) { @@ -475,11 +467,11 @@ export class StraightDownExecutor extends MovementExecutor { return false } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { // Initialization logic cleared; breaks are dynamically handled in performPerTick } - async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { + async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise { if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot, 0))) { this.bot.clearControlStates() return false @@ -511,33 +503,33 @@ export class StraightUpExecutor extends MovementExecutor { private static readonly STOP_DIST = 0.08 private static readonly STOP_SPEED = 0.03 - private _getEntryCenter(thisMove: Move): Vec3 { + private _getEntryCenter (thisMove: Move): Vec3 { return thisMove.entryPos.floored().offset(0.5, 0, 0.5) } - private _getExitCenter(thisMove: Move): Vec3 { + private _getExitCenter (thisMove: Move): Vec3 { return thisMove.exitPos.floored().offset(0.5, 0, 0.5) } - private _getHorizontalOffsetTo(center: Vec3): Vec3 { + private _getHorizontalOffsetTo (center: Vec3): Vec3 { const pos = this.bot.entity.position return new Vec3(center.x - pos.x, 0, center.z - pos.z) } - private _getHorizontalVelocity(): Vec3 { + private _getHorizontalVelocity (): Vec3 { const vel = this.bot.entity.velocity return new Vec3(vel.x, 0, vel.z) } - private _isHorizontallyCentered(thisMove: Move): boolean { + private _isHorizontallyCentered (thisMove: Move): boolean { return this.bot.entity.position.xzDistanceTo(this._getEntryCenter(thisMove)) <= StraightUpExecutor.CENTER_EPS } - private _isHorizontalMotionSmall(): boolean { + private _isHorizontalMotionSmall (): boolean { return this._getHorizontalVelocity().norm() <= StraightUpExecutor.STOP_SPEED } - private _facePoint(point: Vec3): void { + private _facePoint (point: Vec3): void { const pos = this.bot.entity.position const dx = point.x - pos.x const dz = point.z - pos.z @@ -551,7 +543,7 @@ export class StraightUpExecutor extends MovementExecutor { * Using a target slightly "behind" current travel helps botSmartMovement decide * to hold back when we would otherwise overshoot from forward momentum. */ - private _getAirborneCorrectionTarget(thisMove: Move): Vec3 { + private _getAirborneCorrectionTarget (thisMove: Move): Vec3 { const center = this._getEntryCenter(thisMove) const pos = this.bot.entity.position const vel = this._getHorizontalVelocity() @@ -579,7 +571,7 @@ export class StraightUpExecutor extends MovementExecutor { * - use smart forward/back + strict strafing * - sprint only when farther away */ - private _applyGroundCentering(thisMove: Move): boolean { + private _applyGroundCentering (thisMove: Move): boolean { const center = this._getEntryCenter(thisMove) const offset = this._getHorizontalOffsetTo(center) const dist = offset.norm() @@ -606,7 +598,7 @@ export class StraightUpExecutor extends MovementExecutor { * - allow smartMovement to choose forward vs back * - allow strict strafe to counter lateral drift */ - private _applyVerticalAscentControls(thisMove: Move): boolean { + private _applyVerticalAscentControls (thisMove: Move): boolean { const center = this._getEntryCenter(thisMove) const correctionTarget = this._getAirborneCorrectionTarget(thisMove) const distToCenter = this._getHorizontalOffsetTo(center).norm() @@ -629,12 +621,11 @@ export class StraightUpExecutor extends MovementExecutor { return false } - - isAlreadyCompleted(thisMove: Move, tickCount: number, goal: goals.Goal): boolean { + isAlreadyCompleted (thisMove: Move, tickCount: number, goal: goals.Goal): boolean { return this.bot.entity.position.y >= thisMove.exitPos.y } - override async align(thisMove: Move): Promise { + override async align (thisMove: Move): Promise { const inWater = (this.bot.entity as any).isInWater as boolean if (!this.bot.entity.onGround || inWater) { @@ -644,7 +635,7 @@ export class StraightUpExecutor extends MovementExecutor { return this._applyGroundCentering(thisMove) } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { this.bot.clearControlStates() for (const breakH of this.toBreak()) { @@ -664,7 +655,7 @@ export class StraightUpExecutor extends MovementExecutor { } } - performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise { + performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise { if (this.bot.entity.position.y < thisMove.entryPos.y) { throw new CancelError('StraightUp: too low') } @@ -704,31 +695,30 @@ export class ParkourForwardExecutor extends MovementExecutor { protected static readonly APPROACH_YAW_EPS: number = 0.16 // ~9.2 deg — generous for cardinal jumps - protected isComplete(startMove: Move, endMove?: Move, opts: CompleteOpts = {}): boolean { + protected isComplete (startMove: Move, endMove?: Move, opts: CompleteOpts = {}): boolean { const ret = super.isComplete(startMove, endMove, opts) - return ret; + return ret } - private _debugLog(...args: any[]): void { + private _debugLog (...args: any[]): void { logParkour(...args) } - private _lockCurrentYaw(targetYaw: number): void { + private _lockCurrentYaw (targetYaw: number): void { this.lockedYaw = targetYaw } - private _clearLockedYaw(): void { + private _clearLockedYaw (): void { this.lockedYaw = null } - private _applyLockedYaw(): void { - + private _applyLockedYaw (): void { if (this.lockedYaw != null) { this.bot.entity.yaw = this.lockedYaw } } - private _applySmartControls(target: Vec3, jump: boolean): void { + private _applySmartControls (target: Vec3, jump: boolean): void { this._applyLockedYaw() botSmartMovement(this.bot, target, true) botStrafeMovement(this.bot, target, true) @@ -736,12 +726,11 @@ export class ParkourForwardExecutor extends MovementExecutor { this.bot.setControlState('sneak', false) } - private _queueLookAtSync(target: Vec3): Promise { - + private async _queueLookAtSync (target: Vec3): Promise { this._pendingLookTarget = target.offset(0, this.bot.entity.position.y - target.y, 0) if (this._lookAtInFlight != null) { - return this._lookAtInFlight + return await this._lookAtInFlight } this._lookAtInFlight = (async () => { @@ -756,24 +745,24 @@ export class ParkourForwardExecutor extends MovementExecutor { } })() - return this._lookAtInFlight + return await this._lookAtInFlight } - private _clearBackupState(): void { + private _clearBackupState (): void { this.backingUp = false this.backupSettling = false this.backupTarget = null } - private _getTargetBlock(thisMove: Move): Vec3 { + private _getTargetBlock (thisMove: Move): Vec3 { return thisMove.exitPos.offset(0, -1, 0) } - private _getTargetEyeVec(target: Vec3): Vec3 { + private _getTargetEyeVec (target: Vec3): Vec3 { return this.shitterTwo.findGoalVertex(AABB.fromBlockPos(target)) } - private _getUnderlyingBbs(thisMove: Move): AABB[] { + private _getUnderlyingBbs (thisMove: Move): AABB[] { const bbs = getUnderlyingBBs(this.world, this.bot.entity.position, 0.6) if (bbs.length === 0) { bbs.push(AABB.fromBlockPos(thisMove.entryPos.offset(0, -1, 0))) @@ -781,7 +770,7 @@ export class ParkourForwardExecutor extends MovementExecutor { return bbs } - private _getBackupTarget(thisMove: Move): Vec3 | null { + private _getBackupTarget (thisMove: Move): Vec3 | null { const target = this._getTargetBlock(thisMove) const targetEyeVec = this._getTargetEyeVec(target) @@ -803,22 +792,19 @@ export class ParkourForwardExecutor extends MovementExecutor { return backupTarget } - private _shouldSneakDuringBackup(thisMove: Move, target: Vec3): boolean { + private _shouldSneakDuringBackup (thisMove: Move, target: Vec3): boolean { const entryBB = AABB.fromBlockPos(thisMove.entryPos) const botPos = this.bot.entity.position - if (botPos.xzDistanceTo(target) < 0.1) return true; - + if (botPos.xzDistanceTo(target) < 0.1) return true const controls = ControlStateHandler.COPY_BOT(this.bot).set('sneak', false).set('jump', false) const ectx = this.simForward({ ticks: 2, controls }) // console.log(ectx.state.pos, ectx.state.control, ectx.state.pos.y, this.bot.entity.position.y, !ectx.state.onGround) return ectx.state.pos.y < this.bot.entity.position.y && !ectx.state.onGround - - } - private _applyBackupControls(thisMove: Move, target: Vec3): void { + private _applyBackupControls (thisMove: Move, target: Vec3): void { this._lockCurrentYaw(this._desiredYawTo(target)) void this._queueLookAtSync(target) this._applySmartControls(target, false) @@ -826,7 +812,7 @@ export class ParkourForwardExecutor extends MovementExecutor { this.bot.setControlState('sneak', this._shouldSneakDuringBackup(thisMove, target)) } - private _startBackup(thisMove: Move): boolean { + private _startBackup (thisMove: Move): boolean { this.backupTarget ??= this._getBackupTarget(thisMove) if (this.backupTarget == null) { this._debugLog('backup start failed: no backup target') @@ -840,7 +826,7 @@ export class ParkourForwardExecutor extends MovementExecutor { return true } - private _advanceBackup(thisMove: Move): 'backing' | 'recheck' | 'jump' | 'failed' { + private _advanceBackup (thisMove: Move): 'backing' | 'recheck' | 'jump' | 'failed' { this.backupTarget ??= this._getBackupTarget(thisMove) if (this.backupTarget == null) { // this._debugLog('backup advance failed: no backup target') @@ -893,7 +879,7 @@ export class ParkourForwardExecutor extends MovementExecutor { return 'recheck' } - private _getJumpState(thisMove: Move): { + private _getJumpState (thisMove: Move): { target: Vec3 targetEyeVec: Vec3 canDirectJump: boolean @@ -913,7 +899,7 @@ export class ParkourForwardExecutor extends MovementExecutor { } } - private _debugJumpState( + private _debugJumpState ( label: string, jumpState: { target: Vec3 @@ -923,7 +909,6 @@ export class ParkourForwardExecutor extends MovementExecutor { fallOffEdge: boolean } ): void { - (this as any)._lastTime ??= 0 this._debugLog(label, performance.now() - (this as any)._lastTime) @@ -937,7 +922,7 @@ export class ParkourForwardExecutor extends MovementExecutor { 'current bot info:', this.bot.entity.yaw, this.bot.entity.position, - this.bot.entity.velocity, + this.bot.entity.velocity ) this._debugLog( 'yaw info:', @@ -948,7 +933,7 @@ export class ParkourForwardExecutor extends MovementExecutor { (this as any)._lastTime = performance.now() } - private _clearApproachControls(): void { + private _clearApproachControls (): void { this.bot.setControlState('forward', false) this.bot.setControlState('back', false) this.bot.setControlState('left', false) @@ -958,31 +943,31 @@ export class ParkourForwardExecutor extends MovementExecutor { this.bot.setControlState('sneak', false) } - private _startJumpExecution(target: Vec3): void { + private _startJumpExecution (target: Vec3): void { this.lockedYaw = this._desiredYawTo(target) this.executing = true this._applySmartControls(target, true) } - protected _desiredYawTo(target: Vec3): number { + protected _desiredYawTo (target: Vec3): number { const dx = target.x - this.bot.entity.position.x const dz = target.z - this.bot.entity.position.z return Math.atan2(-dx, -dz) } - protected _yawDeltaAbs(targetYaw: number): number { + protected _yawDeltaAbs (targetYaw: number): number { let delta = targetYaw - this.bot.entity.yaw while (delta > Math.PI) delta -= Math.PI * 2 while (delta < -Math.PI) delta += Math.PI * 2 return Math.abs(delta) } - protected _isYawAlignedForApproach(target: Vec3): boolean { + protected _isYawAlignedForApproach (target: Vec3): boolean { const wantedYaw = this._desiredYawTo(target) return this._yawDeltaAbs(wantedYaw) <= ParkourForwardExecutor.APPROACH_YAW_EPS } - private _tryApproachWhenAligned(targetEyeVec: Vec3): boolean { + private _tryApproachWhenAligned (targetEyeVec: Vec3): boolean { void this._queueLookAtSync(targetEyeVec) if (!this._isYawAlignedForApproach(targetEyeVec)) { @@ -994,7 +979,7 @@ export class ParkourForwardExecutor extends MovementExecutor { return true } - async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise { + async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise { this.executing = false this._clearLockedYaw() @@ -1071,7 +1056,7 @@ export class ParkourForwardExecutor extends MovementExecutor { } } - async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise { + async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise { this._clearLockedYaw() this._clearBackupState() this.backupAttempted = false @@ -1081,11 +1066,10 @@ export class ParkourForwardExecutor extends MovementExecutor { void this._queueLookAtSync(targetEyeVec) } - performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise { + performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise { const target = this._getTargetBlock(thisMove) const targetEyeVec = this._getTargetEyeVec(target) - if (this.executing) { // printBotControls(this.bot, logParkour) this._lockCurrentYaw(this._desiredYawTo(targetEyeVec)) @@ -1163,7 +1147,7 @@ export class ParkourDiagonalExecutor extends ParkourForwardExecutor { * already reads `ParkourForwardExecutor.APPROACH_YAW_EPS`, so we * override the instance method to point at the subclass constant instead. */ - protected override _isYawAlignedForApproach(target: Vec3): boolean { + protected override _isYawAlignedForApproach (target: Vec3): boolean { const wantedYaw = this._desiredYawTo(target) return this._yawDeltaAbs(wantedYaw) <= ParkourDiagonalExecutor.APPROACH_YAW_EPS } diff --git a/src/mineflayer-specific/movements/movementProvider.ts b/src/mineflayer-specific/movements/movementProvider.ts index 2bc07cb..2bc1ae0 100644 --- a/src/mineflayer-specific/movements/movementProvider.ts +++ b/src/mineflayer-specific/movements/movementProvider.ts @@ -91,7 +91,6 @@ export abstract class MovementProvider extends Movement { } private getBlockInfoAt (x: number, y: number, z: number, pos?: Vec3): BlockInfo { - const wantedDx = x - this.orgX + this.halfX const wantedDz = z - this.orgZ + this.halfZ const wantedDy = y - this.orgY + this.halfY diff --git a/src/mineflayer-specific/movements/movementUtils.ts b/src/mineflayer-specific/movements/movementUtils.ts index 35c2b74..ad27f78 100644 --- a/src/mineflayer-specific/movements/movementUtils.ts +++ b/src/mineflayer-specific/movements/movementUtils.ts @@ -21,7 +21,7 @@ interface JumpInfo { backTick: number } -export function stateLookAt(state: IEntityState, point: Vec3): void { +export function stateLookAt (state: IEntityState, point: Vec3): void { const delta = point.minus(state.pos.offset(0, state.height - 0.18, 0)) const yaw = Math.atan2(-delta.x, -delta.z) const groundDistance = Math.sqrt(delta.x * delta.x + delta.z * delta.z) @@ -30,7 +30,7 @@ export function stateLookAt(state: IEntityState, point: Vec3): void { state.pitch = pitch } -export function isBlockTypeInChunks(info: Block | number, ...chunks: PCChunk[]): boolean { +export function isBlockTypeInChunks (info: Block | number, ...chunks: PCChunk[]): boolean { info = info instanceof Number ? info : (info as Block).stateId ?? -1 for (const chunk of chunks) { @@ -45,7 +45,7 @@ export function isBlockTypeInChunks(info: Block | number, ...chunks: PCChunk[]): return false } -export function getUnderlyingBBs(world: World, pos: Vec3, width: number, colliding = true): AABB[] { +export function getUnderlyingBBs (world: World, pos: Vec3, width: number, colliding = true): AABB[] { const verts = [ pos.offset(-width / 2, -0.6, -width / 2), pos.offset(-width / 2, -0.6, width / 2), @@ -77,7 +77,7 @@ export function getUnderlyingBBs(world: World, pos: Vec3, width: number, collidi // type FallReason = 'coyote' | 'yChange' | 'none' -export function leavingBlockLevel(bot: Bot, world: World, ticks = 1, ectx?: EPhysicsCtx): boolean { +export function leavingBlockLevel (bot: Bot, world: World, ticks = 1, ectx?: EPhysicsCtx): boolean { const bbs = getUnderlyingBBs(world, bot.entity.position, 0.6) const minY = bbs.reduce((acc, bb) => Math.min(acc, bb.minY), Infinity) @@ -115,14 +115,14 @@ export class JumpCalculator { ctx: EPhysicsCtx readonly world: World - constructor(sim: BaseSimulator, bot: Bot, world: World, ctx: EPhysicsCtx) { + constructor (sim: BaseSimulator, bot: Bot, world: World, ctx: EPhysicsCtx) { this.engine = sim this.bot = bot this.ctx = ctx this.world = world } - public findJumpPoint(goal: Vec3, maxTicks = 20): JumpInfo | null { + public findJumpPoint (goal: Vec3, maxTicks = 20): JumpInfo | null { if (this.checkImmediateSprintJump(goal)) { return { jumpTick: 0, sprintTick: 0, backTick: Infinity } } @@ -167,7 +167,7 @@ export class JumpCalculator { return null } - protected resetState(): PlayerState { + protected resetState (): PlayerState { this.ctx = EPhysicsCtx.FROM_BOT(this.engine.ctx, this.bot) this.ctx.state.age = 0 this.ctx.state.control = ControlStateHandler.DEFAULT() @@ -176,7 +176,7 @@ export class JumpCalculator { return this.ctx.state } - protected checkImmediateSprintJump(goal: Vec3): boolean { + protected checkImmediateSprintJump (goal: Vec3): boolean { const state = this.resetState() stateLookAt(state, goal) this.simJump(state) @@ -186,7 +186,7 @@ export class JumpCalculator { return false } - protected checkSprintJump(goal: Vec3, firstTicks = 0, secondTicks = 0, sprintAfterJump = false, backTicks = Infinity): boolean { + protected checkSprintJump (goal: Vec3, firstTicks = 0, secondTicks = 0, sprintAfterJump = false, backTicks = Infinity): boolean { const state = this.resetState() stateLookAt(state, goal) this.simJumpAdvanced(state, goal, { @@ -215,7 +215,7 @@ export class JumpCalculator { return false } - protected simJump(state: PlayerState, maxTicks = 20): PlayerState { + protected simJump (state: PlayerState, maxTicks = 20): PlayerState { state.control.set('forward', true) state.control.set('jump', true) state.control.set('sprint', true) @@ -230,7 +230,7 @@ export class JumpCalculator { return state } - protected simJumpAdvanced( + protected simJumpAdvanced ( state: PlayerState, goal: Vec3, opts: { @@ -309,13 +309,13 @@ export class ParkourJumpHelper { private readonly bot: Bot private readonly world: World - constructor(bot: Bot, world: World) { + constructor (bot: Bot, world: World) { this.bot = bot this.sim = new JumpSim(new BotcraftPhysics(bot.registry), world) this.world = world } - private _buildBackupCandidates(bbs: AABB[], goalVert: Vec3, orgPos: Vec3): Vec3[] { + private _buildBackupCandidates (bbs: AABB[], goalVert: Vec3, orgPos: Vec3): Vec3[] { const candidates = new Map() for (const bb of bbs) { @@ -340,7 +340,7 @@ export class ParkourJumpHelper { }) } - public findGoalVertex(goal: AABB): Vec3 { + public findGoalVertex (goal: AABB): Vec3 { // get top vertex that is closest to target. const pos = this.bot.entity.position @@ -388,12 +388,12 @@ export class ParkourJumpHelper { return minVert } - public findBackupVertex(bbs: AABB[], goalVert: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 { + public findBackupVertex (bbs: AABB[], goalVert: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 { const candidates = this._buildBackupCandidates(bbs, goalVert, orgPos) return candidates[0] ?? orgPos.clone() } - public findViableBackupVertex(goal: Vec3, eyeTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 | null { + public findViableBackupVertex (goal: Vec3, eyeTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 | null { const bbs = getUnderlyingBBs(this.world, orgPos, 0.6) const goalVert = eyeTarget ?? this.findGoalVertex(AABB.fromBlockPos(goal)) @@ -420,7 +420,7 @@ export class ParkourJumpHelper { return result } - public simJumpFromEdge(srcBBs: AABB[], goal: Vec3, eyeTarget?: Vec3): boolean { + public simJumpFromEdge (srcBBs: AABB[], goal: Vec3, eyeTarget?: Vec3): boolean { // const bbs = this.getUnderlyingBBs(this.bot.entity.position, 0.6); // console.log(bbs) @@ -437,7 +437,7 @@ export class ParkourJumpHelper { return reached(state, 0) as boolean } - public simFallOffEdge(goal: Vec3, target?: Vec3): boolean { + public simFallOffEdge (goal: Vec3, target?: Vec3): boolean { const goalVert = this.findGoalVertex(AABB.fromBlockPos(goal)) // console.log('sim jump goals', goal, goalVert) @@ -446,7 +446,7 @@ export class ParkourJumpHelper { const goalBBs = this.world.getBlockInfo(goal).getBBs() ctx.state.control = ControlStateHandler.DEFAULT() - if (target) stateLookAt(ctx.state, target) + if (target != null) stateLookAt(ctx.state, target) ctx.state.control.set('forward', true) ctx.state.control.set('jump', false) ctx.state.control.set('sprint', true) @@ -474,14 +474,13 @@ export class ParkourJumpHelper { return reached0(state, 0) as boolean } - public simForwardMove(goal: Vec3, eyeTarget?: Vec3, jump = true, ...constraints: SimulationGoal[]): boolean { - + public simForwardMove (goal: Vec3, eyeTarget?: Vec3, jump = true, ...constraints: SimulationGoal[]): boolean { // console.log('sim jump goals', goal, goalVert) const ctx = EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot) // const goalCenter = goal.floored().offset(0.5, 0, 0.5) const goalBBs = this.world.getBlockInfo(goal).getBBs() - + const target = eyeTarget ?? goal // const orgPos = this.bot.entity.position.clone() @@ -524,7 +523,7 @@ export class ParkourJumpHelper { return testwtf } - public simBackupJump(goal: Vec3, eyeTarget?: Vec3, backupTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): boolean { + public simBackupJump (goal: Vec3, eyeTarget?: Vec3, backupTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): boolean { const bbs = getUnderlyingBBs(this.world, orgPos, 0.6) const goalBBs = this.world.getBlockInfo(goal).getBBs() diff --git a/src/mineflayer-specific/movements/simulators/jumpSim.ts b/src/mineflayer-specific/movements/simulators/jumpSim.ts index 62c8ffa..4d38353 100644 --- a/src/mineflayer-specific/movements/simulators/jumpSim.ts +++ b/src/mineflayer-specific/movements/simulators/jumpSim.ts @@ -131,7 +131,7 @@ export class JumpSim extends BaseSimulator { // goalCorner.set(goalBlockTop.x, goalBlockTop.y, goalBlockTop.z) changed = true } - + if (ticks > 0 && srcAABBs.every((src) => !src.intersects(playerBB)) && !jump) { state.control.jump = true jump = true diff --git a/src/mineflayer-specific/pathProducers/continuousPathProducer.ts b/src/mineflayer-specific/pathProducers/continuousPathProducer.ts index a25d3a7..ada0c72 100644 --- a/src/mineflayer-specific/pathProducers/continuousPathProducer.ts +++ b/src/mineflayer-specific/pathProducers/continuousPathProducer.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Bot } from 'mineflayer' import { PathProducer, AStar, AStarNeighbor } from '../../mineflayer-specific/algs' import * as goals from '../goals' diff --git a/src/mineflayer-specific/pathProducers/partialPathProducer.ts b/src/mineflayer-specific/pathProducers/partialPathProducer.ts index 3a5b98b..3a33a80 100644 --- a/src/mineflayer-specific/pathProducers/partialPathProducer.ts +++ b/src/mineflayer-specific/pathProducers/partialPathProducer.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires */ import { Bot } from 'mineflayer' import { PathProducer, AStar, AStarNeighbor } from '../../mineflayer-specific/algs' import * as goals from '../goals' @@ -139,16 +140,16 @@ export class PartialPathProducer implements PathProducer { const time1 = performance.now() - this.lastStartTime const totalTime = performance.now() - this.startTime - + log('Partial Path cost increased by %d to %d. Target Vec: %O', cost, this.latestCost, this.latestMove?.vec) - log('ITERATION METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)', - time1.toFixed(2), nodecount, Math.round((nodecount / time1) * 1000), - seensize, Math.round((seensize / time1) * 1000), - movecount, Math.round((movecount / time1) * 1000)) - log('TOTAL METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)', - totalTime.toFixed(2), this.consideredNodeCount, Math.round((this.consideredNodeCount / totalTime) * 1000), - this.latestClosedNodeCount, Math.round((this.latestClosedNodeCount / totalTime) * 1000), - this.latestMoveCount, Math.round((this.latestMoveCount / totalTime) * 1000)) + log('ITERATION METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)', + time1.toFixed(2), nodecount, Math.round((nodecount / time1) * 1000), + seensize, Math.round((seensize / time1) * 1000), + movecount, Math.round((movecount / time1) * 1000)) + log('TOTAL METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)', + totalTime.toFixed(2), this.consideredNodeCount, Math.round((this.consideredNodeCount / totalTime) * 1000), + this.latestClosedNodeCount, Math.round((this.latestClosedNodeCount / totalTime) * 1000), + this.latestMoveCount, Math.round((this.latestMoveCount / totalTime) * 1000)) this.lastStartTime = performance.now() } else { diff --git a/src/mineflayer-specific/post/index.ts b/src/mineflayer-specific/post/index.ts index 702d52c..d8876c4 100644 --- a/src/mineflayer-specific/post/index.ts +++ b/src/mineflayer-specific/post/index.ts @@ -9,6 +9,5 @@ export * from './registry' export type BuildableMoveOptimizer = new (bot: Bot, world: World, settings: Partial) => MovementOptimizer - export type OptimizationSetup = Map export type ReplacementMap = Map diff --git a/src/mineflayer-specific/post/optimizer.ts b/src/mineflayer-specific/post/optimizer.ts index 8966332..83b6f7c 100644 --- a/src/mineflayer-specific/post/optimizer.ts +++ b/src/mineflayer-specific/post/optimizer.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions */ import { Bot } from 'mineflayer' import type { OptimizationMap } from '.' import type { BuildableMoveProvider } from '../movements' @@ -30,7 +31,7 @@ export abstract class MovementOptimizer { * * Default behavior keeps the start move's provider so existing optimizer/executor * mappings continue to work unchanged. - * + * * For now, changing this here does not work. Do not use this to set a different movement provider. */ protected getMergedMoveType (startIndex: number, endIndex: number, path: readonly Move[]): MovementProvider { @@ -115,7 +116,6 @@ export class Optimizer { return !!this.pathCopy } - async compute (): Promise { if (!this.sanitize()) { throw new Error('Optimizer not sanitized') @@ -163,7 +163,7 @@ export class Optimizer { this.currentIndex++ } - log(`compute() finished. Final optimized path length: ${this.pathCopy.length}. End: ${this.pathCopy[this.pathCopy.length -1 ].exitPos}`) + log(`compute() finished. Final optimized path length: ${this.pathCopy.length}. End: ${this.pathCopy[this.pathCopy.length - 1].exitPos}`) return this.pathCopy } diff --git a/src/mineflayer-specific/post/optimizers.ts b/src/mineflayer-specific/post/optimizers.ts index 5d2738a..253d8ef 100644 --- a/src/mineflayer-specific/post/optimizers.ts +++ b/src/mineflayer-specific/post/optimizers.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions */ import { ControlStateHandler, EPhysicsCtx } from '@nxg-org/mineflayer-physics-util' import { Vec3 } from 'vec3' import { Move } from '../move' @@ -85,7 +86,7 @@ export class LandStraightAheadOpt extends MovementOptimizer { log(`[LandStraightAhead] Optimizing from index ${startIndex} (${thisMove.moveType.constructor.name})`) if (nextMove === undefined) { - log(`[LandStraightAhead] nextMove is undefined, aborting.`) + log('[LandStraightAhead] nextMove is undefined, aborting.') return --currentIndex } @@ -115,20 +116,20 @@ export class LandStraightAheadOpt extends MovementOptimizer { log(`[LandStraightAhead] Index ${currentIndex}: nextMove became undefined.`) return --currentIndex } - + for (const vert of verts) { const offset = vert.minus(orgPos) const test1 = nextMove.exitPos.offset(0, orgY - nextMove.exitPos.y, 0) const test = test1.plus(offset) const dist = nextMove.exitPos.distanceTo(orgPos) - + const raycast0 = this.bot.world.raycast( vert, test.minus(vert).normalize(), dist, (block) => (!BlockInfo.replaceables.has(block.type) || BlockInfo.liquids.has(block.type) || BlockInfo.blocksToAvoid.has(block.type)) && block.shapes.length > 0 ) as unknown as RayType | null - + const valid0 = (raycast0 == null) || raycast0.position.distanceTo(orgPos) > dist if (!valid0) { @@ -143,7 +144,7 @@ export class LandStraightAheadOpt extends MovementOptimizer { const test1 = nextMove.exitPos.offset(0, orgY - nextMove.exitPos.y, 0) const test = test1.plus(offset) const dist = nextMove.exitPos.distanceTo(orgPos) - + const raycast0 = (await this.bot.world.raycast( vert, test.minus(vert).normalize(), @@ -172,13 +173,13 @@ export class LandStraightAheadOpt extends MovementOptimizer { } if (++currentIndex >= path.length) { - log(`[LandStraightAhead] Reached end of path.`) + log('[LandStraightAhead] Reached end of path.') return --currentIndex } lastMove = nextMove nextMove = path[currentIndex] } - + log(`[LandStraightAhead] Y-level changed or loop ended naturally. Returning index ${currentIndex - 1}.`) return --currentIndex } @@ -236,13 +237,13 @@ export class DropDownOpt extends MovementOptimizer { const blockBB1 = AABB.fromBlockPos(nextMove.exitPos.offset(0, -1, 0)) let flag = false let good = false - + this.sim.simulateUntil( (state, ticks) => { const pBB = AABBUtils.getPlayerAABB({ position: ctx.state.pos, width: 0.6, height: 1.8 }) const collided = (pBB.collides(blockBB0) && bb0solid) || (pBB.collides(blockBB1) && bb1solid && (state.onGround || state.isInWater)) - + if (collided) { good = true return true @@ -279,8 +280,7 @@ export class DropDownOpt extends MovementOptimizer { if (flag0) { log(`[DropDownOpt] Index ${currentIndex}: Flag0 triggered. Returning.`) return currentIndex - } - else flag0 = true + } else flag0 = true } if (++currentIndex >= path.length) return --currentIndex @@ -301,7 +301,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer { log(`[ForwardJumpUpOpt] Optimizing from index ${startIndex} (${lastMove.moveType.constructor.name})`) if (lastMove.toPlace.length > 0) { - log(`[ForwardJumpUpOpt] Initial move places a block. Aborting.`) + log('[ForwardJumpUpOpt] Initial move places a block. Aborting.') return --currentIndex } @@ -322,7 +322,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer { log(`[ForwardJumpUpOpt] Index ${currentIndex}: AABB collision failed.`) return --currentIndex } - + if (++currentIndex >= path.length) return --currentIndex lastMove = nextMove nextMove = path[currentIndex] @@ -332,7 +332,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer { while ( lastMove.exitPos.y === nextMove.exitPos.y && - nextMove.exitPos.distanceTo(firstPos) <= 2 && + nextMove.exitPos.distanceTo(firstPos) <= 2 && nextMove.toPlace.length === 0 && nextMove.toBreak.length === 0 ) { diff --git a/src/mineflayer-specific/post/registry.ts b/src/mineflayer-specific/post/registry.ts index 785086c..00f41a3 100644 --- a/src/mineflayer-specific/post/registry.ts +++ b/src/mineflayer-specific/post/registry.ts @@ -6,7 +6,7 @@ import { MovementOptimizer } from './optimizer' export type BuildableMoveOptimizer = new (bot: Bot, world: World, settings: Partial) => MovementOptimizer -export type RegisteredOptimizer = { +export interface RegisteredOptimizer { optimizer: MovementOptimizer optimizedExecutor?: MovementExecutor priority: number diff --git a/src/mineflayer-specific/post/replacement.ts b/src/mineflayer-specific/post/replacement.ts index 2d91f25..d532515 100644 --- a/src/mineflayer-specific/post/replacement.ts +++ b/src/mineflayer-specific/post/replacement.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ import { Move } from '../move' import { Algorithm, Path } from '../../abstract' import type { ReplacementMap } from '.' diff --git a/src/mineflayer-specific/post/replacements.ts b/src/mineflayer-specific/post/replacements.ts index 6e65262..b5a8c0c 100644 --- a/src/mineflayer-specific/post/replacements.ts +++ b/src/mineflayer-specific/post/replacements.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Vec3 } from 'vec3' import { Path, MovementProvider as AMovementProvider, PathNode, Algorithm } from '../../abstract' import { Move } from '../move' diff --git a/src/mineflayer-specific/world/cacheWorld.ts b/src/mineflayer-specific/world/cacheWorld.ts index f1074d5..9cff817 100644 --- a/src/mineflayer-specific/world/cacheWorld.ts +++ b/src/mineflayer-specific/world/cacheWorld.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-non-null-assertion */ import { Vec3 } from 'vec3' import type { World as WorldType } from './worldInterface' import { Bot } from 'mineflayer' @@ -91,7 +92,7 @@ export class BlockInfo { } BlockInfo.PBlock = pBlock(registry) // require('prismarine-block')(registry) - BlockInfo.substituteBlockStateId = registry.blocksByName.dirt.minStateId; + BlockInfo.substituteBlockStateId = registry.blocksByName.dirt.minStateId BlockInfo._waterBlock = BlockInfo.PBlock.fromStateId(registry.blocksByName.water.minStateId, 0) BlockInfo._waterBlock.position = new Vec3(0, 0, 0) @@ -554,7 +555,7 @@ export class CacheSyncWorld implements WorldType { const calls = this.cacheCalls this.cacheCalls = 0 // const used = Object.keys(this.posCache).length === 0 ? this.blocks : this.posCache - const used = this.blockInfos + const used = this.blockInfos return `size = ${used.size}; calls = ${calls}` } diff --git a/src/mineflayer-specific/world/utils.ts b/src/mineflayer-specific/world/utils.ts index 5313c27..0c9af21 100644 --- a/src/mineflayer-specific/world/utils.ts +++ b/src/mineflayer-specific/world/utils.ts @@ -13,7 +13,7 @@ export function fasterGetBlock (this: Bot['world'], pos: Vec3): Block { return null as unknown as Block } - const colPos = new Vec3(pos.x & 0xf, pos.y, pos.z & 0xf).floor(); + const colPos = new Vec3(pos.x & 0xf, pos.y, pos.z & 0xf).floor() const ret1 = col.getBlock(colPos) ret1.position = pos diff --git a/src/pathExecutor.ts b/src/pathExecutor.ts index a82fda6..09b4401 100644 --- a/src/pathExecutor.ts +++ b/src/pathExecutor.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions, no-unmodified-loop-condition, @typescript-eslint/no-throw-literal, @typescript-eslint/strict-boolean-expressions */ import { Bot } from 'mineflayer' import { Vec3 } from 'vec3' import { Path } from './mineflayer-specific/algs' @@ -7,7 +8,7 @@ import { AbortError, CancelError, ManualResetError, ResetError } from './minefla import type { BuildableMoveProvider, ExecutorMap, - MovementExecutor, + MovementExecutor } from './mineflayer-specific/movements' import type { OptimizationMap } from './mineflayer-specific/post' import { Optimizer } from './mineflayer-specific/post' @@ -28,18 +29,18 @@ export interface ExecutionMappings { } export interface PathfinderExecutionHost { - getBot(): Bot - getWorld(): World - getActiveMappings(): ExecutionMappings - cleanupBot(): Promise - getPathFromToRaw(startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise + getBot: () => Bot + getWorld: () => World + getActiveMappings: () => ExecutionMappings + cleanupBot: () => Promise + getPathFromToRaw: (startPos: Vec3, startVel: Vec3, goal: goals.Goal) => Promise } type RunnerStage = 'idle' | 'align' | 'optimize' | 'init' | 'perform' const MAX_TASK_TIME_MS = 40 -function isNodeRuntime(): boolean { +function isNodeRuntime (): boolean { return typeof process !== 'undefined' && process.versions?.bun == null } @@ -51,58 +52,58 @@ export class PathExecutor { private currentExecutor?: MovementExecutor private resetReason?: ResetReason - public constructor(private readonly bot: Bot, private readonly host: ThePathfinder) { } + public constructor (private readonly bot: Bot, private readonly host: ThePathfinder) { } - public bumpExecutionId(): number { + public bumpExecutionId (): number { this.currentExecutionId++ return this.currentExecutionId } - public getCurrentExecutionId(): number { + public getCurrentExecutionId (): number { return this.currentExecutionId } - public getCurrentIndex(): number { + public getCurrentIndex (): number { return this.currentIndex } - public setCurrentIndex(index: number): void { + public setCurrentIndex (index: number): void { this.currentIndex = index } - public getCurrentPath(): Move[] | undefined { + public getCurrentPath (): Move[] | undefined { return this.currentPath } - public setCurrentPath(path?: Move[]): void { + public setCurrentPath (path?: Move[]): void { this.currentPath = path } - public getCurrentMove(): Move | undefined { + public getCurrentMove (): Move | undefined { return this.currentMove } - public setCurrentMove(move?: Move): void { + public setCurrentMove (move?: Move): void { this.currentMove = move } - public getCurrentExecutor(): MovementExecutor | undefined { + public getCurrentExecutor (): MovementExecutor | undefined { return this.currentExecutor } - public setCurrentExecutor(executor?: MovementExecutor): void { + public setCurrentExecutor (executor?: MovementExecutor): void { this.currentExecutor = executor } - public getResetReason(): ResetReason | undefined { + public getResetReason (): ResetReason | undefined { return this.resetReason } - public setResetReason(reason?: ResetReason): void { + public setResetReason (reason?: ResetReason): void { this.resetReason = reason } - public clearResetReason(): void { + public clearResetReason (): void { delete this.resetReason } @@ -158,7 +159,7 @@ export class PathExecutor { return value as T } - private findNextCurrentIdx(move: Move, localPath: Move[], currentIndex: number, adding?: boolean | number): number { + private findNextCurrentIdx (move: Move, localPath: Move[], currentIndex: number, adding?: boolean | number): number { const endIdx = localPath.findIndex( (m, i) => i >= currentIndex && m.exitPos.distanceTo(move.exitPos) < 0.1 ) @@ -172,14 +173,14 @@ export class PathExecutor { return currentIndex } - public async perform(path: Path, goal: goals.Goal, entry = 0): Promise { + public async perform (path: Path, goal: goals.Goal, entry = 0): Promise { const MAX_RECOVERY_DEPTH = 0 if (entry > MAX_RECOVERY_DEPTH) { throw new Error('Too many failures, exiting performing.') } - const bot = this.bot; + const bot = this.bot const myExecutionId = this.bumpExecutionId() const localPath = path.path let currentIndex = 0 @@ -203,7 +204,6 @@ export class PathExecutor { let nonNodeDrainInFlight = false - const completion = new Promise((resolve, reject) => { resolveCompletion = resolve rejectCompletion = reject @@ -218,7 +218,7 @@ export class PathExecutor { settled = true bot.off('physicsTickBegin', moveListener) if (this.getCurrentExecutionId() === myExecutionId) { - await this.timeAsync('finish cleanup', () => this.host.cleanupBot()) + await this.timeAsync('finish cleanup', async () => await this.host.cleanupBot()) } resolveCompletion() } @@ -245,7 +245,6 @@ export class PathExecutor { } const handleExecutionError = async (err: unknown): Promise => { - log(`Error: ${err}`) if (err instanceof AbortError) { @@ -296,7 +295,7 @@ export class PathExecutor { const optimizer = new Optimizer(bot, this.host.world, optimizers) optimizer.loadPath(localPath.slice(currentIndex)) - pendingOptimize = this.timeAsync('optimizer.compute', () => optimizer.compute()).then( + pendingOptimize = this.timeAsync('optimizer.compute', async () => await optimizer.compute()).then( (result) => { if (settled) return if (this.getCurrentExecutionId() !== myExecutionId) return @@ -336,8 +335,6 @@ export class PathExecutor { throw new Error('No executor for movement type ' + move.moveType.constructor.name) } - - this.setCurrentMove(move) this.setCurrentExecutor(executor) this.setCurrentIndex(currentIndex) @@ -346,7 +343,7 @@ export class PathExecutor { log(`[ExecID %d] Entering movement ${move.moveType.constructor.name}. Idx: ${this.getCurrentIndex()}, start: ${move.entryPos}, end: ${move.exitPos}`, myExecutionId) tickCount = 0 - await this.timeAsync('prepare cleanup', () => this.host.cleanupBot()) + await this.timeAsync('prepare cleanup', async () => await this.host.cleanupBot()) executor.loadMove(move) if (executor.isAlreadyCompleted(move, tickCount, goal)) { @@ -405,7 +402,7 @@ export class PathExecutor { const aligned = await this.timeAsync( `align ${move.moveType.constructor.name}`, - () => executor.align(move, tickCount, goal) + async () => await executor.align(move, tickCount, goal) ) tickCount++ @@ -413,7 +410,7 @@ export class PathExecutor { runnerStage = 'init' pendingInit = this.timeAsync( `performInit ${move.moveType.constructor.name}`, - () => executor._performInit(move, currentIndex, localPath) + async () => await executor._performInit(move, currentIndex, localPath) ) pendingInit.then( () => { @@ -445,7 +442,7 @@ export class PathExecutor { const adding = await this.timeAsync( `performPerTick ${move.moveType.constructor.name}`, - () => executor._performPerTick(move, tickCount, currentIndex, localPath) + async () => await executor._performPerTick(move, tickCount, currentIndex, localPath) ) tickCount++ @@ -463,7 +460,6 @@ export class PathExecutor { } } } catch (err) { - log(`Error: ${err}`) if (err instanceof AbortError) { executorSafeReset(this.getCurrentExecutor()) @@ -507,7 +503,7 @@ export class PathExecutor { } } - let tickInFlight = false + const tickInFlight = false const moveListener = (): void => { if (settled || tickInFlight || recoveryInFlight) return @@ -548,8 +544,8 @@ export class PathExecutor { } } - public async recovery(move: Move, path: Path, goal: goals.Goal, entry = 0): Promise { - const bot = this.bot; + public async recovery (move: Move, path: Path, goal: goals.Goal, entry = 0): Promise { + const bot = this.bot log(`recovery ${entry} for ${move.moveType.constructor.name}`) while (!bot.entity.onGround && !(bot.entity as any).isInWater) { @@ -557,7 +553,7 @@ export class PathExecutor { } bot.emit('enteredRecovery', entry) - await this.timeAsync('recovery cleanup', () => this.host.cleanupBot()) + await this.timeAsync('recovery cleanup', async () => await this.host.cleanupBot()) const ind = path.path.findIndex((m) => m.entryPos.distanceTo(move.entryPos) < 0.1) if (ind === -1) { @@ -585,7 +581,7 @@ export class PathExecutor { const path1 = await this.timeAsync( 'recovery pathfinding', - () => this.host.getPathFromToRaw(bot.entity.position, EMPTY_VEC, newGoal) + async () => await this.host.getPathFromToRaw(bot.entity.position, EMPTY_VEC, newGoal) ) if (path1 === null) { @@ -608,7 +604,7 @@ export class PathExecutor { } } - private check(): void { + private check (): void { const resetReason = this.getResetReason() if (resetReason != null) { throw new ResetError(resetReason) diff --git a/src/utils.ts b/src/utils.ts index b89beb8..192e4df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,12 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { Bot, BotEvents } from 'mineflayer' import { Vec3 } from 'vec3' import { BlockInfo } from './mineflayer-specific/world/cacheWorld' -import { BlockFace } from '@nxg-org/mineflayer-util-plugin' -import { AABBUtils } from '@nxg-org/mineflayer-util-plugin' +import { BlockFace, AABBUtils } from '@nxg-org/mineflayer-util-plugin' + import { World } from './mineflayer-specific/world/worldInterface' -export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = console.log): void { +export function printBotControls (bot: Bot, log: (...args: unknown[]) => void = console.log): void { const controls = { forward: bot.getControlState('forward'), back: bot.getControlState('back'), @@ -13,7 +14,7 @@ export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = c right: bot.getControlState('right'), jump: bot.getControlState('jump'), sprint: bot.getControlState('sprint'), - sneak: bot.getControlState('sneak'), + sneak: bot.getControlState('sneak') } log( @@ -28,19 +29,19 @@ export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = c ) } -export function faceToVec(face: BlockFace): Vec3 { - switch (face) { - case BlockFace.BOTTOM: return new Vec3(0, -1, 0) - case BlockFace.TOP: return new Vec3(0, 1, 0) - case BlockFace.NORTH: return new Vec3(0, 0, -1) - case BlockFace.SOUTH: return new Vec3(0, 0, 1) - case BlockFace.WEST: return new Vec3(-1, 0, 0) - case BlockFace.EAST: return new Vec3(1, 0, 0) - default: throw new Error('Invalid face') - } +export function faceToVec (face: BlockFace): Vec3 { + switch (face) { + case BlockFace.BOTTOM: return new Vec3(0, -1, 0) + case BlockFace.TOP: return new Vec3(0, 1, 0) + case BlockFace.NORTH: return new Vec3(0, 0, -1) + case BlockFace.SOUTH: return new Vec3(0, 0, 1) + case BlockFace.WEST: return new Vec3(-1, 0, 0) + case BlockFace.EAST: return new Vec3(1, 0, 0) + default: throw new Error('Invalid face') } +} -export function *interpolateStepPoints(start: Vec3, end: Vec3, step = 0.8): Generator { +export function * interpolateStepPoints (start: Vec3, end: Vec3, step = 0.8): Generator { const delta = end.minus(start) const dist = delta.norm() @@ -61,7 +62,6 @@ export function *interpolateStepPoints(start: Vec3, end: Vec3, step = 0.8): Gene } } - export const debug = (bot: Bot | undefined, ...args: any[]): void => { if (bot != null) { bot.chat(args.join(' ')) @@ -118,7 +118,7 @@ export function getNormalizedPos (bot: Bot, startPos?: Vec3): Vec3 { return pos } -export function getSupportedStartPos(world: World, startPos: Vec3): Vec3 { +export function getSupportedStartPos (world: World, startPos: Vec3): Vec3 { if (!BlockInfo.initialized) throw new Error('BlockInfo not initialized') const pos = startPos.clone()