'use strict'; const Vec3 = require('vec3'); const { sleep } = require('../../utils'); const { goals: { GoalNear } } = require('mineflayer-pathfinder'); const Database = require('./database'); class ShulkerHandler { constructor() { this.operationInProgress = false; this.scanner = null; // set by Storage after init } /** * Find an air block near the bot with a solid block below it. * Used as a spot to place a shulker box on the ground. * Prefers 2-block offsets so the bot isn't standing on the placement block. */ findPlacementSpot(bot, excludePositions = []) { const botPos = bot.bot.entity.position.floored(); const excludeSet = new Set(excludePositions.map(p => `${p.x},${p.y},${p.z}`)); const offsets = [ // 2-block offsets preferred — bot has room to stand and place new Vec3(2, 0, 0), new Vec3(-2, 0, 0), new Vec3(0, 0, 2), new Vec3(0, 0, -2), new Vec3(2, 0, 1), new Vec3(-2, 0, 1), new Vec3(2, 0, -1), new Vec3(-2, 0, -1), new Vec3(1, 0, 2), new Vec3(-1, 0, 2), new Vec3(1, 0, -2), new Vec3(-1, 0, -2), // Fallback: 1-block offsets (tight spaces) new Vec3(1, 0, 0), new Vec3(-1, 0, 0), new Vec3(0, 0, 1), new Vec3(0, 0, -1), new Vec3(1, 0, 1), new Vec3(-1, 0, 1), new Vec3(1, 0, -1), new Vec3(-1, 0, -1), // Y-1 for tight storage areas new Vec3(2, -1, 0), new Vec3(-2, -1, 0), new Vec3(0, -1, 2), new Vec3(0, -1, -2), new Vec3(1, -1, 0), new Vec3(-1, -1, 0), new Vec3(0, -1, 1), new Vec3(0, -1, -1), ]; for (const offset of offsets) { const checkPos = botPos.offset(offset.x, offset.y, offset.z); if (excludeSet.has(`${checkPos.x},${checkPos.y},${checkPos.z}`)) continue; const blockAtPos = bot.bot.blockAt(checkPos); const blockBelow = bot.bot.blockAt(checkPos.offset(0, -1, 0)); if (blockAtPos && blockAtPos.name === 'air' && blockBelow && blockBelow.boundingBox === 'block') { return { position: checkPos, placeOn: blockBelow, faceVec: new Vec3(0, 1, 0), }; } } throw new Error('No suitable placement spot found near bot'); } /** * Break a placed shulker and wait for it to be collected into inventory. * Retries dig if block survives, then walks to drop position if item not picked up. * Returns true if shulker is in inventory, false otherwise. */ async digAndCollectShulker(bot, placedPos) { console.log(`ShulkerHandler: Breaking shulker at ${placedPos}`); // Ensure all movement is fully stopped before digging bot.bot.setControlState('forward', false); bot.bot.setControlState('back', false); bot.bot.setControlState('left', false); bot.bot.setControlState('right', false); bot.bot.setControlState('jump', false); bot.bot.setControlState('sprint', false); bot.bot.setControlState('sneak', false); // Stop pathfinder movement too try { bot.bot.pathfinder.stop(); } catch (e) { /* ignore */ } // Retry dig up to 3 times — shulker closing animation can block the first attempt for (let digAttempt = 0; digAttempt < 3; digAttempt++) { const block = bot.bot.blockAt(placedPos); if (!block || !block.name.includes('shulker_box')) { break; } console.log(`ShulkerHandler: Digging shulker at ${placedPos} (attempt ${digAttempt + 1})`); try { // Look at block center before equipping tool await bot.bot.lookAt(placedPos.offset(0.5, 0.5, 0.5), true); // Equip best tool for the block (anti-cheat expects tool-appropriate timing) const pickaxe = bot.bot.inventory.items().find(i => i.name.includes('pickaxe')); if (pickaxe) { await bot.bot.equip(pickaxe, 'hand'); } else { try { await bot.bot.unequip('hand'); } catch (e) { /* empty hand is fine */ } } // Anti-cheat cooldown before dig await bot.bot.waitForTicks(20); // Use 'raycast' so mineflayer sends the correct block face in dig packets await bot.bot.dig(block, 'raycast'); // Anti-cheat cooldown after dig await bot.bot.waitForTicks(25); } catch (e) { console.log(`ShulkerHandler: Dig error: ${e.message}`); } // Check if block is actually gone const afterDig = bot.bot.blockAt(placedPos); if (!afterDig || !afterDig.name.includes('shulker_box')) { console.log('ShulkerHandler: Shulker block broken successfully'); break; } // Wait between retries console.log('ShulkerHandler: Block still present after dig, waiting before retry...'); await bot.bot.waitForTicks(40); } // Verify block is gone const finalCheck = bot.bot.blockAt(placedPos); if (finalCheck && finalCheck.name.includes('shulker_box')) { console.error('ShulkerHandler: Could not break shulker block after 3 attempts'); return false; } // Wait a few ticks for the item entity to spawn await bot.bot.waitForTicks(3); // Jump onto the drop position to collect it (like a human player) console.log(`ShulkerHandler: Jumping onto ${placedPos} to collect drop`); try { await bot.bot.lookAt(placedPos.offset(0.5, 0, 0.5), true); await bot.bot.waitForTicks(2); bot.bot.setControlState('jump', true); bot.bot.setControlState('forward', true); await sleep(600); bot.bot.setControlState('forward', false); bot.bot.setControlState('jump', false); await sleep(300); } catch (e) { console.log(`ShulkerHandler: Jump to collect failed: ${e.message}`); } // Quick check if already picked up if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) { return true; } // If not collected yet, pathfind directly to the drop try { await bot.goTo({ where: placedPos, range: 0 }); } catch (e) { try { await bot.goTo({ where: placedPos, range: 1 }); } catch (e2) { /* ignore */ } } // Poll for pickup (up to 5 seconds) for (let i = 0; i < 10; i++) { await sleep(500); if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) { return true; } } return !!bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); } /** * Best-effort recovery: return a shulker from bot inventory back to its chest slot. * If placedPos is given, dig the placed shulker first. * Never throws — returns true on success, false on failure. */ async returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId) { try { // If shulker is placed on the ground, break it first if (placedPos) { try { await bot.bot.closeWindow(bot.bot.currentWindow); } catch (e) { /* ignore */ } await sleep(300); await this.digAndCollectShulker(bot, placedPos); } // Find shulker in bot inventory const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); if (!shulkerInInv) { console.error('ShulkerHandler: Recovery failed — no shulker in inventory'); return false; } // Go to chest and open it await bot.goTo({ where: chestPos, range: 3 }); const chestBlock = bot.bot.blockAt(chestPos); if (!chestBlock || !chestBlock.name.includes('chest')) { console.error(`ShulkerHandler: Recovery failed — no chest at ${chestPos}`); return false; } const window = await bot.openContainer(chestBlock); await sleep(300); // Find shulker in inventory portion of the window let shulkerWindowSlot = null; for (let i = window.inventoryStart; i < window.inventoryEnd; i++) { const item = window.slots[i]; if (item && item.name.includes('shulker_box')) { shulkerWindowSlot = i; break; } } if (shulkerWindowSlot === null) { await bot.bot.closeWindow(window); console.error('ShulkerHandler: Recovery failed — shulker not found in window inventory'); return false; } // Try original slot first; if occupied, find any empty chest slot let targetSlot = chestSlot; if (window.slots[chestSlot]) { targetSlot = null; for (let i = 0; i < window.inventoryStart; i++) { if (!window.slots[i]) { targetSlot = i; break; } } } if (targetSlot === null) { await bot.bot.closeWindow(window); console.error('ShulkerHandler: Recovery failed — no empty chest slot'); return false; } await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot); await sleep(300); // Read the NBT and sync DB immediately const updatedSlotItem = window.slots[targetSlot]; await bot.bot.closeWindow(window); await sleep(200); if (updatedSlotItem && chestId && this.scanner) { try { await this.scanner.scanShulkerFromNBT(bot, Database, chestId, targetSlot, updatedSlotItem); await Database.rebuildItemIndex(); console.log(`ShulkerHandler: Recovery DB synced — shulker at chest slot ${targetSlot}`); } catch (e) { console.error(`ShulkerHandler: Recovery DB sync failed: ${e.message}`); } } console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`); return true; } catch (error) { console.error('ShulkerHandler: Recovery error:', error.message); return false; } } /** * Take a shulker from a chest at a given slot into bot inventory. * Immediately marks shulker as removed in DB (slot_count = -1) so DB stays in sync. * Returns the inventory slot index where the shulker ended up. */ async takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId) { console.log(`ShulkerHandler: Taking shulker from chest at ${chestPos}, slot ${chestSlot}`); await bot.goTo({ where: chestPos, range: 3 }); const chestBlock = bot.bot.blockAt(chestPos); if (!chestBlock || !chestBlock.name.includes('chest')) { throw new Error(`No chest at ${chestPos} (found: ${chestBlock?.name || 'null'})`); } const window = await bot.openContainer(chestBlock); await sleep(300); const slotItem = window.slots[chestSlot]; if (!slotItem || !slotItem.name.includes('shulker_box')) { await bot.bot.closeWindow(window); throw new Error(`No shulker at chest slot ${chestSlot}`); } // Find an empty inventory slot to avoid swapping existing items into the chest let targetInvSlot = null; for (let i = window.inventoryStart; i < window.inventoryEnd; i++) { if (!window.slots[i]) { targetInvSlot = i; break; } } if (targetInvSlot === null) { await bot.bot.closeWindow(window); throw new Error('No empty inventory slot — refusing to swap items into chest'); } await bot.bot.moveSlotItem(chestSlot, targetInvSlot); await sleep(300); // Immediately mark shulker as out of chest in DB if (shulkerId) { try { await Database.clearShulkerItems(shulkerId); await Database.updateShulkerCounts(shulkerId, -1, 0); console.log(`ShulkerHandler: DB marked shulker ${shulkerId} as in-transit`); } catch (e) { console.error(`ShulkerHandler: DB sync on take failed: ${e.message}`); } } // Find which inventory slot the shulker ended up in let shulkerInvSlot = null; for (let i = window.inventoryStart; i < window.inventoryEnd; i++) { const item = window.slots[i]; if (item && item.name.includes('shulker_box')) { shulkerInvSlot = i; break; } } await bot.bot.closeWindow(window); await sleep(200); if (shulkerInvSlot === null) { throw new Error('Failed to move shulker to inventory'); } // Convert window slot to bot inventory slot const botInvSlot = shulkerInvSlot - window.inventoryStart; console.log(`ShulkerHandler: Shulker now in inventory window slot ${shulkerInvSlot}`); return shulkerInvSlot; } /** * Take a whole shulker from a chest and leave it in bot inventory (don't unpack). * Used for shulker-mode withdrawals where the player gets the entire shulker box. * Returns the inventory slot index where the shulker ended up. */ async takeWholeShulker(bot, chestPos, chestSlot, shulkerId) { console.log(`ShulkerHandler: Taking whole shulker from chest at ${chestPos}, slot ${chestSlot}`); if (this.operationInProgress) { throw new Error('Another shulker operation is in progress'); } this.operationInProgress = true; try { const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId); console.log(`ShulkerHandler: Whole shulker taken, now in inventory slot ${invSlot}`); return invSlot; } finally { this.operationInProgress = false; } } /** * Place a shulker from inventory onto the ground and open it. * Retries once on failure. * Returns { window, placedPos }. */ async placeAndOpenShulker(bot, inventorySlot) { console.log(`ShulkerHandler: Placing shulker from slot ${inventorySlot}`); const triedPositions = []; for (let attempt = 0; attempt < 5; attempt++) { try { const spot = this.findPlacementSpot(bot, triedPositions); triedPositions.push(spot.position); // Ensure bot is at least 1.5 blocks away from placement spot so it's not standing on it const botPos = bot.bot.entity.position; const placeDist = botPos.distanceTo(spot.position.offset(0.5, 0, 0.5)); if (placeDist < 1.5) { console.log(`ShulkerHandler: Too close to placement spot (${placeDist.toFixed(1)} blocks), stepping back`); const dx = botPos.x - spot.position.x; const dz = botPos.z - spot.position.z; const dist = Math.sqrt(dx * dx + dz * dz) || 1; const retreatX = botPos.x + (dx / dist) * 1.5; const retreatZ = botPos.z + (dz / dist) * 1.5; try { await bot.bot.pathfinder.goto(new GoalNear(retreatX, botPos.y, retreatZ, 0.5)); await sleep(300); } catch (e) { console.log(`ShulkerHandler: Retreat pathfind failed: ${e.message}`); } } // Find the shulker item in bot inventory const shulkerItem = bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); if (!shulkerItem) { throw new Error('No shulker box found in inventory'); } await bot.bot.equip(shulkerItem, 'hand'); await bot.bot.waitForTicks(5); // Verify we're actually holding the shulker const heldItem = bot.bot.heldItem; if (!heldItem || !heldItem.name.includes('shulker_box')) { console.log(`ShulkerHandler: Not holding shulker after equip (holding: ${heldItem?.name || 'nothing'}), retrying equip`); await bot.bot.equip(shulkerItem, 'hand'); await bot.bot.waitForTicks(5); const heldRetry = bot.bot.heldItem; if (!heldRetry || !heldRetry.name.includes('shulker_box')) { throw new Error(`Cannot equip shulker (holding: ${heldRetry?.name || 'nothing'})`); } } // Look at the top face center of the block we're placing on const lookTarget = spot.placeOn.position.offset(0.5, 1, 0.5); await bot.bot.lookAt(lookTarget, true); await bot.bot.waitForTicks(3); // SAFETY: verify held item is a shulker RIGHT before placing — never place anything else const prePlaceItem = bot.bot.heldItem; if (!prePlaceItem || !prePlaceItem.name.includes('shulker_box')) { throw new Error(`ABORT: held item changed before place (holding: ${prePlaceItem?.name || 'nothing'})`); } await bot.bot.placeBlock(spot.placeOn, spot.faceVec); await bot.bot.waitForTicks(10); // Verify shulker was placed const placedBlock = bot.bot.blockAt(spot.position); if (!placedBlock || !placedBlock.name.includes('shulker_box')) { // Something wrong was placed — break it immediately console.error(`ShulkerHandler: WRONG BLOCK placed at ${spot.position} (${placedBlock?.name}), breaking it`); try { await bot.bot.dig(bot.bot.blockAt(spot.position)); } catch (e) { /* best effort */ } throw new Error(`Failed to place shulker at ${spot.position} (found: ${placedBlock?.name || 'null'})`); } const window = await bot.openContainer(placedBlock); await bot.bot.waitForTicks(5); console.log(`ShulkerHandler: Shulker placed and opened at ${spot.position}`); return { window, placedPos: spot.position }; } catch (error) { console.log(`ShulkerHandler: Place attempt ${attempt + 1} failed (${error.message})`); if (attempt < 4) { // Move the bot a few blocks so findPlacementSpot finds new spots console.log('ShulkerHandler: Moving to find a better placement spot...'); try { const pos = bot.bot.entity.position; // Walk 3 blocks in a different direction each attempt const angle = (attempt * Math.PI / 2) + Math.PI / 4; const moveX = pos.x + Math.cos(angle) * 3; const moveZ = pos.z + Math.sin(angle) * 3; await bot.bot.pathfinder.goto(new GoalNear(moveX, pos.y, moveZ, 1)); } catch (e) { console.log(`ShulkerHandler: Move failed: ${e.message}`); } await sleep(500); continue; } throw error; } } } /** * Close a placed shulker, break it, collect it, then put it back in the chest. * Immediately syncs DB via scanShulkerFromNBT after return. * Returns the updated slot item. */ async closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId) { console.log(`ShulkerHandler: Closing shulker at ${placedPos}, returning to chest at ${chestPos} slot ${chestSlot}`); // Close the shulker window — wait for closing animation to fully finish await bot.bot.closeWindow(shulkerWindow); await bot.bot.waitForTicks(30); // Break the shulker block and collect it const collected = await this.digAndCollectShulker(bot, placedPos); if (!collected) { throw new Error('Shulker not found in inventory after breaking'); } // Navigate back to chest and put shulker back await bot.goTo({ where: chestPos, range: 3 }); const chestBlock = bot.bot.blockAt(chestPos); const window = await bot.openContainer(chestBlock); await sleep(300); // Find the shulker in inventory portion of the window let shulkerWindowSlot = null; for (let i = window.inventoryStart; i < window.inventoryEnd; i++) { const item = window.slots[i]; if (item && item.name.includes('shulker_box')) { shulkerWindowSlot = i; break; } } if (shulkerWindowSlot === null) { await bot.bot.closeWindow(window); throw new Error('Shulker not found in inventory window after breaking'); } // Move shulker back to its original chest slot await bot.bot.moveSlotItem(shulkerWindowSlot, chestSlot); await sleep(300); // Read the updated NBT from the chest slot const updatedSlotItem = window.slots[chestSlot]; await bot.bot.closeWindow(window); await sleep(200); // Immediately sync DB with the returned shulker's contents if (updatedSlotItem && chestId && this.scanner) { try { await this.scanner.scanShulkerFromNBT(bot, Database, chestId, chestSlot, updatedSlotItem); await Database.rebuildItemIndex(); console.log(`ShulkerHandler: DB synced after returning shulker to chest slot ${chestSlot}`); } catch (e) { console.error(`ShulkerHandler: DB sync on return failed: ${e.message}`); } } console.log(`ShulkerHandler: Shulker returned to chest slot ${chestSlot}`); return updatedSlotItem; } /** * Full deposit cycle: take shulker from chest, place it, deposit items, break, return. * Returns { deposited, updatedSlotItem }. * On failure, attempts recovery and returns { deposited: 0, updatedSlotItem: null }. */ /** * @param {Function|null} itemFilter - Optional filter: (windowSlotItem) => boolean. * When provided, only shift-click items where the filter returns true. * Used to separate special (named/lore) items from regular ones. */ async depositIntoShulker(bot, chestPos, chestSlot, itemName, count, shulkerId, chestId, itemFilter = null) { console.log(`ShulkerHandler: Depositing ${count}x ${itemName} into shulker at chest ${chestPos} slot ${chestSlot}`); if (this.operationInProgress) { throw new Error('Another shulker operation is in progress'); } this.operationInProgress = true; let placedPos = null; try { // Step 1: Take shulker from chest (DB immediately marks it in-transit) const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId); // Step 2: Place shulker on ground and open it let shulkerWindow; try { const result = await this.placeAndOpenShulker(bot, invSlot); shulkerWindow = result.window; placedPos = result.placedPos; } catch (placeError) { console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); return { deposited: 0, updatedSlotItem: null }; } // Step 3: Wait for shulker window slots to populate, then verify contents const shulkerSlotCount = shulkerWindow.inventoryStart; for (let wait = 0; wait < 30; wait++) { if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break; await bot.bot.waitForTicks(2); } for (let s = 0; s < shulkerSlotCount; s++) { const existing = shulkerWindow.slots[s]; if (existing && existing.name !== itemName) { console.error(`ShulkerHandler: Shulker contains ${existing.name} but expected ${itemName} — aborting deposit to prevent mixing`); try { const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId); return { deposited: 0, updatedSlotItem }; } catch (returnError) { await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { deposited: 0, updatedSlotItem: null }; } } } // Step 4: Move items from bot inventory into the shulker via shift-click let deposited = 0; for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd && deposited < count; i++) { const invItem = shulkerWindow.slots[i]; if (!invItem || invItem.name !== itemName) continue; // If a filter is provided, skip items that don't match if (itemFilter && !itemFilter(invItem)) continue; // Check if shulker has any space (stackable or empty slot) const hasSpace = (() => { for (let s = 0; s < shulkerSlotCount; s++) { const slot = shulkerWindow.slots[s]; if (!slot) return true; if (slot.name === itemName && slot.count < slot.stackSize) return true; } return false; })(); if (!hasSpace) { console.log('ShulkerHandler: Shulker is full'); break; } const beforeCount = invItem.count; try { // Shift-click handles stacking optimally — no cursor issues await bot.bot.clickWindow(i, 0, 1); await sleep(200); // Measure what actually left this slot const afterItem = shulkerWindow.slots[i]; const afterCount = afterItem ? afterItem.count : 0; const moved = beforeCount - afterCount; deposited += moved; console.log(`ShulkerHandler: Shift-clicked slot ${i}: moved ${moved}/${beforeCount} ${itemName}`); } catch (error) { console.error(`ShulkerHandler: Error shift-clicking slot ${i}:`, error); } } // Step 5: Close, break, return to chest (DB synced inside closeBreakReturn) try { const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId); console.log(`ShulkerHandler: Deposited ${deposited}/${count} ${itemName}`); return { deposited, updatedSlotItem }; } catch (returnError) { console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { deposited: 0, updatedSlotItem: null }; } } catch (error) { console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { deposited: 0, updatedSlotItem: null }; } finally { this.operationInProgress = false; } } /** * Unpack a shulker box already in bot inventory. * Places it on the ground, pulls ALL items out, breaks the empty shulker. * Returns { extracted: [{ name, count }], inventoryFull: bool }. */ async unpackShulkerFromInventory(bot, shulkerItem) { console.log(`ShulkerHandler: Unpacking shulker ${shulkerItem.name} from inventory`); if (this.operationInProgress) { throw new Error('Another shulker operation is in progress'); } this.operationInProgress = true; try { // Step 1: Find placement spot const spot = this.findPlacementSpot(bot); // Ensure bot is far enough from placement spot const botPos = bot.bot.entity.position; const placeDist = botPos.distanceTo(spot.position.offset(0.5, 0, 0.5)); if (placeDist < 1.5) { console.log(`ShulkerHandler: Too close to placement spot (${placeDist.toFixed(1)} blocks), stepping back`); const dx = botPos.x - spot.position.x; const dz = botPos.z - spot.position.z; const dist = Math.sqrt(dx * dx + dz * dz) || 1; const retreatX = botPos.x + (dx / dist) * 1.5; const retreatZ = botPos.z + (dz / dist) * 1.5; try { await bot.bot.pathfinder.goto(new GoalNear(retreatX, botPos.y, retreatZ, 0.5)); await sleep(300); } catch (e) { console.log(`ShulkerHandler: Retreat pathfind failed: ${e.message}`); } } // Step 2: Equip and place the shulker await bot.bot.equip(shulkerItem, 'hand'); await bot.bot.waitForTicks(5); // Verify we're actually holding the shulker const heldItem = bot.bot.heldItem; if (!heldItem || !heldItem.name.includes('shulker_box')) { throw new Error(`Cannot equip shulker for unpack (holding: ${heldItem?.name || 'nothing'})`); } // Look at the top face center of placement block await bot.bot.lookAt(spot.placeOn.position.offset(0.5, 1, 0.5), true); await bot.bot.waitForTicks(3); // SAFETY: verify held item is a shulker RIGHT before placing — never place anything else const prePlaceItem = bot.bot.heldItem; if (!prePlaceItem || !prePlaceItem.name.includes('shulker_box')) { throw new Error(`ABORT: held item changed before place (holding: ${prePlaceItem?.name || 'nothing'})`); } await bot.bot.placeBlock(spot.placeOn, spot.faceVec); await bot.bot.waitForTicks(10); // Verify shulker was placed const placedBlock = bot.bot.blockAt(spot.position); if (!placedBlock || !placedBlock.name.includes('shulker_box')) { // Something wrong was placed — break it immediately console.error(`ShulkerHandler: WRONG BLOCK placed at ${spot.position} (${placedBlock?.name}), breaking it`); try { await bot.bot.dig(bot.bot.blockAt(spot.position)); } catch (e) { /* best effort */ } throw new Error(`Failed to place shulker at ${spot.position}`); } // Step 3: Open the shulker const shulkerWindow = await bot.openContainer(placedBlock); await bot.bot.waitForTicks(5); // Step 4: Move ALL items from shulker into bot inventory const extracted = []; let inventoryFull = false; const shulkerSlotCount = shulkerWindow.inventoryStart; for (let s = 0; s < shulkerSlotCount; s++) { const shulkerSlotItem = shulkerWindow.slots[s]; if (!shulkerSlotItem) continue; // Find a target slot in bot inventory (stack first, then empty) let targetSlot = null; for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { const invItem = shulkerWindow.slots[i]; if (invItem && invItem.name === shulkerSlotItem.name && invItem.count < invItem.stackSize) { targetSlot = i; break; } } if (targetSlot === null) { for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { if (!shulkerWindow.slots[i]) { targetSlot = i; break; } } } if (targetSlot === null) { console.log('ShulkerHandler: Bot inventory full during unpack'); inventoryFull = true; break; } try { const itemName = shulkerSlotItem.name; const itemCount = shulkerSlotItem.count; await bot.bot.moveSlotItem(s, targetSlot); await sleep(200); extracted.push({ name: itemName, count: itemCount }); } catch (error) { console.error(`ShulkerHandler: Error moving item from shulker slot ${s}:`, error); } } // Step 5: Close the shulker window — wait for closing animation await bot.bot.closeWindow(shulkerWindow); await bot.bot.waitForTicks(30); // Step 6: Break the shulker block and pick it up const collected = await this.digAndCollectShulker(bot, spot.position); if (!collected) { console.error('ShulkerHandler: Shulker not found in inventory after breaking'); } console.log(`ShulkerHandler: Unpacked ${extracted.length} item stacks from shulker`); return { extracted, inventoryFull }; } finally { this.operationInProgress = false; } } /** * Full withdrawal cycle: take shulker from chest, place it, take items out, break, return. * Returns { withdrawn, updatedSlotItem }. * On failure, attempts recovery and returns { withdrawn: 0, updatedSlotItem: null }. */ async withdrawFromShulker(bot, chestPos, chestSlot, itemName, count, shulkerId, chestId) { console.log(`ShulkerHandler: Withdrawing ${count}x ${itemName} from shulker at chest ${chestPos} slot ${chestSlot}`); if (this.operationInProgress) { throw new Error('Another shulker operation is in progress'); } this.operationInProgress = true; let placedPos = null; try { // Step 1: Take shulker from chest (DB immediately marks it in-transit) const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId); // Step 2: Place shulker on ground and open it let shulkerWindow; try { const result = await this.placeAndOpenShulker(bot, invSlot); shulkerWindow = result.window; placedPos = result.placedPos; } catch (placeError) { console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); return { withdrawn: 0, updatedSlotItem: null }; } // Step 3: Wait for shulker window slots to populate (server sends data after open) const shulkerSlotCount = shulkerWindow.inventoryStart; for (let wait = 0; wait < 30; wait++) { if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break; await bot.bot.waitForTicks(2); } // Move items from shulker into bot inventory let withdrawn = 0; const remaining = () => count - withdrawn; for (let s = 0; s < shulkerSlotCount && remaining() > 0; s++) { const shulkerItem = shulkerWindow.slots[s]; if (!shulkerItem || shulkerItem.name !== itemName) continue; // Check if inventory has space const hasInvSpace = (() => { for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { const slot = shulkerWindow.slots[i]; if (!slot) return true; if (slot.name === itemName && slot.count < slot.stackSize) return true; } return false; })(); if (!hasInvSpace) { console.log('ShulkerHandler: Bot inventory is full'); break; } const beforeCount = shulkerItem.count; try { if (remaining() >= beforeCount) { // Need the whole stack or more — shift-click is optimal await bot.bot.clickWindow(s, 0, 1); await sleep(200); } else { // Need fewer than the full stack — pick up, right-click exact amount, return rest // Find an empty inventory slot to place items into let emptyInvSlot = null; for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { if (!shulkerWindow.slots[i]) { emptyInvSlot = i; break; } } if (emptyInvSlot === null) { console.log('ShulkerHandler: No empty slot for partial withdraw'); break; } // Left-click to pick up full stack onto cursor await bot.bot.clickWindow(s, 0, 0); await sleep(150); // Right-click on empty inventory slot N times to place exactly N items for (let n = 0; n < remaining(); n++) { await bot.bot.clickWindow(emptyInvSlot, 1, 0); await sleep(100); } // Left-click back on shulker slot to return the remainder from cursor await bot.bot.clickWindow(s, 0, 0); await sleep(150); } // Measure what actually left this slot const afterItem = shulkerWindow.slots[s]; const afterCount = afterItem ? afterItem.count : 0; const moved = beforeCount - afterCount; withdrawn += moved; console.log(`ShulkerHandler: Withdrew ${moved}/${beforeCount} ${itemName} from shulker slot ${s}`); } catch (error) { console.error(`ShulkerHandler: Error withdrawing from shulker slot ${s}:`, error); } } // Step 4: Close, break, return to chest (DB synced inside closeBreakReturn) try { const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId); console.log(`ShulkerHandler: Withdrew ${withdrawn}/${count} ${itemName}`); return { withdrawn, updatedSlotItem }; } catch (returnError) { console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { withdrawn: 0, updatedSlotItem: null }; } } catch (error) { console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { withdrawn: 0, updatedSlotItem: null }; } finally { this.operationInProgress = false; } } /** * Withdraw a specific item from a specific shulker slot (for special/named items). * Same lifecycle as withdrawFromShulker but targets an exact slot instead of matching by item name. * Returns { withdrawn, updatedSlotItem }. */ async withdrawFromShulkerSlot(bot, chestPos, chestSlot, shulkerSlot, count, shulkerId, chestId) { console.log(`ShulkerHandler: Withdrawing from shulker slot ${shulkerSlot} at chest ${chestPos} slot ${chestSlot}`); if (this.operationInProgress) { throw new Error('Another shulker operation is in progress'); } this.operationInProgress = true; let placedPos = null; try { // Step 1: Take shulker from chest const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId); // Step 2: Place shulker on ground and open it let shulkerWindow; try { const result = await this.placeAndOpenShulker(bot, invSlot); shulkerWindow = result.window; placedPos = result.placedPos; } catch (placeError) { console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); return { withdrawn: 0, updatedSlotItem: null }; } // Step 3: Wait for slots to populate const shulkerSlotCount = shulkerWindow.inventoryStart; for (let wait = 0; wait < 30; wait++) { if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break; await bot.bot.waitForTicks(2); } // Step 4: Take the item at the specific slot let withdrawn = 0; const targetItem = shulkerWindow.slots[shulkerSlot]; if (targetItem) { const beforeCount = targetItem.count; const toTake = Math.min(count, beforeCount); // Check if inventory has space const hasInvSpace = (() => { for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { const slot = shulkerWindow.slots[i]; if (!slot) return true; if (slot.name === targetItem.name && slot.count < slot.stackSize) return true; } return false; })(); if (hasInvSpace) { try { if (toTake >= beforeCount) { // Take the whole stack await bot.bot.clickWindow(shulkerSlot, 0, 1); await sleep(200); } else { // Partial: pick up, right-click exact amount, return rest let emptyInvSlot = null; for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { if (!shulkerWindow.slots[i]) { emptyInvSlot = i; break; } } if (emptyInvSlot !== null) { await bot.bot.clickWindow(shulkerSlot, 0, 0); await sleep(150); for (let n = 0; n < toTake; n++) { await bot.bot.clickWindow(emptyInvSlot, 1, 0); await sleep(100); } await bot.bot.clickWindow(shulkerSlot, 0, 0); await sleep(150); } } const afterItem = shulkerWindow.slots[shulkerSlot]; const afterCount = afterItem ? afterItem.count : 0; withdrawn = beforeCount - afterCount; console.log(`ShulkerHandler: Withdrew ${withdrawn} from shulker slot ${shulkerSlot}`); } catch (error) { console.error(`ShulkerHandler: Error withdrawing from slot ${shulkerSlot}:`, error); } } else { console.log('ShulkerHandler: Bot inventory is full'); } } else { console.log(`ShulkerHandler: No item at shulker slot ${shulkerSlot}`); } // Step 5: Close, break, return to chest try { const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId); console.log(`ShulkerHandler: Slot withdraw complete: ${withdrawn} items`); return { withdrawn, updatedSlotItem }; } catch (returnError) { console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { withdrawn: 0, updatedSlotItem: null }; } } catch (error) { console.error('ShulkerHandler: Slot withdraw failed, attempting recovery:', error.message); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); return { withdrawn: 0, updatedSlotItem: null }; } finally { this.operationInProgress = false; } } } module.exports = ShulkerHandler;