import express from 'express'; import { WebSocketServer } from 'ws'; import cors from 'cors'; import { createServer } from 'http'; import * as db from './database.js'; import { Turtle } from './Turtle.js'; const app = express(); const PORT = 3001; const WS_PORT = 3002; app.use(cors()); app.use(express.json({ limit: '5mb' })); // Initialize database db.initializeDatabase(); // Load persisted data from database console.log('๐Ÿ“‚ Loading persisted data from database...'); const savedHomes = db.getAllTurtleHomes(); const savedBlocks = db.getWorldBlocks(); console.log(` Loaded ${savedHomes.length} turtle homes`); console.log(` Loaded ${savedBlocks.length} world blocks`); // Store connected web clients and turtle data const webClients = new Set(); const turtles = new Map(); // turtleID -> Turtle instance const worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, timestamp} const turtleHomes = new Map(); // turtleID -> {x, y, z} home position const turtleConfig = new Map(); // turtleID -> {maxDistance, facing, etc} // Legacy compat: turtleData getter (returns serialized Turtle data) const turtleData = { has(id) { return turtles.has(id); }, get(id) { const t = turtles.get(id); return t ? t.toJSON() : undefined; }, set(id, _val) { /* no-op, use getOrCreateTurtle */ }, delete(id) { return turtles.delete(id); }, get size() { return turtles.size; }, entries() { return Array.from(turtles.entries()).map(([id, t]) => [id, t.toJSON()])[Symbol.iterator](); }, values() { return Array.from(turtles.values()).map(t => t.toJSON())[Symbol.iterator](); }, keys() { return turtles.keys(); }, }; // Load saved homes into memory for (const home of savedHomes) { turtleHomes.set(home.turtle_id, { x: home.x, y: home.y, z: home.z }); } // Load saved blocks into memory for (const block of savedBlocks) { const key = `${block.x},${block.y},${block.z}`; worldBlocks.set(key, { name: block.block_name, metadata: block.metadata, discoveredBy: block.discovered_by, timestamp: block.discovered_at }); } // Timeout for considering turtles offline (30 seconds) const TURTLE_TIMEOUT = 30000; // Broadcast to all web clients function broadcastToClients(data) { const message = JSON.stringify(data); webClients.forEach((client) => { if (client.readyState === 1) { // OPEN client.send(message); } }); } // ========== Turtle Instance Management ========== /** * Get or create a Turtle instance for the given ID. * Wires up event handlers for the state machine and command forwarding. */ function getOrCreateTurtle(turtleID) { if (turtles.has(turtleID)) { return turtles.get(turtleID); } console.log(`โœจ Creating Turtle instance for #${turtleID}`); const turtle = new Turtle(turtleID, { worldBlocks, broadcastToClients, storeBlock, db, }); // Restore home position from DB const savedHome = turtleHomes.get(turtleID); if (savedHome) { turtle.homePosition = savedHome; } // ---- Event handlers ---- // Forward eval commands to webbridge via pending command queue turtle.on('sendCommand', (command) => { // Queue eval command for webbridge to poll turtle.pendingLegacyCommands.push({ ...command, timestamp: Date.now(), }); }); // Store discovered blocks turtle.on('blocksDiscovered', (blocks) => { for (const block of blocks) { storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata }, block.discoveredBy); } // Broadcast blocks to web clients broadcastToClients({ type: 'blocks_discovered', blocks: blocks.map(b => ({ x: b.x, y: b.y, z: b.z, name: b.name, metadata: b.metadata, discoveredBy: b.discoveredBy, timestamp: Date.now(), })), }); }); // Broadcast turtle updates to web clients turtle.on('update', (data) => { broadcastToClients({ type: 'turtle_update', turtle: data, }); }); // Handle turtle disconnect turtle.on('disconnect', (id) => { broadcastToClients({ type: 'turtle_removed', turtleID: id, }); }); turtles.set(turtleID, turtle); return turtle; } // Cleanup stale turtles periodically setInterval(() => { const now = Date.now(); let removedCount = 0; for (const [turtleID, turtle] of turtles.entries()) { if (now - turtle.lastUpdate > TURTLE_TIMEOUT) { console.log(`๐Ÿ”Œ Turtle ${turtleID} timed out (last seen ${Math.floor((now - turtle.lastUpdate) / 1000)}s ago)`); turtle.disconnect(); turtles.delete(turtleID); removedCount++; } } if (removedCount > 0) { console.log(`๐Ÿงน Cleaned up ${removedCount} offline turtle(s)`); } }, 10000); // Check every 10 seconds // Helper to store discovered blocks function storeBlock(x, y, z, blockData, turtleID) { const key = `${x},${y},${z}`; worldBlocks.set(key, { ...blockData, discoveredBy: turtleID, timestamp: Date.now() }); // Persist to database db.saveWorldBlock(x, y, z, blockData.name, blockData.metadata, turtleID); } // Helper to calculate block position based on turtle position and facing function getBlockPosition(turtlePos, facing, direction) { if (!turtlePos) return null; const pos = { ...turtlePos }; if (direction === 'up') { pos.y += 1; } else if (direction === 'down') { pos.y -= 1; } else if (direction === 'forward') { if (facing === 0) pos.z -= 1; // North else if (facing === 1) pos.x += 1; // East else if (facing === 2) pos.z += 1; // South else if (facing === 3) pos.x -= 1; // West } return pos; } // Create HTTP server const server = createServer(app); // WebSocket server for web clients const wss = new WebSocketServer({ port: WS_PORT }); console.log(`๐Ÿš€ Turtle Control Server starting...`); console.log(`๐Ÿ“ก HTTP Server: http://localhost:${PORT}`); console.log(`๐Ÿ”Œ WebSocket Server: ws://localhost:${WS_PORT}`); // WebSocket connection handler wss.on('connection', (ws) => { console.log('๐ŸŒ New web client connected'); webClients.add(ws); // Send current turtle data and world blocks to new client const blocks = []; for (const [key, blockData] of worldBlocks.entries()) { const [x, y, z] = key.split(',').map(Number); blocks.push({ x, y, z, ...blockData }); } ws.send(JSON.stringify({ type: 'initial_state', turtles: Array.from(turtles.values()).map(t => t.toJSON()), blocks: blocks })); ws.on('message', (message) => { try { const data = JSON.parse(message); console.log('๐Ÿ“จ Received from web client:', data); // Handle commands from web client if (data.type === 'command') { const turtleID = data.turtleID; const turtle = turtles.get(turtleID); if (turtle) { // Check for state change commands if (data.command === 'set_state' || data.command === 'setState') { const stateName = data.param?.state || data.param; const stateData = data.param?.data || {}; turtle.setState(stateName, stateData); console.log(`โœ… State set for turtle ${turtleID}: ${stateName}`); } else { // Legacy command - queue for webbridge polling turtle.pendingLegacyCommands.push({ command: data.command, param: data.param, timestamp: Date.now() }); console.log(`โœ… Command queued for turtle ${turtleID}: ${data.command}`, data.param ? `(param: ${JSON.stringify(data.param)})` : ''); } } else { console.log(`โŒ Turtle ${turtleID} not found`); console.log(` Available turtles:`, Array.from(turtles.keys())); } } } catch (error) { console.error('โŒ Error processing web client message:', error); } }); ws.on('close', () => { console.log('๐Ÿ‘‹ Web client disconnected'); webClients.delete(ws); }); ws.on('error', (error) => { console.error('โŒ WebSocket error:', error); }); }); // REST API endpoint for turtles to update their status app.post('/api/turtle/update', (req, res) => { try { const turtleUpdate = req.body; const turtleID = turtleUpdate.turtleID; console.log(`๐Ÿข Status update from Turtle ${turtleID}`); // Get or create Turtle instance const turtle = getOrCreateTurtle(turtleID); // Update turtle from status data turtle.updateFromStatus(turtleUpdate); // Server's home takes precedence const serverHome = turtleHomes.get(turtleID); if (serverHome) { turtle.homePosition = serverHome; } // If turtle reports a home but server doesn't have one, store it if (turtleUpdate.homePosition && !serverHome) { turtleHomes.set(turtleID, turtleUpdate.homePosition); turtle.homePosition = turtleUpdate.homePosition; console.log(` ๐Ÿ“ Stored home position for turtle ${turtleID}:`, turtleUpdate.homePosition); } // Store discovered blocks in world map if (turtleUpdate.surroundings && turtleUpdate.position && turtleUpdate.facing !== undefined) { for (const [direction, blockData] of Object.entries(turtleUpdate.surroundings)) { const blockPos = getBlockPosition(turtleUpdate.position, turtleUpdate.facing, direction); if (blockPos) { storeBlock(blockPos.x, blockPos.y, blockPos.z, blockData, turtleID); } } } // Broadcast to web clients broadcastToClients({ type: 'turtle_update', turtle: turtle.toJSON() }); // Send back server-authoritative data res.json({ success: true, homePosition: turtleHomes.get(turtleID), config: turtleConfig.get(turtleID) || {} }); } catch (error) { console.error('โŒ Error processing turtle update:', error); res.status(500).json({ error: error.message }); } }); // Endpoint for webbridge to poll for commands (includes eval + legacy) app.get('/api/turtle/:id/commands', (req, res) => { try { const turtleID = parseInt(req.params.id); const turtle = turtles.get(turtleID); if (turtle) { // Combine eval commands + legacy commands const commands = turtle.pendingLegacyCommands || []; // Clean up old commands (older than 30 seconds) const now = Date.now(); turtle.pendingLegacyCommands = commands.filter(cmd => (now - cmd.timestamp) < 30000); if (turtle.pendingLegacyCommands.length > 0) { console.log(`๐Ÿ“ค Sending ${turtle.pendingLegacyCommands.length} command(s) to turtle ${turtleID}`); turtle.pendingLegacyCommands.forEach(cmd => { if (cmd.type === 'eval') { console.log(` - EVAL ${(cmd.uuid || '').substring(0, 8)}`); } else { console.log(` - ${cmd.command}`, cmd.param ? `(${JSON.stringify(cmd.param)})` : ''); } }); } res.json({ commands: turtle.pendingLegacyCommands }); } else { res.json({ commands: [] }); } } catch (error) { console.error('โŒ Error getting commands:', error); res.status(500).json({ error: error.message }); } }); // Acknowledge commands were delivered (clears them) app.post('/api/turtle/:id/commands/ack', (req, res) => { try { const turtleID = parseInt(req.params.id); const turtle = turtles.get(turtleID); if (turtle) { const clearedCount = (turtle.pendingLegacyCommands || []).length; if (clearedCount > 0) { console.log(`โœ… Turtle ${turtleID} acknowledged ${clearedCount} command(s)`); turtle.pendingLegacyCommands = []; } res.json({ success: true, cleared: clearedCount }); } else { res.status(404).json({ error: 'Turtle not found' }); } } catch (error) { console.error('โŒ Error acknowledging commands:', error); res.status(500).json({ error: error.message }); } }); // Receive real-time events from turtles (inventory, peripherals) app.post('/api/turtle/event', (req, res) => { try { const { turtleID, type, message } = req.body; if (!turtleID || !type) { return res.status(400).json({ error: 'Missing turtleID or type' }); } const turtle = turtles.get(turtleID); if (turtle) { turtle.handleEvent(type, message); console.log(`๐Ÿ“ก Event from T#${turtleID}: ${type}`); } res.json({ success: true }); } catch (error) { console.error('โŒ Error processing turtle event:', error); res.status(500).json({ error: error.message }); } }); // Get all turtles app.get('/api/turtles', (req, res) => { res.json({ turtles: Array.from(turtles.values()).map(t => t.toJSON()) }); }); // Set home position for a turtle app.post('/api/turtle/:id/home', (req, res) => { try { const turtleID = parseInt(req.params.id); const { position } = req.body; if (!position || position.x === undefined || position.y === undefined || position.z === undefined) { return res.status(400).json({ error: 'Invalid position' }); } turtleHomes.set(turtleID, position); console.log(`๐Ÿ“ Set home for turtle ${turtleID}:`, position); // Persist to database db.saveTurtleHome(turtleID, position); // Update turtle instance const turtle = turtles.get(turtleID); if (turtle) { turtle.homePosition = position; // Broadcast update broadcastToClients({ type: 'turtle_update', turtle: turtle.toJSON() }); } res.json({ success: true, homePosition: position }); } catch (error) { console.error('โŒ Error setting home:', error); res.status(500).json({ error: error.message }); } }); // Get turtle home position app.get('/api/turtle/:id/home', (req, res) => { try { const turtleID = parseInt(req.params.id); const home = turtleHomes.get(turtleID); res.json({ success: true, homePosition: home || null }); } catch (error) { console.error('โŒ Error getting home:', error); res.status(500).json({ error: error.message }); } }); // Get world blocks for map visualization app.get('/api/world/blocks', (req, res) => { const blocks = []; for (const [key, blockData] of worldBlocks.entries()) { const [x, y, z] = key.split(',').map(Number); blocks.push({ x, y, z, ...blockData }); } res.json({ blocks }); }); // Store a discovered block app.post('/api/world/blocks', (req, res) => { try { const { x, y, z, blockName, metadata, discoveredBy } = req.body; if (x === undefined || y === undefined || z === undefined || !blockName) { return res.status(400).json({ error: 'Missing required fields: x, y, z, blockName' }); } const blockData = { name: blockName, metadata: metadata || 0 }; storeBlock(x, y, z, blockData, discoveredBy); // Broadcast to web clients broadcastToClients({ type: 'block_discovered', block: { x, y, z, name: blockName, metadata: metadata || 0, discoveredBy, timestamp: Date.now() } }); res.json({ success: true }); } catch (error) { console.error('โŒ Error storing block:', error); res.status(500).json({ error: error.message }); } }); // Send command to turtle app.post('/api/turtle/:id/command', (req, res) => { try { const turtleID = parseInt(req.params.id); const { command, param } = req.body; const turtle = turtles.get(turtleID); if (turtle) { // Check for state change commands if (command === 'set_state' || command === 'setState') { const stateName = param?.state || param; const stateData = param?.data || {}; turtle.setState(stateName, stateData); console.log(`๐Ÿ“ค State set for turtle ${turtleID}: ${stateName}`); } else { // Legacy command - queue for webbridge turtle.pendingLegacyCommands.push({ command, param, timestamp: Date.now() }); console.log(`๐Ÿ“ค Command queued for turtle ${turtleID}:`, command); } res.json({ success: true }); } else { res.status(404).json({ error: 'Turtle not found' }); } } catch (error) { console.error('โŒ Error sending command:', error); res.status(500).json({ error: error.message }); } }); // ========== EVAL PROTOCOL ENDPOINTS ========== // Receive eval response from webbridge (turtle -> webbridge -> server) app.post('/api/turtle/eval-response', (req, res) => { try { const { turtleID, uuid, result, error } = req.body; if (!turtleID || !uuid) { return res.status(400).json({ error: 'Missing turtleID or uuid' }); } const turtle = turtles.get(turtleID); if (turtle) { const handled = turtle.handleResponse(uuid, result, error); if (handled) { console.log(`๐Ÿ“ฉ Eval response from T#${turtleID} uuid:${uuid.substring(0, 8)} - resolved`); } else { console.log(`๐Ÿ“ฉ Eval response from T#${turtleID} uuid:${uuid.substring(0, 8)} - no matching pending command`); } } else { console.log(`๐Ÿ“ฉ Eval response from unknown turtle ${turtleID}`); } res.json({ success: true }); } catch (error) { console.error('โŒ Error processing eval response:', error); res.status(500).json({ error: error.message }); } }); // Set turtle state (state machine control) app.post('/api/turtle/:id/state', (req, res) => { try { const turtleID = parseInt(req.params.id); const { state, data } = req.body; if (!state) { return res.status(400).json({ error: 'Missing state name' }); } const turtle = turtles.get(turtleID); if (turtle) { turtle.setState(state, data || {}); console.log(`๐Ÿ”„ State set for turtle ${turtleID}: ${state}`); res.json({ success: true, state: turtle.stateName, description: turtle.stateDescription }); } else { res.status(404).json({ error: 'Turtle not found' }); } } catch (error) { console.error('โŒ Error setting state:', error); res.status(500).json({ error: error.message }); } }); // Get turtle state app.get('/api/turtle/:id/state', (req, res) => { try { const turtleID = parseInt(req.params.id); const turtle = turtles.get(turtleID); if (turtle) { res.json({ state: turtle.stateName, description: turtle.stateDescription, turtleID, }); } else { res.status(404).json({ error: 'Turtle not found' }); } } catch (error) { res.status(500).json({ error: error.message }); } }); // Execute Lua code on a turtle directly (for debugging/admin) app.post('/api/turtle/:id/exec', async (req, res) => { try { const turtleID = parseInt(req.params.id); const { code, timeout } = req.body; if (!code) { return res.status(400).json({ error: 'Missing code' }); } const turtle = turtles.get(turtleID); if (!turtle) { return res.status(404).json({ error: 'Turtle not found' }); } const result = await turtle.exec(code, timeout || 30000); res.json({ success: true, result }); } catch (error) { console.error(`โŒ Exec error for turtle ${req.params.id}:`, error.message); res.json({ success: false, error: error.message }); } }); // Bulk blocks discovery (from webbridge batch uploads) app.post('/api/world/blocks/batch', (req, res) => { try { const { blocks, turtleID } = req.body; if (!Array.isArray(blocks)) { return res.status(400).json({ error: 'blocks must be an array' }); } let stored = 0; for (const block of blocks) { if (block.x !== undefined && block.y !== undefined && block.z !== undefined && block.name) { storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata || 0 }, turtleID || block.discoveredBy); stored++; } } // Broadcast to web clients if (stored > 0) { broadcastToClients({ type: 'blocks_discovered', blocks: blocks.filter(b => b.x !== undefined && b.name).map(b => ({ x: b.x, y: b.y, z: b.z, name: b.name, metadata: b.metadata || 0, discoveredBy: turtleID || b.discoveredBy, timestamp: Date.now(), })), }); } res.json({ success: true, stored }); } catch (error) { console.error('โŒ Error batch storing blocks:', error); res.status(500).json({ error: error.message }); } }); // ========== PATH RECORDING ENDPOINTS ========== // Save a recorded path app.post('/api/paths', (req, res) => { try { const { turtleId, pathName, pathData } = req.body; const pathId = db.savePath(turtleId, pathName, pathData || []); res.json({ success: true, pathId, message: 'Path saved' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all paths (optionally filtered by turtleId) app.get('/api/paths', (req, res) => { try { const paths = db.getPaths(); res.json(paths.map(p => ({ pathId: p.id, turtleId: p.turtle_id, name: p.path_name, pathData: p.path_data, waypointCount: Array.isArray(p.path_data) ? p.path_data.length : 0, createdAt: p.created_at, updatedAt: p.updated_at }))); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get paths for a specific turtle app.get('/api/paths/turtle/:turtleId', (req, res) => { try { const turtleId = parseInt(req.params.turtleId); const paths = db.getPaths(turtleId); res.json(paths.map(p => ({ pathId: p.id, turtleId: p.turtle_id, name: p.path_name, pathData: p.path_data, waypointCount: Array.isArray(p.path_data) ? p.path_data.length : 0, createdAt: p.created_at, updatedAt: p.updated_at }))); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get a specific path with full details app.get('/api/paths/:pathId', (req, res) => { try { const pathId = parseInt(req.params.pathId); const path = db.getPath(pathId); if (!path) { return res.status(404).json({ error: 'Path not found' }); } res.json({ pathId: path.id, turtleId: path.turtle_id, name: path.path_name, waypoints: path.path_data, createdAt: path.created_at, updatedAt: path.updated_at }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete a path app.delete('/api/paths/:pathId', (req, res) => { try { const pathId = parseInt(req.params.pathId); db.deletePath(pathId); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ========== TASK QUEUE ENDPOINTS ========== // Create a new task app.post('/api/tasks', (req, res) => { try { const { taskType, priority, assignedTurtleId, parameters } = req.body; const taskData = parameters || {}; const taskId = db.createTask(taskType, taskData, priority || 0, assignedTurtleId || null); broadcastToClients({ type: 'task_created', taskId, taskType, priority }); res.json({ success: true, taskId }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all tasks (with optional status filter) app.get('/api/tasks', (req, res) => { try { const status = req.query.status || null; const rawTasks = db.getAllTasks(status); const tasks = rawTasks.map(t => ({ taskId: t.id, taskType: t.task_type, priority: t.priority, status: t.status, assignedTurtleId: t.assigned_turtle_id, parameters: t.task_data, result: t.task_data?.result || null, createdAt: t.created_at, updatedAt: t.updated_at })); res.json(tasks); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get next available task app.get('/api/tasks/next', (req, res) => { try { const task = db.getNextTask(); res.json({ task }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Update task status app.put('/api/tasks/:taskId', (req, res) => { try { const taskId = parseInt(req.params.taskId); const { status, result } = req.body; db.updateTaskStatus(taskId, status, result); broadcastToClients({ type: 'task_updated', taskId, status }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete a task app.delete('/api/tasks/:taskId', (req, res) => { try { const taskId = parseInt(req.params.taskId); db.deleteTask(taskId); broadcastToClients({ type: 'task_deleted', taskId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Assign task to turtle app.post('/api/tasks/:taskId/assign', (req, res) => { try { const taskId = parseInt(req.params.taskId); const { turtleId } = req.body; db.assignTask(taskId, turtleId); broadcastToClients({ type: 'task_assigned', taskId, turtleId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Complete a task app.post('/api/tasks/:taskId/complete', (req, res) => { try { const taskId = parseInt(req.params.taskId); db.completeTask(taskId); broadcastToClients({ type: 'task_completed', taskId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ========== MINING AREA ENDPOINTS ========== // Helper to format mining area for API response function formatMiningArea(area) { return { areaID: area.id, turtleID: area.turtle_id, startX: area.min_x, startY: area.min_y, startZ: area.min_z, endX: area.max_x, endY: area.max_y, endZ: area.max_z, status: area.status, createdAt: area.created_at, updatedAt: area.updated_at }; } // Save a mining area app.post('/api/mining-areas', (req, res) => { try { const { turtleId, turtleID, bounds, startX, startY, startZ, endX, endY, endZ, areaName, status } = req.body; const tid = turtleId || turtleID; // Support both {turtleId, bounds} and flat {startX,startY,...} formats const areaBounds = bounds || { minX: Math.min(Number(startX), Number(endX)), minY: Math.min(Number(startY), Number(endY)), minZ: Math.min(Number(startZ), Number(endZ)), maxX: Math.max(Number(startX), Number(endX)), maxY: Math.max(Number(startY), Number(endY)), maxZ: Math.max(Number(startZ), Number(endZ)) }; db.saveMiningArea(tid, areaBounds, areaName || null, status || 'planned'); const areas = db.getMiningAreas(); broadcastToClients({ type: 'mining_areas_updated', areas: areas.map(formatMiningArea) }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all mining areas app.get('/api/mining-areas', (req, res) => { try { const areas = db.getMiningAreas(); res.json(areas.map(formatMiningArea)); } catch (error) { res.status(500).json({ error: error.message }); } }); // Update mining area status app.put('/api/mining-areas/:areaId', (req, res) => { try { const areaId = parseInt(req.params.areaId); const { status } = req.body; db.updateMiningAreaStatus(areaId, status); const areas = db.getMiningAreas(); broadcastToClients({ type: 'mining_areas_updated', areas: areas.map(formatMiningArea) }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete a mining area app.delete('/api/mining-areas/:areaId', (req, res) => { try { const areaId = parseInt(req.params.areaId); db.deleteMiningArea(areaId); const areas = db.getMiningAreas(); broadcastToClients({ type: 'mining_areas_updated', areas: areas.map(formatMiningArea) }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Close a mining area (legacy endpoint) app.post('/api/mining-areas/:areaId/close', (req, res) => { try { const areaId = parseInt(req.params.areaId); db.closeMiningArea(areaId); const areas = db.getMiningAreas(); broadcastToClients({ type: 'mining_areas_updated', areas: areas.map(formatMiningArea) }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ========== CHUNK ANALYSIS ENDPOINTS ========== // Get all chunk analyses app.get('/api/chunks', (req, res) => { try { const chunks = db.getAllChunkAnalyses(); res.json(chunks); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get specific chunk analysis app.get('/api/chunks/:x/:z', (req, res) => { try { const x = parseInt(req.params.x); const z = parseInt(req.params.z); const chunk = db.getChunkAnalysis(x, z); res.json(chunk || { x, z, analysis: {} }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Save chunk analysis app.post('/api/chunks', (req, res) => { try { const { x, z, analysis } = req.body; db.saveChunkAnalysis(x, z, analysis || {}); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Search blocks by name pattern in area app.get('/api/world/blocks/search', (req, res) => { try { const { fromX, fromY, fromZ, toX, toY, toZ, pattern } = req.query; if (!pattern) { return res.status(400).json({ error: 'Missing pattern parameter' }); } const blocks = db.getBlocksWithNameLike( parseInt(fromX) || -1000, parseInt(fromY) || -64, parseInt(fromZ) || -1000, parseInt(toX) || 1000, parseInt(toY) || 320, parseInt(toZ) || 1000, pattern ); res.json({ blocks }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get single block info app.get('/api/world/block/:x/:y/:z', (req, res) => { try { const x = parseInt(req.params.x); const y = parseInt(req.params.y); const z = parseInt(req.params.z); const block = db.getBlock(x, y, z); res.json(block || null); } catch (error) { res.status(500).json({ error: error.message }); } }); // ========== STATISTICS ENDPOINTS ========== // Get server statistics app.get('/api/stats', (req, res) => { try { const stats = { activeTurtles: turtles.size, turtles: turtles.size, totalBlocks: worldBlocks.size, blocks: worldBlocks.size, savedHomes: turtleHomes.size, connectedClients: webClients.size, tasks: db.getAllTasks().length, miningAreas: db.getMiningAreas().length, groups: db.getAllGroups().length }; res.json(stats); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get mining statistics app.get('/api/stats/mining/:turtleId?', (req, res) => { try { const turtleId = req.params.turtleId ? parseInt(req.params.turtleId) : null; const days = parseInt(req.query.days) || 7; const stats = db.getMiningStats(turtleId, days); if (turtleId) { // Single turtle: return { turtleId, blocks: [{blockType, count}] } res.json({ turtleId, blocks: stats.map(s => ({ blockType: s.block_type, count: s.total_count })) }); } else { // All turtles: group by turtle_id, return array of { turtleId, blocks: [...] } const grouped = {}; stats.forEach(s => { if (!grouped[s.turtle_id]) { grouped[s.turtle_id] = { turtleId: s.turtle_id, blocks: [] }; } grouped[s.turtle_id].blocks.push({ blockType: s.block_type, count: s.total_count }); }); res.json(Object.values(grouped)); } } catch (error) { res.status(500).json({ error: error.message }); } }); // Get top miners leaderboard app.get('/api/stats/top-miners', (req, res) => { try { const limit = parseInt(req.query.limit) || 10; const topMiners = db.getTopMiners(limit); res.json(topMiners.map(m => ({ turtleId: m.turtle_id, totalBlocks: m.total_blocks, uniqueTypes: m.unique_types }))); } catch (error) { res.status(500).json({ error: error.message }); } }); // Record block mined (called by turtles) app.post('/api/stats/block-mined', (req, res) => { try { const { turtleId, blockType } = req.body; db.recordBlockMined(turtleId, blockType); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ========== TURTLE GROUPS/TEAMS ENDPOINTS ========== // Helper to format group for API response function formatGroup(group, members) { return { groupId: group.id, groupName: group.group_name, color: group.color, memberCount: members.length, members: members.map(m => ({ turtleId: m.turtle_id, joinedAt: m.joined_at })), createdAt: group.created_at }; } // Create a new group app.post('/api/groups', (req, res) => { try { const { groupName, color } = req.body; const groupId = db.createGroup(groupName, color); broadcastToClients({ type: 'group_created', groupId, groupName, color }); res.json({ success: true, groupId }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all groups app.get('/api/groups', (req, res) => { try { const groups = db.getAllGroups(); const result = groups.map(group => { const members = db.getGroupMembers(group.id); return formatGroup(group, members); }); res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete a group app.delete('/api/groups/:groupId', (req, res) => { try { const groupId = parseInt(req.params.groupId); db.deleteGroup(groupId); broadcastToClients({ type: 'group_deleted', groupId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Add turtle to group app.post('/api/groups/:groupId/members', (req, res) => { try { const groupId = parseInt(req.params.groupId); const { turtleId } = req.body; db.addTurtleToGroup(turtleId, groupId); broadcastToClients({ type: 'turtle_added_to_group', groupId, turtleId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Remove turtle from group app.delete('/api/groups/:groupId/members/:turtleId', (req, res) => { try { const groupId = parseInt(req.params.groupId); const turtleId = parseInt(req.params.turtleId); db.removeTurtleFromGroup(turtleId, groupId); broadcastToClients({ type: 'turtle_removed_from_group', groupId, turtleId }); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get turtle's groups app.get('/api/turtles/:turtleId/groups', (req, res) => { try { const turtleId = parseInt(req.params.turtleId); const groups = db.getTurtleGroups(turtleId); res.json({ groups }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Send command to all turtles in a group app.post('/api/groups/:groupId/command', (req, res) => { try { const groupId = parseInt(req.params.groupId); const { command, param } = req.body; const members = db.getGroupMembers(groupId); let successCount = 0; for (const member of members) { const turtle = turtles.get(member.turtle_id); if (turtle) { if (command === 'set_state' || command === 'setState') { const stateName = param?.state || param; const stateData = param?.data || {}; turtle.setState(stateName, stateData); } else { turtle.pendingLegacyCommands.push({ command, param, timestamp: Date.now() }); } successCount++; } } res.json({ success: true, sentTo: successCount }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Player position endpoints app.post('/api/player/update', (req, res) => { try { const { playerID, position, timestamp } = req.body; if (!playerID || !position) { return res.status(400).json({ error: 'Missing playerID or position' }); } db.savePlayerPosition(playerID, position); // Broadcast to WebSocket clients broadcastToClients({ type: 'player_update', playerID, position, timestamp: timestamp || Date.now() }); res.json({ success: true, playerID, position }); } catch (error) { console.error('Error updating player position:', error); res.status(500).json({ error: error.message }); } }); app.get('/api/players', (req, res) => { try { const players = db.getAllPlayerPositions(); res.json(players); } catch (error) { res.status(500).json({ error: error.message }); } }); app.get('/api/player/:id', (req, res) => { try { const playerId = parseInt(req.params.id); const player = db.getPlayerPosition(playerId); if (!player) { return res.status(404).json({ error: 'Player not found' }); } res.json(player); } catch (error) { res.status(500).json({ error: error.message }); } }); // Graceful shutdown process.on('SIGINT', () => { console.log('\n๐Ÿ›‘ Shutting down server...'); db.closeDatabase(); process.exit(0); }); server.listen(PORT, () => { console.log(`โœ… Server ready!`); console.log(`\nConfigured turtles to send updates to:`); console.log(` http://localhost:${PORT}/api/turtle/update`); });