1019 lines
28 KiB
JavaScript
1019 lines
28 KiB
JavaScript
import express from 'express';
|
|
import { WebSocketServer } from 'ws';
|
|
import cors from 'cors';
|
|
import { createServer } from 'http';
|
|
import * as db from './database.js';
|
|
|
|
const app = express();
|
|
const PORT = 3001;
|
|
const WS_PORT = 3002;
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// 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 turtleData = new Map(); // turtleID -> turtle state
|
|
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}
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Cleanup stale turtles periodically
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
let removedCount = 0;
|
|
|
|
for (const [turtleID, turtle] of turtleData.entries()) {
|
|
if (now - turtle.lastUpdate > TURTLE_TIMEOUT) {
|
|
console.log(`🔌 Turtle ${turtleID} timed out (last seen ${Math.floor((now - turtle.lastUpdate) / 1000)}s ago)`);
|
|
turtleData.delete(turtleID);
|
|
removedCount++;
|
|
|
|
// Notify web clients
|
|
broadcastToClients({
|
|
type: 'turtle_removed',
|
|
turtleID: turtleID
|
|
});
|
|
}
|
|
}
|
|
|
|
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') {
|
|
// Calculate based on facing direction
|
|
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(turtleData.values()),
|
|
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') {
|
|
// Store command for turtle to poll
|
|
const turtleID = data.turtleID;
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
turtle.pendingCommands = turtle.pendingCommands || [];
|
|
turtle.pendingCommands.push({
|
|
command: data.command,
|
|
param: data.param,
|
|
timestamp: Date.now()
|
|
});
|
|
console.log(`✅ Command queued for turtle ${turtleID}: ${data.command}`, data.param ? `(param: ${data.param})` : '');
|
|
console.log(` Pending commands: ${turtle.pendingCommands.length}`);
|
|
} else {
|
|
console.log(`❌ Turtle ${turtleID} not found in turtleData`);
|
|
console.log(` Available turtles:`, Array.from(turtleData.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}`);
|
|
|
|
// Update turtle data
|
|
if (!turtleData.has(turtleID)) {
|
|
console.log(`✨ New turtle registered: ${turtleID}`);
|
|
}
|
|
|
|
// Merge with existing data, keeping server's home position if it exists
|
|
const existingData = turtleData.get(turtleID) || {};
|
|
const serverHome = turtleHomes.get(turtleID);
|
|
|
|
turtleData.set(turtleID, {
|
|
...existingData,
|
|
...turtleUpdate,
|
|
homePosition: serverHome || turtleUpdate.homePosition, // Server's home takes precedence
|
|
lastUpdate: Date.now()
|
|
});
|
|
|
|
// If turtle reports a home but server doesn't have one, store it
|
|
if (turtleUpdate.homePosition && !serverHome) {
|
|
turtleHomes.set(turtleID, 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: turtleData.get(turtleID)
|
|
});
|
|
|
|
// 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 turtles to poll for commands
|
|
app.get('/api/turtle/:id/commands', (req, res) => {
|
|
try {
|
|
const turtleID = parseInt(req.params.id);
|
|
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
const commands = turtle.pendingCommands || [];
|
|
|
|
// Clean up old commands (older than 30 seconds - they're probably lost)
|
|
const now = Date.now();
|
|
turtle.pendingCommands = commands.filter(cmd => (now - cmd.timestamp) < 30000);
|
|
|
|
if (turtle.pendingCommands.length > 0) {
|
|
console.log(`📤 Sending ${turtle.pendingCommands.length} command(s) to turtle ${turtleID}`);
|
|
turtle.pendingCommands.forEach(cmd => console.log(` - ${cmd.command}`, cmd.param ? `(${cmd.param})` : ''));
|
|
}
|
|
|
|
res.json({ commands: turtle.pendingCommands });
|
|
} 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);
|
|
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
const clearedCount = (turtle.pendingCommands || []).length;
|
|
|
|
if (clearedCount > 0) {
|
|
console.log(`✅ Turtle ${turtleID} acknowledged ${clearedCount} command(s)`);
|
|
turtle.pendingCommands = [];
|
|
}
|
|
|
|
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 });
|
|
}
|
|
});
|
|
|
|
// Get all turtles
|
|
app.get('/api/turtles', (req, res) => {
|
|
res.json({
|
|
turtles: Array.from(turtleData.values())
|
|
});
|
|
});
|
|
|
|
// 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 data
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
turtle.homePosition = position;
|
|
turtleData.set(turtleID, turtle);
|
|
|
|
// Broadcast update
|
|
broadcastToClients({
|
|
type: 'turtle_update',
|
|
turtle: turtle
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
if (turtleData.has(turtleID)) {
|
|
const turtle = turtleData.get(turtleID);
|
|
turtle.pendingCommands = turtle.pendingCommands || [];
|
|
turtle.pendingCommands.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 });
|
|
}
|
|
});
|
|
|
|
// ========== 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 });
|
|
}
|
|
});
|
|
|
|
// ========== STATISTICS ENDPOINTS ==========
|
|
|
|
// Get server statistics
|
|
app.get('/api/stats', (req, res) => {
|
|
try {
|
|
const stats = {
|
|
activeTurtles: turtleData.size,
|
|
turtles: turtleData.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) {
|
|
if (turtleData.has(member.turtle_id)) {
|
|
const turtle = turtleData.get(member.turtle_id);
|
|
turtle.pendingCommands = turtle.pendingCommands || [];
|
|
turtle.pendingCommands.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`);
|
|
});
|