feat: Enhance Turtle class with state recovery, command chaining, and new movement methods
This commit is contained in:
605
server/Turtle.js
605
server/Turtle.js
@@ -3,9 +3,13 @@
|
||||
*
|
||||
* Each connected turtle gets a Turtle instance that manages:
|
||||
* - State machine (idle, mining, exploring, etc.)
|
||||
* - UUID-based command-response correlation
|
||||
* - UUID-based command-response correlation with promise chaining
|
||||
* - Position/facing/inventory tracking
|
||||
* - Event emission for real-time UI updates
|
||||
* - Turn-to-direction, equip, rename, inventory transfer
|
||||
* - External inventory interaction (connectToInventory)
|
||||
* - Block deletion on movement
|
||||
* - State recovery on reconnection
|
||||
*
|
||||
* Inspired by runi95/turtle-control-panel Turtle class.
|
||||
*/
|
||||
@@ -26,6 +30,15 @@ import {
|
||||
AutocraftState,
|
||||
} from './states/index.js';
|
||||
|
||||
// Random name list for unnamed turtles (inspired by reference)
|
||||
const TURTLE_NAMES = [
|
||||
'Atlas', 'Bolt', 'Chip', 'Dash', 'Echo', 'Flint', 'Gizmo', 'Hex',
|
||||
'Iron', 'Jazz', 'Kit', 'Lumen', 'Miko', 'Nova', 'Onyx', 'Pixel',
|
||||
'Quartz', 'Rust', 'Spark', 'Terra', 'Volt', 'Widget', 'Xenon', 'Zinc',
|
||||
'Amber', 'Blaze', 'Cobalt', 'Drake', 'Ember', 'Forge', 'Granite', 'Haze',
|
||||
'Ingot', 'Jasper', 'Kindle', 'Lapis', 'Marble', 'Nickel', 'Opal', 'Prism',
|
||||
];
|
||||
|
||||
const STATE_MAP = {
|
||||
idle: IdleState,
|
||||
moving: MovingState,
|
||||
@@ -50,6 +63,9 @@ const STATE_MAP = {
|
||||
// Timeout for exec() commands (ms)
|
||||
const EXEC_TIMEOUT = 30000;
|
||||
|
||||
// Direction enum matching reference: 0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X)
|
||||
const DIRECTION = { NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3 };
|
||||
|
||||
export class Turtle extends EventEmitter {
|
||||
/**
|
||||
* @param {number} id - The ComputerCraft turtle ID
|
||||
@@ -77,9 +93,10 @@ export class Turtle extends EventEmitter {
|
||||
this._state = null;
|
||||
this._stateRunner = null; // The running async generator loop
|
||||
|
||||
// Command correlation
|
||||
// Command correlation with promise chaining (prevents concurrent commands racing)
|
||||
this._pendingCommands = new Map(); // uuid -> { resolve, reject, timeout }
|
||||
this._messageIndex = 0;
|
||||
this._lastPromise = Promise.resolve(); // Promise chain for sequential exec
|
||||
|
||||
// Connection tracking
|
||||
this.connected = false;
|
||||
@@ -159,6 +176,7 @@ export class Turtle extends EventEmitter {
|
||||
|
||||
set selectedSlot(value) {
|
||||
this._selectedSlot = value;
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
get fuelLimit() {
|
||||
@@ -169,6 +187,15 @@ export class Turtle extends EventEmitter {
|
||||
this._fuelLimit = value;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set label(value) {
|
||||
this._label = value;
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this._error;
|
||||
}
|
||||
@@ -218,11 +245,46 @@ export class Turtle extends EventEmitter {
|
||||
this._state = new StateClass(this, data);
|
||||
console.log(`[Turtle ${this.id}] State: ${this._state.name} - ${this._state.description}`);
|
||||
|
||||
// Persist state for recovery (skip idle to avoid DB noise)
|
||||
if (stateName !== 'idle' && this.server?.db) {
|
||||
try {
|
||||
this.server.db.saveTurtleState(this.id, stateName, data);
|
||||
} catch (e) {
|
||||
// Ignore errors in state persistence
|
||||
}
|
||||
} else if (stateName === 'idle' && this.server?.db) {
|
||||
try {
|
||||
this.server.db.deleteTurtleState(this.id);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// Run the state's act() generator loop
|
||||
this._runStateLoop();
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover state from database on reconnection
|
||||
*/
|
||||
recoverState() {
|
||||
if (!this.server?.db) return false;
|
||||
|
||||
try {
|
||||
const saved = this.server.db.getTurtleState(this.id);
|
||||
if (saved && saved.stateName && saved.stateName !== 'idle') {
|
||||
const StateClass = STATE_MAP[saved.stateName];
|
||||
if (StateClass) {
|
||||
console.log(`[Turtle ${this.id}] Recovering state: ${saved.stateName}`);
|
||||
this.setState(saved.stateName, saved.stateData || {});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Turtle ${this.id}] Error recovering state:`, e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the current state's act() generator in a loop
|
||||
*/
|
||||
@@ -246,47 +308,54 @@ export class Turtle extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Command Execution (UUID-based) ==========
|
||||
// ========== Command Execution (UUID-based with Promise Chaining) ==========
|
||||
|
||||
/**
|
||||
* Execute Lua code on the turtle and return the result.
|
||||
* Uses UUID-based command-response correlation.
|
||||
* Commands are chained sequentially via lastPromise to prevent race conditions.
|
||||
*
|
||||
* @param {string} luaCode - The Lua code to execute
|
||||
* @param {number} timeout - Timeout in ms (default: 30s)
|
||||
* @returns {Promise<any>} The result of the Lua execution
|
||||
*/
|
||||
exec(luaCode, timeout = EXEC_TIMEOUT) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uuid = randomUUID();
|
||||
const messageIndex = this._messageIndex++;
|
||||
// Chain through lastPromise to ensure sequential execution
|
||||
const execPromise = new Promise((resolve, reject) => {
|
||||
this._lastPromise.catch(() => {}).finally(() => {
|
||||
const uuid = randomUUID();
|
||||
const messageIndex = this._messageIndex++;
|
||||
|
||||
// Set up timeout
|
||||
const timer = setTimeout(() => {
|
||||
this._pendingCommands.delete(uuid);
|
||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
// Set up timeout
|
||||
const timer = setTimeout(() => {
|
||||
this._pendingCommands.delete(uuid);
|
||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
// Store the pending command
|
||||
this._pendingCommands.set(uuid, {
|
||||
resolve: (result) => {
|
||||
clearTimeout(timer);
|
||||
this._pendingCommands.delete(uuid);
|
||||
resolve(result);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timer);
|
||||
this._pendingCommands.delete(uuid);
|
||||
reject(error);
|
||||
},
|
||||
timeout: timer,
|
||||
luaCode,
|
||||
sentAt: Date.now(),
|
||||
// Store the pending command
|
||||
this._pendingCommands.set(uuid, {
|
||||
resolve: (result) => {
|
||||
clearTimeout(timer);
|
||||
this._pendingCommands.delete(uuid);
|
||||
resolve(result);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timer);
|
||||
this._pendingCommands.delete(uuid);
|
||||
reject(error);
|
||||
},
|
||||
timeout: timer,
|
||||
luaCode,
|
||||
sentAt: Date.now(),
|
||||
});
|
||||
|
||||
// Send the command to the turtle (via webbridge modem relay)
|
||||
this._sendExecCommand(uuid, messageIndex, luaCode);
|
||||
});
|
||||
|
||||
// Send the command to the turtle (via webbridge modem relay)
|
||||
this._sendExecCommand(uuid, messageIndex, luaCode);
|
||||
});
|
||||
|
||||
this._lastPromise = execPromise;
|
||||
return execPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +374,458 @@ export class Turtle extends EventEmitter {
|
||||
this.emit('sendCommand', command);
|
||||
}
|
||||
|
||||
// ========== Turtle API Methods ==========
|
||||
|
||||
/**
|
||||
* Turn the turtle to face a specific cardinal direction.
|
||||
* Calculates the optimal turn direction (left or right).
|
||||
* @param {number} direction - 0=North, 1=East, 2=South, 3=West
|
||||
*/
|
||||
async turnToDirection(direction) {
|
||||
if (this._facing === direction) return;
|
||||
|
||||
const turn = (direction - this._facing + 4) % 4;
|
||||
if (turn === 1) {
|
||||
await this.exec('return turtle.turnRight()');
|
||||
this._facing = direction;
|
||||
this._emitUpdate();
|
||||
} else if (turn === 2) {
|
||||
await this.exec('turtle.turnLeft(); return turtle.turnLeft()');
|
||||
this._facing = direction;
|
||||
this._emitUpdate();
|
||||
} else if (turn === 3) {
|
||||
await this.exec('return turtle.turnLeft()');
|
||||
this._facing = direction;
|
||||
this._emitUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the turtle forward. Updates position and deletes block at destination.
|
||||
*/
|
||||
async forward() {
|
||||
const result = await this.exec('return turtle.forward()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
this.updatePositionForward();
|
||||
this._deleteBlockAtPosition(this._position);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the turtle backward.
|
||||
*/
|
||||
async back() {
|
||||
const result = await this.exec('return turtle.back()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
// Update position for backward movement
|
||||
if (this._position) {
|
||||
const pos = { ...this._position };
|
||||
if (this._facing === 0) pos.z += 1;
|
||||
else if (this._facing === 1) pos.x -= 1;
|
||||
else if (this._facing === 2) pos.z -= 1;
|
||||
else if (this._facing === 3) pos.x += 1;
|
||||
this.position = pos;
|
||||
this._deleteBlockAtPosition(pos);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the turtle up. Updates position and deletes block at destination.
|
||||
*/
|
||||
async up() {
|
||||
const result = await this.exec('return turtle.up()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
this.updatePositionUp();
|
||||
this._deleteBlockAtPosition(this._position);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the turtle down. Updates position and deletes block at destination.
|
||||
*/
|
||||
async down() {
|
||||
const result = await this.exec('return turtle.down()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
this.updatePositionDown();
|
||||
this._deleteBlockAtPosition(this._position);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dig the block in front
|
||||
*/
|
||||
async dig() {
|
||||
const result = await this.exec('return turtle.dig()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
const pos = this.getBlockPositionInDirection('forward');
|
||||
if (pos) this._deleteBlockAtPosition(pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dig the block above
|
||||
*/
|
||||
async digUp() {
|
||||
const result = await this.exec('return turtle.digUp()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
const pos = this.getBlockPositionInDirection('up');
|
||||
if (pos) this._deleteBlockAtPosition(pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dig the block below
|
||||
*/
|
||||
async digDown() {
|
||||
const result = await this.exec('return turtle.digDown()');
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
const pos = this.getBlockPositionInDirection('down');
|
||||
if (pos) this._deleteBlockAtPosition(pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an inventory slot
|
||||
*/
|
||||
async select(slot = 1) {
|
||||
const result = await this.exec(`return turtle.select(${slot})`);
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
this._selectedSlot = slot;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer items from current slot to another slot
|
||||
*/
|
||||
async transferTo(slot, count) {
|
||||
const cmd = count != null
|
||||
? `return turtle.transferTo(${slot}, ${count})`
|
||||
: `return turtle.transferTo(${slot})`;
|
||||
return this.exec(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer items between inventory slots (select from, transferTo target, reselect)
|
||||
*/
|
||||
async inventoryTransfer(fromSlot, toSlot, count) {
|
||||
const currentSlot = this._selectedSlot;
|
||||
await this.select(fromSlot);
|
||||
await this.transferTo(toSlot, count);
|
||||
await this.select(currentSlot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop items from current slot (front direction)
|
||||
*/
|
||||
async drop(count) {
|
||||
const cmd = count != null ? `return turtle.drop(${count})` : 'return turtle.drop()';
|
||||
const result = await this.exec(cmd);
|
||||
// Auto-refresh adjacent inventory if present
|
||||
if (this._peripherals?.front?.types?.includes('inventory')) {
|
||||
try { await this.connectToInventory('front'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop items upward
|
||||
*/
|
||||
async dropUp(count) {
|
||||
const cmd = count != null ? `return turtle.dropUp(${count})` : 'return turtle.dropUp()';
|
||||
const result = await this.exec(cmd);
|
||||
if (this._peripherals?.top?.types?.includes('inventory')) {
|
||||
try { await this.connectToInventory('top'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop items downward
|
||||
*/
|
||||
async dropDown(count) {
|
||||
const cmd = count != null ? `return turtle.dropDown(${count})` : 'return turtle.dropDown()';
|
||||
const result = await this.exec(cmd);
|
||||
if (this._peripherals?.bottom?.types?.includes('inventory')) {
|
||||
try { await this.connectToInventory('bottom'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suck items from front
|
||||
*/
|
||||
async suck(count) {
|
||||
const cmd = count != null ? `return turtle.suck(${count})` : 'return turtle.suck()';
|
||||
return this.exec(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suck items from above
|
||||
*/
|
||||
async suckUp(count) {
|
||||
const cmd = count != null ? `return turtle.suckUp(${count})` : 'return turtle.suckUp()';
|
||||
return this.exec(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suck items from below
|
||||
*/
|
||||
async suckDown(count) {
|
||||
const cmd = count != null ? `return turtle.suckDown(${count})` : 'return turtle.suckDown()';
|
||||
return this.exec(cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Place block in front
|
||||
*/
|
||||
async place(text) {
|
||||
const cmd = text ? `return turtle.place("${text}")` : 'return turtle.place()';
|
||||
const result = await this.exec(cmd);
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
await this.inspect(); // Auto-inspect to update world map
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Place block above
|
||||
*/
|
||||
async placeUp(text) {
|
||||
const cmd = text ? `return turtle.placeUp("${text}")` : 'return turtle.placeUp()';
|
||||
const result = await this.exec(cmd);
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
await this.inspectUp();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Place block below
|
||||
*/
|
||||
async placeDown(text) {
|
||||
const cmd = text ? `return turtle.placeDown("${text}")` : 'return turtle.placeDown()';
|
||||
const result = await this.exec(cmd);
|
||||
if (result === true || (Array.isArray(result) && result[0] === true)) {
|
||||
await this.inspectDown();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect block in front and update world map
|
||||
*/
|
||||
async inspect() {
|
||||
const result = await this.exec('local h, d = turtle.inspect(); if h then return d else return nil end');
|
||||
if (result && result.name) {
|
||||
const pos = this.getBlockPositionInDirection('forward');
|
||||
if (pos) this.emit('blocksDiscovered', [{ ...pos, name: result.name, metadata: result.metadata || 0, discoveredBy: this.id }]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect block above
|
||||
*/
|
||||
async inspectUp() {
|
||||
const result = await this.exec('local h, d = turtle.inspectUp(); if h then return d else return nil end');
|
||||
if (result && result.name) {
|
||||
const pos = this.getBlockPositionInDirection('up');
|
||||
if (pos) this.emit('blocksDiscovered', [{ ...pos, name: result.name, metadata: result.metadata || 0, discoveredBy: this.id }]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect block below
|
||||
*/
|
||||
async inspectDown() {
|
||||
const result = await this.exec('local h, d = turtle.inspectDown(); if h then return d else return nil end');
|
||||
if (result && result.name) {
|
||||
const pos = this.getBlockPositionInDirection('down');
|
||||
if (pos) this.emit('blocksDiscovered', [{ ...pos, name: result.name, metadata: result.metadata || 0, discoveredBy: this.id }]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batched 3-direction inspect (up/down/forward) in a single eval call.
|
||||
* More efficient than 3 separate calls.
|
||||
*/
|
||||
async explore() {
|
||||
const result = await this.exec(`
|
||||
local results = {}
|
||||
local h, d
|
||||
h, d = turtle.inspect()
|
||||
if h then results.forward = d end
|
||||
h, d = turtle.inspectUp()
|
||||
if h then results.up = d end
|
||||
h, d = turtle.inspectDown()
|
||||
if h then results.down = d end
|
||||
return results
|
||||
`);
|
||||
if (result && typeof result === 'object') {
|
||||
this.processScanResults(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuel the turtle
|
||||
*/
|
||||
async refuel(count) {
|
||||
const cmd = count != null ? `return turtle.refuel(${count})` : 'return turtle.refuel()';
|
||||
const result = await this.exec(cmd);
|
||||
// Update fuel level
|
||||
const fuelLevel = await this.exec('return turtle.getFuelLevel()');
|
||||
if (fuelLevel != null) this._fuel = fuelLevel;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft using workbench peripheral
|
||||
*/
|
||||
async craft() {
|
||||
const result = await this.exec(`
|
||||
local hasWorkbench = peripheral.find("workbench") ~= nil
|
||||
if not hasWorkbench then return false, "No workbench" end
|
||||
return peripheral.find("workbench").craft()
|
||||
`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip an item on the left side of the turtle
|
||||
*/
|
||||
async equipLeft() {
|
||||
return this.exec('return turtle.equipLeft()');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip an item on the right side of the turtle
|
||||
*/
|
||||
async equipRight() {
|
||||
return this.exec('return turtle.equipRight()');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the turtle (set computer label)
|
||||
*/
|
||||
async rename(name) {
|
||||
await this.exec(`os.setComputerLabel("${name}")`);
|
||||
this._label = name;
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-assign a random name if the turtle has no label
|
||||
*/
|
||||
autoName() {
|
||||
if (!this._label) {
|
||||
const name = TURTLE_NAMES[Math.floor(Math.random() * TURTLE_NAMES.length)];
|
||||
this._label = name;
|
||||
// Send rename command to turtle when connected
|
||||
if (this.connected) {
|
||||
this.rename(name).catch(() => {});
|
||||
}
|
||||
return name;
|
||||
}
|
||||
return this._label;
|
||||
}
|
||||
|
||||
/**
|
||||
* GPS locate
|
||||
*/
|
||||
async gpsLocate() {
|
||||
const result = await this.exec('return gps.locate(5)');
|
||||
if (result && typeof result === 'object' && result[1] != null) {
|
||||
this.position = { x: Math.floor(result[1]), y: Math.floor(result[2]), z: Math.floor(result[3]) };
|
||||
} else if (typeof result === 'number') {
|
||||
// Some versions return x,y,z as separate values
|
||||
const pos = await this.exec('local x,y,z = gps.locate(5); return {x=x, y=y, z=z}');
|
||||
if (pos && pos.x != null) {
|
||||
this.position = { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) };
|
||||
}
|
||||
}
|
||||
return this._position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an adjacent inventory peripheral and read its contents
|
||||
*/
|
||||
async connectToInventory(side) {
|
||||
const result = await this.exec(`
|
||||
local size = peripheral.call("${side}", "size")
|
||||
local content = peripheral.call("${side}", "list")
|
||||
if next(content) == nil then content = nil end
|
||||
return {size = size, content = content}
|
||||
`);
|
||||
|
||||
if (result) {
|
||||
this._peripherals = {
|
||||
...this._peripherals,
|
||||
[side]: {
|
||||
...this._peripherals[side],
|
||||
data: {
|
||||
size: result.size,
|
||||
content: result.content,
|
||||
},
|
||||
},
|
||||
};
|
||||
this._emitUpdate();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all attached peripherals (scan what's connected)
|
||||
*/
|
||||
async updateAllAttachedPeripherals() {
|
||||
const result = await this.exec(`
|
||||
local peripherals = {}
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
peripherals[name] = {types = types}
|
||||
end
|
||||
return peripherals
|
||||
`);
|
||||
|
||||
if (result && typeof result === 'object') {
|
||||
this._peripherals = result;
|
||||
this._emitUpdate();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a peripheral method by side
|
||||
*/
|
||||
async usePeripheralWithSide(side, method, ...args) {
|
||||
const argsStr = args.map(a => {
|
||||
if (a === null) return 'nil';
|
||||
if (typeof a === 'string') return `"${a}"`;
|
||||
return String(a);
|
||||
}).join(', ');
|
||||
|
||||
return this.exec(`return peripheral.call("${side}", "${method}"${argsStr ? ', ' + argsStr : ''})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for a duration (on the turtle side)
|
||||
*/
|
||||
async sleep(seconds) {
|
||||
return this.exec(`sleep(${seconds})`);
|
||||
}
|
||||
|
||||
// ========== Real-time Event Handling ==========
|
||||
|
||||
/**
|
||||
@@ -414,6 +935,29 @@ export class Turtle extends EventEmitter {
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a block from both in-memory world map and database (turtle moved into it = now air)
|
||||
*/
|
||||
_deleteBlockAtPosition(pos) {
|
||||
if (!pos) return;
|
||||
const key = `${pos.x},${pos.y},${pos.z}`;
|
||||
if (this.server?.worldBlocks) {
|
||||
this.server.worldBlocks.delete(key);
|
||||
}
|
||||
if (this.server?.db) {
|
||||
try {
|
||||
this.server.db.deleteBlock(pos.x, pos.y, pos.z);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
// Broadcast block deletion to web clients
|
||||
if (this.server?.broadcastToClients) {
|
||||
this.server.broadcastToClients({
|
||||
type: 'block_deleted',
|
||||
x: pos.x, y: pos.y, z: pos.z,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process scan results and update world blocks on the server
|
||||
*/
|
||||
@@ -484,6 +1028,9 @@ export class Turtle extends EventEmitter {
|
||||
if (statusData.inventory) {
|
||||
this._inventory = statusData.inventory;
|
||||
}
|
||||
if (statusData.label) {
|
||||
this._label = statusData.label;
|
||||
}
|
||||
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user