Files
mc-bot-town-2/nodejs/controller/storage/shulker-handler.js
T
2026-05-03 11:13:06 -04:00

1089 lines
39 KiB
JavaScript

'use strict';
const Vec3 = require('vec3');
const { sleep } = require('../../utils');
const Database = require('./database');
class ShulkerHandler {
constructor() {
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
bot.bot.clearControlStates();
// 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);
// Attempt 1: 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;
}
// Attempt 2: Pathfind directly to the drop position
try {
await bot.goTo({ where: placedPos, range: 0 });
} catch (e) {
try { await bot.goTo({ where: placedPos, range: 1 }); } catch (e2) { /* ignore */ }
}
await bot.bot.waitForTicks(5);
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
return true;
}
// Attempt 3: Search for the dropped item entity nearby and walk to it
// Item may have bounced away from the block position
let droppedEntity = null;
for (const entity of Object.values(bot.bot.entities)) {
if (!entity.name || entity.name !== 'item') continue;
const dist = entity.position.distanceTo(placedPos);
if (dist < 5 && (!droppedEntity || dist < droppedEntity.position.distanceTo(placedPos))) {
droppedEntity = entity;
}
}
if (droppedEntity) {
console.log(`ShulkerHandler: Found item entity at ${droppedEntity.position}, walking to it`);
try {
await bot.goTo({ where: droppedEntity.position, range: 0 });
} catch (e) {
try { await bot.goTo({ where: droppedEntity.position, range: 1 }); } catch (e2) { /* ignore */ }
}
await bot.bot.waitForTicks(5);
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
return true;
}
}
// Final poll — wait up to 5 seconds in case of lag
for (let i = 0; i < 10; i++) {
await sleep(500);
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
return true;
}
}
console.error('ShulkerHandler: Failed to collect shulker after all attempts');
return !!bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
}
/**
* Return a shulker from bot inventory back to its chest slot.
* If placedPos is given, dig the placed shulker first.
* @throws Error if the shulker cannot be returned (caller should handle the failure).
*/
async returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId) {
// 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 — throw if not found so caller knows recovery failed
const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
if (!shulkerInInv) {
throw new Error('Recovery failed — no shulker in inventory');
}
// 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')) {
throw new Error(`Recovery failed — no chest at ${chestPos}`);
}
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);
throw new Error('Recovery failed — shulker not found in window inventory');
}
// 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);
throw new Error('Recovery failed — no empty chest slot');
}
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}`);
}
}
// Verify the shulker is actually back in the chest
await bot.goTo({ where: chestPos, range: 3 });
const verifyBlock = bot.bot.blockAt(chestPos);
if (!verifyBlock || !verifyBlock.name.includes('chest')) {
throw new Error('Recovery verification failed — chest not found');
}
const verifyWindow = await bot.openContainer(verifyBlock);
await sleep(200);
const verifyItem = verifyWindow.slots[targetSlot];
await bot.bot.closeWindow(verifyWindow);
await sleep(200);
if (!verifyItem || !verifyItem.name.includes('shulker_box')) {
throw new Error(`Recovery verification failed — shulker not found at chest slot ${targetSlot} after return`);
}
console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`);
return true;
}
/**
* 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}`);
const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId);
console.log(`ShulkerHandler: Whole shulker taken, now in inventory slot ${invSlot}`);
return invSlot;
}
/**
* 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.goTo({ where: new Vec3(retreatX, botPos.y, retreatZ), range: 0 });
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.goTo({ where: new Vec3(moveX, pos.y, moveZ), range: 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}`);
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);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) {
// Recovery failed — shulker state is uncertain, propagate error upward
// so the caller knows not to retry this same shulker
throw recoverError;
}
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);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw returnError;
return { deposited, updatedSlotItem: null };
}
} catch (error) {
console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw error;
return { deposited: 0, updatedSlotItem: null };
}
}
/**
* 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`);
// 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.goTo({ where: new Vec3(retreatX, botPos.y, retreatZ), range: 0 });
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 };
}
/**
* 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}`);
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);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw placeError;
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;
// Skip special/named items — only withdraw via special item handler
if (shulkerItem.nbt?.value?.display) 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);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw returnError;
return { withdrawn: 0, updatedSlotItem: null };
}
} catch (error) {
console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw error;
return { withdrawn: 0, updatedSlotItem: null };
}
}
/**
* 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}`);
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);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw placeError;
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 };
}
}
}
module.exports = ShulkerHandler;