AI #2

Open
wmantly wants to merge 7 commits from ai into master
15 changed files with 3608 additions and 213 deletions
Showing only changes of commit 7b326a112e - Show all commits

View File

@@ -13,6 +13,25 @@ module.exports = {
"plugings": {
"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 provider: 'gemini' (default) or 'ollama'
"provider": "gemini",

View File

@@ -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}`);
}
}
},
};

View File

@@ -1,8 +1,11 @@
'use strict';
const conf = require('.');
module.exports = {
default: require('./default'),
fun: require('./fun'),
invite: require('./invite'),
trade: require('./trade'),
storage: require('./storage'),
};

View 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');
}
},
};

View File

@@ -30,6 +30,45 @@ module.exports = {
'.trade': {
desc: 'Bot will take trade requests',
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

View File

@@ -15,6 +15,7 @@ CJbot.pluginAdd(require('./ai'));
CJbot.pluginAdd(require('./guardianFarm'));
CJbot.pluginAdd(require('./goldFarm'));
CJbot.pluginAdd(require('./craft_chests'));
CJbot.pluginAdd(require('./storage'));
for(let name in conf.mc.bots){
if(CJbot.bots[name]) continue;

View File

@@ -0,0 +1,4 @@
'use strict';
// Re-export the Storage plugin from the storage directory
module.exports = require('./storage/index');

View 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();

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

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

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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,18 @@
"dependencies": {
"@google/generative-ai": "^0.17.1",
"axios": "^1.7.7",
"cors": "^2.8.6",
"dotenv": "^16.0.1",
"express": "^5.2.1",
"extend": "^3.0.2",
"minecraft-data": "^3.101.0",
"mineflayer": "^4.33.0",
"mineflayer-pathfinder": "^2.4.5",
"mineflayer-web-inventory": "^1.3.0",
"moment": "^2.29.3",
"prismarine-windows": "^2.9.0"
"prismarine-windows": "^2.9.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.1.7"

BIN
nodejs/storage/storage.db Normal file

Binary file not shown.