519 lines
15 KiB
Lua
519 lines
15 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 Channels = require('platform.channels')
|
|
|
|
local CHANNEL_RECEIVE = Channels.get('remoteturtle.command')
|
|
local CHANNEL_SEND = Channels.get('remoteturtle.response')
|
|
local STATUS_CHANNEL = Channels.get('remoteturtle.status')
|
|
|
|
-- 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 WebBridge = require('platform.webbridge')
|
|
local modem, modemSide = WebBridge.findModem(true)
|
|
if not modem then
|
|
error("No wireless modem found!")
|
|
end
|
|
-- Open command channel (respects dual-mode migration)
|
|
WebBridge.openChannels(modem, { 'remoteturtle.command' })
|
|
|
|
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)
|
|
-- Uses Channels.match() for dual-mode safety: accepts messages on
|
|
-- both legacy (100) and target (4210) channels during migration.
|
|
while true do
|
|
local event, side, channel, replyChannel, message = os.pullEvent("modem_message")
|
|
if Channels.match('remoteturtle.command', channel) 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
|
|
)
|