feat: Add turtle auto-naming, state recovery, and multiple action endpoints for inventory management
This commit is contained in:
250
server/server.js
250
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...');
|
||||
|
||||
Reference in New Issue
Block a user