here
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Vec3 = require('vec3');
|
||||
const { sleep } = require('../../utils');
|
||||
|
||||
class Scanner {
|
||||
constructor() {
|
||||
@@ -15,11 +16,12 @@ class Scanner {
|
||||
}
|
||||
}
|
||||
|
||||
this._scanRadius = radius;
|
||||
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
|
||||
count: Infinity,
|
||||
});
|
||||
|
||||
console.log(`Scanner: Found ${chestPositions.length} chest block(s)`);
|
||||
@@ -58,13 +60,29 @@ class Scanner {
|
||||
});
|
||||
}
|
||||
|
||||
// Don't delete orphans for now - just add new ones
|
||||
// await database.deleteOrphanChests(discoveredChests);
|
||||
// Remove DB records for chest positions no longer discovered
|
||||
// (e.g., the old canonical half of a double chest that switched sides)
|
||||
if (discoveredChests.length > 0) {
|
||||
await database.deleteOrphanChests(discoveredChests);
|
||||
}
|
||||
console.log(`Scanner: Discovered/updated ${discoveredChests.length} chest(s)`);
|
||||
return discoveredChests;
|
||||
}
|
||||
|
||||
detectChestType(bot, position) {
|
||||
const block = bot.bot.blockAt(position);
|
||||
if (!block) return { type: 'single' };
|
||||
|
||||
// Use block state properties (Minecraft 1.13+ has type: single/left/right)
|
||||
const props = typeof block.getProperties === 'function' ? block.getProperties() : null;
|
||||
if (props && props.type) {
|
||||
if (props.type === 'single') return { type: 'single' };
|
||||
// Register the 'left' half as canonical, skip 'right'
|
||||
if (props.type === 'left') return { type: 'double' };
|
||||
return { type: 'skip' }; // 'right' half
|
||||
}
|
||||
|
||||
// Fallback: adjacency check for older versions
|
||||
const directions = [
|
||||
new Vec3(1, 0, 0),
|
||||
new Vec3(-1, 0, 0),
|
||||
@@ -73,10 +91,10 @@ class Scanner {
|
||||
];
|
||||
|
||||
for (const dir of directions) {
|
||||
const adjacentPos = position.offset(dir);
|
||||
const adjacentPos = position.offset(dir.x, dir.y, dir.z);
|
||||
const adjacentBlock = bot.bot.blockAt(adjacentPos);
|
||||
|
||||
if (adjacentBlock && adjacentBlock.name.includes('chest')) {
|
||||
if (adjacentBlock && adjacentBlock.name === 'chest') {
|
||||
if (dir.x === -1 || dir.z === -1) {
|
||||
return { type: 'double' };
|
||||
}
|
||||
@@ -107,20 +125,26 @@ class Scanner {
|
||||
console.log(`Scanner: Scanning chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}`);
|
||||
|
||||
try {
|
||||
// Ensure bot is close enough to interact
|
||||
const distance = bot.bot.entity.position.distanceTo(chestPosition);
|
||||
if (distance > 4) {
|
||||
await bot.goTo({ where: chestPosition, range: 3 });
|
||||
}
|
||||
|
||||
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 [];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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 [];
|
||||
return 0;
|
||||
}
|
||||
|
||||
const window = await bot.bot.openChest(chestBlock);
|
||||
const window = await bot.openContainer(chestBlock);
|
||||
const slots = window.slots;
|
||||
let shulkerCount = 0;
|
||||
|
||||
@@ -128,17 +152,37 @@ class Scanner {
|
||||
const chestSlotCount = window.inventoryStart || 27;
|
||||
console.log(`Scanner: Chest has ${chestSlotCount} slots`);
|
||||
|
||||
// Correct DB chest_type if it doesn't match the actual window size
|
||||
const actualType = chestSlotCount > 27 ? 'double' : 'single';
|
||||
if (chest.chest_type !== actualType) {
|
||||
console.log(`Scanner: Correcting chest type: DB says '${chest.chest_type}', actual is '${actualType}'`);
|
||||
await database.upsertChest(
|
||||
chestPosition.x, chestPosition.y, chestPosition.z,
|
||||
actualType, chest.row, chest.column, chest.category
|
||||
);
|
||||
}
|
||||
|
||||
// Clear previous loose item records before re-scanning
|
||||
await database.clearLooseItems(chest.id);
|
||||
|
||||
const looseItems = [];
|
||||
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);
|
||||
await this.scanShulkerFromNBT(bot, database, chest.id, i, slot);
|
||||
shulkerCount++;
|
||||
} else {
|
||||
looseItems.push({ slot: i, name: slot.name, id: slot.type, count: slot.count });
|
||||
}
|
||||
}
|
||||
|
||||
if (looseItems.length > 0) {
|
||||
await database.batchUpsertLooseItems(chest.id, looseItems);
|
||||
}
|
||||
|
||||
await bot.bot.closeWindow(window);
|
||||
console.log(`Scanner: Found ${shulkerCount} shulkers in chest`);
|
||||
return shulkerCount;
|
||||
@@ -157,39 +201,74 @@ class Scanner {
|
||||
let scannedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const chest of chests) {
|
||||
const position = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);
|
||||
// Track scanned positions so we don't re-scan or re-queue
|
||||
const scannedPositions = new Set();
|
||||
|
||||
// Check distance to chest
|
||||
// Visit chests in nearest-neighbor order to minimize travel
|
||||
const remaining = chests.map(c => ({ ...c, pos: new Vec3(c.pos_x, c.pos_y, c.pos_z) }));
|
||||
for (const c of remaining) {
|
||||
scannedPositions.add(`${c.pos.x},${c.pos.y},${c.pos.z}`);
|
||||
}
|
||||
|
||||
while (remaining.length > 0) {
|
||||
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)})`);
|
||||
// Find the closest unscanned chest
|
||||
let closestIdx = 0;
|
||||
let closestDist = botPos.distanceTo(remaining[0].pos);
|
||||
for (let i = 1; i < remaining.length; i++) {
|
||||
const dist = botPos.distanceTo(remaining[i].pos);
|
||||
if (dist < closestDist) {
|
||||
closestDist = dist;
|
||||
closestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
const chest = remaining.splice(closestIdx, 1)[0];
|
||||
|
||||
if (closestDist > 4.5) {
|
||||
console.log(`Scanner: Walking to chest at ${chest.pos} (distance: ${closestDist.toFixed(1)})`);
|
||||
try {
|
||||
await bot.goTo({
|
||||
where: position,
|
||||
const reached = await bot.goTo({
|
||||
where: chest.pos,
|
||||
range: 3,
|
||||
});
|
||||
if (reached === false) {
|
||||
console.log(`Scanner: Could not reach chest at ${chest.pos}: no path`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Scanner: Could not reach chest at ${position}: ${error.message}`);
|
||||
console.log(`Scanner: Could not reach chest at ${chest.pos}: ${error.message}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const shulkerCount = await this.scanChest(bot, database, position);
|
||||
// Wait for anti-ESP to reveal nearby blocks after arriving
|
||||
await sleep(250);
|
||||
|
||||
// Discover any new chests now visible from this position (every 5th stop or first)
|
||||
if (scannedCount % 5 === 0) {
|
||||
const newChests = await this.discoverChests(bot, this._scanRadius || 30, database);
|
||||
for (const nc of newChests) {
|
||||
const key = `${nc.x},${nc.y},${nc.z}`;
|
||||
if (!scannedPositions.has(key)) {
|
||||
scannedPositions.add(key);
|
||||
remaining.push({ ...nc, pos: new Vec3(nc.x, nc.y, nc.z) });
|
||||
console.log(`Scanner: Discovered new chest at ${nc.x},${nc.y},${nc.z} while walking`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shulkerCount = await this.scanChest(bot, database, chest.pos);
|
||||
totalShulkers += shulkerCount;
|
||||
scannedCount++;
|
||||
|
||||
// Progress update every 10 chests
|
||||
if (scannedCount % 10 === 0) {
|
||||
console.log(`Scanner: Progress - ${scannedCount}/${chests.length} chests scanned, ${totalShulkers} shulkers found`);
|
||||
console.log(`Scanner: Progress - ${scannedCount}/${scannedCount + remaining.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();
|
||||
@@ -198,51 +277,53 @@ class Scanner {
|
||||
}
|
||||
|
||||
// Read shulker contents from NBT data (no physical interaction needed)
|
||||
async scanShulkerFromNBT(database, chestId, chestSlot, shulkerItem) {
|
||||
async scanShulkerFromNBT(bot, database, chestId, chestSlot, shulkerItem) {
|
||||
console.log(`Scanner: Reading shulker NBT at slot ${chestSlot}`);
|
||||
|
||||
try {
|
||||
// Create/update shulker record
|
||||
const shulkerId = await database.upsertShulker(
|
||||
// Create/update shulker record and get its ID in one call
|
||||
const shulkerRecord = await database.upsertAndGetShulker(
|
||||
chestId,
|
||||
chestSlot,
|
||||
shulkerItem.name,
|
||||
null // category will be set based on contents
|
||||
);
|
||||
if (!shulkerRecord) {
|
||||
console.error(`Scanner: No shulker record found for chest ${chestId} slot ${chestSlot}`);
|
||||
return [];
|
||||
}
|
||||
const shulkerId = shulkerRecord.id;
|
||||
|
||||
await database.clearShulkerItems(shulkerId);
|
||||
|
||||
// Extract items from shulker NBT
|
||||
const items = this.extractShulkerContents(shulkerItem);
|
||||
const items = this.extractShulkerContents(bot, 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
|
||||
);
|
||||
await database.batchUpsertShulkerItems(shulkerId, items);
|
||||
|
||||
for (const item of items) {
|
||||
totalItems += item.count;
|
||||
itemTypes.add(item.name);
|
||||
}
|
||||
|
||||
// Update shulker stats
|
||||
const itemFocus = itemTypes.size === 1 ? Array.from(itemTypes)[0] : null;
|
||||
let itemFocus = itemTypes.size === 1 ? Array.from(itemTypes)[0] : null;
|
||||
const usedSlots = items.length;
|
||||
|
||||
// If any item in the shulker is special, append #special to the focus
|
||||
if (itemFocus) {
|
||||
const hasSpecial = items.some(item => Scanner.isSpecialItem(item.nbt));
|
||||
if (hasSpecial) {
|
||||
itemFocus = itemFocus + '#special';
|
||||
}
|
||||
}
|
||||
|
||||
await database.updateShulkerCounts(shulkerId, usedSlots, totalItems);
|
||||
|
||||
if (itemFocus && database.db) {
|
||||
await database.db.run(
|
||||
'UPDATE shulkers SET item_focus = ? WHERE id = ?',
|
||||
[itemFocus, shulkerId]
|
||||
);
|
||||
}
|
||||
await database.updateShulkerItemFocus(shulkerId, itemFocus);
|
||||
|
||||
console.log(`Scanner: Shulker has ${items.length} slot(s) used, ${totalItems} total items`);
|
||||
return items;
|
||||
@@ -254,7 +335,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
// Extract items from shulker box NBT data
|
||||
extractShulkerContents(shulkerItem) {
|
||||
extractShulkerContents(bot, shulkerItem) {
|
||||
const items = [];
|
||||
|
||||
if (!shulkerItem.nbt) {
|
||||
@@ -304,12 +385,16 @@ class Scanner {
|
||||
// Clean up the id (remove minecraft: prefix)
|
||||
const cleanId = String(id).replace('minecraft:', '');
|
||||
|
||||
if (count <= 0 || cleanId === 'air') continue;
|
||||
|
||||
// tag may be a prismarine-nbt compound or a plain object
|
||||
const tag = nbtItem.tag?.value ?? nbtItem.tag ?? null;
|
||||
items.push({
|
||||
slot: slot,
|
||||
name: cleanId,
|
||||
id: typeof nbtItem.id === 'object' ? 0 : nbtItem.id,
|
||||
id: bot.mcData.itemsByName[cleanId]?.id || 0,
|
||||
count: count,
|
||||
nbt: nbtItem.tag ? this.parseNBT(nbtItem.tag) : null
|
||||
nbt: tag ? this.parseNBT(tag) : null
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -320,6 +405,27 @@ class Scanner {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Recursively unwrap prismarine-nbt {type, value} structures into plain objects
|
||||
simplifyNBT(nbt) {
|
||||
if (nbt === null || nbt === undefined) return nbt;
|
||||
if (typeof nbt !== 'object') return nbt;
|
||||
|
||||
// prismarine-nbt compound/value wrapper
|
||||
if (nbt.type !== undefined && nbt.value !== undefined) {
|
||||
return this.simplifyNBT(nbt.value);
|
||||
}
|
||||
|
||||
if (Array.isArray(nbt)) {
|
||||
return nbt.map(v => this.simplifyNBT(v));
|
||||
}
|
||||
|
||||
const out = {};
|
||||
for (const key of Object.keys(nbt)) {
|
||||
out[key] = this.simplifyNBT(nbt[key]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
parseNBT(nbt) {
|
||||
if (!nbt) return null;
|
||||
if (typeof nbt === 'string') {
|
||||
@@ -330,13 +436,19 @@ class Scanner {
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap prismarine-nbt wrappers so we can access keys directly
|
||||
nbt = this.simplifyNBT(nbt);
|
||||
|
||||
const result = {};
|
||||
|
||||
if (nbt.Enchantments) {
|
||||
result.enchantments = nbt.Enchantments.map(e => ({
|
||||
id: e.id,
|
||||
level: e.lvl
|
||||
}));
|
||||
let enchList = nbt.Enchantments;
|
||||
if (Array.isArray(enchList)) {
|
||||
result.enchantments = enchList.map(e => ({
|
||||
id: e.id,
|
||||
level: e.lvl
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (nbt.Damage) {
|
||||
@@ -344,7 +456,23 @@ class Scanner {
|
||||
}
|
||||
|
||||
if (nbt.display?.Name) {
|
||||
result.displayName = nbt.display.Name;
|
||||
const name = nbt.display.Name;
|
||||
if (typeof name === 'string') {
|
||||
try { result.displayName = JSON.parse(name).text || name; } catch (e) { result.displayName = name; }
|
||||
} else {
|
||||
result.displayName = name?.text || String(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (nbt.display?.Lore) {
|
||||
let lore = nbt.display.Lore;
|
||||
if (!Array.isArray(lore)) lore = [lore];
|
||||
result.lore = lore.map(l => {
|
||||
if (typeof l === 'string') {
|
||||
try { return JSON.parse(l).text || l; } catch (e) { return l; }
|
||||
}
|
||||
return l?.text || String(l);
|
||||
});
|
||||
}
|
||||
|
||||
if (nbt.CustomModelData) {
|
||||
@@ -357,6 +485,18 @@ class Scanner {
|
||||
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if parsed NBT data indicates a "special" item — one with a custom
|
||||
* display name, lore, or custom model data that should be stored separately.
|
||||
*/
|
||||
static isSpecialItem(nbtData) {
|
||||
if (!nbtData) return false;
|
||||
if (typeof nbtData === 'string') {
|
||||
try { nbtData = JSON.parse(nbtData); } catch (e) { return false; }
|
||||
}
|
||||
return !!(nbtData.displayName || nbtData.lore || nbtData.customModelData);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scanner;
|
||||
Reference in New Issue
Block a user