diff --git a/server/server.js b/server/server.js index 554bd43..5a3125f 100644 --- a/server/server.js +++ b/server/server.js @@ -95,6 +95,12 @@ function getOrCreateTurtle(turtleID) { turtle.homePosition = savedHome; } + // Auto-name if no label + turtle.autoName(); + + // Try to recover previous state from DB (e.g., if turtle rebooted) + turtle.recoverState(); + // ---- Event handlers ---- // Forward eval commands to webbridge via pending command queue @@ -1330,6 +1336,250 @@ app.get('/api/player/:id', (req, res) => { } }); +// ========== TURTLE ACTION ENDPOINTS ========== + +// Rename a turtle +app.post('/api/turtle/:id/rename', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { name } = req.body; + + if (!name || typeof name !== 'string') { + return res.status(400).json({ error: 'Missing or invalid name' }); + } + + const turtle = turtles.get(turtleID); + if (!turtle) { + return res.status(404).json({ error: 'Turtle not found' }); + } + + await turtle.rename(name.trim()); + console.log(`šŸ“ Turtle ${turtleID} renamed to "${name.trim()}"`); + res.json({ success: true, label: turtle.label }); + } catch (error) { + console.error(`āŒ Error renaming turtle ${req.params.id}:`, error.message); + res.json({ success: false, error: error.message }); + } +}); + +// Equip item on left side +app.post('/api/turtle/:id/equip-left', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + const result = await turtle.equipLeft(); + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Equip item on right side +app.post('/api/turtle/:id/equip-right', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + const result = await turtle.equipRight(); + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Select inventory slot +app.post('/api/turtle/:id/select', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { slot } = req.body; + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + const result = await turtle.select(slot); + res.json({ success: true, result, selectedSlot: turtle.selectedSlot }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Transfer items between slots +app.post('/api/turtle/:id/transfer', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { fromSlot, toSlot, count } = req.body; + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + await turtle.inventoryTransfer(fromSlot, toSlot, count); + res.json({ success: true }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Sort inventory (compact items on turtle) +app.post('/api/turtle/:id/sort', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + // Send a Lua eval that sorts inventory by compacting items + const result = await turtle.exec(` + local moved = 0 + for slot = 1, 16 do + local item = turtle.getItemDetail(slot) + if item then + for target = 1, slot - 1 do + local targetItem = turtle.getItemDetail(target) + if not targetItem then + turtle.select(slot) + turtle.transferTo(target) + moved = moved + 1 + break + elseif targetItem.name == item.name and targetItem.count < targetItem.maxCount then + turtle.select(slot) + turtle.transferTo(target) + moved = moved + 1 + if turtle.getItemCount(slot) == 0 then break end + end + end + end + end + turtle.select(1) + return {moved = moved} + `); + + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Connect to adjacent inventory (peripheral) +app.post('/api/turtle/:id/connect-inventory', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { side } = req.body; + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + if (!side) return res.status(400).json({ error: 'Missing side parameter' }); + + const result = await turtle.connectToInventory(side); + res.json({ success: true, inventory: result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Drop items (front/up/down) +app.post('/api/turtle/:id/drop', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { direction, count } = req.body; + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + let result; + if (direction === 'up') result = await turtle.dropUp(count); + else if (direction === 'down') result = await turtle.dropDown(count); + else result = await turtle.drop(count); + + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Suck items (front/up/down) +app.post('/api/turtle/:id/suck', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const { direction, count } = req.body; + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + let result; + if (direction === 'up') result = await turtle.suckUp(count); + else if (direction === 'down') result = await turtle.suckDown(count); + else result = await turtle.suck(count); + + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// Update turtle config +app.post('/api/turtle/:id/config', (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const configData = req.body; + + if (!configData || typeof configData !== 'object') { + return res.status(400).json({ error: 'Invalid config data' }); + } + + // Merge with existing config + const existing = turtleConfig.get(turtleID) || {}; + const merged = { ...existing, ...configData }; + turtleConfig.set(turtleID, merged); + + // Persist to database + try { + db.saveTurtleConfig(turtleID, merged); + } catch (e) { /* ignore if method doesn't exist */ } + + console.log(`āš™ļø Config updated for turtle ${turtleID}:`, merged); + res.json({ success: true, config: merged }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get turtle config +app.get('/api/turtle/:id/config', (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const config = turtleConfig.get(turtleID) || {}; + res.json({ success: true, config }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Explore (batched 3-direction inspect) +app.post('/api/turtle/:id/explore', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + const result = await turtle.explore(); + res.json({ success: true, result }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + +// GPS locate +app.post('/api/turtle/:id/gps', async (req, res) => { + try { + const turtleID = parseInt(req.params.id); + const turtle = turtles.get(turtleID); + if (!turtle) return res.status(404).json({ error: 'Turtle not found' }); + + const position = await turtle.gpsLocate(); + res.json({ success: true, position }); + } catch (error) { + res.json({ success: false, error: error.message }); + } +}); + // Graceful shutdown process.on('SIGINT', () => { console.log('\nšŸ›‘ Shutting down server...');