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;
|
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 ----
|
// ---- Event handlers ----
|
||||||
|
|
||||||
// Forward eval commands to webbridge via pending command queue
|
// 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
|
// Graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\n🛑 Shutting down server...');
|
console.log('\n🛑 Shutting down server...');
|
||||||
|
|||||||
Reference in New Issue
Block a user