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!

579 lines (578 loc) 23.6 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; class MouseManager { bot; settings; buttons = [false, false, false]; lastButtons = [false, false, false]; cursorBlock = 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; constructor(bot, settings = {}) { this.bot = bot; this.settings = settings; this.blockHandlers = { ...defaultBlockHandlers, ...(settings.blockInteractionHandlers ?? {}) }; this.initBotEvents(); } 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', () => { 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 server } else if (this.buttons[2] && !this.lastButtons[2]) { // Right click - interact 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.bot.entity ? (0, entityRaycast_1.raycastEntity)(this.bot) : null; // 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; } const cursorChanged = cursorBlock && this.cursorBlock ? !this.cursorBlock.position.equals(cursorBlock.position) : this.cursorBlock !== cursorBlock; if (cursorChanged) { this.bot.emit('highlightCursorBlock', cursorBlock ? { block: cursorBlock } : undefined); } this.cursorBlock = cursorBlock; return { cursorBlock, cursorBlockDiggable, cursorChanged, 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); 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; } digTime(block) { const time = this.bot.digTime(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 onGround = this.bot.entity?.onGround || this.bot.game.gameMode === 'creative'; 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]; const blockChanged = cursorChanged || (this.lastDugBlock && cursorBlock && !this.lastDugBlock.equals(cursorBlock.position)); const enoughTimePassed = !this.lastDugTime || (Date.now() - this.lastDugTime > BLOCK_BREAK_DELAY_TICKS * 1000 / 20); const breakTimeConditionsChanged = onGround !== this.prevOnGround; if (cursorBlockDiggable) { if (onGround && (justStartingNewBreak || (enoughTimePassed && (blockChanged || breakTimeConditionsChanged)))) { 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(); } } startBreaking(block) { 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]).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}`); }