825 lines
22 KiB
JavaScript
825 lines
22 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 || [];
|
|
|
|
if (commands.length > 0) {
|
|
console.log(`📤 Sending ${commands.length} command(s) to turtle ${turtleID}`);
|
|
commands.forEach(cmd => console.log(` - ${cmd.command}`, cmd.param ? `(${cmd.param})` : ''));
|
|
}
|
|
|
|
// Mark commands as sent but don't clear them yet - they'll be cleared on next poll if turtle is processing
|
|
// This allows the webbridge to retry if the turtle didn't receive them
|
|
if (!turtle.lastCommandPollTime || (Date.now() - turtle.lastCommandPollTime) > 5000) {
|
|
// First poll or more than 5 seconds since last poll - send commands
|
|
turtle.lastCommandPollTime = Date.now();
|
|
res.json({ commands });
|
|
} else {
|
|
// Recent poll - assume previous commands were received, clear them
|
|
if (commands.length > 0) {
|
|
console.log(`✅ Clearing ${commands.length} command(s) for turtle ${turtleID} (acknowledged)`);
|
|
turtle.pendingCommands = [];
|
|
}
|
|
res.json({ commands: [] });
|
|
}
|
|
} 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 });
|
|
});
|
|
|
|
// 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 });
|
|
}
|
|
});
|
|
|
|
// Cleanup stale turtles (haven't updated in 30 seconds)
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const timeout = 30000; // 30 seconds
|
|
|
|
for (const [turtleID, turtle] of turtleData.entries()) {
|
|
if (now - turtle.lastUpdate > timeout) {
|
|
console.log(`🗑️ Removing stale turtle: ${turtleID}`);
|
|
turtleData.delete(turtleID);
|
|
|
|
broadcastToClients({
|
|
type: 'turtle_disconnected',
|
|
turtleID
|
|
});
|
|
}
|
|
}
|
|
}, 10000); // Check every 10 seconds
|
|
|
|
// ========== PATH RECORDING ENDPOINTS ==========
|
|
|
|
// Save a recorded path
|
|
app.post('/api/paths', (req, res) => {
|
|
try {
|
|
const { turtleId, pathName, pathData } = req.body;
|
|
db.savePath(turtleId, pathName, pathData);
|
|
res.json({ success: true, message: 'Path saved' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all paths for a turtle
|
|
app.get('/api/paths/:turtleId', (req, res) => {
|
|
try {
|
|
const turtleId = parseInt(req.params.turtleId);
|
|
const paths = db.getPaths(turtleId);
|
|
res.json({ paths });
|
|
} 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, taskData, priority } = req.body;
|
|
const taskId = db.createTask(taskType, taskData, priority || 0);
|
|
|
|
broadcastToClients({
|
|
type: 'task_created',
|
|
taskId,
|
|
taskType,
|
|
priority
|
|
});
|
|
|
|
res.json({ success: true, taskId });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all tasks
|
|
app.get('/api/tasks', (req, res) => {
|
|
try {
|
|
const tasks = db.getAllTasks();
|
|
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 });
|
|
}
|
|
});
|
|
|
|
// 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 ==========
|
|
|
|
// Save a mining area claim
|
|
app.post('/api/mining-areas', (req, res) => {
|
|
try {
|
|
const { turtleId, bounds } = req.body;
|
|
db.saveMiningArea(turtleId, bounds);
|
|
|
|
const areas = db.getMiningAreas();
|
|
broadcastToClients({
|
|
type: 'mining_areas_updated',
|
|
areas
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get all active mining areas
|
|
app.get('/api/mining-areas', (req, res) => {
|
|
try {
|
|
const areas = db.getMiningAreas();
|
|
res.json({ areas });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Close a mining area
|
|
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
|
|
});
|
|
|
|
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,
|
|
totalBlocks: 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);
|
|
res.json({ stats });
|
|
} 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 });
|
|
} 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 ==========
|
|
|
|
// 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();
|
|
// Enhance with member count
|
|
const groupsWithMembers = groups.map(group => ({
|
|
...group,
|
|
members: db.getGroupMembers(group.id)
|
|
}));
|
|
res.json({ groups: groupsWithMembers });
|
|
} 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`);
|
|
});
|