forked from wmantly/mc-bot-town
1036 lines
37 KiB
JavaScript
1036 lines
37 KiB
JavaScript
'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;
|