Compare commits
6 Commits
dca14643c6
...
668d4a3685
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
668d4a3685 | ||
|
|
e401c39bb3 | ||
|
|
d4e6b469df | ||
|
|
150cc4b41a | ||
|
|
220f2a90d4 | ||
|
|
ba11d2c9d2 |
@@ -53,8 +53,13 @@ export class Turtle extends EventEmitter {
|
||||
this._homePosition = null;
|
||||
this._facing = 0;
|
||||
this._fuel = 0;
|
||||
this._fuelLimit = 100000;
|
||||
this._inventory = {};
|
||||
this._selectedSlot = 1;
|
||||
this._label = null;
|
||||
this._peripherals = {}; // { side: { types: [...], data: {...} } }
|
||||
this._error = null;
|
||||
this._warning = null;
|
||||
|
||||
// State machine
|
||||
this._state = null;
|
||||
@@ -127,6 +132,49 @@ export class Turtle extends EventEmitter {
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
get peripherals() {
|
||||
return this._peripherals;
|
||||
}
|
||||
|
||||
set peripherals(value) {
|
||||
this._peripherals = value;
|
||||
this._emitUpdate();
|
||||
}
|
||||
|
||||
get selectedSlot() {
|
||||
return this._selectedSlot;
|
||||
}
|
||||
|
||||
set selectedSlot(value) {
|
||||
this._selectedSlot = value;
|
||||
}
|
||||
|
||||
get fuelLimit() {
|
||||
return this._fuelLimit;
|
||||
}
|
||||
|
||||
set fuelLimit(value) {
|
||||
this._fuelLimit = value;
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
set error(value) {
|
||||
this._error = value;
|
||||
if (value) this._emitUpdate();
|
||||
}
|
||||
|
||||
get warning() {
|
||||
return this._warning;
|
||||
}
|
||||
|
||||
set warning(value) {
|
||||
this._warning = value;
|
||||
if (value) this._emitUpdate();
|
||||
}
|
||||
|
||||
get stateName() {
|
||||
return this._state?.name || 'idle';
|
||||
}
|
||||
@@ -245,6 +293,53 @@ export class Turtle extends EventEmitter {
|
||||
this.emit('sendCommand', command);
|
||||
}
|
||||
|
||||
// ========== Real-time Event Handling ==========
|
||||
|
||||
/**
|
||||
* Handle real-time events from the turtle (inventory, peripherals, etc.)
|
||||
* These come via the webbridge as separate event messages.
|
||||
*/
|
||||
handleEvent(eventType, eventData) {
|
||||
switch (eventType) {
|
||||
case 'INVENTORY_UPDATE':
|
||||
this._inventory = eventData;
|
||||
this._emitUpdate();
|
||||
break;
|
||||
case 'PERIPHERAL_ATTACHED':
|
||||
// Merge new peripherals
|
||||
this._peripherals = { ...this._peripherals, ...eventData };
|
||||
this._emitUpdate();
|
||||
break;
|
||||
case 'PERIPHERAL_DETACHED':
|
||||
// eventData is the side name string
|
||||
if (typeof eventData === 'string' && this._peripherals[eventData]) {
|
||||
const { [eventData]: _, ...rest } = this._peripherals;
|
||||
this._peripherals = rest;
|
||||
this._emitUpdate();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log(`[Turtle ${this.id}] Unknown event type: ${eventType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if turtle has a peripheral with the given type name
|
||||
*/
|
||||
hasPeripheral(typeName) {
|
||||
return Object.values(this._peripherals).some(p =>
|
||||
p.types && p.types.includes(typeName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chunk coordinates for this turtle's position
|
||||
*/
|
||||
get chunk() {
|
||||
if (!this._position) return null;
|
||||
return [Math.floor(this._position.x / 16), Math.floor(this._position.z / 16)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a response from the turtle (matched by UUID)
|
||||
*/
|
||||
|
||||
@@ -145,6 +145,25 @@ export function initializeDatabase() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Chunk analysis table (ore density per chunk)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
x INTEGER NOT NULL,
|
||||
z INTEGER NOT NULL,
|
||||
analysis TEXT DEFAULT '{}',
|
||||
scanned_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (x, z)
|
||||
)
|
||||
`);
|
||||
|
||||
// Add block_state and block_tags columns to world_blocks if not present
|
||||
try {
|
||||
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_state TEXT DEFAULT '{}'`);
|
||||
} catch (e) { /* column already exists */ }
|
||||
try {
|
||||
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_tags TEXT DEFAULT '{}'`);
|
||||
} catch (e) { /* column already exists */ }
|
||||
|
||||
// Create indexes for better performance
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_world_blocks_discovered
|
||||
@@ -156,6 +175,16 @@ export function initializeDatabase() {
|
||||
ON task_queue(status, priority DESC);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_world_blocks_name
|
||||
ON world_blocks(block_name);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_coords
|
||||
ON chunks(x, z);
|
||||
`);
|
||||
|
||||
console.log('✅ Database initialized');
|
||||
}
|
||||
|
||||
@@ -203,12 +232,14 @@ export function getTurtleConfig(turtleId) {
|
||||
}
|
||||
|
||||
// World Blocks
|
||||
export function saveWorldBlock(x, y, z, blockName, metadata, discoveredBy) {
|
||||
export function saveWorldBlock(x, y, z, blockName, metadata, discoveredBy, blockState = null, blockTags = null) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at, block_state, block_tags)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now());
|
||||
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now(),
|
||||
blockState ? JSON.stringify(blockState) : '{}',
|
||||
blockTags ? JSON.stringify(blockTags) : '{}');
|
||||
}
|
||||
|
||||
export function getWorldBlocks(limit = 10000) {
|
||||
@@ -545,5 +576,82 @@ export function closeDatabase() {
|
||||
db.close();
|
||||
}
|
||||
|
||||
// ========== CHUNK ANALYSIS ==========
|
||||
|
||||
export function saveChunkAnalysis(x, z, analysis) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO chunks (x, z, analysis, scanned_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(x, z, JSON.stringify(analysis), Date.now());
|
||||
}
|
||||
|
||||
export function getChunkAnalysis(x, z) {
|
||||
const stmt = db.prepare('SELECT * FROM chunks WHERE x = ? AND z = ?');
|
||||
const row = stmt.get(x, z);
|
||||
if (row) {
|
||||
return { ...row, analysis: JSON.parse(row.analysis) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAllChunkAnalyses() {
|
||||
const stmt = db.prepare('SELECT * FROM chunks');
|
||||
return stmt.all().map(row => ({
|
||||
...row,
|
||||
analysis: JSON.parse(row.analysis)
|
||||
}));
|
||||
}
|
||||
|
||||
// ========== BLOCK SEARCH ==========
|
||||
|
||||
export function getBlocksWithNameLike(fromX, fromY, fromZ, toX, toY, toZ, namePattern) {
|
||||
const stmt = db.prepare(`
|
||||
SELECT x, y, z, block_name as name, metadata, block_state, block_tags
|
||||
FROM world_blocks
|
||||
WHERE x BETWEEN ? AND ?
|
||||
AND y BETWEEN ? AND ?
|
||||
AND z BETWEEN ? AND ?
|
||||
AND block_name LIKE ?
|
||||
`);
|
||||
return stmt.all(
|
||||
Math.min(fromX, toX), Math.max(fromX, toX),
|
||||
Math.min(fromY, toY), Math.max(fromY, toY),
|
||||
Math.min(fromZ, toZ), Math.max(fromZ, toZ),
|
||||
namePattern
|
||||
).map(row => ({
|
||||
...row,
|
||||
state: row.block_state ? JSON.parse(row.block_state) : {},
|
||||
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
|
||||
}));
|
||||
}
|
||||
|
||||
export function getBlock(x, y, z) {
|
||||
const stmt = db.prepare('SELECT block_name as name, metadata, block_state, block_tags FROM world_blocks WHERE x = ? AND y = ? AND z = ?');
|
||||
const row = stmt.get(x, y, z);
|
||||
if (row) {
|
||||
return {
|
||||
...row,
|
||||
state: row.block_state ? JSON.parse(row.block_state) : {},
|
||||
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function deleteBlocksInArea(fromX, fromY, fromZ, toX, toY, toZ) {
|
||||
const stmt = db.prepare(`
|
||||
DELETE FROM world_blocks
|
||||
WHERE x BETWEEN ? AND ?
|
||||
AND y BETWEEN ? AND ?
|
||||
AND z BETWEEN ? AND ?
|
||||
`);
|
||||
return stmt.run(
|
||||
Math.min(fromX, toX), Math.max(fromX, toX),
|
||||
Math.min(fromY, toY), Math.max(fromY, toY),
|
||||
Math.min(fromZ, toZ), Math.max(fromZ, toZ)
|
||||
);
|
||||
}
|
||||
|
||||
// Export database instance for custom queries if needed
|
||||
export { db };
|
||||
|
||||
222
server/states/AutocraftState.js
Normal file
222
server/states/AutocraftState.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* AutocraftState - Automated crafting using a workbench peripheral
|
||||
*
|
||||
* The turtle must have a crafting table equipped as a peripheral.
|
||||
* Given a recipe (16-slot inventory layout), it will:
|
||||
* 1. Verify workbench peripheral is present
|
||||
* 2. Arrange items in the correct slots
|
||||
* 3. Pull materials from adjacent inventories
|
||||
* 4. Craft continuously
|
||||
* 5. Push crafted items into adjacent inventories
|
||||
*/
|
||||
import { BaseState } from './BaseState.js';
|
||||
|
||||
export class AutocraftState extends BaseState {
|
||||
constructor(turtle, data = {}) {
|
||||
super(turtle, data);
|
||||
// recipe: { 1: {name, count}, 2: null, 3: {name, count}, ... } (1-16 slots)
|
||||
this.recipe = data.recipe || {};
|
||||
this.craftCount = 0;
|
||||
this.totalCrafted = 0;
|
||||
this.maxCrafts = data.maxCrafts || Infinity;
|
||||
this.warning = null;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'autocrafting';
|
||||
}
|
||||
|
||||
get description() {
|
||||
if (this.warning) return `Crafting - ${this.warning}`;
|
||||
return `Crafting - ${this.totalCrafted} items crafted`;
|
||||
}
|
||||
|
||||
async *act() {
|
||||
console.log(`[${this.turtle.id}] Starting autocraft`);
|
||||
|
||||
// Verify workbench peripheral
|
||||
const hasWorkbench = await this.exec(`
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
for _, t in ipairs(types) do
|
||||
if t == "workbench" then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
`);
|
||||
|
||||
if (!hasWorkbench) {
|
||||
console.log(`[${this.turtle.id}] No workbench peripheral found`);
|
||||
this.warning = 'No crafting table equipped';
|
||||
await this._sleep(3000);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
yield;
|
||||
|
||||
// Initial craft to verify recipe works
|
||||
const initialCraft = await this.exec(`
|
||||
local workbench = peripheral.find("workbench")
|
||||
if workbench then
|
||||
local ok, msg = workbench.craft(0)
|
||||
return ok
|
||||
end
|
||||
return false
|
||||
`);
|
||||
|
||||
if (!initialCraft) {
|
||||
console.log(`[${this.turtle.id}] Recipe validation failed`);
|
||||
this.warning = 'Invalid recipe';
|
||||
await this._sleep(3000);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
yield;
|
||||
|
||||
// Main crafting loop
|
||||
while (!this.cancelled && this.totalCrafted < this.maxCrafts) {
|
||||
this.warning = null;
|
||||
let recipeReady = true;
|
||||
|
||||
// Arrange items for recipe
|
||||
for (let slot = 1; slot <= 16; slot++) {
|
||||
if (this.cancelled) return;
|
||||
|
||||
const recipeItem = this.recipe[slot];
|
||||
|
||||
// Get current item in slot
|
||||
const currentItem = await this.exec(`
|
||||
local item = turtle.getItemDetail(${slot})
|
||||
if item then return {name=item.name, count=item.count} end
|
||||
return nil
|
||||
`);
|
||||
yield;
|
||||
|
||||
if (!recipeItem) {
|
||||
// Slot should be empty - move item out if present
|
||||
if (currentItem) {
|
||||
await this._transferSlotOut(slot);
|
||||
yield;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slot needs a specific item
|
||||
if (currentItem) {
|
||||
if (currentItem.name === recipeItem.name) {
|
||||
continue; // Already has correct item
|
||||
}
|
||||
// Wrong item - move it out
|
||||
await this._transferSlotOut(slot);
|
||||
yield;
|
||||
}
|
||||
|
||||
// Pull the required item into this slot
|
||||
const pulled = await this._pullItemToSlot(recipeItem.name, slot);
|
||||
yield;
|
||||
|
||||
if (!pulled) {
|
||||
this.warning = `Missing: ${recipeItem.name.replace('minecraft:', '')}`;
|
||||
recipeReady = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipeReady) {
|
||||
yield;
|
||||
await this._sleep(5000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Craft!
|
||||
const craftResult = await this.exec(`
|
||||
local workbench = peripheral.find("workbench")
|
||||
if workbench then
|
||||
local ok, msg = workbench.craft()
|
||||
if ok then return true
|
||||
else return false
|
||||
end
|
||||
end
|
||||
return false
|
||||
`);
|
||||
|
||||
if (craftResult) {
|
||||
this.totalCrafted++;
|
||||
this.craftCount++;
|
||||
console.log(`[${this.turtle.id}] Crafted item #${this.totalCrafted}`);
|
||||
|
||||
// Push crafted items out to adjacent inventory
|
||||
await this._pushCraftedItems();
|
||||
yield;
|
||||
} else {
|
||||
this.warning = 'Craft failed';
|
||||
await this._sleep(2000);
|
||||
}
|
||||
|
||||
yield;
|
||||
await this._sleep(500);
|
||||
}
|
||||
|
||||
console.log(`[${this.turtle.id}] Autocraft finished: ${this.totalCrafted} total`);
|
||||
this.turtle.setState('idle');
|
||||
}
|
||||
|
||||
async _transferSlotOut(slot) {
|
||||
// Try to drop the item in the given slot into an adjacent inventory
|
||||
await this.exec(`
|
||||
turtle.select(${slot})
|
||||
if not turtle.dropDown() then
|
||||
if not turtle.dropUp() then
|
||||
turtle.drop()
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
`);
|
||||
}
|
||||
|
||||
async _pullItemToSlot(itemName, targetSlot) {
|
||||
// Try to pull item from adjacent inventories
|
||||
// First try suckDown, then suckUp, then suck
|
||||
const result = await this.exec(`
|
||||
turtle.select(${targetSlot})
|
||||
|
||||
-- Try each direction
|
||||
for _, suckFn in ipairs({turtle.suckDown, turtle.suckUp, turtle.suck}) do
|
||||
suckFn(1)
|
||||
local item = turtle.getItemDetail(${targetSlot})
|
||||
if item and item.name == "${itemName}" then
|
||||
return true
|
||||
elseif item then
|
||||
-- Wrong item, put it back
|
||||
for _, dropFn in ipairs({turtle.dropDown, turtle.dropUp, turtle.drop}) do
|
||||
if dropFn() then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
turtle.select(1)
|
||||
return false
|
||||
`);
|
||||
return result === true;
|
||||
}
|
||||
|
||||
async _pushCraftedItems() {
|
||||
// Push all items out of inventory
|
||||
await this.exec(`
|
||||
for slot = 1, 16 do
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item then
|
||||
turtle.select(slot)
|
||||
if not turtle.dropDown() then
|
||||
if not turtle.dropUp() then
|
||||
turtle.drop()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
`);
|
||||
}
|
||||
}
|
||||
311
server/states/BuildingState.js
Normal file
311
server/states/BuildingState.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* BuildingState - Blueprint-based autonomous block placement
|
||||
*
|
||||
* Given a list of blocks (x, y, z, name, state), the turtle will:
|
||||
* 1. Calculate required materials
|
||||
* 2. Go home to collect materials from nearby inventories
|
||||
* 3. Navigate to each block position and place it
|
||||
* 4. Handle facing-aware placement (stairs, logs, etc.)
|
||||
* 5. Track failed placements and retry later
|
||||
*/
|
||||
import { BaseState } from './BaseState.js';
|
||||
|
||||
export class BuildingState extends BaseState {
|
||||
constructor(turtle, data = {}) {
|
||||
super(turtle, data);
|
||||
// blocks: [{x, y, z, name, state?}]
|
||||
this.blocks = data.blocks || [];
|
||||
this.placedCount = 0;
|
||||
this.failedPlacements = new Map(); // "x,y,z" -> retry count
|
||||
this.maxRetries = 3;
|
||||
|
||||
// Group blocks by column (x,z) for efficient placement
|
||||
this.columns = new Map(); // "x,z" -> [{x,y,z,name,state}] sorted by y ascending
|
||||
this._organizeBlocks();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'building';
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Building - ${this.placedCount}/${this.blocks.length} placed`;
|
||||
}
|
||||
|
||||
_organizeBlocks() {
|
||||
// Filter out blocks that might already be placed (we can't check DB here,
|
||||
// but we organize for efficient traversal)
|
||||
for (const block of this.blocks) {
|
||||
const key = `${block.x},${block.z}`;
|
||||
if (!this.columns.has(key)) {
|
||||
this.columns.set(key, []);
|
||||
}
|
||||
this.columns.get(key).push(block);
|
||||
}
|
||||
|
||||
// Sort each column by Y ascending (place from bottom up)
|
||||
for (const [key, column] of this.columns) {
|
||||
column.sort((a, b) => a.y - b.y);
|
||||
}
|
||||
}
|
||||
|
||||
async *act() {
|
||||
console.log(`[${this.turtle.id}] Starting build: ${this.blocks.length} blocks to place`);
|
||||
|
||||
while (!this.cancelled) {
|
||||
// Check if we have any blocks left to place
|
||||
const remainingBlocks = this._getRemainingBlocks();
|
||||
if (remainingBlocks.length === 0) {
|
||||
console.log(`[${this.turtle.id}] Build complete: ${this.placedCount} blocks placed`);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety checks
|
||||
const fuel = await this.checkFuel();
|
||||
if (fuel !== 'unlimited' && fuel < 500) {
|
||||
const refueled = await this.tryRefuel();
|
||||
if (!refueled) {
|
||||
console.log(`[${this.turtle.id}] Low fuel during build, going home`);
|
||||
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'building', returnData: this.data });
|
||||
return;
|
||||
}
|
||||
}
|
||||
yield;
|
||||
|
||||
// Calculate required materials
|
||||
const requiredMaterials = this._calculateMaterials(remainingBlocks);
|
||||
|
||||
// Check if we have the materials
|
||||
const inventory = await this.getInventory();
|
||||
const hasMaterials = this._hasRequiredMaterials(requiredMaterials, inventory);
|
||||
yield;
|
||||
|
||||
if (!hasMaterials) {
|
||||
// Go home to collect materials
|
||||
const home = this.turtle.homePosition;
|
||||
if (!home) {
|
||||
console.log(`[${this.turtle.id}] No home set, cannot collect materials`);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[${this.turtle.id}] Going home to collect building materials`);
|
||||
|
||||
// Navigate home
|
||||
const reachedHome = yield* this.navigateTo(home);
|
||||
if (!reachedHome) {
|
||||
console.log(`[${this.turtle.id}] Cannot reach home`);
|
||||
await this._sleep(5000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
yield;
|
||||
|
||||
// Dump current inventory into nearby chests
|
||||
await this._dumpInventory();
|
||||
yield;
|
||||
|
||||
// Try to pull required materials
|
||||
const gotMaterials = await this._pullMaterials(requiredMaterials);
|
||||
yield;
|
||||
|
||||
if (!gotMaterials) {
|
||||
console.log(`[${this.turtle.id}] Cannot get required materials, waiting...`);
|
||||
await this._sleep(10000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the next block to place (closest to turtle)
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) {
|
||||
await this._sleep(1000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextBlock = this._findClosestBlock(remainingBlocks, pos);
|
||||
if (!nextBlock) {
|
||||
await this._sleep(1000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Navigate to one block above the target (to place down)
|
||||
const placePos = { x: nextBlock.x, y: nextBlock.y + 1, z: nextBlock.z };
|
||||
const reached = yield* this.navigateTo(placePos);
|
||||
yield;
|
||||
|
||||
if (!reached) {
|
||||
// Mark as failed
|
||||
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
|
||||
const retries = (this.failedPlacements.get(key) || 0) + 1;
|
||||
this.failedPlacements.set(key, retries);
|
||||
|
||||
if (retries >= this.maxRetries) {
|
||||
console.log(`[${this.turtle.id}] Cannot reach ${key}, skipping permanently`);
|
||||
}
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle facing - turn turtle to face correct direction before placing
|
||||
if (nextBlock.state && nextBlock.state.facing) {
|
||||
await this._turnForPlacement(nextBlock.state.facing);
|
||||
yield;
|
||||
}
|
||||
|
||||
// Select the correct item
|
||||
const selected = await this._selectItem(nextBlock.name);
|
||||
if (!selected) {
|
||||
console.log(`[${this.turtle.id}] Don't have ${nextBlock.name} to place`);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Place the block
|
||||
const placed = await this.exec('return turtle.placeDown()');
|
||||
yield;
|
||||
|
||||
if (placed) {
|
||||
this.placedCount++;
|
||||
// Remove from our list
|
||||
this._markBlockPlaced(nextBlock);
|
||||
console.log(`[${this.turtle.id}] Placed ${nextBlock.name} at ${nextBlock.x},${nextBlock.y},${nextBlock.z} (${this.placedCount}/${this.blocks.length})`);
|
||||
} else {
|
||||
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
|
||||
const retries = (this.failedPlacements.get(key) || 0) + 1;
|
||||
this.failedPlacements.set(key, retries);
|
||||
}
|
||||
|
||||
yield;
|
||||
await this._sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
_getRemainingBlocks() {
|
||||
const remaining = [];
|
||||
for (const [key, column] of this.columns) {
|
||||
for (const block of column) {
|
||||
const blockKey = `${block.x},${block.y},${block.z}`;
|
||||
const retries = this.failedPlacements.get(blockKey) || 0;
|
||||
if (retries < this.maxRetries) {
|
||||
remaining.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
_calculateMaterials(blocks) {
|
||||
const materials = new Map(); // name -> count
|
||||
for (const block of blocks) {
|
||||
materials.set(block.name, (materials.get(block.name) || 0) + 1);
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
_hasRequiredMaterials(required, inventory) {
|
||||
if (!inventory) return false;
|
||||
const available = new Map();
|
||||
for (const [slot, item] of Object.entries(inventory)) {
|
||||
if (item && item.name) {
|
||||
available.set(item.name, (available.get(item.name) || 0) + item.count);
|
||||
}
|
||||
}
|
||||
for (const [name, count] of required) {
|
||||
if ((available.get(name) || 0) < Math.min(count, 1)) {
|
||||
return false; // Need at least 1 of each material
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_findClosestBlock(blocks, from) {
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
for (const block of blocks) {
|
||||
const dist = Math.abs(block.x - from.x) + Math.abs(block.y - from.y) + Math.abs(block.z - from.z);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = block;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
_markBlockPlaced(block) {
|
||||
const key = `${block.x},${block.z}`;
|
||||
const column = this.columns.get(key);
|
||||
if (column) {
|
||||
const idx = column.findIndex(b => b.x === block.x && b.y === block.y && b.z === block.z);
|
||||
if (idx >= 0) column.splice(idx, 1);
|
||||
if (column.length === 0) this.columns.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async _turnForPlacement(facing) {
|
||||
// When placing blocks with facing, the turtle should face the opposite direction
|
||||
// because placed blocks face toward the placer
|
||||
const facingMap = { north: 2, south: 0, east: 3, west: 1 };
|
||||
const targetFacing = facingMap[facing];
|
||||
if (targetFacing !== undefined) {
|
||||
await this.turnToFace(targetFacing);
|
||||
}
|
||||
}
|
||||
|
||||
async _selectItem(blockName) {
|
||||
const result = await this.exec(`
|
||||
for slot = 1, 16 do
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item and item.name == "${blockName}" then
|
||||
turtle.select(slot)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
`);
|
||||
return result === true;
|
||||
}
|
||||
|
||||
async _dumpInventory() {
|
||||
await this.exec(`
|
||||
for slot = 1, 16 do
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item then
|
||||
turtle.select(slot)
|
||||
turtle.dropDown()
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
`);
|
||||
}
|
||||
|
||||
async _pullMaterials(required) {
|
||||
// Try to suck items from chests below (at home position)
|
||||
let gotAny = false;
|
||||
for (const [name, count] of required) {
|
||||
const result = await this.exec(`
|
||||
local needed = "${name}"
|
||||
for slot = 1, 16 do
|
||||
if turtle.getItemCount(slot) == 0 then
|
||||
turtle.select(slot)
|
||||
turtle.suckDown(64)
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item and item.name == needed then
|
||||
return true
|
||||
elseif item then
|
||||
turtle.dropDown()
|
||||
end
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
return false
|
||||
`);
|
||||
if (result) gotAny = true;
|
||||
}
|
||||
return gotAny;
|
||||
}
|
||||
}
|
||||
321
server/states/ExtractionState.js
Normal file
321
server/states/ExtractionState.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* ExtractionState - Smart ore extraction in a defined area
|
||||
*
|
||||
* Uses scanner peripherals to find ores, navigates to them,
|
||||
* mines them, and returns home when full or low on fuel.
|
||||
* Much more targeted than basic MiningState.
|
||||
*/
|
||||
import { BaseState } from './BaseState.js';
|
||||
|
||||
const ORE_BLOCKS = new Set([
|
||||
'minecraft:coal_ore', 'minecraft:iron_ore', 'minecraft:gold_ore',
|
||||
'minecraft:diamond_ore', 'minecraft:emerald_ore', 'minecraft:redstone_ore',
|
||||
'minecraft:lapis_ore', 'minecraft:copper_ore',
|
||||
'minecraft:deepslate_coal_ore', 'minecraft:deepslate_iron_ore',
|
||||
'minecraft:deepslate_gold_ore', 'minecraft:deepslate_diamond_ore',
|
||||
'minecraft:deepslate_emerald_ore', 'minecraft:deepslate_redstone_ore',
|
||||
'minecraft:deepslate_lapis_ore', 'minecraft:deepslate_copper_ore',
|
||||
'minecraft:nether_gold_ore', 'minecraft:nether_quartz_ore',
|
||||
'minecraft:ancient_debris',
|
||||
]);
|
||||
|
||||
export class ExtractionState extends BaseState {
|
||||
constructor(turtle, data = {}) {
|
||||
super(turtle, data);
|
||||
// Area bounds for extraction
|
||||
this.area = data.area || []; // [{x,y,z}, ...] coordinates to scan/mine
|
||||
this.minY = data.minY ?? -64;
|
||||
this.maxY = data.maxY ?? 320;
|
||||
this.oreTargets = []; // Known ore positions to mine
|
||||
this.scanPositions = []; // Positions still needing scanning
|
||||
this.remainingPositions = []; // Ore positions still to mine
|
||||
this.blocksMined = 0;
|
||||
this.oresExtracted = 0;
|
||||
this.scannerType = null;
|
||||
this.hasInitialized = false;
|
||||
|
||||
// If area not specified, generate from bounds
|
||||
if (this.area.length === 0 && data.bounds) {
|
||||
this._generateAreaFromBounds(data.bounds);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'extracting';
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Extracting - ${this.oresExtracted} ores, ${this.remainingPositions.length} remaining`;
|
||||
}
|
||||
|
||||
_generateAreaFromBounds(bounds) {
|
||||
const { minX, minY, minZ, maxX, maxY, maxZ } = bounds;
|
||||
for (let x = minX; x <= maxX; x++) {
|
||||
for (let y = minY; y <= maxY; y++) {
|
||||
for (let z = minZ; z <= maxZ; z++) {
|
||||
this.area.push({ x, y, z });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async *act() {
|
||||
console.log(`[${this.turtle.id}] Starting extraction (${this.area.length} area blocks)`);
|
||||
|
||||
// Detect scanner
|
||||
this.scannerType = await this._detectScanner();
|
||||
yield;
|
||||
|
||||
// Initialize: query DB for known ores in area, compute scan positions
|
||||
if (!this.hasInitialized) {
|
||||
await this._initializeScanPlan();
|
||||
this.hasInitialized = true;
|
||||
}
|
||||
yield;
|
||||
|
||||
while (!this.cancelled) {
|
||||
// Done if no more scan positions and no remaining ore positions
|
||||
if (this.scanPositions.length === 0 && this.remainingPositions.length === 0) {
|
||||
console.log(`[${this.turtle.id}] Extraction complete: ${this.oresExtracted} ores extracted`);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety: check fuel
|
||||
const fuel = await this.checkFuel();
|
||||
if (fuel !== 'unlimited' && fuel < 500) {
|
||||
const refueled = await this.tryRefuel();
|
||||
if (!refueled) {
|
||||
console.log(`[${this.turtle.id}] Low fuel, going home`);
|
||||
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'extracting', returnData: this.data });
|
||||
return;
|
||||
}
|
||||
}
|
||||
yield;
|
||||
|
||||
// Check inventory fullness
|
||||
const isFull = await this.isInventoryFull();
|
||||
if (isFull) {
|
||||
console.log(`[${this.turtle.id}] Inventory full, going home to dump`);
|
||||
this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'extracting', returnData: this.data });
|
||||
return;
|
||||
}
|
||||
yield;
|
||||
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) {
|
||||
await this._sleep(1000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Priority 1: Navigate to scan positions and scan
|
||||
if (this.scanPositions.length > 0) {
|
||||
const scanTarget = this._findClosest(this.scanPositions, pos);
|
||||
if (scanTarget) {
|
||||
// Navigate to scan position
|
||||
const success = yield* this.navigateTo(scanTarget);
|
||||
if (success) {
|
||||
// Perform scan
|
||||
const scannedBlocks = await this._performScan();
|
||||
yield;
|
||||
|
||||
// Process results - find ores
|
||||
if (scannedBlocks) {
|
||||
for (const block of scannedBlocks) {
|
||||
if (this._isOre(block.name)) {
|
||||
this.remainingPositions.push({ x: block.x, y: block.y, z: block.z });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this scan position
|
||||
const idx = this.scanPositions.findIndex(p => p.x === scanTarget.x && p.y === scanTarget.y && p.z === scanTarget.z);
|
||||
if (idx >= 0) this.scanPositions.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Priority 2: Navigate to and mine ore positions
|
||||
if (this.remainingPositions.length > 0) {
|
||||
const oreTarget = this._findClosest(this.remainingPositions, pos);
|
||||
if (oreTarget) {
|
||||
// Navigate adjacent to ore
|
||||
const success = yield* this.navigateTo(oreTarget);
|
||||
yield;
|
||||
|
||||
if (success) {
|
||||
// Mine the ore (we should be at or adjacent to it)
|
||||
await this._mineAt(oreTarget);
|
||||
this.oresExtracted++;
|
||||
}
|
||||
|
||||
// Remove from remaining
|
||||
const idx = this.remainingPositions.findIndex(p => p.x === oreTarget.x && p.y === oreTarget.y && p.z === oreTarget.z);
|
||||
if (idx >= 0) this.remainingPositions.splice(idx, 1);
|
||||
}
|
||||
yield;
|
||||
}
|
||||
|
||||
await this._sleep(200);
|
||||
yield;
|
||||
}
|
||||
}
|
||||
|
||||
async _detectScanner() {
|
||||
const result = await this.exec(`
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
for _, t in ipairs(types) do
|
||||
if t == "geoScanner" then return "geoScanner"
|
||||
elseif t == "universal_scanner" then return "universal_scanner"
|
||||
elseif t == "plethora:scanner" then return "plethora:scanner"
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
return result || 'native';
|
||||
}
|
||||
|
||||
async _initializeScanPlan() {
|
||||
// Query the DB for known ores in the area
|
||||
if (this.area.length > 0) {
|
||||
const bounds = this._getBoundingBox();
|
||||
try {
|
||||
const knownOres = this.turtle.server.db.getBlocksWithNameLike(
|
||||
bounds.minX, bounds.minY, bounds.minZ,
|
||||
bounds.maxX, bounds.maxY, bounds.maxZ,
|
||||
'%_ore'
|
||||
);
|
||||
for (const ore of knownOres) {
|
||||
this.remainingPositions.push({ x: ore.x, y: ore.y, z: ore.z });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[${this.turtle.id}] DB ore query failed:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute scan positions (evenly spaced grid across area)
|
||||
if (this.scannerType !== 'native' && this.area.length > 0) {
|
||||
const bounds = this._getBoundingBox();
|
||||
const scanSpacing = this.scannerType === 'plethora:scanner' ? 16 : 32;
|
||||
|
||||
for (let x = bounds.minX; x <= bounds.maxX; x += scanSpacing) {
|
||||
for (let z = bounds.minZ; z <= bounds.maxZ; z += scanSpacing) {
|
||||
const y = Math.floor((bounds.minY + bounds.maxY) / 2);
|
||||
this.scanPositions.push({ x, y, z });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getBoundingBox() {
|
||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
||||
for (const p of this.area) {
|
||||
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
|
||||
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
|
||||
minZ = Math.min(minZ, p.z); maxZ = Math.max(maxZ, p.z);
|
||||
}
|
||||
return { minX, minY, minZ, maxX, maxY, maxZ };
|
||||
}
|
||||
|
||||
_findClosest(positions, from) {
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
for (const pos of positions) {
|
||||
const dist = Math.abs(pos.x - from.x) + Math.abs(pos.y - from.y) + Math.abs(pos.z - from.z);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = pos;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
_isOre(name) {
|
||||
return ORE_BLOCKS.has(name) || (name && name.endsWith('_ore'));
|
||||
}
|
||||
|
||||
async _performScan() {
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) return null;
|
||||
|
||||
let rawBlocks = null;
|
||||
|
||||
if (this.scannerType === 'geoScanner') {
|
||||
rawBlocks = await this.exec(`
|
||||
local scanner = peripheral.find("geoScanner")
|
||||
if scanner then
|
||||
local ok, blocks = pcall(scanner.scan, 16)
|
||||
if ok then return blocks end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
} else if (this.scannerType === 'universal_scanner') {
|
||||
rawBlocks = await this.exec(`
|
||||
local scanner = peripheral.find("universal_scanner")
|
||||
if scanner then
|
||||
local ok, blocks = pcall(scanner.scan, "block", 16)
|
||||
if ok then return blocks end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
} else if (this.scannerType === 'plethora:scanner') {
|
||||
rawBlocks = await this.exec(`
|
||||
local scanner = peripheral.find("plethora:scanner")
|
||||
if scanner then
|
||||
local ok, blocks = pcall(scanner.scan)
|
||||
if ok then return blocks end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
}
|
||||
|
||||
if (!rawBlocks || !Array.isArray(rawBlocks)) return null;
|
||||
|
||||
// Convert relative positions to absolute
|
||||
return rawBlocks
|
||||
.filter(b => b && b.name && b.name !== 'minecraft:air')
|
||||
.map(b => ({
|
||||
x: pos.x + (b.x || 0),
|
||||
y: pos.y + (b.y || 0),
|
||||
z: pos.z + (b.z || 0),
|
||||
name: b.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async _mineAt(target) {
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) return;
|
||||
|
||||
const dx = target.x - pos.x;
|
||||
const dy = target.y - pos.y;
|
||||
const dz = target.z - pos.z;
|
||||
|
||||
// We should be at or adjacent - mine in appropriate direction
|
||||
if (dy === 1 || (dy === 0 && dx === 0 && dz === 0)) {
|
||||
await this.exec('turtle.digUp()');
|
||||
} else if (dy === -1) {
|
||||
await this.exec('turtle.digDown()');
|
||||
} else {
|
||||
// Face the block and dig
|
||||
let targetFacing;
|
||||
if (dx === 1) targetFacing = 1;
|
||||
else if (dx === -1) targetFacing = 3;
|
||||
else if (dz === 1) targetFacing = 2;
|
||||
else if (dz === -1) targetFacing = 0;
|
||||
else targetFacing = this.turtle.facing;
|
||||
|
||||
await this.turnToFace(targetFacing);
|
||||
await this.exec('turtle.dig()');
|
||||
}
|
||||
|
||||
this.blocksMined++;
|
||||
this.turtle.emit('blockMined', { blockType: 'ore' });
|
||||
}
|
||||
}
|
||||
184
server/states/ScanState.js
Normal file
184
server/states/ScanState.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* ScanState - Systematic scanning of surroundings using peripheral scanners
|
||||
*
|
||||
* Supports multiple scanner types:
|
||||
* - geoScanner (Advanced Peripherals)
|
||||
* - universal_scanner
|
||||
* - plethora:scanner (Plethora mod)
|
||||
* - Native turtle.inspect() fallback (6-directional scan)
|
||||
*/
|
||||
import { BaseState } from './BaseState.js';
|
||||
|
||||
export class ScanState extends BaseState {
|
||||
constructor(turtle, data = {}) {
|
||||
super(turtle, data);
|
||||
this.scanRadius = data.scanRadius || 16;
|
||||
this.blocksScanned = 0;
|
||||
this.scannerType = null;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'scanning';
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Scanning (${this.scannerType || 'detecting'}) - ${this.blocksScanned} blocks`;
|
||||
}
|
||||
|
||||
async *act() {
|
||||
console.log(`[${this.turtle.id}] Starting scan`);
|
||||
|
||||
// Detect available scanner peripheral
|
||||
this.scannerType = await this._detectScanner();
|
||||
yield;
|
||||
|
||||
if (this.scannerType === 'geoScanner') {
|
||||
yield* this._geoScan();
|
||||
} else if (this.scannerType === 'universal_scanner') {
|
||||
yield* this._universalScan();
|
||||
} else if (this.scannerType === 'plethora:scanner') {
|
||||
yield* this._plethuraScan();
|
||||
} else {
|
||||
// Fallback to native inspect-based scanning
|
||||
yield* this._nativeScan();
|
||||
}
|
||||
|
||||
console.log(`[${this.turtle.id}] Scan complete: ${this.blocksScanned} blocks`);
|
||||
this.turtle.setState('idle');
|
||||
}
|
||||
|
||||
async _detectScanner() {
|
||||
// Check for geoScanner
|
||||
const geoResult = await this.exec(`
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
for _, t in ipairs(types) do
|
||||
if t == "geoScanner" then return "geoScanner" end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
if (geoResult) return geoResult;
|
||||
|
||||
// Check for universal_scanner
|
||||
const uniResult = await this.exec(`
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
for _, t in ipairs(types) do
|
||||
if t == "universal_scanner" then return "universal_scanner" end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
if (uniResult) return uniResult;
|
||||
|
||||
// Check for plethora scanner
|
||||
const plResult = await this.exec(`
|
||||
local names = peripheral.getNames()
|
||||
for _, name in ipairs(names) do
|
||||
local types = {peripheral.getType(name)}
|
||||
for _, t in ipairs(types) do
|
||||
if t == "plethora:scanner" then return "plethora:scanner" end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
`);
|
||||
if (plResult) return plResult;
|
||||
|
||||
return 'native';
|
||||
}
|
||||
|
||||
async *_geoScan() {
|
||||
// Wait for cooldown
|
||||
yield;
|
||||
await this._sleep(500);
|
||||
|
||||
const result = await this.exec(`
|
||||
local scanner = peripheral.find("geoScanner")
|
||||
if not scanner then return nil end
|
||||
local ok, blocks = pcall(scanner.scan, ${this.scanRadius})
|
||||
if ok then return blocks end
|
||||
return nil
|
||||
`);
|
||||
|
||||
if (result && Array.isArray(result)) {
|
||||
this._processScannedBlocks(result);
|
||||
}
|
||||
yield;
|
||||
}
|
||||
|
||||
async *_universalScan() {
|
||||
yield;
|
||||
await this._sleep(500);
|
||||
|
||||
const result = await this.exec(`
|
||||
local scanner = peripheral.find("universal_scanner")
|
||||
if not scanner then return nil end
|
||||
local ok, blocks = pcall(scanner.scan, "block", ${this.scanRadius})
|
||||
if ok then return blocks end
|
||||
return nil
|
||||
`);
|
||||
|
||||
if (result && Array.isArray(result)) {
|
||||
this._processScannedBlocks(result);
|
||||
}
|
||||
yield;
|
||||
}
|
||||
|
||||
async *_plethuraScan() {
|
||||
yield;
|
||||
|
||||
const result = await this.exec(`
|
||||
local scanner = peripheral.find("plethora:scanner")
|
||||
if not scanner then return nil end
|
||||
local ok, blocks = pcall(scanner.scan)
|
||||
if ok then return blocks end
|
||||
return nil
|
||||
`);
|
||||
|
||||
if (result && Array.isArray(result)) {
|
||||
this._processScannedBlocks(result);
|
||||
}
|
||||
yield;
|
||||
}
|
||||
|
||||
async *_nativeScan() {
|
||||
// Fallback: scan all 6 directions using turtle.inspect
|
||||
const scanResult = await this.scanSurroundings();
|
||||
if (scanResult) {
|
||||
this.blocksScanned += Object.keys(scanResult).length;
|
||||
}
|
||||
yield;
|
||||
}
|
||||
|
||||
_processScannedBlocks(blocks) {
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) return;
|
||||
|
||||
const discoveredBlocks = [];
|
||||
for (const block of blocks) {
|
||||
if (!block || !block.name) continue;
|
||||
if (block.name === 'minecraft:air') continue;
|
||||
if (block.x === 0 && block.y === 0 && block.z === 0) continue;
|
||||
|
||||
discoveredBlocks.push({
|
||||
x: pos.x + (block.x || 0),
|
||||
y: pos.y + (block.y || 0),
|
||||
z: pos.z + (block.z || 0),
|
||||
name: block.name,
|
||||
metadata: block.metadata || 0,
|
||||
state: block.state || {},
|
||||
tags: block.tags || {},
|
||||
discoveredBy: this.turtle.id,
|
||||
});
|
||||
}
|
||||
|
||||
this.blocksScanned = discoveredBlocks.length;
|
||||
|
||||
if (discoveredBlocks.length > 0) {
|
||||
this.turtle.emit('blocksDiscovered', discoveredBlocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user