/** * DumpInventoryState - Dump inventory into nearby containers * Keeps fuel items, dumps everything else. * * Improvements over v1: * - Uses Levenshtein distance for fuzzy container name matching * - Detects containers via peripheral inspection on all sides * - Tracks dump success/failure per direction * - Reports detailed dump stats * - Optionally navigates home before dumping */ import { BaseState } from './BaseState.js'; import { findBestMatch } from '../helpers/levenshtein.js'; // Items to keep (fuel sources and tools) const KEEP_ITEMS = new Set([ 'minecraft:coal', 'minecraft:charcoal', 'minecraft:coal_block', 'minecraft:torch', 'minecraft:lava_bucket', ]); // Known container block names (used for fuzzy matching) const CONTAINER_NAMES = [ 'chest', 'barrel', 'shulker_box', 'hopper', 'dropper', 'dispenser', 'trapped_chest', 'ender_chest', 'furnace', 'blast_furnace', 'smoker', ]; /** * Check if a block name is likely a container using fuzzy matching */ function isContainerBlock(blockName) { if (!blockName) return false; const shortName = blockName.replace(/^[^:]+:/, '').toLowerCase(); // Exact substring match first for (const container of CONTAINER_NAMES) { if (shortName.includes(container)) return true; } // Fuzzy match as fallback const { match, distance } = findBestMatch(shortName, CONTAINER_NAMES, 3); return match !== null && distance <= 2; } export class DumpInventoryState extends BaseState { constructor(turtle, data = {}) { super(turtle, data); this.returnState = data.returnState || null; this.returnData = data.returnData || {}; this.navigateHome = data.navigateHome || false; this.keepItems = data.keepItems ? new Set([...KEEP_ITEMS, ...data.keepItems]) : KEEP_ITEMS; } get name() { return 'dumping'; } get description() { return this.navigateHome ? 'Returning home to dump inventory' : 'Dumping inventory'; } async *act() { console.log(`[${this.turtle.id}] Starting inventory dump`); // Optionally navigate home first if (this.navigateHome && this.turtle.homePosition) { console.log(`[${this.turtle.id}] Navigating home before dumping`); yield* this.navigateTo(this.turtle.homePosition); yield; } // First, detect containers by inspecting all directions const containerDirections = await this._detectContainers(); if (containerDirections.length === 0) { console.log(`[${this.turtle.id}] No containers found nearby, trying blind dump`); containerDirections.push('front', 'up', 'down'); } else { console.log(`[${this.turtle.id}] Found containers: ${containerDirections.join(', ')}`); } // Build keep items set for Lua const keepItemsList = [...this.keepItems].map(n => `"${n}"`).join(', '); // Map direction names to Lua drop functions const dropFnMap = { front: 'turtle.drop', up: 'turtle.dropUp', down: 'turtle.dropDown', }; // Build the drop functions array for only detected container directions const dropFnEntries = containerDirections .filter(d => dropFnMap[d]) .map(d => dropFnMap[d]); if (dropFnEntries.length === 0) { console.log(`[${this.turtle.id}] No valid drop directions`); this._transitionOut(); return; } const result = await this.exec(` local keepItems = {${keepItemsList}} local keepSet = {} for _, name in ipairs(keepItems) do keepSet[name] = true end local dropFns = {${dropFnEntries.join(', ')}} local dumpedCount = 0 local failedCount = 0 for _, dropFn in ipairs(dropFns) do for slot = 1, 16 do local item = turtle.getItemDetail(slot) if item and not keepSet[item.name] then turtle.select(slot) local prevCount = item.count if dropFn() then local remaining = turtle.getItemCount(slot) dumpedCount = dumpedCount + (prevCount - remaining) else failedCount = failedCount + 1 end end end end turtle.select(1) return {dumped = dumpedCount, failed = failedCount} `); if (result) { console.log(`[${this.turtle.id}] Dumped ${result.dumped} items (${result.failed} drops failed)`); if (result.failed > 0) { this.turtle.warning = `Dump: ${result.failed} drops failed (container full?)`; } } yield; // Update inventory after dumping await this.getInventory(); // Refresh any adjacent inventory peripherals for (const dir of containerDirections) { if (this.turtle._peripherals?.[dir]?.types?.includes('inventory')) { try { await this.turtle.connectToInventory(dir); } catch (e) { /* ignore */ } } } this._transitionOut(); } /** * Detect which directions have containers by inspecting blocks */ async _detectContainers() { const result = await this.exec(` local dirs = {} local h, d h, d = turtle.inspect() if h then dirs.front = d.name end h, d = turtle.inspectUp() if h then dirs.up = d.name end h, d = turtle.inspectDown() if h then dirs.down = d.name end return dirs `); if (!result || typeof result !== 'object') return []; const containerDirs = []; for (const [dir, blockName] of Object.entries(result)) { if (isContainerBlock(blockName)) { containerDirs.push(dir); } } return containerDirs; } /** * Transition to the next state (return state or idle) */ _transitionOut() { if (this.returnState) { this.turtle.setState(this.returnState, this.returnData); } else { this.turtle.setState('idle'); } } getRecoveryData() { return { ...super.getRecoveryData(), data: { returnState: this.returnState, returnData: this.returnData, navigateHome: this.navigateHome, keepItems: this.keepItems !== KEEP_ITEMS ? [...this.keepItems].filter(i => !KEEP_ITEMS.has(i)) : undefined, }, }; } }