Files
remoteturtle/turtle.lua

513 lines
14 KiB
Lua

-- Server-Driven Turtle v5 (Pure Eval Protocol)
-- All behavior is driven by the server via eval commands.
-- This script only handles: eval execution, status broadcasting,
-- GPS tracking, inventory/peripheral events.
local CHANNEL_RECEIVE = 100
local CHANNEL_SEND = 101
local STATUS_CHANNEL = 102
-- State tracking (lightweight - server drives everything)
local state = {
position = nil,
homePosition = nil,
fuel = 0,
inventory = {},
facing = 0,
lastStatusUpdate = 0,
}
-- Configuration
local config = {
statusUpdateInterval = 5,
}
-- Check for modem
local modem = peripheral.find("modem")
if not modem then
error("No wireless modem found!")
end
modem.open(CHANNEL_RECEIVE)
print("Server-Driven Turtle v5 (Pure Eval Protocol)")
print("ID: " .. os.getComputerID())
print("Modem opened on channel " .. CHANNEL_RECEIVE)
-- Store facing in global for eval access
_G._turtleFacing = 0
-- ========== GPS Functions ==========
local function updatePosition()
local x, y, z = gps.locate(2)
if x then
state.position = {x = math.floor(x), y = math.floor(y), z = math.floor(z)}
return true
end
return false
end
-- ========== Fuel & Inventory ==========
local function updateFuel()
state.fuel = turtle.getFuelLevel()
return state.fuel
end
local function updateInventory()
state.inventory = {}
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
table.insert(state.inventory, {
slot = slot,
name = item.name,
count = item.count
})
end
end
end
-- ========== Status Broadcasting ==========
local function broadcastStatus()
updateFuel()
updateInventory()
local surroundings = {}
local hasBlock, data
hasBlock, data = turtle.inspect()
if hasBlock then surroundings.forward = {name = data.name, metadata = data.metadata or 0} end
hasBlock, data = turtle.inspectUp()
if hasBlock then surroundings.up = {name = data.name, metadata = data.metadata or 0} end
hasBlock, data = turtle.inspectDown()
if hasBlock then surroundings.down = {name = data.name, metadata = data.metadata or 0} end
local statusPacket = {
type = "status",
turtleID = os.getComputerID(),
position = state.position,
homePosition = state.homePosition,
fuel = state.fuel,
inventoryCount = #state.inventory,
inventory = state.inventory,
facing = state.facing,
surroundings = surroundings,
evalSupported = true,
label = os.getComputerLabel(),
}
modem.transmit(STATUS_CHANNEL, CHANNEL_RECEIVE, statusPacket)
end
-- ========== Eval/Response Protocol ==========
local function executeEval(uuid, code)
local fn, compileError = load(code, "eval", "t")
if not fn then
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval_response",
uuid = uuid,
result = nil,
error = "Compile error: " .. tostring(compileError),
turtleID = os.getComputerID()
})
return
end
local results = table.pack(pcall(fn))
local ok = results[1]
if ok then
local returnVal
if results.n <= 2 then
returnVal = results[2]
else
returnVal = {}
for i = 2, results.n do
returnVal[i - 1] = results[i]
end
end
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval_response",
uuid = uuid,
result = returnVal,
error = nil,
turtleID = os.getComputerID()
})
else
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "eval_response",
uuid = uuid,
result = nil,
error = "Runtime error: " .. tostring(results[2]),
turtleID = os.getComputerID()
})
end
end
-- ========== Message Processing ==========
local function processMessage(channel, message)
if type(message) ~= "table" then return end
local myID = os.getComputerID()
if message.target and message.target ~= myID then return end
if message.type == "eval" and message.uuid and message.code then
print("Eval: " .. message.uuid:sub(1, 8) .. "...")
executeEval(message.uuid, message.code)
elseif message.type == "home_position" and message.turtleID == myID then
if message.homePosition then
state.homePosition = message.homePosition
print("Home synced from server")
end
elseif message.type == "home_set_confirm" and message.turtleID == myID then
if message.homePosition then
state.homePosition = message.homePosition
print("Home confirmed by server")
end
elseif message.type == "rename" and message.name then
os.setComputerLabel(message.name)
print("Renamed to: " .. message.name)
broadcastStatus()
end
end
-- ========== Sync Home ==========
local function syncHomeWithServer()
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "request_home",
turtleID = os.getComputerID()
})
return true
end
-- ========== Initialization ==========
print("Initializing...")
local x, y, z = gps.locate(5)
if x then
state.position = {x = math.floor(x), y = math.floor(y), z = math.floor(z)}
print("GPS: OK - " .. state.position.x .. "," .. state.position.y .. "," .. state.position.z)
else
print("GPS: Not available")
state.position = nil
end
syncHomeWithServer()
updateFuel()
-- ========== Movement Wrapping (heading + position tracking) ==========
-- Heading: 0=south(+z), 1=west(-x), 2=north(-z), 3=east(+x)
local MOVE_DELTA = {
[0] = { x = 0, z = 1 }, -- south
[1] = { x = -1, z = 0 }, -- west
[2] = { x = 0, z = -1 }, -- north
[3] = { x = 1, z = 0 }, -- east
}
local _rawForward = turtle.forward
local _rawBack = turtle.back
local _rawUp = turtle.up
local _rawDown = turtle.down
local _rawTurnLeft = turtle.turnLeft
local _rawTurnRight = turtle.turnRight
turtle.forward = function()
local ok, reason = _rawForward()
if ok and state.position then
local d = MOVE_DELTA[state.facing]
state.position.x = state.position.x + d.x
state.position.z = state.position.z + d.z
end
return ok, reason
end
turtle.back = function()
local ok, reason = _rawBack()
if ok and state.position then
local d = MOVE_DELTA[state.facing]
state.position.x = state.position.x - d.x
state.position.z = state.position.z - d.z
end
return ok, reason
end
turtle.up = function()
local ok, reason = _rawUp()
if ok and state.position then
state.position.y = state.position.y + 1
end
return ok, reason
end
turtle.down = function()
local ok, reason = _rawDown()
if ok and state.position then
state.position.y = state.position.y - 1
end
return ok, reason
end
turtle.turnLeft = function()
local ok = _rawTurnLeft()
if ok then
state.facing = (state.facing + 3) % 4
_G._turtleFacing = state.facing
end
return ok
end
turtle.turnRight = function()
local ok = _rawTurnRight()
if ok then
state.facing = (state.facing + 1) % 4
_G._turtleFacing = state.facing
end
return ok
end
-- Detect heading via GPS triangulation
local function detectHeading()
local x1, _, z1 = gps.locate(2)
if not x1 then return false end
if _rawForward() then
local x2, _, z2 = gps.locate(2)
_rawBack()
if x2 then
local dx = math.floor(x2) - math.floor(x1)
local dz = math.floor(z2) - math.floor(z1)
if dz > 0 then state.facing = 0 -- south
elseif dz < 0 then state.facing = 2 -- north
elseif dx > 0 then state.facing = 3 -- east
elseif dx < 0 then state.facing = 1 -- west
end
_G._turtleFacing = state.facing
return true
end
end
return false
end
if state.position then
if detectHeading() then
print("Heading: " .. ({"south","west","north","east"})[state.facing + 1])
else
print("Heading: unknown (blocked)")
end
end
-- ========== Pathfinding Module ==========
local pathfind = {}
--- Face a target heading.
function pathfind.face(targetH)
targetH = targetH % 4
while state.facing ~= targetH do
local diff = (targetH - state.facing) % 4
if diff == 1 then
turtle.turnRight()
elseif diff == 3 then
turtle.turnLeft()
else
turtle.turnRight()
turtle.turnRight()
end
end
end
--- Navigate to target coordinates with simple obstacle avoidance.
-- @param tx, ty, tz target position
-- @param options { dig = false, maxAttempts = 256 }
-- @return true or false, error
function pathfind.goto(tx, ty, tz, options)
options = options or {}
local maxAttempts = options.maxAttempts or 256
local dig = options.dig or false
local attempts = 0
while attempts < maxAttempts do
local pos = state.position
if not pos then return false, "No GPS position" end
-- Arrived?
if pos.x == tx and pos.y == ty and pos.z == tz then
return true
end
attempts = attempts + 1
local moved = false
-- Priority: Y first (get to correct height), then X, then Z
if not moved and pos.y ~= ty then
if pos.y < ty then
moved = turtle.up()
if not moved and dig then turtle.digUp(); moved = turtle.up() end
else
moved = turtle.down()
if not moved and dig then turtle.digDown(); moved = turtle.down() end
end
end
if not moved and pos.x ~= tx then
pathfind.face(tx > pos.x and 3 or 1)
moved = turtle.forward()
if not moved and dig then turtle.dig(); moved = turtle.forward() end
end
if not moved and pos.z ~= tz then
pathfind.face(tz > pos.z and 0 or 2)
moved = turtle.forward()
if not moved and dig then turtle.dig(); moved = turtle.forward() end
end
-- Obstacle avoidance: try going around
if not moved then
if turtle.up() then
moved = true
elseif turtle.down() then
moved = true
else
turtle.turnRight()
if turtle.forward() then
moved = true
else
turtle.turnLeft()
turtle.turnLeft()
if turtle.forward() then
moved = true
else
turtle.turnRight() -- restore heading
return false, "Stuck at " .. pos.x .. "," .. pos.y .. "," .. pos.z
end
end
end
end
end
return false, "Max attempts exceeded"
end
--- Go home (to saved home position).
function pathfind.goHome(options)
if not state.homePosition then return false, "No home position set" end
return pathfind.goto(state.homePosition.x, state.homePosition.y, state.homePosition.z, options)
end
--- Get current heading name.
function pathfind.headingName()
return ({"south","west","north","east"})[state.facing + 1]
end
-- Expose globally for eval access
_G._pathfind = pathfind
print("Ready! Turtle " .. os.getComputerID() .. " online (v5 server-driven + pathfinding)")
broadcastStatus()
-- ========== Main Loop ==========
parallel.waitForAny(
function()
-- GPS retry loop
while true do
if not state.position then
sleep(10)
if updatePosition() then
print("GPS acquired!")
broadcastStatus()
end
else
sleep(30)
end
end
end,
function()
-- Status broadcast loop
while true do
sleep(config.statusUpdateInterval)
broadcastStatus()
end
end,
function()
-- Command processing (eval protocol)
while true do
local event, side, channel, replyChannel, message = os.pullEvent("modem_message")
if channel == CHANNEL_RECEIVE then
processMessage(channel, message)
end
end
end,
function()
-- Inventory change events (real-time)
while true do
os.pullEvent("turtle_inventory")
local inventory = {}
for i = 1, 16 do
local item = turtle.getItemDetail(i)
if item then
table.insert(inventory, {
slot = i,
name = item.name,
count = item.count
})
end
end
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "inventory_update",
turtleID = os.getComputerID(),
inventory = inventory,
timestamp = os.epoch("utc")
})
end
end,
function()
-- Peripheral attached events (real-time)
while true do
os.pullEvent("peripheral")
sleep(0.1) -- Small delay to let CC register
local peripherals = {}
local names = peripheral.getNames()
for _, name in ipairs(names) do
peripherals[name] = {types = {peripheral.getType(name)}}
end
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "peripheral_attached",
turtleID = os.getComputerID(),
peripherals = peripherals,
timestamp = os.epoch("utc")
})
end
end,
function()
-- Peripheral detached events (real-time)
while true do
local _, side = os.pullEvent("peripheral_detach")
if not peripheral.isPresent(side) then
modem.transmit(CHANNEL_SEND, CHANNEL_RECEIVE, {
type = "peripheral_detached",
turtleID = os.getComputerID(),
side = side,
timestamp = os.epoch("utc")
})
end
end
end
)