storage
This commit is contained in:
362
nodejs/controller/storage/scanner.js
Normal file
362
nodejs/controller/storage/scanner.js
Normal file
@@ -0,0 +1,362 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user