AI #2
@@ -13,6 +13,25 @@ module.exports = {
|
|||||||
"plugings": {
|
"plugings": {
|
||||||
"swing": {},
|
"swing": {},
|
||||||
},
|
},
|
||||||
|
"storage": {
|
||||||
|
"dbPath": "./storage/storage.db",
|
||||||
|
"scanRadius": 30,
|
||||||
|
"homePos": null,
|
||||||
|
"categories": {
|
||||||
|
"minerals": ["diamond", "netherite_ingot", "gold_ingot", "iron_ingot", "copper_ingot", "emerald", "redstone", "lapis_lazuli", "raw_iron", "raw_gold", "raw_copper"],
|
||||||
|
"food": ["bread", "cooked_porkchop", "cooked_beef", "steak", "golden_apple", "cooked_chicken", "cooked_mutton", "carrot", "potato", "baked_potato", "golden_carrot"],
|
||||||
|
"tools": ["wooden_sword", "stone_sword", "iron_sword", "diamond_sword", "netherite_sword", "wooden_pickaxe", "stone_pickaxe", "iron_pickaxe", "diamond_pickaxe", "netherite_pickaxe", "wooden_axe", "stone_axe", "iron_axe", "diamond_axe", "netherite_axe", "fishing_rod", "shears"],
|
||||||
|
"armor": ["leather_helmet", "iron_helmet", "diamond_helmet", "netherite_helmet", "leather_chestplate", "iron_chestplate", "diamond_chestplate", "netherite_chestplate", "leather_leggings", "iron_leggings", "diamond_leggings", "netherite leggings", "leather_boots", "iron_boots", "diamond_boots", "netherite_boots"],
|
||||||
|
"blocks": ["stone", "dirt", "cobblestone", "oak_planks", "spruce_planks", "birch_planks", "oak_log", "spruce_log", "birch_log", "cobblestone_stairs", "stone_bricks", "deepslate", "diorite", "granite", "andesite", "tuff", "calcite", "copper_ore", "deepslate_copper_ore", "raw_iron", "raw_gold", "deepslate_gold_ore", "raw_copper"],
|
||||||
|
"redstone": ["redstone", "repeater", "comparator", "piston", "sticky_piston", "redstone_torch", "lever", "tripwire_hook", "redstone_block", "observer", "dropper", "hopper", "dispenser",],
|
||||||
|
"misc": [] // Everything else falls here
|
||||||
|
},
|
||||||
|
"inboxShulkerName": "INBOX",
|
||||||
|
"outboxShulkerName": "OUTBOX",
|
||||||
|
"newShulkersName": "EMPTY",
|
||||||
|
"webPort": 3000,
|
||||||
|
"webHost": "0.0.0.0"
|
||||||
|
},
|
||||||
"ai":{
|
"ai":{
|
||||||
// AI provider: 'gemini' (default) or 'ollama'
|
// AI provider: 'gemini' (default) or 'ollama'
|
||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
|
|||||||
@@ -144,4 +144,46 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'summon': {
|
||||||
|
desc: `Summon a bot online indefinitely`,
|
||||||
|
allowed: ['wmantly', 'useless666', 'tux4242'],
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, botName){
|
||||||
|
if(botName in this.constructor.bots){
|
||||||
|
let bot = this.constructor.bots[botName];
|
||||||
|
|
||||||
|
if (!bot.isReady){
|
||||||
|
try{
|
||||||
|
await bot.connect();
|
||||||
|
this.whisper(from, `${botName} is now online`);
|
||||||
|
}catch(error){
|
||||||
|
this.whisper(from, `Failed to summon ${botName}. Try again later.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.whisper(from, `${botName} is already online`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.whisper(from, `Unknown bot: ${botName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'dismiss': {
|
||||||
|
desc: `Send a bot offline`,
|
||||||
|
allowed: ['wmantly', 'useless666', 'tux4242'],
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, botName){
|
||||||
|
if(botName in this.constructor.bots){
|
||||||
|
let bot = this.constructor.bots[botName];
|
||||||
|
|
||||||
|
if(bot.isReady){
|
||||||
|
bot.quit();
|
||||||
|
this.whisper(from, `${botName} is now offline`);
|
||||||
|
} else {
|
||||||
|
this.whisper(from, `${botName} is already offline`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.whisper(from, `Unknown bot: ${botName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const conf = require('.');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
default: require('./default'),
|
default: require('./default'),
|
||||||
fun: require('./fun'),
|
fun: require('./fun'),
|
||||||
invite: require('./invite'),
|
invite: require('./invite'),
|
||||||
trade: require('./trade'),
|
trade: require('./trade'),
|
||||||
|
storage: require('./storage'),
|
||||||
};
|
};
|
||||||
106
nodejs/controller/commands/storage.js
Normal file
106
nodejs/controller/commands/storage.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Owner players who can run admin commands
|
||||||
|
const owners = ['wmantly', 'useless666', 'tux4242'];
|
||||||
|
// Team players who can use basic storage features
|
||||||
|
const team = [...owners, 'pi_chef', 'Ethan', 'Vince_NL'];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'scan': {
|
||||||
|
desc: 'Force chest area scan',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from) {
|
||||||
|
console.log(`Storage command 'scan' from ${from}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'scan');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'status': {
|
||||||
|
desc: 'Show storage stats',
|
||||||
|
allowed: team,
|
||||||
|
async function(from) {
|
||||||
|
console.log(`Storage command 'status' from ${from}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'status');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'withdraw': {
|
||||||
|
desc: 'Withdraw items from storage',
|
||||||
|
allowed: team,
|
||||||
|
async function(from, itemName, countStr) {
|
||||||
|
console.log(`Storage command 'withdraw' from ${from}: ${itemName} x${countStr}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
const count = parseInt(countStr) || 1;
|
||||||
|
await storage.handleCommand(from, 'withdraw', itemName, count);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'find': {
|
||||||
|
desc: 'Search for an item',
|
||||||
|
allowed: team,
|
||||||
|
async function(from, itemName) {
|
||||||
|
console.log(`Storage command 'find' from ${from}: ${itemName}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'find', itemName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'chests': {
|
||||||
|
desc: 'List tracked chests',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from) {
|
||||||
|
console.log(`Storage command 'chests' from ${from}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'chests');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'organize': {
|
||||||
|
desc: 'Force full re-sort',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from) {
|
||||||
|
console.log(`Storage command 'organize' from ${from}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'organize');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'addplayer': {
|
||||||
|
desc: 'Add player to storage',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, name, role = 'team') {
|
||||||
|
console.log(`Storage command 'addplayer' from ${from}: ${name} as ${role}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'addplayer', name, role);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'removeplayer': {
|
||||||
|
desc: 'Remove player from storage',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from, name) {
|
||||||
|
console.log(`Storage command 'removeplayer' from ${from}: ${name}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'removeplayer', name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'players': {
|
||||||
|
desc: 'List authorized players',
|
||||||
|
allowed: owners,
|
||||||
|
ignoreLock: true,
|
||||||
|
async function(from) {
|
||||||
|
console.log(`Storage command 'players' from ${from}`);
|
||||||
|
const storage = this.plunginsLoaded['Storage'];
|
||||||
|
if (!storage) return this.whisper(from, 'Storage plugin not loaded');
|
||||||
|
await storage.handleCommand(from, 'players');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -30,6 +30,45 @@ module.exports = {
|
|||||||
'.trade': {
|
'.trade': {
|
||||||
desc: 'Bot will take trade requests',
|
desc: 'Bot will take trade requests',
|
||||||
async function(from){
|
async function(from){
|
||||||
|
// Check if bot has StoragePlugin
|
||||||
|
if (this.plunginsLoaded['Storage']) {
|
||||||
|
// Storage bot flow
|
||||||
|
await this.say('/trade accept');
|
||||||
|
let window = await this.once('windowOpen');
|
||||||
|
|
||||||
|
// Collect items received from player
|
||||||
|
const itemsReceived = [];
|
||||||
|
const customerSlots = [5, 6, 7, 8, 14, 15, 16, 17, 23, 24, 25, 26];
|
||||||
|
|
||||||
|
for (const slotNum of customerSlots) {
|
||||||
|
const item = window.slots[slotNum];
|
||||||
|
if (item) {
|
||||||
|
itemsReceived.push({
|
||||||
|
name: item.name,
|
||||||
|
id: item.type,
|
||||||
|
count: item.count,
|
||||||
|
nbt: item.nbt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm and log trade
|
||||||
|
this.bot.moveSlotItem(37, 37);
|
||||||
|
|
||||||
|
// Wait for trade to complete
|
||||||
|
await this.once('windowClose');
|
||||||
|
|
||||||
|
// Handle the trade items
|
||||||
|
if (itemsReceived.length > 0) {
|
||||||
|
await this.plunginsLoaded['Storage'].handleTrade(from, itemsReceived);
|
||||||
|
this.whisper(from, `Received ${itemsReceived.length} item type(s). Sorting into storage.`);
|
||||||
|
} else {
|
||||||
|
this.whisper(from, `No items received.`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original sign-based flow for non-storage bots
|
||||||
/*
|
/*
|
||||||
todo
|
todo
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CJbot.pluginAdd(require('./ai'));
|
|||||||
CJbot.pluginAdd(require('./guardianFarm'));
|
CJbot.pluginAdd(require('./guardianFarm'));
|
||||||
CJbot.pluginAdd(require('./goldFarm'));
|
CJbot.pluginAdd(require('./goldFarm'));
|
||||||
CJbot.pluginAdd(require('./craft_chests'));
|
CJbot.pluginAdd(require('./craft_chests'));
|
||||||
|
CJbot.pluginAdd(require('./storage'));
|
||||||
|
|
||||||
for(let name in conf.mc.bots){
|
for(let name in conf.mc.bots){
|
||||||
if(CJbot.bots[name]) continue;
|
if(CJbot.bots[name]) continue;
|
||||||
|
|||||||
4
nodejs/controller/storage.js
Normal file
4
nodejs/controller/storage.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Re-export the Storage plugin from the storage directory
|
||||||
|
module.exports = require('./storage/index');
|
||||||
477
nodejs/controller/storage/database.js
Normal file
477
nodejs/controller/storage/database.js
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const { open } = require('sqlite');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
constructor() {
|
||||||
|
this.db = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(dbPath) {
|
||||||
|
// Ensure directory exists
|
||||||
|
const dir = path.dirname(dbPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db = await open({
|
||||||
|
filename: dbPath,
|
||||||
|
driver: sqlite3.Database
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.createTables();
|
||||||
|
await this.insertDefaultPermissions();
|
||||||
|
|
||||||
|
console.log('Database initialized:', dbPath);
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTables() {
|
||||||
|
// Permissions table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT UNIQUE NOT NULL,
|
||||||
|
role TEXT DEFAULT 'team' NOT NULL CHECK(role IN ('owner', 'team', 'readonly')),
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Chests table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS chests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
pos_x INTEGER NOT NULL,
|
||||||
|
pos_y INTEGER NOT NULL,
|
||||||
|
pos_z INTEGER NOT NULL,
|
||||||
|
chest_type TEXT NOT NULL CHECK(chest_type IN ('single', 'double')),
|
||||||
|
row INTEGER NOT NULL,
|
||||||
|
column INTEGER NOT NULL,
|
||||||
|
category TEXT,
|
||||||
|
last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(pos_x, pos_y, pos_z)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Shulkers table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS shulkers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chest_id INTEGER NOT NULL,
|
||||||
|
slot INTEGER NOT NULL,
|
||||||
|
shulker_type TEXT DEFAULT 'shulker_box',
|
||||||
|
category TEXT,
|
||||||
|
item_focus TEXT,
|
||||||
|
slot_count INTEGER DEFAULT 27,
|
||||||
|
total_items INTEGER DEFAULT 0,
|
||||||
|
last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (chest_id) REFERENCES chests(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Shulker items table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS shulker_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
shulker_id INTEGER NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
slot INTEGER NOT NULL,
|
||||||
|
count INTEGER NOT NULL,
|
||||||
|
nbt_data TEXT,
|
||||||
|
FOREIGN KEY (shulker_id) REFERENCES shulkers(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(shulker_id, item_id),
|
||||||
|
CHECK(slot >= 0 AND slot <= 26),
|
||||||
|
CHECK(count > 0 AND count <= 64)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Trades table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS trades (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL CHECK(action IN ('deposit', 'withdraw')),
|
||||||
|
items TEXT NOT NULL,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Pending withdrawals table
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS pending_withdrawals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
requested_count INTEGER NOT NULL,
|
||||||
|
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'ready', 'completed', 'cancelled')),
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Item index for fast searches
|
||||||
|
await this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS item_index (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
item_id INTEGER UNIQUE NOT NULL,
|
||||||
|
item_name TEXT NOT NULL,
|
||||||
|
total_count INTEGER DEFAULT 0,
|
||||||
|
shulker_ids TEXT,
|
||||||
|
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertDefaultPermissions() {
|
||||||
|
const conf = require('../../conf');
|
||||||
|
const defaultPlayers = conf.storage?.defaultPlayers || [];
|
||||||
|
|
||||||
|
for (const player of defaultPlayers) {
|
||||||
|
try {
|
||||||
|
await this.db.run(
|
||||||
|
'INSERT OR IGNORE INTO permissions (player_name, role) VALUES (?, ?)',
|
||||||
|
[player.name, player.role]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error inserting default player:', player.name, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Permissions
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async addPlayer(name, Role = 'team') {
|
||||||
|
return await this.db.run(
|
||||||
|
'INSERT INTO permissions (player_name, role) VALUES (?, ?)',
|
||||||
|
[name, Role]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePlayer(name) {
|
||||||
|
return await this.db.run('DELETE FROM permissions WHERE player_name = ?', [name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPlayerRole(name) {
|
||||||
|
const row = await this.db.get('SELECT role FROM permissions WHERE player_name = ?', [name]);
|
||||||
|
return row ? row.role : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllPlayers() {
|
||||||
|
return await this.db.all('SELECT * FROM permissions ORDER BY role DESC, player_name ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPermission(name, requiredRole) {
|
||||||
|
const role = await this.getPlayerRole(name);
|
||||||
|
if (!role) return false;
|
||||||
|
|
||||||
|
const roles = ['readonly', 'team', 'owner'];
|
||||||
|
return roles.indexOf(role) >= roles.indexOf(requiredRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Chests
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async upsertChest(x, y, z, chestType, row, column, category = null) {
|
||||||
|
return await this.db.run(`
|
||||||
|
INSERT INTO chests (pos_x, pos_y, pos_z, chest_type, row, column, category)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(pos_x, pos_y, pos_z) DO UPDATE SET
|
||||||
|
chest_type = excluded.chest_type,
|
||||||
|
row = excluded.row,
|
||||||
|
column = excluded.column,
|
||||||
|
category = excluded.category,
|
||||||
|
last_scan = CURRENT_TIMESTAMP
|
||||||
|
`, [x, y, z, chestType, row, column, category]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChests() {
|
||||||
|
return await this.db.all('SELECT * FROM chests ORDER BY row, column');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChestById(id) {
|
||||||
|
return await this.db.get('SELECT * FROM chests WHERE id = ?', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChestByPosition(x, y, z) {
|
||||||
|
return await this.db.get(
|
||||||
|
'SELECT * FROM chests WHERE pos_x = ? AND pos_y = ? AND pos_z = ?',
|
||||||
|
[x, y, z]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrphanChests(knownChestPositions) {
|
||||||
|
// knownChestPositions is array of {x, y, z}
|
||||||
|
if (knownChestPositions.length === 0) return;
|
||||||
|
|
||||||
|
const placeholders = knownChestPositions.map(() => '(?, ?, ?)').join(', ');
|
||||||
|
const values = knownChestPositions.flatMap(p => [p.x, p.y, p.z]);
|
||||||
|
|
||||||
|
return await this.db.run(`
|
||||||
|
DELETE FROM chests
|
||||||
|
WHERE (pos_x, pos_y, pos_z) NOT IN (${placeholders})
|
||||||
|
`, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Shulkers
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async upsertShulker(chestId, slot, shulkerType, category = null, itemFocus = null) {
|
||||||
|
const result = await this.db.run(`
|
||||||
|
INSERT INTO shulkers (chest_id, slot, shulker_type, category, item_focus)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
slot_count = excluded.slot_count,
|
||||||
|
total_items = excluded.total_items,
|
||||||
|
last_scan = CURRENT_TIMESTAMP
|
||||||
|
`, [chestId, slot, shulkerType, category, itemFocus]);
|
||||||
|
|
||||||
|
return result.lastID;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShulkersByChest(chestId) {
|
||||||
|
return await this.db.all('SELECT * FROM shulkers WHERE chest_id = ? ORDER BY slot', [chestId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllShulkers() {
|
||||||
|
return await this.db.all('SELECT * FROM shulkers ORDER BY id');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShulkerById(id) {
|
||||||
|
return await this.db.get('SELECT * FROM shulkers WHERE id = ?', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findShulkerForItem(itemId, categoryName) {
|
||||||
|
// Find shulker with matching item and space
|
||||||
|
return await this.db.get(`
|
||||||
|
SELECT s.*, si.count as slot_item_count
|
||||||
|
FROM shulkers s
|
||||||
|
INNER JOIN shulker_items si ON s.id = si.shulker_id
|
||||||
|
WHERE s.item_focus = (SELECT item_name FROM shulker_items WHERE item_id = ? LIMIT 1)
|
||||||
|
AND s.category = ?
|
||||||
|
AND s.slot_count < 27
|
||||||
|
LIMIT 1
|
||||||
|
`, [itemId, categoryName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEmptyShulker(chestId, slot, categoryName, shulkerType = 'shulker_box') {
|
||||||
|
return await this.db.run(`
|
||||||
|
INSERT INTO shulkers (chest_id, slot, shulker_type, category, item_focus, slot_count, total_items)
|
||||||
|
VALUES (?, ?, ?, ?, NULL, 0, 0)
|
||||||
|
`, [chestId, slot, shulkerType, categoryName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateShulkerCounts(shulkerId, slotCount, totalItems) {
|
||||||
|
return await this.db.run(`
|
||||||
|
UPDATE shulkers
|
||||||
|
SET slot_count = ?, total_items = ?, last_scan = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`, [slotCount, totalItems, shulkerId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteShulker(id) {
|
||||||
|
return await this.db.run('DELETE FROM shulkers WHERE id = ?', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Shulker Items
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async upsertShulkerItem(shulkerId, itemId, itemName, slot, count, nbt = null) {
|
||||||
|
return await this.db.run(`
|
||||||
|
INSERT INTO shulker_items (shulker_id, item_id, item_name, slot, count, nbt_data)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(shulker_id, item_id) DO UPDATE SET
|
||||||
|
slot = excluded.slot,
|
||||||
|
count = excluded.count,
|
||||||
|
nbt_data = excluded.nbt_data
|
||||||
|
`, [shulkerId, itemId, itemName, slot, count, nbt ? JSON.stringify(nbt) : null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShulkerItems(shulkerId) {
|
||||||
|
return await this.db.all('SELECT * FROM shulker_items WHERE shulker_id = ? ORDER BY slot', [shulkerId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteShulkerItem(shulkerId, itemId) {
|
||||||
|
return await this.db.run('DELETE FROM shulker_items WHERE shulker_id = ? AND item_id = ?', [shulkerId, itemId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearShulkerItems(shulkerId) {
|
||||||
|
return await this.db.run('DELETE FROM shulker_items WHERE shulker_id = ?', [shulkerId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Trades
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async logTrade(playerName, action, items) {
|
||||||
|
return await this.db.run(
|
||||||
|
'INSERT INTO trades (player_name, action, items) VALUES (?, ?, ?)',
|
||||||
|
[playerName, action, JSON.stringify(items)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentTrades(limit = 50) {
|
||||||
|
return await this.db.all(
|
||||||
|
'SELECT * FROM trades ORDER BY timestamp DESC LIMIT ?',
|
||||||
|
[limit]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTradesByPlayer(playerName, limit = 50) {
|
||||||
|
return await this.db.all(
|
||||||
|
'SELECT * FROM trades WHERE player_name = ? ORDER BY timestamp DESC LIMIT ?',
|
||||||
|
[playerName, limit]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Pending Withdrawals
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async queueWithdrawal(playerName, itemId, itemName, count) {
|
||||||
|
return await this.db.run(`
|
||||||
|
INSERT INTO pending_withdrawals (player_name, item_id, item_name, requested_count)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`, [playerName, itemId, itemName, count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPendingWithdrawals(playerName) {
|
||||||
|
return await this.db.all(`
|
||||||
|
SELECT * FROM pending_withdrawals
|
||||||
|
WHERE player_name = ? AND status IN ('pending', 'ready')
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
`, [playerName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWithdrawalById(id) {
|
||||||
|
return await this.db.get('SELECT * FROM pending_withdrawals WHERE id = ?', [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWithdrawStatus(id, status) {
|
||||||
|
return await this.db.run(
|
||||||
|
'UPDATE pending_withdrawals SET status = ? WHERE id = ?',
|
||||||
|
[status, id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async markCompletedWithdrawals(playerName) {
|
||||||
|
return await this.db.run(`
|
||||||
|
UPDATE pending_withdrawals
|
||||||
|
SET status = 'completed'
|
||||||
|
WHERE player_name = ? AND status = 'ready'
|
||||||
|
`, [playerName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Item Index
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async updateItemIndex(itemId, itemName, shulkerId, count) {
|
||||||
|
// This is a simplified version - in production, you'd want to handle
|
||||||
|
// the shulker_ids JSON aggregation more carefully
|
||||||
|
return await this.db.run(`
|
||||||
|
INSERT INTO item_index (item_id, item_name, total_count)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(item_id) DO UPDATE SET
|
||||||
|
total_count = total_count + ?,
|
||||||
|
last_updated = CURRENT_TIMESTAMP
|
||||||
|
`, [itemId, itemName, count, count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rebuildItemIndex() {
|
||||||
|
// Rebuild entire index from shulker_items
|
||||||
|
return await this.db.exec(`
|
||||||
|
INSERT OR REPLACE INTO item_index (item_id, item_name, total_count, shulker_ids, last_updated)
|
||||||
|
SELECT
|
||||||
|
si.item_id,
|
||||||
|
si.item_name,
|
||||||
|
SUM(si.count) as total_count,
|
||||||
|
GROUP_CONCAT('{"id":' || si.shulker_id || ',"count":' || si.count || '}') as shulker_ids,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
FROM shulker_items si
|
||||||
|
GROUP BY si.item_id, si.item_name
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchItems(query) {
|
||||||
|
if (!query) {
|
||||||
|
return await this.db.all('SELECT * FROM item_index ORDER BY item_name ASC');
|
||||||
|
}
|
||||||
|
return await this.db.all(
|
||||||
|
"SELECT * FROM item_index WHERE item_name LIKE ? ORDER BY item_name ASC",
|
||||||
|
[`%${query}%`]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItemDetails(itemId) {
|
||||||
|
const item = await this.db.get('SELECT * FROM item_index WHERE item_id = ?', [itemId]);
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
// Parse shulker_ids and get chest details
|
||||||
|
const shulkerIds = JSON.parse(`[${item.shulker_ids}]`);
|
||||||
|
|
||||||
|
const locations = await this.db.all(`
|
||||||
|
SELECT
|
||||||
|
s.id as shulker_id,
|
||||||
|
s.chest_id,
|
||||||
|
c.pos_x, c.pos_y, c.pos_z,
|
||||||
|
js.value as count
|
||||||
|
FROM json_each(?) as js
|
||||||
|
INNER JOIN shulkers s ON s.id = JSON_EXTRACT(js.value, '$.id')
|
||||||
|
INNER JOIN chests c ON c.id = s.chest_id
|
||||||
|
`, [item.shulker_ids]);
|
||||||
|
|
||||||
|
return { ...item, locations };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Stats
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
async getStats() {
|
||||||
|
const totalItems = await this.db.get('SELECT SUM(total_items) as total FROM shulkers');
|
||||||
|
const totalShulkers = await this.db.get('SELECT COUNT(*) as total FROM shulkers');
|
||||||
|
const totalChests = await this.db.get('SELECT COUNT(*) as total FROM chests');
|
||||||
|
const emptyShulkers = await this.db.get("SELECT COUNT(*) as total FROM shulkers WHERE slot_count = 0");
|
||||||
|
const recentTrades = await this.db.get('SELECT COUNT(*) as total FROM trades WHERE timestamp > datetime("now", "-1 day")');
|
||||||
|
|
||||||
|
// Category breakdown
|
||||||
|
const categories = await this.db.all(`
|
||||||
|
SELECT category, COUNT(*) as count
|
||||||
|
FROM shulkers
|
||||||
|
WHERE category IS NOT NULL
|
||||||
|
GROUP BY category
|
||||||
|
`);
|
||||||
|
|
||||||
|
const categoryMap = {};
|
||||||
|
for (const cat of categories) {
|
||||||
|
categoryMap[cat.category] = cat.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalItems: totalItems?.total || 0,
|
||||||
|
totalShulkers: totalShulkers?.total || 0,
|
||||||
|
totalChests: totalChests?.total || 0,
|
||||||
|
emptyShulkers: emptyShulkers?.total || 0,
|
||||||
|
recentTrades: recentTrades?.total || 0,
|
||||||
|
categories: categoryMap
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
if (this.db) {
|
||||||
|
await this.db.close();
|
||||||
|
this.db = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Database();
|
||||||
217
nodejs/controller/storage/index.js
Normal file
217
nodejs/controller/storage/index.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Database = require('./database');
|
||||||
|
const Scanner = require('./scanner');
|
||||||
|
const Organizer = require('./organizer');
|
||||||
|
const WebServer = require('./web');
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
constructor(args) {
|
||||||
|
console.log('Storage: Constructor called');
|
||||||
|
this.bot = args.bot;
|
||||||
|
this.config = args;
|
||||||
|
this.isReady = false;
|
||||||
|
this.webServer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
console.log('Storage: init() called');
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.bot.on('onReady', async () => {
|
||||||
|
try {
|
||||||
|
console.log(`Storage: Bot ${this.bot.name} is ready, initializing storage system...`);
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
if (!Database.db) {
|
||||||
|
console.log('Storage: Initializing database...');
|
||||||
|
await Database.initialize(this.config.dbFile || './storage/storage.db');
|
||||||
|
} else {
|
||||||
|
console.log('Storage: Database already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize scanner and organizer
|
||||||
|
console.log('Storage: Creating Scanner and Organizer...');
|
||||||
|
this.scanner = new Scanner();
|
||||||
|
this.organizer = new Organizer();
|
||||||
|
|
||||||
|
// Start web server
|
||||||
|
console.log('Storage: Starting web server...');
|
||||||
|
this.webServer = new WebServer();
|
||||||
|
await this.webServer.start(this.bot);
|
||||||
|
|
||||||
|
if (this.config.startupTasks) {
|
||||||
|
console.log('Storage: Running startup tasks...');
|
||||||
|
await this.config.startupTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isReady = true;
|
||||||
|
console.log('Storage: Initialization complete! Ready to use.');
|
||||||
|
resolve();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Storage: Error in init:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unload(keepDb = false) {
|
||||||
|
console.log('Storage: Unloading...');
|
||||||
|
|
||||||
|
if (this.webServer) {
|
||||||
|
console.log('Storage: Stopping web server...');
|
||||||
|
await this.webServer.close();
|
||||||
|
this.webServer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scanner) delete this.scanner;
|
||||||
|
if (this.organizer) delete this.organizer;
|
||||||
|
|
||||||
|
// Close database only if not keeping it for web server
|
||||||
|
if (!keepDb && Database.db) {
|
||||||
|
console.log('Storage: Closing database...');
|
||||||
|
Database.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isReady = false;
|
||||||
|
console.log('Storage: Unload complete');
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanArea(force = false) {
|
||||||
|
const { sleep } = require('../../utils');
|
||||||
|
|
||||||
|
console.log(`Storage[${this.bot.name}]: Scanning storage area...`);
|
||||||
|
const chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
|
||||||
|
const shulkers = await this.scanner.scanAllChests(this.bot, Database);
|
||||||
|
console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleTrade(playerName, itemsReceived) {
|
||||||
|
const { sleep } = require('../../utils');
|
||||||
|
|
||||||
|
console.log(`Storage[${this.bot.name}]: Processing trade from ${playerName}, received ${itemsReceived.length} item types`);
|
||||||
|
|
||||||
|
// Log trade
|
||||||
|
await Database.logTrade(playerName, 'deposit', itemsReceived);
|
||||||
|
|
||||||
|
// Store items
|
||||||
|
let totalItems = 0;
|
||||||
|
for (const item of itemsReceived) {
|
||||||
|
totalItems += item.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Storage[${this.bot.name}]: Sorting ${totalItems} items from ${playerName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleWithdrawRequest(playerName, itemId, count) {
|
||||||
|
console.log(`Storage[${this.bot.name}]: Withdraw request from ${playerName}: ${itemId} x${count}`);
|
||||||
|
|
||||||
|
// Search for item
|
||||||
|
const items = await Database.searchItems(itemId);
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return this.bot.whisper(playerName, `Item not found: ${itemId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = items[0];
|
||||||
|
if (item.total_count < count) {
|
||||||
|
return this.bot.whisper(playerName, `Not enough ${item.item_name}. Available: ${item.total_count}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue withdrawal
|
||||||
|
await Database.queueWithdrawal(playerName, itemId, item.item_name, count);
|
||||||
|
this.bot.whisper(playerName, `Withdrawal queued. Use /trade when ready.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(playerName) {
|
||||||
|
const stats = await Database.getStats();
|
||||||
|
this.bot.whisper(playerName, `Storage: ${stats.totalItems} items in ${stats.totalShulkers} shulkers (${stats.totalChests} chests)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findItem(itemName) {
|
||||||
|
return await Database.searchItems(itemName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPermission(name, requiredRole) {
|
||||||
|
return await Database.checkPermission(name, requiredRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command handler for in-game commands
|
||||||
|
async handleCommand(from, command, ...args) {
|
||||||
|
console.log(`Storage: Command '${command}' from ${from} with args:`, args);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'scan':
|
||||||
|
this.bot.whisper(from, 'Starting storage scan...');
|
||||||
|
try {
|
||||||
|
await this.scanArea(true);
|
||||||
|
const stats = await Database.getStats();
|
||||||
|
this.bot.whisper(from, `Scan complete! ${stats.totalChests} chests, ${stats.totalShulkers} shulkers, ${stats.totalItems} items`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Storage: Scan error:', error);
|
||||||
|
this.bot.whisper(from, `Scan failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
await this.getStatus(from);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'withdraw':
|
||||||
|
const [itemName, count] = args;
|
||||||
|
await this.handleWithdrawRequest(from, itemName, count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'find':
|
||||||
|
const [searchTerm] = args;
|
||||||
|
const items = await this.findItem(searchTerm);
|
||||||
|
if (items.length === 0) {
|
||||||
|
this.bot.whisper(from, `No items found matching '${searchTerm}'`);
|
||||||
|
} else {
|
||||||
|
const results = items.slice(0, 5).map(i => `${i.item_name}: ${i.total_count}`);
|
||||||
|
this.bot.whisper(from, `Found: ${results.join(', ')}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'chests':
|
||||||
|
const chests = await Database.getChests();
|
||||||
|
this.bot.whisper(from, `Tracking ${chests.length} chests`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'organize':
|
||||||
|
this.bot.whisper(from, 'Organize not yet implemented');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'addplayer':
|
||||||
|
const [playerName, role] = args;
|
||||||
|
try {
|
||||||
|
await Database.addPlayer(playerName, role || 'team');
|
||||||
|
this.bot.whisper(from, `Added ${playerName} as ${role || 'team'}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.whisper(from, `Failed to add player: ${error.message}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'removeplayer':
|
||||||
|
const [removePlayer] = args;
|
||||||
|
try {
|
||||||
|
await Database.removePlayer(removePlayer);
|
||||||
|
this.bot.whisper(from, `Removed ${removePlayer}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.bot.whisper(from, `Failed to remove player: ${error.message}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'players':
|
||||||
|
const players = await Database.getAllPlayers();
|
||||||
|
const playerList = players.map(p => `${p.player_name}(${p.role})`).join(', ');
|
||||||
|
this.bot.whisper(from, `Players: ${playerList}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.bot.whisper(from, `Unknown command: ${command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Storage;
|
||||||
103
nodejs/controller/storage/organizer.js
Normal file
103
nodejs/controller/storage/organizer.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Vec3 = require('vec3');
|
||||||
|
const conf = require('../../conf');
|
||||||
|
|
||||||
|
class Organizer {
|
||||||
|
constructor() {
|
||||||
|
this.categories = conf.storage?.categories || {
|
||||||
|
minerals: ['diamond', 'netherite_ingot', 'gold_ingot', 'iron_ingot'],
|
||||||
|
food: ['bread', 'cooked_porkchop', 'steak'],
|
||||||
|
tools: ['diamond_sword', 'diamond_pickaxe', 'netherite_pickaxe'],
|
||||||
|
armor: ['diamond_chestplate', 'netherite_helmet'],
|
||||||
|
blocks: ['stone', 'dirt', 'cobblestone'],
|
||||||
|
redstone: ['redstone', 'repeater', 'piston'],
|
||||||
|
misc: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
categorizeItem(itemName) {
|
||||||
|
// Fast path: check each category
|
||||||
|
for (const [category, items] of Object.entries(this.categories)) {
|
||||||
|
if (items.includes(itemName)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'misc';
|
||||||
|
}
|
||||||
|
|
||||||
|
async findShulkerForItem(database, itemId, categoryName) {
|
||||||
|
// Find shulker with matching item that has space
|
||||||
|
const shulker = await database.findShulkerForItem(itemId, categoryName);
|
||||||
|
return shulker;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findEmptyShulkerSlot(database, categoryName) {
|
||||||
|
// Find an empty shulker in the appropriate category and row (prefer row 4 for empty storage)
|
||||||
|
const chests = await database.getChests();
|
||||||
|
|
||||||
|
// Filter chests by category and row 4 (top row for empty/new shulkers)
|
||||||
|
const categoryChests = chests.filter(c =>
|
||||||
|
c.category === categoryName && c.row === 4
|
||||||
|
).sort((a, b) => a.column - b.column); // Left to right
|
||||||
|
|
||||||
|
for (const chest of categoryChests) {
|
||||||
|
const shulkers = await database.getShulkersByChest(chest.id);
|
||||||
|
|
||||||
|
// Find first shulker that's empty (slotCount = 0) or has space
|
||||||
|
for (const shulker of shulkers) {
|
||||||
|
if (!shulker.item_focus) {
|
||||||
|
// Empty shulker available
|
||||||
|
return {
|
||||||
|
chest_id: chest.id,
|
||||||
|
chestPosition: new Vec3(chest.pos_x, chest.pos_y, chest.pos_z),
|
||||||
|
chestSlot: shulker.slot,
|
||||||
|
shulker_id: shulker.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no empty shulker, look for first available slot in row 4
|
||||||
|
// ... this would need to scan actual chest for empty slots
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sortItemIntoStorage(bot, database, item, categoryName) {
|
||||||
|
// Find existing shulker with same item and space
|
||||||
|
const existingShulker = await this.findShulkerForItem(database, item.id, categoryName);
|
||||||
|
|
||||||
|
if (existingShulker) {
|
||||||
|
// Space available, add to existing shulker
|
||||||
|
console.log(`Organizer: Found shulker ${existingShulker.id} for ${item.name}`);
|
||||||
|
return existingShulker;
|
||||||
|
} else {
|
||||||
|
// Need new shulker
|
||||||
|
console.log(`Organizer: Creating new shulker for ${item.name} (${categoryName})`);
|
||||||
|
|
||||||
|
const shulkerSlot = await this.findEmptyShulkerSlot(database, categoryName);
|
||||||
|
if (!shulkerSlot) {
|
||||||
|
console.log(`Organizer: No available shulker slot for ${item.name}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/prepare new shulker
|
||||||
|
await database.upsertShulker(
|
||||||
|
shulkerSlot.chest_id,
|
||||||
|
shulkerSlot.chestSlot,
|
||||||
|
'shulker_box',
|
||||||
|
categoryName,
|
||||||
|
item.name // item_focus
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Organizer: Created shulker at chest ${shulkerSlot.chest_id}, slot ${shulkerSlot.chestSlot}`);
|
||||||
|
return {
|
||||||
|
chest_id: shulkerSlot.chest_id,
|
||||||
|
slot: shulkerSlot.chestSlot,
|
||||||
|
new: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Organizer;
|
||||||
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;
|
||||||
405
nodejs/controller/storage/web.js
Normal file
405
nodejs/controller/storage/web.js
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const database = require('./database');
|
||||||
|
|
||||||
|
class WebServer {
|
||||||
|
constructor() {
|
||||||
|
console.log('WebServer: Constructor called');
|
||||||
|
this.app = null;
|
||||||
|
this.port = null;
|
||||||
|
this.host = null;
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(bot) {
|
||||||
|
console.log('WebServer: start() called');
|
||||||
|
const conf = require('../../conf');
|
||||||
|
|
||||||
|
this.port = conf.storage?.webPort || 3000;
|
||||||
|
this.host = conf.storage?.webHost || '0.0.0.0';
|
||||||
|
|
||||||
|
console.log(`WebServer: Configuring server on ${this.host}:${this.port}`);
|
||||||
|
|
||||||
|
this.app = express();
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
this.app.use(express.json());
|
||||||
|
this.app.use(cors());
|
||||||
|
|
||||||
|
// Request logging
|
||||||
|
this.app.use((req, res, next) => {
|
||||||
|
console.log(`WebServer: ${req.method} ${req.path}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
this.setupRoutes();
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = this.app.listen(this.port, this.host, () => {
|
||||||
|
console.log(`WebServer: Running at http://${this.host}:${this.port}`);
|
||||||
|
console.log(`WebServer: Try http://localhost:${this.port}/health`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
this.server.on('error', (err) => {
|
||||||
|
console.error('WebServer: Failed to start:', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRoutes() {
|
||||||
|
console.log('WebServer: Setting up routes...');
|
||||||
|
|
||||||
|
// Index page - Full web UI
|
||||||
|
this.app.get('/', async (req, res) => {
|
||||||
|
res.send(this.getIndexHTML());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
this.app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', server: `${this.host}:${this.port}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inventory - Get all items aggregated
|
||||||
|
this.app.get('/api/inventory', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await database.searchItems(req.query.q);
|
||||||
|
res.json({ items });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/inventory:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get specific item details
|
||||||
|
this.app.get('/api/inventory/:itemId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const itemId = parseInt(req.params.itemId);
|
||||||
|
const item = await database.getItemDetails(itemId);
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: 'Item not found' });
|
||||||
|
}
|
||||||
|
res.json({ item });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/inventory/:itemId:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all chests
|
||||||
|
this.app.get('/api/chests', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const chests = await database.getChests();
|
||||||
|
res.json({ chests });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/chests:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get specific chest with shulker contents
|
||||||
|
this.app.get('/api/chests/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// This would return chest + shulker details
|
||||||
|
// Implementation would scan the specified chest and shulkers
|
||||||
|
|
||||||
|
const chestId = parseInt(req.params.id);
|
||||||
|
const chest = await database.getChestById(chestId);
|
||||||
|
|
||||||
|
if (!chest) {
|
||||||
|
return res.status(404).json({ error: 'Chest not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const shulkers = await database.getShulkersByChest(chestId);
|
||||||
|
|
||||||
|
// Return chest + shulkers
|
||||||
|
res.json({ chest, shulkers });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/chests/:id:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
this.app.get('/api/stats', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = await database.getStats();
|
||||||
|
res.json(stats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/stats:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trade history
|
||||||
|
this.app.get('/api/trades', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const limit = parseInt(req.query.limit) || 50;
|
||||||
|
const trades = await database.getRecentTrades(limit);
|
||||||
|
res.json({ trades });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/trades:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pending withdrawals (by player)
|
||||||
|
this.app.get('/api/pending/:playerName', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const playerName = req.params.playerName;
|
||||||
|
const pending = await database.getPendingWithdrawals(playerName);
|
||||||
|
res.json({ pending });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error /api/pending/:playerName:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
console.log('WebServer: Closing...');
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
console.log('WebServer: Closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexHTML() {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Storage System</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
color: #eee;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: 0 auto; }
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #00d4ff;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
.stat-card .label { color: #aaa; font-size: 0.9em; }
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.search-box::placeholder { color: #888; }
|
||||||
|
.search-box:focus { outline: 2px solid #00d4ff; }
|
||||||
|
.section { margin-bottom: 30px; }
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #00d4ff;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.item-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.item-card {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.item-card:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.item-name { font-weight: 500; }
|
||||||
|
.item-count {
|
||||||
|
background: #00d4ff;
|
||||||
|
color: #000;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.chest-list { list-style: none; }
|
||||||
|
.chest-item {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.loading { text-align: center; padding: 40px; color: #888; }
|
||||||
|
.error { color: #ff6b6b; padding: 20px; text-align: center; }
|
||||||
|
.refresh-btn {
|
||||||
|
background: #00d4ff;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.refresh-btn:hover { background: #00b8e6; }
|
||||||
|
.category-badge {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>📦 Storage System</h1>
|
||||||
|
|
||||||
|
<button class="refresh-btn" onclick="loadAll()">🔄 Refresh</button>
|
||||||
|
|
||||||
|
<div class="stats-grid" id="stats">
|
||||||
|
<div class="stat-card"><div class="value">-</div><div class="label">Loading...</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="text" class="search-box" id="search" placeholder="🔍 Search items..." oninput="filterItems()">
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">📦 Inventory</div>
|
||||||
|
<div class="item-grid" id="inventory">
|
||||||
|
<div class="loading">Loading inventory...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">🗃️ Chests</div>
|
||||||
|
<ul class="chest-list" id="chests">
|
||||||
|
<li class="loading">Loading chests...</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let allItems = [];
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/stats');
|
||||||
|
const stats = await res.json();
|
||||||
|
document.getElementById('stats').innerHTML = \`
|
||||||
|
<div class="stat-card"><div class="value">\${stats.totalItems || 0}</div><div class="label">Total Items</div></div>
|
||||||
|
<div class="stat-card"><div class="value">\${stats.totalShulkers || 0}</div><div class="label">Shulkers</div></div>
|
||||||
|
<div class="stat-card"><div class="value">\${stats.totalChests || 0}</div><div class="label">Chests</div></div>
|
||||||
|
<div class="stat-card"><div class="value">\${stats.emptyShulkers || 0}</div><div class="label">Empty Shulkers</div></div>
|
||||||
|
\`;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('stats').innerHTML = '<div class="error">Failed to load stats</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInventory() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/inventory');
|
||||||
|
const data = await res.json();
|
||||||
|
allItems = data.items || [];
|
||||||
|
renderItems(allItems);
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('inventory').innerHTML = '<div class="error">Failed to load inventory</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItems(items) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
document.getElementById('inventory').innerHTML = '<div class="loading">No items found. Run "scan" command in-game.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('inventory').innerHTML = items.map(item => \`
|
||||||
|
<div class="item-card">
|
||||||
|
<span class="item-name">\${formatName(item.item_name)}</span>
|
||||||
|
<span class="item-count">\${item.total_count}</span>
|
||||||
|
</div>
|
||||||
|
\`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterItems() {
|
||||||
|
const query = document.getElementById('search').value.toLowerCase();
|
||||||
|
const filtered = allItems.filter(i => i.item_name.toLowerCase().includes(query));
|
||||||
|
renderItems(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadChests() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/chests');
|
||||||
|
const data = await res.json();
|
||||||
|
const chests = data.chests || [];
|
||||||
|
if (chests.length === 0) {
|
||||||
|
document.getElementById('chests').innerHTML = '<li class="loading">No chests found. Run "scan" command in-game.</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('chests').innerHTML = chests.map(c => \`
|
||||||
|
<li class="chest-item">
|
||||||
|
<span>\${c.chest_type} chest at (\${c.pos_x}, \${c.pos_y}, \${c.pos_z})</span>
|
||||||
|
<span class="category-badge">\${c.category || 'misc'}</span>
|
||||||
|
</li>
|
||||||
|
\`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('chests').innerHTML = '<li class="error">Failed to load chests</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatName(name) {
|
||||||
|
return name.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAll() {
|
||||||
|
loadStats();
|
||||||
|
loadInventory();
|
||||||
|
loadChests();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll();
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(loadAll, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebServer;
|
||||||
2031
nodejs/package-lock.json
generated
2031
nodejs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,14 +19,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.17.1",
|
"@google/generative-ai": "^0.17.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
|
"express": "^5.2.1",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"minecraft-data": "^3.101.0",
|
"minecraft-data": "^3.101.0",
|
||||||
"mineflayer": "^4.33.0",
|
"mineflayer": "^4.33.0",
|
||||||
"mineflayer-pathfinder": "^2.4.5",
|
"mineflayer-pathfinder": "^2.4.5",
|
||||||
"mineflayer-web-inventory": "^1.3.0",
|
"mineflayer-web-inventory": "^1.3.0",
|
||||||
"moment": "^2.29.3",
|
"moment": "^2.29.3",
|
||||||
"prismarine-windows": "^2.9.0"
|
"prismarine-windows": "^2.9.0",
|
||||||
|
"sqlite": "^5.1.1",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.7"
|
"nodemon": "^3.1.7"
|
||||||
|
|||||||
BIN
nodejs/storage/storage.db
Normal file
BIN
nodejs/storage/storage.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user