UNPKG

mineflayer-schem

Version:

A mineflayer plugin for building structures from schematic files (modern and legacy) with advanced chest and block support.

492 lines (400 loc) 18.8 kB
const { goals, Movements } = require('mineflayer-pathfinder'); const Vec3 = require('vec3'); const Build = require('./lib/build.js'); const interactable = require('./lib/interactable.json'); const facing = require('./lib/facing.json'); function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function inject(bot, options = {}) { if (!bot.pathfinder) { throw new Error('mineflayer-pathfinder must be loaded before mineflayer-schem'); } const mcData = require('minecraft-data')(bot.version); const Item = require('prismarine-item')(bot.version); const defaultOptions = { buildSpeed: 1.0, onError: 'skip', clearArea: false, maxRetries: 3, digCost: 10, maxDropDown: 256, searchRadius: 10 }; const settings = { ...defaultOptions, ...options }; const movements = new Movements(bot, mcData); movements.digCost = settings.digCost; movements.maxDropDown = settings.maxDropDown; movements.canDig = false; movements.allow1by1towers = false; bot.pathfinder.setMovements(movements); bot.pathfinder.searchRadius = settings.searchRadius; bot.builder = {}; let currentBuild = null; function getPossibleDirections(pos) { const directions = []; const offsets = [ { offset: new Vec3(0, -1, 0), face: new Vec3(0, 1, 0) }, { offset: new Vec3(0, 0, -1), face: new Vec3(0, 0, 1) }, { offset: new Vec3(0, 0, 1), face: new Vec3(0, 0, -1) }, { offset: new Vec3(-1, 0, 0), face: new Vec3(1, 0, 0) }, { offset: new Vec3(1, 0, 0), face: new Vec3(-1, 0, 0) }, { offset: new Vec3(0, 1, 0), face: new Vec3(0, -1, 0) } ]; for (const { offset, face } of offsets) { const refPos = pos.plus(offset); const block = bot.blockAt(refPos); if (block && block.name !== 'air' && block.boundingBox !== 'empty') { directions.push({ block, face, refPos }); } } return directions; } function isInteractable(blockName) { return interactable.includes(blockName); } function hasLineOfSight(fromPos, toPos) { const direction = toPos.minus(fromPos); const distance = direction.norm(); if (distance > 5) return false; const step = direction.scaled(1 / distance); let current = fromPos.clone(); for (let i = 0; i < distance; i++) { current = current.plus(step); const block = bot.blockAt(current.floored()); if (block && block.name !== 'air' && !block.position.equals(toPos.floored())) { return false; } } return true; } function getBlockFacing(metadata, blockName) { if (blockName.includes('stairs')) { const direction = metadata & 0x3; const upsideDown = (metadata & 0x4) !== 0; const facings = [ new Vec3(0, 0, 1), new Vec3(0, 0, -1), new Vec3(1, 0, 0), new Vec3(-1, 0, 0) ]; return { facing: facings[direction], half: upsideDown ? 'top' : 'bottom' }; } if (blockName.includes('trapdoor')) { const direction = metadata & 0x3; const isOpen = (metadata & 0x4) !== 0; const isTop = (metadata & 0x8) !== 0; const facings = [ new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0) ]; return { facing: facings[direction], half: isTop ? 'top' : 'bottom', open: isOpen }; } if (blockName.includes('door')) { const direction = metadata & 0x3; const isOpen = (metadata & 0x4) !== 0; const isTop = (metadata & 0x8) !== 0; const facings = [ new Vec3(1, 0, 0), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(0, 0, -1) ]; return { facing: facings[direction], half: isTop ? 'upper' : 'lower', open: isOpen }; } if (blockName.includes('log') || blockName.includes('pillar')) { const axis = metadata & 0xC; if (axis === 0x4) return { axis: 'x' }; if (axis === 0x8) return { axis: 'z' }; return { axis: 'y' }; } return null; } bot.builder.equipItem = async function(itemId) { try { if (bot.inventory.items().length > 30) { await bot.chat('/clear'); await wait(100); } let item = bot.inventory.items().find(i => i.type === itemId); if (!item && bot.game && bot.game.gameMode === 'creative') { const emptySlot = bot.inventory.firstEmptyInventorySlot(); const slot = emptySlot !== null ? emptySlot : 36; const newItem = new Item(itemId, 64, 0); await bot.creative.setInventorySlot(slot, newItem); await wait(50); item = bot.inventory.items().find(i => i.type === itemId); } if (!item) { throw new Error(`Could not get item ${itemId}`); } await bot.equip(item, 'hand'); return item; } catch (error) { throw new Error(`Error equipping item: ${error.message}`); } }; bot.builder.clearArea = async function(build) { console.log('🧹 Clearing build area...'); const blocksToRemove = []; for (let y = build.min.y; y < build.max.y; y++) { for (let x = build.min.x; x < build.max.x; x++) { for (let z = build.min.z; z < build.max.z; z++) { const pos = new Vec3(x, y, z); const block = bot.blockAt(pos); if (block && block.name !== 'air' && block.diggable) { blocksToRemove.push(pos); } } } } console.log(`🧹 Blocks to remove: ${blocksToRemove.length}`); let removed = 0; let failed = 0; for (const pos of blocksToRemove) { try { const block = bot.blockAt(pos); if (!block || block.name === 'air') continue; const distance = bot.entity.position.distanceTo(pos); if (distance > 4.5) { try { await bot.pathfinder.goto(new goals.GoalNear(pos.x, pos.y, pos.z, 3)); } catch (pathError) { failed++; continue; } } await bot.dig(block); removed++; await wait(50); } catch (e) { failed++; } } console.log(`✅ Area cleared: ${removed} blocks removed, ${failed} failed`); }; bot.builder.build = async function(build) { currentBuild = build; try { if (settings.clearArea) { await bot.builder.clearArea(build); } console.log(`🏗️ Starting build of ${build.actions.length} blocks...`); console.log(`⚡ Speed: ${settings.buildSpeed} blocks/second`); let consecutiveFailures = 0; const maxConsecutiveFailures = 10; while (build.actions.length > 0) { if (build.isCancelled) { bot.emit('builder_cancelled'); break; } if (build.isPaused) { await wait(1000); continue; } const availableActions = build.actions.filter(action => { const directions = getPossibleDirections(action.pos); return directions.length > 0; }); if (availableActions.length === 0) { console.warn('⚠️ No available actions'); await wait(2000); const toMove = Math.min(20, build.actions.length); for (let i = 0; i < toMove; i++) { const action = build.actions.shift(); build.actions.push(action); } continue; } availableActions.sort((a, b) => { const distA = a.pos.offset(0.5, 0.5, 0.5).distanceSquared(bot.entity.position); const distB = b.pos.offset(0.5, 0.5, 0.5).distanceSquared(bot.entity.position); return distA - distB; }); const action = availableActions[0]; try { if (action.type === 'place') { const item = build.getItemForState(action.state); if (!item) { build.actions = build.actions.filter(a => a !== action); continue; } const targetBlock = bot.blockAt(action.pos); if (targetBlock && targetBlock.name === item.name) { build.actions = build.actions.filter(a => a !== action); build.markActionComplete(action); bot.emit('builder_progress', build.getProgress()); consecutiveFailures = 0; continue; } if (targetBlock && targetBlock.name !== 'air') { try { const distance = bot.entity.position.distanceTo(action.pos); if (distance > 4.5) { await bot.pathfinder.goto(new goals.GoalNear(action.pos.x, action.pos.y, action.pos.z, 3)); } await bot.dig(targetBlock); await wait(200); } catch (e) { } } const directions = getPossibleDirections(action.pos); if (directions.length === 0) { if (!action.retryCount) action.retryCount = 0; action.retryCount++; if (action.retryCount > settings.maxRetries) { build.actions = build.actions.filter(a => a !== action); build.statistics.blocksFailed++; consecutiveFailures++; } else { build.actions = build.actions.filter(a => a !== action); build.actions.push(action); consecutiveFailures++; } continue; } let selectedDirection = null; for (const dir of directions) { const faceCenter = dir.refPos.offset(0.5, 0.5, 0.5); if (hasLineOfSight(bot.entity.position.offset(0, 1.6, 0), faceCenter)) { selectedDirection = dir; break; } } if (!selectedDirection) { selectedDirection = directions[0]; } const { block: refBlock, face } = selectedDirection; const distance = bot.entity.position.distanceTo(action.pos); if (distance > 4.5) { try { await bot.pathfinder.goto(new goals.GoalNear(action.pos.x, action.pos.y, action.pos.z, 3)); } catch (e) { build.actions = build.actions.filter(a => a !== action); build.statistics.blocksFailed++; consecutiveFailures++; continue; } } await bot.builder.equipItem(item.id); const blockFacing = getBlockFacing(action.metadata, action.blockName); const faceCenter = refBlock.position.offset(0.5, 0.5, 0.5).plus(face.scaled(0.5)); await bot.lookAt(faceCenter); if (blockFacing && blockFacing.facing) { const targetLook = action.pos.offset(0.5, 0.5, 0.5).plus(blockFacing.facing.scaled(0.3)); await bot.lookAt(targetLook); } const shouldSneak = isInteractable(refBlock.name); if (shouldSneak) { bot.setControlState('sneak', true); } try { await bot.placeBlock(refBlock, face); if (shouldSneak) { bot.setControlState('sneak', false); } await wait(100); const placedBlock = bot.blockAt(action.pos); if (placedBlock && placedBlock.name !== 'air') { let orientationCorrect = true; if (blockFacing) { if (blockFacing.half && placedBlock.getProperties) { const props = placedBlock.getProperties(); if (props.half && props.half !== blockFacing.half) { orientationCorrect = false; } } } if (orientationCorrect) { build.markActionComplete(action); consecutiveFailures = 0; } else { if (!action.orientationRetries) action.orientationRetries = 0; action.orientationRetries++; if (action.orientationRetries < 2) { build.actions.push(action); } else { build.markActionComplete(action); } consecutiveFailures = 0; } } else { build.statistics.blocksFailed++; consecutiveFailures++; } } catch (e) { if (shouldSneak) { bot.setControlState('sneak', false); } build.statistics.blocksFailed++; consecutiveFailures++; } build.actions = build.actions.filter(a => a !== action); bot.emit('builder_progress', build.getProgress()); } if (consecutiveFailures >= maxConsecutiveFailures) { console.warn(`⚠️ Reorganizing queue...`); const toMove = Math.min(20, build.actions.length); for (let i = 0; i < toMove; i++) { const action = build.actions.shift(); build.actions.push(action); } consecutiveFailures = 0; await wait(2000); } await wait(1000 / settings.buildSpeed); } catch (e) { console.error('❌ Error:', e.message); bot.emit('builder_error', e); consecutiveFailures++; if (settings.onError === 'pause') { build.pause(); bot.emit('builder_paused'); break; } else if (settings.onError === 'skip') { build.actions = build.actions.filter(a => a !== action); } else if (settings.onError === 'cancel') { build.cancel(); break; } } } if (!build.isCancelled && build.actions.length === 0) { bot.emit('builder_finished'); } } catch (e) { bot.emit('builder_error', e); } finally { currentBuild = null; } }; bot.builder.pause = () => { if (currentBuild) { currentBuild.pause(); bot.emit('builder_paused'); } }; bot.builder.resume = () => { if (currentBuild) { currentBuild.resume(); bot.emit('builder_resumed'); } }; bot.builder.cancel = () => { if (currentBuild) { currentBuild.cancel(); } }; bot.builder.getProgress = () => { if (currentBuild) { return currentBuild.getProgress(); } return null; }; } module.exports = { Build: Build, builder: inject, };