362 lines
9.7 KiB
JavaScript
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; |