craft fix

This commit is contained in:
2026-01-31 18:49:08 -05:00
parent 80bbfbe58e
commit 9acd38c94b
9 changed files with 1535 additions and 102 deletions

305
nodejs/OLLAMA_SETUP.md Normal file
View File

@@ -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": "<configure in conf/secrets.js>",
// 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 <personality> # Change personality
/msg botname ai <personality> custom message # Use custom prompt
/msg wmantly load botname Ai <personality> # Reload AI with new config
```

923
nodejs/STORAGE_SYSTEM.md Normal file
View File

@@ -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 <name> # Add authorized player
/msg ez removeplayer <name> # 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 |

View File

@@ -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": "<configure in secrets>",
// 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)=>`

View File

@@ -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();
module.exports = Ai;

View File

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

View File

@@ -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
};

View File

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

View File

@@ -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++;
}

View File

@@ -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++;
}