Files
mc-bot-town/nodejs/controller/storage/scanner.js
2026-01-31 22:34:20 -05:00

362 lines
9.7 KiB
JavaScript

'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;