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:
|
* Each connected turtle gets a Turtle instance that manages:
|
||||||
* - State machine (idle, mining, exploring, etc.)
|
* - State machine (idle, mining, exploring, etc.)
|
||||||
* - UUID-based command-response correlation
|
* - UUID-based command-response correlation with promise chaining
|
||||||
* - Position/facing/inventory tracking
|
* - Position/facing/inventory tracking
|
||||||
* - Event emission for real-time UI updates
|
* - 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.
|
* Inspired by runi95/turtle-control-panel Turtle class.
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +30,15 @@ import {
|
|||||||
AutocraftState,
|
AutocraftState,
|
||||||
} from './states/index.js';
|
} 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 = {
|
const STATE_MAP = {
|
||||||
idle: IdleState,
|
idle: IdleState,
|
||||||
moving: MovingState,
|
moving: MovingState,
|
||||||
@@ -50,6 +63,9 @@ const STATE_MAP = {
|
|||||||
// Timeout for exec() commands (ms)
|
// Timeout for exec() commands (ms)
|
||||||
const EXEC_TIMEOUT = 30000;
|
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 {
|
export class Turtle extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @param {number} id - The ComputerCraft turtle ID
|
* @param {number} id - The ComputerCraft turtle ID
|
||||||
@@ -77,9 +93,10 @@ export class Turtle extends EventEmitter {
|
|||||||
this._state = null;
|
this._state = null;
|
||||||
this._stateRunner = null; // The running async generator loop
|
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._pendingCommands = new Map(); // uuid -> { resolve, reject, timeout }
|
||||||
this._messageIndex = 0;
|
this._messageIndex = 0;
|
||||||
|
this._lastPromise = Promise.resolve(); // Promise chain for sequential exec
|
||||||
|
|
||||||
// Connection tracking
|
// Connection tracking
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
@@ -159,6 +176,7 @@ export class Turtle extends EventEmitter {
|
|||||||
|
|
||||||
set selectedSlot(value) {
|
set selectedSlot(value) {
|
||||||
this._selectedSlot = value;
|
this._selectedSlot = value;
|
||||||
|
this._emitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
get fuelLimit() {
|
get fuelLimit() {
|
||||||
@@ -169,6 +187,15 @@ export class Turtle extends EventEmitter {
|
|||||||
this._fuelLimit = value;
|
this._fuelLimit = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._label;
|
||||||
|
}
|
||||||
|
|
||||||
|
set label(value) {
|
||||||
|
this._label = value;
|
||||||
|
this._emitUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
get error() {
|
get error() {
|
||||||
return this._error;
|
return this._error;
|
||||||
}
|
}
|
||||||
@@ -218,11 +245,46 @@ export class Turtle extends EventEmitter {
|
|||||||
this._state = new StateClass(this, data);
|
this._state = new StateClass(this, data);
|
||||||
console.log(`[Turtle ${this.id}] State: ${this._state.name} - ${this._state.description}`);
|
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
|
// Run the state's act() generator loop
|
||||||
this._runStateLoop();
|
this._runStateLoop();
|
||||||
this._emitUpdate();
|
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
|
* 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.
|
* Execute Lua code on the turtle and return the result.
|
||||||
* Uses UUID-based command-response correlation.
|
* 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 {string} luaCode - The Lua code to execute
|
||||||
* @param {number} timeout - Timeout in ms (default: 30s)
|
* @param {number} timeout - Timeout in ms (default: 30s)
|
||||||
* @returns {Promise<any>} The result of the Lua execution
|
* @returns {Promise<any>} The result of the Lua execution
|
||||||
*/
|
*/
|
||||||
exec(luaCode, timeout = EXEC_TIMEOUT) {
|
exec(luaCode, timeout = EXEC_TIMEOUT) {
|
||||||
return new Promise((resolve, reject) => {
|
// Chain through lastPromise to ensure sequential execution
|
||||||
const uuid = randomUUID();
|
const execPromise = new Promise((resolve, reject) => {
|
||||||
const messageIndex = this._messageIndex++;
|
this._lastPromise.catch(() => {}).finally(() => {
|
||||||
|
const uuid = randomUUID();
|
||||||
|
const messageIndex = this._messageIndex++;
|
||||||
|
|
||||||
// Set up timeout
|
// Set up timeout
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
this._pendingCommands.delete(uuid);
|
this._pendingCommands.delete(uuid);
|
||||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
// Store the pending command
|
// Store the pending command
|
||||||
this._pendingCommands.set(uuid, {
|
this._pendingCommands.set(uuid, {
|
||||||
resolve: (result) => {
|
resolve: (result) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
this._pendingCommands.delete(uuid);
|
this._pendingCommands.delete(uuid);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
},
|
},
|
||||||
reject: (error) => {
|
reject: (error) => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
this._pendingCommands.delete(uuid);
|
this._pendingCommands.delete(uuid);
|
||||||
reject(error);
|
reject(error);
|
||||||
},
|
},
|
||||||
timeout: timer,
|
timeout: timer,
|
||||||
luaCode,
|
luaCode,
|
||||||
sentAt: Date.now(),
|
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);
|
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 ==========
|
// ========== Real-time Event Handling ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,6 +935,29 @@ export class Turtle extends EventEmitter {
|
|||||||
return pos;
|
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
|
* Process scan results and update world blocks on the server
|
||||||
*/
|
*/
|
||||||
@@ -484,6 +1028,9 @@ export class Turtle extends EventEmitter {
|
|||||||
if (statusData.inventory) {
|
if (statusData.inventory) {
|
||||||
this._inventory = statusData.inventory;
|
this._inventory = statusData.inventory;
|
||||||
}
|
}
|
||||||
|
if (statusData.label) {
|
||||||
|
this._label = statusData.label;
|
||||||
|
}
|
||||||
|
|
||||||
this._emitUpdate();
|
this._emitUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user