'use strict'; const Vec3 = require('vec3'); class Scanner { constructor() { this.chestBlockType = null; } async discoverChests(bot, radius, database) { if (!this.chestBlockType) { this.chestBlockType = bot.mcData.blocksByName.chest?.id; if (!this.chestBlockType) { throw new Error('Chest block not found in minecraft-data'); } } console.log(`Scanner: Discovering chests within ${radius} blocks...`); const chestPositions = bot.bot.findBlocks({ matching: this.chestBlockType, maxDistance: radius, count: 1000, // Find up to 1000 chests }); console.log(`Scanner: Found ${chestPositions.length} chest block(s)`); const discoveredChests = []; const processed = new Set(); for (const pos of chestPositions) { const key = `${pos.x},${pos.y},${pos.z}`; if (processed.has(key)) continue; processed.add(key); const chestInfo = this.detectChestType(bot, pos); // Skip the second half of double chests if (chestInfo.type === 'skip') { continue; } const rowColumn = this.assignRowColumn(pos); const category = this.columnToCategory(rowColumn.column); await database.upsertChest( pos.x, pos.y, pos.z, chestInfo.type, rowColumn.row, rowColumn.column, category ); discoveredChests.push({ x: pos.x, y: pos.y, z: pos.z, type: chestInfo.type, ...rowColumn, category }); } // Don't delete orphans for now - just add new ones // await database.deleteOrphanChests(discoveredChests); console.log(`Scanner: Discovered/updated ${discoveredChests.length} chest(s)`); return discoveredChests; } detectChestType(bot, position) { const directions = [ new Vec3(1, 0, 0), new Vec3(-1, 0, 0), new Vec3(0, 0, 1), new Vec3(0, 0, -1) ]; for (const dir of directions) { const adjacentPos = position.offset(dir); const adjacentBlock = bot.bot.blockAt(adjacentPos); if (adjacentBlock && adjacentBlock.name.includes('chest')) { if (dir.x === -1 || dir.z === -1) { return { type: 'double' }; } return { type: 'skip' }; } } return { type: 'single' }; } assignRowColumn(position) { const row = Math.floor(position.y / 4) + 1; const column = position.x; return { row, column }; } columnToCategory(column) { if (column <= 1) return 'minerals'; if (column === 2) return 'food'; if (column === 3) return 'tools'; if (column === 4) return 'armor'; if (column === 5) return 'blocks'; if (column === 6) return 'redstone'; return 'misc'; } async scanChest(bot, database, chestPosition) { console.log(`Scanner: Scanning chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}`); try { const chestBlock = bot.bot.blockAt(chestPosition); if (!chestBlock || !chestBlock.name.includes('chest')) { console.log(`Scanner: Not a chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}`); return []; } // Get chest from database const chest = await database.getChestByPosition(chestPosition.x, chestPosition.y, chestPosition.z); if (!chest) { console.log(`Scanner: Chest not in database`); return []; } const window = await bot.bot.openChest(chestBlock); const slots = window.slots; let shulkerCount = 0; // Only scan chest inventory slots (not player inventory) const chestSlotCount = window.inventoryStart || 27; console.log(`Scanner: Chest has ${chestSlotCount} slots`); for (let i = 0; i < chestSlotCount; i++) { const slot = slots[i]; if (!slot) continue; if (slot.name.includes('shulker_box')) { console.log(`Scanner: Found shulker at slot ${i}: ${slot.name}`); await this.scanShulkerFromNBT(database, chest.id, i, slot); shulkerCount++; } } await bot.bot.closeWindow(window); console.log(`Scanner: Found ${shulkerCount} shulkers in chest`); return shulkerCount; } catch (error) { console.error(`Scanner: Error scanning chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}:`, error); return 0; } } async scanAllChests(bot, database) { const chests = await database.getChests(); console.log(`Scanner: Scanning all ${chests.length} tracked chests`); let totalShulkers = 0; let scannedCount = 0; let skippedCount = 0; for (const chest of chests) { const position = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z); // Check distance to chest const botPos = bot.bot.entity.position; const distance = botPos.distanceTo(position); if (distance > 4.5) { // Try to walk to the chest console.log(`Scanner: Walking to chest at ${position} (distance: ${distance.toFixed(1)})`); try { await bot.goTo({ where: position, range: 3, }); } catch (error) { console.log(`Scanner: Could not reach chest at ${position}: ${error.message}`); skippedCount++; continue; } } const shulkerCount = await this.scanChest(bot, database, position); totalShulkers += shulkerCount; scannedCount++; // Progress update every 10 chests if (scannedCount % 10 === 0) { console.log(`Scanner: Progress - ${scannedCount}/${chests.length} chests scanned, ${totalShulkers} shulkers found`); } // Small delay between chests to avoid overwhelming the server await new Promise(resolve => setTimeout(resolve, 250)); } await database.rebuildItemIndex(); console.log(`Scanner: Scanned ${scannedCount} chests, skipped ${skippedCount}, found ${totalShulkers} shulkers`); return totalShulkers; } // Read shulker contents from NBT data (no physical interaction needed) async scanShulkerFromNBT(database, chestId, chestSlot, shulkerItem) { console.log(`Scanner: Reading shulker NBT at slot ${chestSlot}`); try { // Create/update shulker record const shulkerId = await database.upsertShulker( chestId, chestSlot, shulkerItem.name, null // category will be set based on contents ); await database.clearShulkerItems(shulkerId); // Extract items from shulker NBT const items = this.extractShulkerContents(shulkerItem); let totalItems = 0; const itemTypes = new Set(); for (const item of items) { await database.upsertShulkerItem( shulkerId, item.id, item.name, item.slot, item.count, item.nbt ); totalItems += item.count; itemTypes.add(item.name); } // Update shulker stats const itemFocus = itemTypes.size === 1 ? Array.from(itemTypes)[0] : null; const usedSlots = items.length; await database.updateShulkerCounts(shulkerId, usedSlots, totalItems); if (itemFocus && database.db) { await database.db.run( 'UPDATE shulkers SET item_focus = ? WHERE id = ?', [itemFocus, shulkerId] ); } console.log(`Scanner: Shulker has ${items.length} slot(s) used, ${totalItems} total items`); return items; } catch (error) { console.error(`Scanner: Error reading shulker NBT:`, error); return []; } } // Extract items from shulker box NBT data extractShulkerContents(shulkerItem) { const items = []; if (!shulkerItem.nbt) { console.log('Scanner: Shulker has no NBT data (empty)'); return items; } try { // Navigate the NBT structure to find Items array // Structure is: nbt.value.BlockEntityTag.value.Items.value.value (array) let nbtItems = null; const nbt = shulkerItem.nbt; // Try multiple paths to find the items array const paths = [ () => nbt.value?.BlockEntityTag?.value?.Items?.value?.value, // Full nested path () => nbt.value?.BlockEntityTag?.value?.Items?.value, // One less nesting () => nbt.BlockEntityTag?.Items?.value?.value, // Without top value () => nbt.BlockEntityTag?.Items?.value, // Simpler () => nbt.BlockEntityTag?.Items, // Direct () => nbt.value?.Items?.value?.value, // No BlockEntityTag () => nbt.Items?.value?.value, // Even simpler () => nbt.Items, // Direct Items ]; for (const pathFn of paths) { const result = pathFn(); if (Array.isArray(result)) { nbtItems = result; break; } } if (!nbtItems || !Array.isArray(nbtItems)) { console.log('Scanner: No items array found in shulker (may be empty)'); return items; } console.log(`Scanner: Found ${nbtItems.length} items in shulker NBT`); for (const nbtItem of nbtItems) { // Extract slot, id, count from NBT item const slot = nbtItem.Slot?.value ?? nbtItem.Slot ?? 0; const id = nbtItem.id?.value ?? nbtItem.id ?? 'unknown'; const count = nbtItem.Count?.value ?? nbtItem.Count ?? 1; // Clean up the id (remove minecraft: prefix) const cleanId = String(id).replace('minecraft:', ''); items.push({ slot: slot, name: cleanId, id: typeof nbtItem.id === 'object' ? 0 : nbtItem.id, count: count, nbt: nbtItem.tag ? this.parseNBT(nbtItem.tag) : null }); } } catch (error) { console.error('Scanner: Error parsing shulker NBT:', error); console.log('Scanner: Raw NBT:', JSON.stringify(shulkerItem.nbt).substring(0, 500)); } return items; } parseNBT(nbt) { if (!nbt) return null; if (typeof nbt === 'string') { try { nbt = JSON.parse(nbt); } catch (e) { return null; } } const result = {}; if (nbt.Enchantments) { result.enchantments = nbt.Enchantments.map(e => ({ id: e.id, level: e.lvl })); } if (nbt.Damage) { result.damage = nbt.Damage; } if (nbt.display?.Name) { result.displayName = nbt.display.Name; } if (nbt.CustomModelData) { result.customModelData = nbt.CustomModelData; } if (nbt.RepairCost) { result.repairCost = nbt.RepairCost; } return Object.keys(result).length > 0 ? result : null; } } module.exports = Scanner;