UNPKG

mineflayer-mouse

Version:

First-class **battle-tested** simple API for emulating real Minecraft mouse control in Mineflayer. You should use it for digging (mining) or placing blocks and entity attacking or using and using items!

670 lines (669 loc) 28.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MousePlugin = exports.versionToNumber = exports.MouseManager = void 0; exports.inject = inject; const vec3_1 = require("vec3"); const prismarine_item_1 = __importDefault(require("prismarine-item")); const debug_1 = require("./debug"); const entityRaycast_1 = require("./entityRaycast"); const blockPlacePrediction_1 = require("./blockPlacePrediction"); const itemActivatable_1 = require("./itemActivatable"); const defaultBlockHandlers = { bed: { test: (block) => block.name === 'bed' || block.name.endsWith('_bed'), handle: async (block, bot) => { bot.emit('goingToSleep', block); await bot.sleep(block); } } }; // The delay is always 5 ticks between blocks // https://github.com/extremeheat/extracted_minecraft_data/blob/158aff8ad2a9051505e05703f554af8e50741d69/client/net/minecraft/client/multiplayer/MultiPlayerGameMode.java#L200 const BLOCK_BREAK_DELAY_TICKS = 5; /** * Checks if an entity is a vehicle (minecart or boat) that could cause the bot to get stuck * @param entity The entity to check * @returns true if the entity is a minecart or boat */ function isProblematicVehicleEntity(entity) { if (!entity.name) return false; const name = entity.name.toLowerCase(); return name === 'minecart' || name.includes('boat'); } class MouseManager { bot; settings; /** stateId - seconds */ customBreakTime = {}; customBreakTimeToolAllowance = new Set(); buttons = [false, false, false]; lastButtons = [false, false, false]; cursorBlock = null; tick = 0; entityRaycastCache = null; prevBreakState = null; currentDigTime = null; prevOnGround = null; rightClickDelay = 4; breakStartTime = undefined; ended = false; lastDugBlock = null; lastDugTime = 0; /** a visually synced one */ currentBreakBlock = null; debugDigStatus = 'none'; debugLastStopReason = 'none'; brokenBlocks = []; lastSwing = 0; itemBeingUsed = null; swingTimeout = null; // todo clear when got a packet from server blockHandlers; originalDigTime; constructor(bot, settings = {}) { this.bot = bot; this.settings = settings; this.blockHandlers = { ...defaultBlockHandlers, ...(settings.blockInteractionHandlers ?? {}) }; this.initBotEvents(); // patch mineflayer this.originalDigTime = bot.digTime; bot.digTime = this.digTime.bind(this); } resetDiggingVisual(block) { this.bot.emit('blockBreakProgressStage', block, null); this.currentBreakBlock = null; this.prevBreakState = null; } stopDiggingCompletely(reason, tempStopping = false) { // try { this.bot.stopDigging() } catch (err) { console.warn('stopDiggingCompletely', err) } try { this.bot.stopDigging(); } catch (err) { } this.breakStartTime = undefined; if (this.currentBreakBlock) { this.resetDiggingVisual(this.currentBreakBlock.block); } this.debugDigStatus = `stopped by ${reason}`; this.debugLastStopReason = reason; this.currentDigTime = null; if (!tempStopping) { this.bot.emit('botArmSwingEnd', 'right'); } } initBotEvents() { this.bot.on('physicsTick', () => { this.tick++; if (this.rightClickDelay < 4) this.rightClickDelay++; this.update(); }); this.bot.on('end', () => { this.ended = true; }); this.bot.on('diggingCompleted', (block) => { this.breakStartTime = undefined; this.lastDugBlock = block.position; this.lastDugTime = Date.now(); this.debugDigStatus = 'success'; this.brokenBlocks = [...this.brokenBlocks.slice(-5), block]; this.resetDiggingVisual(block); // TODO: If the tool and enchantments immediately exceed the hardness times 30, the block breaks with no delay; SO WE NEED TO CHECK THAT // TODO: Any blocks with a breaking time of 0.05 }); this.bot.on('diggingAborted', (block) => { if (!this.cursorBlock?.position.equals(block.position)) return; this.debugDigStatus = 'aborted'; this.breakStartTime = undefined; if (this.buttons[0]) { this.buttons[0] = false; this.update(); this.buttons[0] = true; // trigger again } this.lastDugBlock = null; this.resetDiggingVisual(block); }); this.bot.on('entitySwingArm', (entity) => { if (this.bot.entity && entity.id === this.bot.entity.id) { if (this.swingTimeout) { clearTimeout(this.swingTimeout); } this.bot.swingArm('right'); this.bot.emit('botArmSwingStart', 'right'); this.swingTimeout = setTimeout(() => { if (this.ended) return; this.bot.emit('botArmSwingEnd', 'right'); this.swingTimeout = null; }, 250); } }); //@ts-ignore this.bot.on('blockBreakProgressStageObserved', (block, destroyStage, entity) => { if (this.bot.entity && this.cursorBlock?.position.equals(block.position) && entity.id === this.bot.entity.id) { if (!this.buttons[0]) { this.buttons[0] = true; this.update(); } } }); //@ts-ignore this.bot.on('blockBreakProgressStageEnd', (block, entity) => { if (this.bot.entity && this.currentBreakBlock?.block.position.equals(block.position) && entity.id === this.bot.entity.id) { if (!this.buttons[0]) { this.buttons[0] = false; this.update(); } } }); this.bot._client.on('acknowledge_player_digging', (data) => { if ('location' in data && !data.successful) { const packetPos = new vec3_1.Vec3(data.location.x, data.location.y, data.location.z); if (this.cursorBlock?.position.equals(packetPos)) { // restore the block to the world if already digged if (this.bot.world.getBlockStateId(packetPos) === 0) { const block = this.brokenBlocks.find(b => b.position.equals(packetPos)); if (block) { this.bot.world.setBlock(packetPos, block); } else { (0, debug_1.debug)(`Cannot find block to restore at ${packetPos}`); } } this.buttons[0] = false; this.update(); } } }); this.bot.on('heldItemChanged', () => { if (this.itemBeingUsed && !this.itemBeingUsed.isOffhand) { this.stopUsingItem(); } }); } activateEntity(entity) { this.bot.emit('botArmSwingStart', 'right'); this.bot.emit('botArmSwingEnd', 'right'); // mineflayer has completely wrong implementation of this action if (this.bot.supportFeature('armAnimationBeforeUse')) { this.bot.swingArm('right'); } this.bot._client.write('use_entity', { target: entity.id, mouse: 2, // todo do not fake x: 0.581_012_585_759_162_9, y: 0.581_012_585_759_162_9, z: 0.581_012_585_759_162_9, sneaking: this.bot.getControlState('sneak'), hand: 0 }); this.bot._client.write('use_entity', { target: entity.id, mouse: 0, sneaking: this.bot.getControlState('sneak'), hand: 0 }); if (!this.bot.supportFeature('armAnimationBeforeUse')) { this.bot.swingArm('right'); } } beforeUpdateChecks() { } update() { this.beforeUpdateChecks(); const { cursorBlock, cursorBlockDiggable, cursorChanged, entity } = this.getCursorState(); // Handle item deactivation if (this.itemBeingUsed && !this.buttons[2]) { this.stopUsingItem(); } // Handle entity interactions if (entity) { if (this.buttons[0] && !this.lastButtons[0]) { // Left click - attack this.bot.emit('botArmSwingStart', 'right'); this.bot.emit('botArmSwingEnd', 'right'); this.bot.attack(entity); // already swings to servers } else if (this.buttons[2] && !this.lastButtons[2]) { // Right click - interact // Prevent activation of vehicles (minecarts/boats) to avoid getting stuck const preventVehicle = this.settings.preventVehicleInteraction !== false; if (preventVehicle && isProblematicVehicleEntity(entity)) { // Skip activation with vehicles to prevent getting stuck inside them // which could cause server kick for flying console.warn(`Prevented vehicle interaction with ${entity.name} to avoid getting stuck because of Mineflayer bug`); } else { this.activateEntity(entity); } } } else { if (this.buttons[2] && (this.rightClickDelay >= 4 || !this.lastButtons[2])) { this.updatePlaceInteract(cursorBlock); } this.updateBreaking(cursorBlock, cursorBlockDiggable, cursorChanged); } this.updateButtonStates(); } getCursorState() { const inSpectator = this.bot.game.gameMode === 'spectator'; const inAdventure = this.bot.game.gameMode === 'adventure'; const entity = this.getCachedRaycastEntity(); // If entity is found, we should stop any current digging let cursorBlock = this.bot.entity ? this.bot.blockAtCursor(5) : null; if (entity) { cursorBlock = null; if (this.breakStartTime !== undefined) { this.stopDiggingCompletely('entity interference'); } } let cursorBlockDiggable = cursorBlock; if (cursorBlock && (!this.bot.canDigBlock(cursorBlock) || inAdventure) && this.bot.game.gameMode !== 'creative') { cursorBlockDiggable = null; } let cursorChanged = this.cursorBlock !== cursorBlock; if (cursorBlock && this.cursorBlock) { const samePos = this.cursorBlock.position.equals(cursorBlock.position); const sameState = this.cursorBlock.stateId === cursorBlock.stateId; cursorChanged = !(samePos && sameState); } if (cursorChanged) { this.bot.emit('highlightCursorBlock', cursorBlock ? { block: cursorBlock } : undefined); } this.cursorBlock = cursorBlock; return { cursorBlock, cursorBlockDiggable, cursorChanged, entity }; } getCachedRaycastEntity() { if (!this.bot.entity) return null; if (this.entityRaycastCache?.tick === this.tick) { return this.entityRaycastCache.entity; } const entity = (0, entityRaycast_1.raycastEntity)(this.bot); this.entityRaycastCache = { tick: this.tick, entity }; return entity; } async placeBlock(cursorBlock, direction, delta, offhand, forceLook = 'ignore', doClientSwing = true) { const handToPlaceWith = offhand ? 1 : 0; if (offhand && this.bot.supportFeature('doesntHaveOffHandSlot')) { return; } let dx = 0.5 + direction.x * 0.5; let dy = 0.5 + direction.y * 0.5; let dz = 0.5 + direction.z * 0.5; if (delta) { dx = delta.x; dy = delta.y; dz = delta.z; } if (forceLook !== 'ignore') { await this.bot.lookAt(cursorBlock.position.offset(dx, dy, dz), forceLook === 'lookAtForce'); } const pos = cursorBlock.position; const Item = (0, prismarine_item_1.default)(this.bot.version); const { bot } = this; if (bot.supportFeature('blockPlaceHasHeldItem')) { const packet = { location: pos, direction: vectorToDirection(direction), heldItem: Item.toNotch(bot.heldItem), cursorX: Math.floor(dx * 16), cursorY: Math.floor(dy * 16), cursorZ: Math.floor(dz * 16) }; bot._client.write('block_place', packet); } else if (bot.supportFeature('blockPlaceHasHandAndIntCursor')) { bot._client.write('block_place', { location: pos, direction: vectorToDirection(direction), hand: handToPlaceWith, cursorX: Math.floor(dx * 16), cursorY: Math.floor(dy * 16), cursorZ: Math.floor(dz * 16) }); } else if (bot.supportFeature('blockPlaceHasHandAndFloatCursor')) { bot._client.write('block_place', { location: pos, direction: vectorToDirection(direction), hand: handToPlaceWith, cursorX: dx, cursorY: dy, cursorZ: dz }); } else if (bot.supportFeature('blockPlaceHasInsideBlock')) { bot._client.write('block_place', { location: pos, direction: vectorToDirection(direction), hand: handToPlaceWith, cursorX: dx, cursorY: dy, cursorZ: dz, insideBlock: false, sequence: 0, // 1.19.0 worldBorderHit: false // 1.21.3 }); } if (!offhand) { this.bot.swingArm(offhand ? 'left' : 'right'); } if (doClientSwing) { this.bot.emit('botArmSwingStart', offhand ? 'left' : 'right'); this.bot.emit('botArmSwingEnd', offhand ? 'left' : 'right'); } } updatePlaceInteract(cursorBlock) { // Check for special block handlers first let handled = false; if (!this.bot.getControlState('sneak') && cursorBlock) { for (const handler of Object.values(this.blockHandlers)) { if (handler.test(cursorBlock)) { try { handler.handle(cursorBlock, this.bot); handled = true; break; } catch (err) { this.bot.emit('error', err); } } } } const activateMain = this.bot.heldItem && (0, itemActivatable_1.isItemActivatable)(this.bot.version, this.bot.heldItem); const offHandItem = this.bot.inventory.slots[45]; if (!handled) { let possiblyPlaceOffhand = () => { }; if (cursorBlock) { const delta = cursorBlock['intersect'].minus(cursorBlock.position); const faceNum = cursorBlock['face']; const direction = blockPlacePrediction_1.directionToVector[faceNum]; // TODO support offhand prediction const blockPlacementPredicted = (0, blockPlacePrediction_1.botTryPlaceBlockPrediction)(this.bot, cursorBlock, faceNum, delta, this.settings.blockPlacePrediction ?? true, this.settings.blockPlacePredictionDelay ?? 0, this.settings.blockPlacePredictionHandler ?? null, this.settings.blockPlacePredictionCheckEntities ?? true); if (blockPlacementPredicted) { this.bot.emit('mouseBlockPlaced', cursorBlock, direction, delta, false, true); } // always emit block_place when looking at block this.placeBlock(cursorBlock, direction, delta, false, undefined, !activateMain); if (!this.bot.supportFeature('doesntHaveOffHandSlot')) { possiblyPlaceOffhand = () => { this.placeBlock(cursorBlock, direction, delta, true, undefined, false /* todo. complex. many scenarious like pickaxe or food */); }; } } if (activateMain || !cursorBlock) { const offhand = activateMain ? false : (0, itemActivatable_1.isItemActivatable)(this.bot.version, offHandItem); const item = offhand ? offHandItem : this.bot.heldItem; if (item) { this.startUsingItem(item, offhand); } } possiblyPlaceOffhand(); } this.rightClickDelay = 0; } getCustomBreakTime(block) { if (this.customBreakTimeToolAllowance.size) { const heldItemId = this.bot.heldItem?.name; if (!this.customBreakTimeToolAllowance.has(heldItemId ?? '')) { return undefined; } } return this.customBreakTime[block.stateId] ?? this.customBreakTime[block.name] ?? this.customBreakTime['*']; } digTime(block) { const customTime = this.getCustomBreakTime(block); if (customTime !== undefined) return customTime * 1000; const time = this.originalDigTime(block); if (!time) return time; return time; } startUsingItem(item, isOffhand) { if (this.itemBeingUsed) return; // hands busy if (isOffhand && this.bot.supportFeature('doesntHaveOffHandSlot')) return; const slot = isOffhand ? 45 : this.bot.quickBarSlot; this.bot.activateItem(isOffhand); this.itemBeingUsed = { item, isOffhand, name: item.name }; this.bot.emit('startUsingItem', item, slot, isOffhand, -1); } // TODO use it when item cant be used (define another map) // useItemOnce(item: { name: string }, isOffhand: boolean) { // } stopUsingItem() { if (this.itemBeingUsed) { const { isOffhand, item } = this.itemBeingUsed; const slot = isOffhand ? 45 : this.bot.quickBarSlot; this.bot.emit('stopUsingItem', item, slot, isOffhand); this.bot.deactivateItem(); this.itemBeingUsed = null; } } updateBreaking(cursorBlock, cursorBlockDiggable, cursorChanged) { if (cursorChanged) { this.stopDiggingCompletely('block change delay', !!cursorBlockDiggable); } // We stopped breaking if (!this.buttons[0] && this.lastButtons[0]) { this.stopDiggingCompletely('user stopped'); } const hasCustomBreakTime = cursorBlockDiggable ? this.getCustomBreakTime(cursorBlockDiggable) !== undefined : false; const onGround = this.bot.entity?.onGround || this.bot.game.gameMode === 'creative' || hasCustomBreakTime; this.prevOnGround ??= onGround; // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down this.updateBreakingBlockState(cursorBlockDiggable); // Start break if (this.buttons[0]) { this.maybeStartBreaking(cursorBlock, cursorBlockDiggable, cursorChanged, onGround); } this.prevOnGround = onGround; } updateBreakingBlockState(cursorBlockDiggable) { // Calculate and emit break progress if (cursorBlockDiggable && this.breakStartTime !== undefined && this.bot.game.gameMode !== 'creative') { const elapsed = performance.now() - this.breakStartTime; const time = this.digTime(cursorBlockDiggable); if (time !== this.currentDigTime) { console.warn('dig time changed! cancelling!', this.currentDigTime, '->', time); this.stopDiggingCompletely('dig time changed'); } else { const state = Math.floor((elapsed / time) * 10); if (state !== this.prevBreakState) { this.bot.emit('blockBreakProgressStage', cursorBlockDiggable, Math.min(state, 9)); this.currentBreakBlock = { block: cursorBlockDiggable, stage: state }; } this.prevBreakState = state; } } } maybeStartBreaking(cursorBlock, cursorBlockDiggable, cursorChanged, onGround) { const justStartingNewBreak = !this.lastButtons[0]; // Allow resume when stopped and still on a diggable block (e.g. server restored block at same position) const stoppedWithBlock = this.breakStartTime === undefined && !!cursorBlockDiggable; const blockChanged = cursorChanged || (this.lastDugBlock && cursorBlock && !this.lastDugBlock.equals(cursorBlock.position)) || stoppedWithBlock; const diggingCompletedEnoughTimePassed = !this.lastDugTime || (Date.now() - this.lastDugTime > BLOCK_BREAK_DELAY_TICKS * 1000 / 20); const hasCustomBreakTime = cursorBlockDiggable && this.getCustomBreakTime(cursorBlockDiggable) !== undefined; const breakStartConditionsChanged = onGround !== this.prevOnGround && !this.currentBreakBlock; if (cursorBlockDiggable) { if (onGround && (justStartingNewBreak || (diggingCompletedEnoughTimePassed && (blockChanged || breakStartConditionsChanged)))) { this.startBreaking(cursorBlockDiggable); } } else if (performance.now() - this.lastSwing > 200) { this.bot.swingArm('right'); this.bot.emit('botArmSwingStart', 'right'); this.bot.emit('botArmSwingEnd', 'right'); this.lastSwing = performance.now(); } } setConfigFromPacket(packet) { if (packet.customBreakTime) { this.customBreakTime = packet.customBreakTime; } if (packet.customBreakTimeToolAllowance) { this.customBreakTimeToolAllowance = new Set(packet.customBreakTimeToolAllowance); } if (packet.noBreakPositiveUpdate !== undefined) { this.settings.noBreakPositiveUpdate = packet.noBreakPositiveUpdate; } if (packet.blockPlacePrediction !== undefined) { this.settings.blockPlacePrediction = packet.blockPlacePrediction; } if (packet.blockPlacePredictionDelay !== undefined) { this.settings.blockPlacePredictionDelay = packet.blockPlacePredictionDelay; } if (packet.blockPlacePredictionCheckEntities !== undefined) { this.settings.blockPlacePredictionCheckEntities = packet.blockPlacePredictionCheckEntities; } if (packet.preventVehicleInteraction !== undefined) { this.settings.preventVehicleInteraction = packet.preventVehicleInteraction; } } startBreaking(block) { // patch mineflayer if (this.settings.noBreakPositiveUpdate && !this.bot['_updateBlockStateOld']) { this.bot['_updateBlockStateOld'] = this.bot['_updateBlockState']; this.bot['_updateBlockState'] = () => { }; } else if (!this.settings.noBreakPositiveUpdate && this.bot['_updateBlockStateOld']) { this.bot['_updateBlockState'] = this.bot['_updateBlockStateOld']; delete this.bot['_updateBlockStateOld']; } this.lastDugBlock = null; this.debugDigStatus = 'breaking'; this.currentDigTime = this.digTime(block); this.breakStartTime = performance.now(); // Reset break state when starting new break this.prevBreakState = null; const vecArray = [new vec3_1.Vec3(0, -1, 0), new vec3_1.Vec3(0, 1, 0), new vec3_1.Vec3(0, 0, -1), new vec3_1.Vec3(0, 0, 1), new vec3_1.Vec3(-1, 0, 0), new vec3_1.Vec3(1, 0, 0)]; this.bot.dig( //@ts-ignore block, 'ignore', vecArray[block.face], block.face).catch((err) => { if (err.message === 'Digging aborted') return; throw err; }); this.bot.emit('startDigging', block); this.bot.emit('botArmSwingStart', 'right'); } updateButtonStates() { this.lastButtons[0] = this.buttons[0]; this.lastButtons[1] = this.buttons[1]; this.lastButtons[2] = this.buttons[2]; } getDataFromShape(shape) { const width = shape[3] - shape[0]; const height = shape[4] - shape[1]; const depth = shape[5] - shape[2]; const centerX = (shape[3] + shape[0]) / 2; const centerY = (shape[4] + shape[1]) / 2; const centerZ = (shape[5] + shape[2]) / 2; const position = new vec3_1.Vec3(centerX, centerY, centerZ); return { position, width, height, depth }; } getBlockCursorShapes(block) { const shapes = [...block.shapes ?? [], ...block['interactionShapes'] ?? []]; if (!shapes.length) return []; return shapes; } getMergedCursorShape(block) { const shapes = this.getBlockCursorShapes(block); if (!shapes.length) return undefined; return shapes.reduce((acc, cur) => { return [ Math.min(acc[0], cur[0]), Math.min(acc[1], cur[1]), Math.min(acc[2], cur[2]), Math.max(acc[3], cur[3]), Math.max(acc[4], cur[4]), Math.max(acc[5], cur[5]) ]; }); } } exports.MouseManager = MouseManager; exports.MousePlugin = MouseManager; const versionToNumber = (ver) => { const [x, y = '0', z = '0'] = ver.split('.'); return +`${x.padStart(2, '0')}${y.padStart(2, '0')}${z.padStart(2, '0')}`; }; exports.versionToNumber = versionToNumber; const OLD_UNSUPPORTED_VERSIONS = (0, exports.versionToNumber)('1.16.5'); let warningPrinted = false; function inject(bot, settings) { if (settings.warnings !== false && !warningPrinted && (0, exports.versionToNumber)(bot.version) <= OLD_UNSUPPORTED_VERSIONS) { console.warn(`[mineflayer-mouse] This version of Minecraft (${bot.version}) has known issues like doors interactions or item using. Please upgrade to a newer, better tested version for now.`); warningPrinted = true; } const mouse = new MouseManager(bot, settings); bot.mouse = mouse; bot.rightClickStart = () => { mouse.buttons[2] = true; mouse.update(); }; bot.rightClickEnd = () => { mouse.buttons[2] = false; mouse.update(); }; bot.leftClickStart = () => { mouse.buttons[0] = true; mouse.update(); }; bot.leftClickEnd = () => { mouse.buttons[0] = false; mouse.update(); }; bot.leftClick = () => { bot.leftClickStart(); bot.leftClickEnd(); }; bot.rightClick = () => { bot.rightClickStart(); bot.rightClickEnd(); }; Object.defineProperty(bot, 'usingItem', { get: () => mouse.itemBeingUsed }); return mouse; } function vectorToDirection(v) { if (v.y < 0) { return 0; } else if (v.y > 0) { return 1; } else if (v.z < 0) { return 2; } else if (v.z > 0) { return 3; } else if (v.x < 0) { return 4; } else if (v.x > 0) { return 5; } throw new Error(`invalid direction vector ${v}`); }