From 9acd38c94bf37d20720ef021376da9aa94821d4a Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 31 Jan 2026 18:49:08 -0500 Subject: [PATCH] craft fix --- nodejs/OLLAMA_SETUP.md | 305 ++++++++ nodejs/STORAGE_SYSTEM.md | 923 +++++++++++++++++++++++ nodejs/conf/base.js | 12 + nodejs/controller/ai.js | 144 ++-- nodejs/controller/ai/providers/gemini.js | 88 +++ nodejs/controller/ai/providers/index.js | 25 + nodejs/controller/ai/providers/ollama.js | 108 +++ nodejs/controller/craft.js | 16 +- nodejs/controller/craft_chests.js | 16 +- 9 files changed, 1535 insertions(+), 102 deletions(-) create mode 100644 nodejs/OLLAMA_SETUP.md create mode 100644 nodejs/STORAGE_SYSTEM.md create mode 100644 nodejs/controller/ai/providers/gemini.js create mode 100644 nodejs/controller/ai/providers/index.js create mode 100644 nodejs/controller/ai/providers/ollama.js diff --git a/nodejs/OLLAMA_SETUP.md b/nodejs/OLLAMA_SETUP.md new file mode 100644 index 0000000..515f84d --- /dev/null +++ b/nodejs/OLLAMA_SETUP.md @@ -0,0 +1,305 @@ +# Ollama Integration Guide + +This project now supports Ollama as an AI backend alongside Google Gemini, with **per-bot configuration** allowing you to mix providers and personalities across multiple bots. + +## Configuration Hierarchy + +AI settings are merged in this order: +1. **Global defaults** in `conf/base.js` → `ai` object +2. **Bot-specific overrides** in `conf/secrets.js` → `mc.bots.{botName}.plugins.Ai` + +## Configuration + +### Global Defaults (Optional) + +Edit `conf/base.js` to set defaults for all bots: + +```javascript +"ai":{ + // Default provider (can be overridden per-bot) + "provider": "gemini", // or "ollama" + + // Gemini API key (used by Gemini provider) + "key": "", + + // Ollama settings (used by Ollama provider) + "baseUrl": "http://localhost:11434", + "model": "llama3.2", + "timeout": 30000, + + // Generation settings (applies to both providers) + "temperature": 1, + "topP": 0.95, + "topK": 64, + "maxOutputTokens": 8192, + "interval": 20, + // ... prompts +} +``` + +### Per-Bot Configuration + +Edit `conf/secrets.js` to configure each bot individually: + +#### Example 1: Bot using default global settings + +```javascript +"art": { + "username": "art@vm42.us", + "commands": ['fun', 'invite', 'default'], + "auth": "microsoft", + "plugins": { + "Ai":{ + "promptName": "helpful", + // Uses global provider settings from base.js + } + }, +}, +``` + +#### Example 2: Bot using specific Ollama instance + +```javascript +"ayay": { + "username": "limtisengyes@gmail.com", + "commands": ['fun', 'invite', 'default'], + "auth": "microsoft", + "plugins": { + "Ai":{ + "promptName": "asshole", + "provider": "ollama", + "baseUrl": "http://192.168.1.50:11434", // Remote Ollama + "model": "llama3.2:7b", + "interval": 25, + } + } +}, +``` + +#### Example 3: Bot using Gemini with custom settings + +```javascript +"nova": { + "username": "your@email.com", + "auth": "microsoft", + "commands": ['default', 'fun'], + "plugins": { + "Ai":{ + "promptName": "helpful", + "provider": "gemini", + "model": "gemini-2.0-flash-exp", + "temperature": 0.7, + } + } +}, +``` + +### Multiple Bots with Different Providers + +You can run multiple bots with different providers simultaneously: + +```javascript +// conf/secrets.js +"bots": { + "bot1": { + "plugins": { + "Ai": { + "promptName": "helpful", + "provider": "gemini", // Uses Google Gemini + "model": "gemini-2.0-flash-exp", + } + } + }, + "bot2": { + "plugins": { + "Ai": { + "promptName": "asshole", + "provider": "ollama", // Uses local Ollama + "baseUrl": "http://localhost:11434", + "model": "llama3.2", + } + } + }, + "bot3": { + "plugins": { + "Ai": { + "promptName": "Ashley", + "provider": "ollama", // Uses remote Ollama + "baseUrl": "http://192.168.1.50:11434", + "model": "mistral", + } + } + } +} +``` + +### Mixing Personalities and Models + +Each bot can have: +- **Different provider** (Gemini or different Ollama instances) +- **Different model** (llama3.2, mistral, qwen2.5, etc.) +- **Different personality** (helpful, asshole, Ashley, custom) +- **Different settings** (temperature, interval, etc.) + +```javascript +"helpfulBot": { + "plugins": { + "Ai": { + "promptName": "helpful", + "provider": "ollama", + "baseUrl": "http://server1:11434", + "model": "llama3.2:3b", + "temperature": 0.5, + "interval": 15, + } + } +}, +"toxicBot": { + "plugins": { + "Ai": { + "promptName": "asshole", + "provider": "ollama", + "baseUrl": "http://server2:11434", + "model": "llama3.2:70b", + "temperature": 1.2, + "interval": 30, + } + } +}, +``` + +## Ollama Setup + +### Install Ollama + +```bash +# Linux/macOS +curl -fsSL https://ollama.com/install.sh | sh + +# Or download from https://ollama.com/download +``` + +### Pull Models + +```bash +# Recommended for chat bots: +ollama pull llama3.2 +ollama pull mistral +ollama pull qwen2.5 + +# Specific sizes for performance tuning: +ollama pull llama3.2:3b # Fast, lightweight +ollama pull llama3.2:7b # Good balance +ollama pull llama3.2:70b # Smarter, slower +``` + +### Start Ollama Server + +```bash +# Local (only) +ollama serve + +# Allow remote connections (for multiple servers) +OLLAMA_HOST=0.0.0.0:11434 ollama serve +``` + +### Configure Remote Ollama + +To use Ollama on another machine: + +1. On the Ollama server: + ```bash + OLLAMA_HOST=0.0.0.0:11434 ollama serve + ``` + +2. In bot config: + ```javascript + "Ai": { + "provider": "ollama", + "baseUrl": "http://ollama-server-ip:11434", + "model": "llama3.2", + } + ``` + +## Ollama Model Recommendations + +| Model | Size | Speed | Quality | Best For | +|-------|------|-------|---------|----------| +| `llama3.2:3b` | 3B | Very Fast | Good | Bots needing fast responses | +| `llama3.2:7b` | 7B | Fast | Very Good | General purpose | +| `llama3.2:70b` | 70B | Moderate | Excellent | Smart bots, complex prompts | +| `mistral` | 7B | Fast | Good | Balanced solution | +| `qwen2.5:7b` | 7B | Fast | Very Good | Good instruction following | +| `gemma2:9b` | 9B | Fast | Good | Lightweight alternative | + +## Troubleshooting + +### Connection Refused + +```bash +# Check if Ollama is running +curl http://localhost:11434/api/tags + +# Check specific server +curl http://192.168.1.50:11434/api/tags +``` + +### Model Not Found + +```bash +# Check available models +ollama list + +# Pull missing model +ollama pull llama3.2 +``` + +### JSON Parsing Errors + +Most models support JSON mode. If issues occur: +1. Switch to `llama3.1`, `qwen2.5`, or `mistral` +2. Lower `temperature:` (e.g., 0.7) +3. Increase `maxOutputTokens:` for longer responses + +### Slow Responses + +- Use smaller models (`llama3.2:3b` vs `70b`) +- Increase `interval:` in config +- Reduce `maxOutputTokens:` +- Check network latency for remote Ollama instances + +### Multiple Bots Overloading Ollama + +If running many bots on one Ollama server: +1. Use lighter models for less important bots +2. Increase `interval:` to space requests +3. Distribute bots across multiple Ollama instances + +## Available Personality Prompts + +| Personality | Description | Best Model | +|-------------|-------------|------------| +| `helpful` | Shy, helpful Jimmy | llama3.2, mistral | +| `asshole` | Sarcastic, unfiltered | llama3.2:70b, gemini | +| `Ashley` | Adult content | llama3.2, gemini | +| `custom` | Template for custom prompts | Any | + +## Comparing Providers + +| Feature | Gemini | Ollama | +|---------|--------|--------| +| Cost | API cost | Free (local) | +| Latency | 200-500ms | 50-500ms (local) | +| Privacy | Cloud | 100% local | +| Multiple Servers | No | Yes | +| Model Choice | Limited | Any | +| Hardware | None Required | GPU Recommended | +| Offline | No | Yes | + +## Command Reference + +```bash +/msg botname ai # Change personality +/msg botname ai custom message # Use custom prompt +/msg wmantly load botname Ai # Reload AI with new config +``` \ No newline at end of file diff --git a/nodejs/STORAGE_SYSTEM.md b/nodejs/STORAGE_SYSTEM.md new file mode 100644 index 0000000..79acf32 --- /dev/null +++ b/nodejs/STORAGE_SYSTEM.md @@ -0,0 +1,923 @@ +# Storage/Trade Bot System Documentation + +## Overview + +A plugin-based storage and trading system for Minecraft bots. Any bot (`ez` or others) equipped with the StoragePlugin can: +- Automatically discover and track chests in a storage area +- Sort incoming items into shulker boxes +- Track full inventory metadata (NBT, enchantments, durability) +- Provide web API for inventory viewing (24/7) +- Handle deposit/withdraw via the `/trade` command + +**Key Design Principle**: The system is NOT hardcoded to any specific bot name. Any bot can be configured with the StoragePlugin via the plugin system. + +--- + +## Requirements + +### Functional Requirements + +#### 1. Chest Discovery +- Scan all chests within configurable render distance +- Automatically detect single vs double chests +- Assign row/column positions for organization +- No signs required - chests are positional + +#### 2. Storage Organization +- **Only shulker boxes stored in chests** - no loose items +- **One item type per shulker** - no mixing items in a shulker +- Automatic categorization of items (minerals, food, tools, etc.) +- Unlimited empty shulkers available from reserve + +#### 3. Trade Integration +- Use existing `/trade` command for all item transfers +- Max 12 slots per trade (server limitation) +- Deposit flow: player trades → bot sorts → items stored +- Withdraw flow: player requests → bot gathers → trade window + +#### 4. Database Persistence +- SQLite database for inventory tracking +- Database independent of bot being online +- Web API reads directly from database (24/7 availability) + +#### 5. Permission System +- Database-driven permissions (no file editing) +- Roles: `owner`, `team`, `readonly` +- Commands and web access limited by role + +#### 6. Web Interface +- Simple but fully functional UI +- Search inventory +- View item counts and locations +- Request withdrawals via web +- No login required (whisper challenge for auth) + +### Technical Requirements + +#### Stack +- Node.js + mineflayer (existing infrastructure) +- SQLite for database (via `sqlite3` npm package) +- Express for web API +- Minecraft server: CoreJourney (existing) + +#### Constraints +- Plain shulker boxes only (no dyes, no NBT names) +- Trade window max 12 slots +- Bot location is secret (no player access to chests) +- Web server must run 24/7 (separate from bot process) + +--- + +## Architecture + +### System Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ GAME LAYER │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ StorageBot │ (Optional) │ Other Bots │ │ +│ │ (e.g., ez) │ │ │ │ +│ │ - Scan │ │ - Proxy │ │ +│ │ - Store │ │ - Messages │ │ +│ │ - Trade │ │ │ │ +│ └──────┬──────┘ └─────────────┘ │ +│ │ │ +│ │ Commands, Trade Events │ +│ ▼ │ +│ ┌────────────────────────┐ │ +│ │ StoragePlugin │ ← ANY bot can use this │ +│ │ (Business Logic) │ via plugin system │ +│ └────────────┬───────────┘ │ +└────────────────┼──────────────────────────────────────────────┘ + │ +┌────────────────┼──────────────────────────────────────────────┐ +│ DATABASE LAYER │ +├─────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ permissions │ │ chests │ │ shulkers │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ shulker_items│ │ trades │ │ item_index │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└────────────────┼──────────────────────────────────────────────┘ + │ (SQLite: ./storage/storage.db) +┌────────────────┼──────────────────────────────────────────────┐ +│ WEB LAYER (24/7) │ +├─────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Express │────────▶│ Web UI │ │ +│ │ Server │ API │ (HTML/JS) │ │ +│ └──────────────┘ └──────────────┘ │ +│ ▲ │ +│ │ REST API │ +│ │ │ +│ /api/inventory, /api/chests, /api/withdraw, ... │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Plugin Structure (Bot-Agnostic) + +```javascript +// Configuration in conf/secrets.js +"mc": { + "bots": { + "ez": { + "plugins": { + "Storage": { + // Bot can be swapped anytime + } + } + }, + // Another bot can use Storage plugin: + "art": { + "plugins": { + "Storage": { + // Different location, same functionality + } + } + } + } +} +``` + +--- + +## Database Schema + +### Tables + +#### `permissions` +Manage access to the storage system. + +```sql +CREATE TABLE 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 +); +``` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | INTEGER | Primary key | +| `player_name` | TEXT | Minecraft username (unique) | +| `role` | TEXT | 'owner', 'team', or 'readonly' | +| `joined_at` | TIMESTAMP | When player was added | + +#### `chests` +Tracked chest blocks in storage area. + +```sql +CREATE TABLE 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, -- 1-4 (vertical) + column INTEGER NOT NULL, -- horizontal grouping + category TEXT, -- 'minerals', 'food', etc. + last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(pos_x, pos_y, pos_z) +); +``` + +#### `shulkers` +Shulker boxes stored in chests. + +```sql +CREATE TABLE shulkers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chest_id INTEGER NOT NULL, + slot INTEGER NOT NULL, -- 0-53 (single) or 0-107 (double) + shulker_type TEXT DEFAULT 'shulker_box', + category TEXT, -- 'minerals', 'tools', etc. + item_focus TEXT, -- Item type stored (e.g., 'diamond') + slot_count INTEGER DEFAULT 27, -- Used slots (1-27) + total_items INTEGER DEFAULT 0, -- Total item count + last_scan TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (chest_id) REFERENCES chests(id) ON DELETE CASCADE +); +``` + +#### `shulker_items` +Items inside shulker boxes. Enforces one item type per shulker. + +```sql +CREATE TABLE 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, -- 0-26 (shulker slots) + count INTEGER NOT NULL, + nbt_data TEXT, -- JSON: {enchantments: [...], damage: 5} + 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` +Trade history logs. + +```sql +CREATE TABLE trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_name TEXT NOT NULL, + action TEXT NOT NULL CHECK(action IN ('deposit', 'withdraw')), + items TEXT NOT NULL, -- JSON: [{name, count, nbt}, ...] + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +#### `pending_withdrawals` +Withdrawal requests from players (sync between web and in-game). + +```sql +CREATE TABLE 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` +Cached aggregated item counts for fast searches. + +```sql +CREATE TABLE 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, -- JSON: [{id, count}, ...] + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## File Structure + +``` +nodejs/ +├── controller/ +│ ├── storage/ +│ │ ├── index.js # StoragePlugin main class +│ │ ├── database.js # SQLite setup and all DB operations +│ │ ├── scanner.js # Chest discovery and scanning +│ │ ├── organizer.js # Item sorting and categorization +│ │ └── web.js # Express server (24/7 API) +│ ├── storage.js # Export for mc-bot.js plugin loading +│ └── commands/ +│ ├── default.js # Add: summon, dismiss commands +│ └── trade.js # Add StoragePlugin special handling +├── storage/ +│ ├── storage.db # SQLite database (automatically created) +│ └── public/ +│ ├── index.html # Web UI (single page) +│ ├── app.js # Frontend logic +│ └── style.css # Styling +├── conf/ +│ ├── base.js # Add storage config +│ └── secrets.js # DB path, permissions init +└── ... +``` + +--- + +## Component Specifications + +### 1. StoragePlugin (`controller/storage/index.js`) + +**Purpose**: Main plugin class that ties together database, scanner, organizer, and trade handling. + +**Constructor Arguments**: +- `bot`: The CJbot instance +- `dbFile`: Path to SQLite database +- `homePos`: Starting position (optional, auto-detect on first scan) + +**Key Methods**: +```javascript +class StoragePlugin { + constructor(args) { ... } + + async init() { + // Initialize database + // Register commands + // Start on bot 'onReady' + } + + async unload() { + // Clean up + } + + async scanArea(force = false) { + // Discover chests within render distance + // Update database + } + + async handleTrade(playerName, itemsReceived) { + // Process incoming trade items + // Sort into shulkers + // Update database + } + + async handleWithdrawRequest(playerName, itemId, count) { + // Gather items to OUTBOX shulker + // Mark as ready for pickup + } + + async organize() { + // Full re-sort (manual command) + } +} +``` + +### 2. Database Module (`controller/storage/database.js`) + +**Purpose**: All SQLite operations. + +**Key Functions**: +```javascript +// Initialize +async initialize(dbFile) + +// Permissions +async addPlayer(name, role = 'team') +async removePlayer(name) +async getPlayerRole(name) +async getAllPlayers() +async checkPermission(name, requiredRole) + +// Chests +async upsertChest(position, chestType) +async getChests() +async deleteOrphanChests() + +// Shulkers +async upsertShulker(chestId, slot, category, itemFocus) +async getShulkersByChest(chestId) +async findShulkerForItem(itemId) // Find shulker with same item and space +async createEmptyShulker(chestId, slot) +async updateShulkerCounts(shulkerId, slotCount, totalItems) + +// Shulker Items +async upsertShulkerItem(shulkerId, item) +async getShulkerItems(shulkerId) +async deleteShulkerItem(shulkerId, itemId) + +// Trades +async logTrade(playerName, action, items) +async getRecentTrades(limit = 50) + +// Pending Withdrawals +async queueWithdrawal(playerName, itemId, itemName, count) +async getPendingWithdrawals(playerName) +async updateWithdrawStatus(id, status) +async markCompletedWithdrawals(playerName) + +// Item Index +async updateItemCount(itemId, shulkerId, count) +async searchItems(query = null) +async getItemDetails(itemId) +``` + +### 3. Scanner Module (`controller/storage/scanner.js`) + +**Purpose**: Discover and scan chests/shulkers. + +**Key Functions**: +```javascript +async discoverChests(bot, radius) { + // Find all chest blocks within radius + // Detect single vs double + // Assign row/column based on position + // Return array of chest positions +} + +async scanChest(bot, chestPosition) { + // Open chest + // Read all slots + // Scan any shulkers found + // Update database +} + +async scanShulker(bot, chestSlot, chestPosition) { + // Click shulker to open + // Read all 27 slots + // Parse NBT data + // Return item array +} + +function detectChestType(position) { + // Check adjacent blocks to detect double chest + // Return 'single' or 'double' +} + +function assignRowColumn(position, minPos) { + // Calculate row from Y (1-4) + // Calculate column from X/Z + // Return {row, column} +} +``` + +### 4. Organizer Module (`controller/storage/organizer.js`) + +**Purpose**: Sort items into shulkers, categorize items. + +**Key Functions**: +```javascript +function categorizeItem(itemName) { + // Returns: 'minerals', 'food', 'tools', 'armor', 'blocks', 'redstone', 'misc' +} + +async sortItems(itemsDb, itemsToSort) { + // For each item: + // - Find shulker with same item AND space + // - If found: move to that shulker, consolidate stacks + // - If not found: create new shulker at category column + // Return: moves to execute +} + +function findCategoryColumn(category, row) { + // Map (category, row) to chest column + // Return column number +} + +async consolidateStacks(shulkerId) { + // Merge partial stacks + // Update database +} +``` + +### 5. Web Server (`controller/storage/web.js`) + +**Purpose**: Express API for 24/7 inventory access. + +**API Endpoints**: + +``` +GET /api/inventory +GET /api/inventory/:itemId +GET /api/chests +GET /api/chests/:id +GET /api/shulkers +GET /api/shulkers/:id +GET /api/stats +GET /api/trades?limit=50 +POST /api/withdraw +GET /api/pending/:playerName +POST /api/auth +``` + +**Detailed API Spec**: + +``` +GET /api/inventory +Response: { + items: [ + { + item_id: 1, + item_name: "diamond", + total_count: 2304, + locations: [ + {shulker_id: 1, count: 1728}, + {shulker_id: 5, count: 576} + ] + }, + ... + ] +} + +GET /api/inventory/:itemId +Response: { + item_id: 1, + item_name: "diamond", + total_count: 2304, + locations: [ + { + shulker_id: 1, + chest_id: 3, + chest_pos: {x: 100, y: 64, z: 200}, + count: 1728 + }, + ... + ] +} + +GET /api/chests +Response: { + chests: [ + { + id: 1, + pos_x: 100, pos_y: 64, pos_z: 200, + chest_type: "double", + row: 1, + column: 1, + category: "minerals", + shulker_count: 26 + }, + ... + ] +} + +GET /api/stats +Response: { + totalItems: 15432, + totalShulkers: 156, + totalChests: 24, + emptyShulkers: 12, + categories: { + minerals: 42, + food: 18, + tools: 24, + ... + }, + recentTrades: [ + {player: "wmantly", action: "deposit", item_count: 45, time: "..."} + ] +} + +POST /api/withdraw +Body: {player_name: "wmantly", item_id: 1, count: 64} +Response: {success: true, withdraw_id: 123} + +GET /api/pending/:playerName +Response: { + pending: [ + { + id: 123, + item_name: "diamond", + requested_count: 64, + status: "ready" + } + ] +} +``` + +### 6. Web UI (`storage/public/`) + +**index.html**: Single page application +- Search bar +- Filter by category +- Item list with counts +- Click to see details (shulker locations) +- "Request Withdraw" button (opens modal) +- Stats sidebar + +**app.js**: Frontend logic +- Fetch API calls +- Search/filter logic +- Withdraw request modal +- Auto-refresh pending withdrawals + +**style.css**: Simple, clean styling + +### 7. Modified Commands + +**default.js** - Add new commands: +```javascript +'summon': { + desc: 'Summon a bot online indefinitely', + allowed: ['owner'], + ignoreLock: true, + async function(from, botName) { ... } +}, +'dismiss': { + desc: 'Send a bot offline', + allowed: ['owner'], + ignoreLock: true, + async function(from, botName) { ... } +}, +``` + +**trade.js** - Add StoragePlugin handling: +```javascript +module.exports = { + '.trade': { + desc: 'Bot will take trade requests', + async function(from) { + // Check if bot has StoragePlugin + if (this.plunginsLoaded['Storage']) { + await this.plunginsLoaded['Storage'].handleTrade(from, ...); + } else { + // Original sign-based flow + let chestBlock = findChestBySign(this, from); + // ... + } + } + } +} +``` + +--- + +## Configuration + +### conf/base.js + +```javascript +"storage": { + // Database location + "dbPath": "./storage/storage.db", + + // Chest discovery + "scanRadius": 30, // Render distance + "homePos": null, // Auto-detect on first scan + + // Category mappings + "categories": { + "minerals": ["diamond", "netherite_ingot", "gold_ingot", "iron_ingot", + "copper_ingot", "emerald", "redstone", "lapis_lazuli"], + "food": ["bread", "cooked_porkchop", "steak", "golden_apple", "cooked_beef", + "cooked_chicken", "cooked_mutton", "carrot", "potato", "baked_potato"], + "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"], + "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", "cobblestone_stairs"], + "redstone": ["redstone", "repeater", "comparator", "piston", "sticky_piston", + "redstone_torch", "lever", "tripwire_hook"], + "misc": [] // Everything else falls here + }, + + // Special shulkers (for bookkeeping) + "inboxShulkerName": "INBOX", + "outboxShulkerName": "OUTBOX", + "newShulkersName": "EMPTY", + + // Web server + "webPort": 3000, + "webHost": "0.0.0.0" +} +``` + +### conf/secrets.js + +```javascript +"storage": { + // Database can override location + // "dbPath": "./storage/storage.db", + + // Default permissions (inserted on DB init) + "defaultPlayers": [ + {name: "wmantly", role: "owner"}, + {name: "useless666", role: "owner"}, + {name: "tux4242", role: "owner"}, + {name: "pi_chef", role: "team"}, + {name: "Ethan", role: "team"}, + {name: "Vince_NL", role: "team"} + ] +} +``` + +### Bot Configuration (Plugin-Based, Bot-Agnostic) + +```javascript +// conf/secrets.js - Example: ez bot +"mc": { + "bots": { + "ez": { + "username": "mc3@vm42.us", + "auth": "microsoft", + "commands": ['default'], + "autoConnect": false, + "plugins": { + "Storage": { + // Bot uses Storage plugin + } + } + }, + // Any bot can be moved to storage location: + "art": { + "username": "art@vm42.us", + "auth": "microsoft", + "plugins": { + "Storage": { + // Same plugin, different bot + } + } + } + } +} +``` + +--- + +## User Workflows + +### Deposit Flow (Player Perspective) + +1. **Player collects items** from farms/raids (max 12 slots due to trade window) +2. **Player types**: `/msg ez trade` or uses `/trade` command with ez +3. **Trade window opens** +4. **Player puts items** in their side of trade window +5. **Confirm trade** +6. **ez automatically**: + - Moves all items to INBOX shulker + - Categorizes each item + - Finds appropriate shulker (creates new if needed) + - Moves items to organized shulkers + - Updates database +7. **ez whispers**: `Received X items. Stored successfully.` +8. **Player can verify** on web: `http://server:3000` + +### Withdraw Flow (Player Perspective) + +**Option A: In-Game Only** +1. **Player types**: `/msg ez withdraw diamond 64` +2. **ez searches database** for diamond locations +3. **ez gathers items** to OUTBOX shulker +4. **ez whispers**: `Items ready for pickup. /trade with me.` +5. **Player trades** with ez +6. **ez moves items** from OUTBOX to trade window +7. **Confirm trade** +8. **ez updates database** + +**Option B: Web Request** +1. **Player visits** web: `http://server:3000` +2. **Finds item** and enters count +3. **Click "Withdraw"** +4. **Queues request** to database +5. **When ez is online**, processes pending requests +6. **ez whispers player**: `Your items are ready. /trade with me.` + +### Admin Workflow (Owner) + +``` +/msg ez summon # Bring bot online +/msg ez dismiss # Send bot offline +/msg ez scan # Force chest scan +/msg ez chests # List tracked chests +/msg ez organize # Force full re-sort +/msg ez status # Show storage stats +/msg ez addplayer # Add authorized player +/msg ez removeplayer # Remove player +/msg ez players # List authorized players +``` + +--- + +## Security Considerations + +### Database Security +- SQLite file permissions: Read/write by bot process only +- No direct SQL injection (parameterized queries throughout) +- Web API uses read-only connections for GET requests + +### Game Security +- Bot location kept secret (no `/tp` to chests allowed to team) +- Only trade window access for players +- No `/msg` command execution to other bots + +### Web Security +- No login required (simpler) +- Auth via whisper challenge code (6-digit code generated, whispered to player for verification) +- Rate limiting on API endpoints +- CORS restricted to same origin + +--- + +## Implementation Plan + +### Phase 1: Core Database and Plugin Structure +- Create `database.js` - SQLite setup and all queries +- Create `index.js` - StoragePlugin main class skeleton +- Initialize plugin in `mc-bot.js` via plugin system +- Test: Database creation, basic connectivity + +### Phase 2: Scanner +- Create `scanner.js` - Chest discovery and scanning +- Scan loop: Find chests → Detect single/double → Assign row/column +- Scan individual chests → Detect shulkers → Read NBT → Update DB +- Test: Scan a test chest area, verify DB entries + +### Phase 3: Organizer +- Create `organizer.js` - Categorization and sorting +- Implement `categorizeItem()` function +- Implement sort logic (find shulker, move items, create new if needed) +- Test: Sort items from INBOX to organized shulkers + +### Phase 4: Trade Integration +- Modify `trade.js` - Add StoragePlugin handling +- Implement `handleTrade()` in StoragePlugin +- Implement `handleWithdrawRequest()` in StoragePlugin +- Test: Deposit and withdraw via trade window + +### Phase 5: Commands +- Add `summon`, `dismiss` to `default.js` +- Add storage commands (`scan`, `status`, `chests`, `organize`) +- Add player management commands (`addplayer`, `removeplayer`, `players`) +- Test: All commands work with proper permissions + +### Phase 6: Web Server +- Create `web.js` - Express server +- Implement API endpoints +- Test: API returns correct data from database + +### Phase 7: Web UI +- Create `index.html` - Single page UI +- Create `app.js` - Frontend logic +- Create `style.css` - Styling +- Test: View inventory, search, request withdrawal + +### Phase 8: Integration Testing +- Full deposit flow end-to-end +- Full withdraw flow end-to-end +- Web + in-game sync +- Multiple trades in sequence +- Database persistence after bot restart + +### Phase 9: Documentation and Polish +- Update this doc with any changes +- Add inline code comments +- Error handling improvements +- Performance optimization if needed + +--- + +## Dependencies + +### New npm packages required: +``` +sqlite3 # SQLite database +express # Web server +cors # CORS handling (optional, for external API access) +``` + +Add to `package.json`: +```json +{ + "dependencies": { + "sqlite3": "^5.1.7", + "express": "^4.19.2", + "cors": "^2.8.5" + } +} +``` + +--- + +## Troubleshooting Guide + +### Database Issues +- **Database locked**: Ensure only one process writes at a time +- **Corruption**: Use `sqlite3 storage.db "PRAGMA integrity_check;"` to verify + +### Scan Issues +- **No chests found**: Check `homePos` is correct, or bot is in render distance +- **Double chest detection fails**: Ensure chests are properly adjacent + +### Trade Issues +- **Items not sorted**: Check INBOX shulker exists and has space +- **Withdraw fails**: Verify item exists in database with sufficient count + +### Web Issues +- **Can't access API**: Check `webHost` (use `0.0.0.0` for external access) +- **Database not found**: Ensure `dbPath` directory exists and has write permissions + +--- + +## Future Enhancements (Out of Scope for MVP) + +- [ ] Shulker color coding for categories (needs dye access, blocked by server rules) +- [ ] Custom shulker names via NBT (blocked by server rules) +- [ ] Partial shulker consolidation (current: one item type per shulker, no combining) +- [ ] Auto-trading system (bot initiates trades) +- [ ] Multi-location support (multiple storage areas) +- [ ] Real-time web updates via WebSocket +- [ ] Export inventory to CSV/JSON +- [ ] Integration with trading APIs (like BulbaStore) +- [ ] Recipe calculator (what can be crafted with current storage) +- [ ] Value estimation (diamond equivalent of all items) + +--- + +## Glossary + +| Term | Meaning | +|------|---------| +| **StoragePlugin** | The main plugin class that provides storage functionality to any bot | +| **INBOX shulker** | Temporary holding shulker for incoming trade items | +| **OUTBOX shulker** | Temporary holding shulker for items ready for withdrawal | +| **EMPTY shulkers** | Reserve stock of new shulker boxes | +| **One item type per shulker** | Each shulker stores only one item type (e.g., only diamonds) | +| **Category** | Item group (minerals, food, tools, armor, blocks, redstone, misc) | +| **Row/Column** | Chest positioning: Row (1-4, vertical), Column (horizontal grouping) | +| **Render distance** | Distance within which bot can see/click chests | \ No newline at end of file diff --git a/nodejs/conf/base.js b/nodejs/conf/base.js index b7600f5..532bd3e 100644 --- a/nodejs/conf/base.js +++ b/nodejs/conf/base.js @@ -14,7 +14,19 @@ module.exports = { "swing": {}, }, "ai":{ + // AI provider: 'gemini' (default) or 'ollama' + "provider": "gemini", + // Gemini API key (required if using gemini provider) "key": "", + // Ollama settings (only used if provider is 'ollama') + "baseUrl": "http://localhost:11434", + "model": "llama3.2", // Default for Ollama; for Gemini use gemini-2.0-flash-exp, etc. + "timeout": 30000, + // Generation settings (applies to both providers) + "temperature": 1, + "topP": 0.95, + "topK": 64, + "maxOutputTokens": 8192, "interval": 20, "prompts":{ "custom": (name, interval, currentPlayers, custom)=>` diff --git a/nodejs/controller/ai.js b/nodejs/controller/ai.js index 46ab7de..f85178a 100644 --- a/nodejs/controller/ai.js +++ b/nodejs/controller/ai.js @@ -1,16 +1,8 @@ 'use strict'; -const axios = require('axios'); - const conf = require('../conf'); const {sleep} = require('../utils'); - - -const { - GoogleGenerativeAI, - HarmCategory, - HarmBlockThreshold, -} = require("@google/generative-ai"); +const { ProviderFactory } = require('./ai/providers'); class Ai{ @@ -21,6 +13,18 @@ class Ai{ this.intervalLength = args.intervalLength || 30; this.intervalStop; this.messageListener; + this.provider = null; + + // Bot-specific AI config (overrides global config) + this.botConfig = args.botConfig || {}; + } + + // Get merged config: bot-specific settings override global settings + __getConfig() { + return { + ...conf.ai, // Global defaults + ...this.botConfig, // Bot-specific overrides + }; } async init(){ @@ -38,11 +42,10 @@ class Ai{ console.log(`Message ${type}: ${message.toString()}`) messages.push('>', message.toString()); }); - + this.intervalStop = setInterval(async ()=>{ let result; - // if(messages.length ===0) return; - + try{ result = await this.chat(JSON.stringify({ messages, currentTime:Date.now()+1} @@ -55,9 +58,9 @@ class Ai{ try{ messages = ['']; - if(!result.response.text()) return; + if(!this.provider.getResponse(result)) return; - for(let message of JSON.parse(result.response.text())){ + for(let message of JSON.parse(this.provider.getResponse(result))){ console.log('toSay', message.delay, message.text); if(message.text === '___') return; setTimeout(async (message)=>{ @@ -66,12 +69,16 @@ class Ai{ } }catch(error){ console.log('Error in AI message loop', error, result); - if(result || result.response || result.response.text()){ - console.log(result.response.text()) + try { + if(result && this.provider.getResponse(result)){ + console.log(this.provider.getResponse(result)) + } + } catch(e) { + // Ignore } } }, this.intervalLength*1000); - + }catch(error){ console.log('error in onReady', error); } @@ -83,107 +90,56 @@ class Ai{ clearInterval(this.intervalStop); this.intervalStop = undefined; } - this.messageListener(); - + if(this.messageListener){ + this.messageListener(); + } + if(this.provider){ + await this.provider.close(); + } return true; } - __settings(history){ - return { - generationConfig: { - temperature: 1, - topP: 0.95, - topK: 64, - maxOutputTokens: 8192, - responseMimeType: "application/json", - }, - safetySettings:[ - // See https://ai.google.dev/gemini-api/docs/safety-settings - { - category: HarmCategory.HARM_CATEGORY_HARASSMENT, - threshold: HarmBlockThreshold.BLOCK_NONE, - }, - { - category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: HarmBlockThreshold.BLOCK_NONE, - }, - { - category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, - threshold: HarmBlockThreshold.BLOCK_NONE, - }, - { - category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, - threshold: HarmBlockThreshold.BLOCK_NONE, - }, - ], - history: history || [ - { - role: "user", - parts: [ - {text: this.prompt}, - ], - }, - { - role: "model", - parts: [ - {text: "Chat stuff"}, - ], - } - ], - } - } - async start(history){ - const genAI = new GoogleGenerativeAI(conf.ai.key); + const config = this.__getConfig(); + let bulbaItems = {}; - const model = genAI.getGenerativeModel({ - model: "gemini-1.5-flash", + console.log(`${this.bot.name} AI config:`, { + provider: config.provider, + model: config.model, + promptName: this.promptName, + baseUrl: config.baseUrl, }); - let bulbaItems = await axios.get('https://webstore.bulbastore.uk/api/listings'); - bulbaItems = bulbaItems.data.listings.map(i=>i.listing_name); - - console.log('AI for prompts', conf.ai.prompts) - - this.prompt = conf.ai.prompts[this.promptName]( + const prompt = conf.ai.prompts[this.promptName]( this.bot.bot.entity.username, - conf.ai.interval, + config.interval, Object.values(this.bot.getPlayers()).map(player=>`<[${player.lvl}] ${player.username}>`).join('\n'), bulbaItems, this.prompCustom, ); - this.session = model.startChat({ - ...this.__settings(history), - // systemInstruction: this.prompt, + // Create the provider instance with merged config and prompt + this.provider = ProviderFactory.create({ + ...config, + prompt: prompt, }); + + await this.provider.start(history); + console.log(`${this.bot.name} AI ${config.provider} provider started (model: ${config.model})`); } async chat(message, retryCount=0){ - console.log('chat', retryCount) + console.log(`chat ${this.bot.name}`, retryCount) try{ - let result = await this.session.sendMessage(message); - + let result = await this.provider.chat(message); return result }catch(error){ console.log('AI chat error', error) - - if(retryCount > 3){ - console.log('hit retry count'); - return ; - }; - await sleep(500); - this.session.params.history.pop(); - this.start(this.session.params.history); - return await this.chat(message, retryCount++) + throw error; } } } - - - -module.exports = Ai; -// run(); \ No newline at end of file +module.exports = Ai; \ No newline at end of file diff --git a/nodejs/controller/ai/providers/gemini.js b/nodejs/controller/ai/providers/gemini.js new file mode 100644 index 0000000..d21aadf --- /dev/null +++ b/nodejs/controller/ai/providers/gemini.js @@ -0,0 +1,88 @@ +'use strict'; + +const { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } = require("@google/generative-ai"); + +class GeminiProvider { + constructor(config) { + this.config = config; + this.session = null; + } + + async start(history) { + const genAI = new GoogleGenerativeAI(this.config.key); + const model = genAI.getGenerativeModel({ + model: this.config.model || "gemini-2.0-flash-exp", + }); + + this.session = model.startChat(this.__settings(history)); + } + + __settings(history) { + return { + generationConfig: { + temperature: this.config.temperature || 1, + topP: this.config.topP || 0.95, + topK: this.config.topK || 64, + maxOutputTokens: this.config.maxOutputTokens || 8192, + responseMimeType: "application/json", + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + ], + history: history || [ + { + role: "user", + parts: [{ text: this.config.prompt }], + }, + { + role: "model", + parts: [{ text: "Chat stuff" }], + }, + ], + }; + } + + async chat(message, retryCount = 0) { + try { + let result = await this.session.sendMessage(message); + return result; + } catch (error) { + if (retryCount > 3) { + throw new Error(`Gemini API error after ${retryCount} retries: ${error.message}`); + } + // Recover by removing last history entry and restarting + this.session.params.history.pop(); + await this.start(this.session.params.history); + return await this.chat(message, retryCount + 1); + } + } + + setPrompt(prompt) { + this.config.prompt = prompt; + } + + getResponse(result) { + return result.response.text(); + } + + async close() { + this.session = null; + } +} + +module.exports = GeminiProvider; \ No newline at end of file diff --git a/nodejs/controller/ai/providers/index.js b/nodejs/controller/ai/providers/index.js new file mode 100644 index 0000000..072da14 --- /dev/null +++ b/nodejs/controller/ai/providers/index.js @@ -0,0 +1,25 @@ +'use strict'; + +const GeminiProvider = require('./gemini'); +const OllamaProvider = require('./ollama'); + +class ProviderFactory { + static create(config) { + const provider = config.provider || 'gemini'; + + switch (provider.toLowerCase()) { + case 'gemini': + return new GeminiProvider(config); + case 'ollama': + return new OllamaProvider(config); + default: + throw new Error(`Unknown AI provider: ${provider}. Supported: 'gemini', 'ollama'`); + } + } +} + +module.exports = { + ProviderFactory, + GeminiProvider, + OllamaProvider +}; \ No newline at end of file diff --git a/nodejs/controller/ai/providers/ollama.js b/nodejs/controller/ai/providers/ollama.js new file mode 100644 index 0000000..9fb7574 --- /dev/null +++ b/nodejs/controller/ai/providers/ollama.js @@ -0,0 +1,108 @@ +'use strict'; + +const axios = require('axios'); + +class OllamaProvider { + constructor(config) { + this.config = config; + this.baseUrl = config.baseUrl || 'http://localhost:11434'; + this.model = config.model || 'llama3.2'; + this.messages = []; + } + + async start(history) { + // Convert Gemini-style history to Ollama format if needed + this.messages = history || []; + + if (this.config.prompt) { + console.log('Ollama provider initialized with model:', this.model); + } + } + + __settings() { + return { + temperature: this.config.temperature || 1, + top_p: this.config.topP || 0.95, + top_k: this.config.topK || 64, + num_predict: this.config.maxOutputTokens || 8192, + }; + } + + async chat(message, retryCount = 0) { + try { + // Build conversation from prompt + history + const messages = [ + { + role: 'system', + content: this.config.prompt || 'You are a helpful assistant.' + }, + ...this.messages.map(msg => ({ + role: msg.role === 'model' ? 'assistant' : 'user', + content: msg.parts ? msg.parts.map(p => p.text).join('') : (msg.content || '') + })), + { + role: 'user', + content: message + } + ]; + + const response = await axios.post( + `${this.baseUrl}/api/chat`, + { + model: this.model, + messages: messages, + stream: false, + format: 'json', // Request JSON response + options: this.__settings() + }, + { + timeout: this.config.timeout || 30000, + headers: { + 'Content-Type': 'application/json' + } + } + ); + + // Update history + this.messages.push({ + role: 'user', + parts: [{ text: message }], + content: message + }); + + this.messages.push({ + role: 'model', + parts: [{ text: response.data.message.content }], + content: response.data.message.content + }); + + // Return in a format compatible with the Ai class + return { + response: { + text: () => response.data.message.content + } + }; + } catch (error) { + if (retryCount > 3) { + throw new Error(`Ollama API error after ${retryCount} retries: ${error.message}`); + } + // Retry after delay + await new Promise(resolve => setTimeout(resolve, 500 * (retryCount + 1))); + return await this.chat(message, retryCount + 1); + } + } + + setPrompt(prompt) { + this.config.prompt = prompt; + } + + getResponse(result) { + return result.response.text(); + } + + async close() { + this.messages = []; + } +} + +module.exports = OllamaProvider; \ No newline at end of file diff --git a/nodejs/controller/craft.js b/nodejs/controller/craft.js index 770330a..a346168 100644 --- a/nodejs/controller/craft.js +++ b/nodejs/controller/craft.js @@ -138,16 +138,24 @@ class Craft{ // Move these into openCrating function let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve)); - let inventory = ()=> window.slots.slice(window.inventoryStart, window.inventoryEnd) + let inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd); // Move the items into the crafting grid + // Keep track of used inventory slots to avoid reusing the same slot + let usedInventorySlots = new Set(); let slotCount = 1; for(let shapeRow of recipe.inShape){ for(let shape of shapeRow){ - this.bot.bot.moveSlotItem( - inventory().find((element)=> element && element.type === shape.id).slot, - slotCount + let inventorySlot = inventory.findIndex((element, index) => + element && element.type === shape.id && !usedInventorySlots.has(index) ); + if (inventorySlot === -1) { + throw new Error(`Not enough items of type ${shape.id} in inventory`); + } + let actualSlot = window.inventoryStart + inventorySlot; + usedInventorySlots.add(inventorySlot); + + this.bot.bot.moveSlotItem(actualSlot, slotCount); await windowOnce(`updateSlot:${slotCount}`); slotCount++; } diff --git a/nodejs/controller/craft_chests.js b/nodejs/controller/craft_chests.js index d6fbc93..ebb7b09 100644 --- a/nodejs/controller/craft_chests.js +++ b/nodejs/controller/craft_chests.js @@ -152,16 +152,24 @@ class CraftChests{ // Move these into openCrating function let windowOnce = (event)=> new Promise((resolve, reject)=> window.once(event, resolve)); - let inventory = ()=> window.slots.slice(window.inventoryStart, window.inventoryEnd) + let inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd); // Move the items into the crafting grid + // Keep track of used inventory slots to avoid reusing the same slot + let usedInventorySlots = new Set(); let slotCount = 1; for(let shapeRow of recipe.inShape){ for(let shape of shapeRow){ - this.bot.bot.moveSlotItem( - inventory().find((element)=> element && element.type === shape.id).slot, - slotCount + let inventorySlot = inventory.findIndex((element, index) => + element && element.type === shape.id && !usedInventorySlots.has(index) ); + if (inventorySlot === -1) { + throw new Error(`Not enough items of type ${shape.id} in inventory`); + } + let actualSlot = window.inventoryStart + inventorySlot; + usedInventorySlots.add(inventorySlot); + + this.bot.bot.moveSlotItem(actualSlot, slotCount); await windowOnce(`updateSlot:${slotCount}`); slotCount++; }