Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- ### Evil_Bengt, 2024-05-01 00:33:07
- -- ### MODULE: constants
- -- ### PATH: ./src/constants.lua
- WorkingSide = {
- left = "left",
- right = "right"
- }
- Direction = {
- facingCorridor = "the corridor",
- awayFromCorridor = "away from the corridor",
- outbound = "outbound",
- inbound = "inbound"
- }
- Communication = {}
- Communication.protocol = {
- request = "EVB_TeamTurtles_Request",
- response = "EVB_TeamTurtles_Response"
- }
- Communication.messages = {
- requestLayer = "requestLayer",
- getProject = "getProject"
- }
- Filenames = {
- project = "tt_project",
- state = "tt_state",
- turnFile = "tt_turns",
- filter = "tt_filter",
- serverState = "tt_server_state"
- }
- TurtleBlockTag = "computercraft:turtle"
- RefuelPosition = {
- home = 1,
- spawn = 2
- }
- MinimumNeededFuel = 200
- ProjectIdLength = 10
- -- ### MODULE: core
- -- ### PATH: ./src/core.lua
- -- MINING, MAIN FUNCTIONS --
- function Mine(nextPhaseIfFullFunction, inspectFunction, digFunction)
- local any, data = inspectFunction()
- if any and IsInteresting(data) then
- if CheckInventoryIsFull() then
- return nextPhaseIfFullFunction()
- end
- digFunction()
- end
- end
- function MineInfront(nextPhaseIfFullFunction)
- return function ()
- return Mine(nextPhaseIfFullFunction, turtle.inspect, turtle.dig)
- end
- end
- function MineAbove(nextPhaseIfFullFunction)
- return function ()
- return Mine(nextPhaseIfFullFunction, turtle.inspectUp, turtle.digUp)
- end
- end
- function MineBelow(nextPhaseIfFullFunction)
- return function ()
- return Mine(nextPhaseIfFullFunction, turtle.inspectDown, turtle.digDown)
- end
- end
- -- MINING, HELPERS --
- function CheckInventoryIsFull()
- return turtle.getItemCount(16) > 0
- end
- function IsInteresting(blockData)
- local result = true
- local opcode = nil
- local filterText = nil
- local subResult = nil
- for _, line in pairs(Project.filters) do
- opcode = string.sub(line, 1, 2)
- filterText = string.sub(line, 4, -1)
- subResult = FilterFunctions[opcode](filterText, blockData)
- if subResult ~= nil then
- result = subResult
- end
- end
- return result
- end
- FilterFunctions = {
- ["++"] = function (filter, blockData) return true end,
- ["--"] = function (filter, blockData) return false end,
- ["+n"] = function (filter, blockData)
- if blockData.name == filter then
- return true
- end
- return nil
- end,
- ["-n"] = function (filter, blockData)
- if blockData.name == filter then
- return false
- end
- return nil
- end,
- ["+t"] = function (filter, blockData)
- if blockData.tags[filter] == true then
- return true
- end
- return nil
- end,
- ["-t"] = function (filter, blockData)
- if blockData.tags[filter] == true then
- return false
- end
- return nil
- end
- }
- -- REFUELING --
- function Refuel(refuelPosition)
- local function suckFuelInfront()
- turtle.suck()
- return turtle.getItemCount() > 0
- end
- local function suckFuelBelow()
- turtle.suckDown()
- return turtle.getItemCount() > 0
- end
- local neededFuel = 0
- neededFuel = neededFuel + AssignedLayer * 2
- neededFuel = neededFuel + Project.width * Project.height / 3
- neededFuel = neededFuel + Project.height * 2
- neededFuel = neededFuel + 10
- neededFuel = neededFuel + (refuelPosition == RefuelPosition.spawn and 1 or 0)
- neededFuel = math.max(neededFuel, MinimumNeededFuel)
- turtle.select(1)
- while turtle.getFuelLevel() < neededFuel do
- Ensure(refuelPosition == RefuelPosition.home and suckFuelInfront or suckFuelBelow,
- true, "Cannot refuel.", "Got fuel.")
- if not turtle.refuel() then
- if refuelPosition == RefuelPosition.spawn then
- error("Cannot refuel, non-fuel items in fuel chest!")
- end
- Ensure(turtle.dropDown, true, "Cannot empty.", "Emptied successfully.")
- end
- end
- end
- -- COMMUNICATION --
- function RequestLayer()
- local payload = {
- message = Communication.messages.requestLayer,
- previousLayer = AssignedLayer,
- projectId = Project.projectId
- }
- local responseRaw = SendRequest(Project.serverAddress, textutils.serialize(payload))
- local response = textutils.unserialize(responseRaw)
- if not response.layer then
- BroadcastFatalError("Decomissioned.")
- end
- AssignedLayer = response.layer
- print("AssignedLayer = " .. AssignedLayer)
- end
- function FetchProject(serverAddress)
- local payload = {
- message = Communication.messages.getProject
- }
- local response = SendRequest(serverAddress, textutils.serialize(payload))
- Project = textutils.unserialize(response)
- print("Project = " .. textutils.serialize(Project))
- end
- function SendRequest(id, msg)
- while true do
- rednet.send(id, msg, Communication.protocol.request)
- local responseId, responseMsg = rednet.receive(Communication.protocol.response, 10)
- if responseId == id then
- return responseMsg
- end
- BroadcastError("Cannot reach '" .. id .. "'.")
- end
- end
- -- ### MODULE: globalState
- -- ### PATH: ./src/globalState.lua
- -- Persisted
- Project = {
- serverAddress = nil, -- integer
- projectId = nil, -- string
- width = nil, -- integer
- height = nil, -- integer
- workingSide = nil, -- WorkingSide,
- filters = nil, -- table
- }
- -- Persisted
- AssignedLayer = nil
- InitialFuel = nil
- ActivePhase = nil
- PhaseArgs = nil
- -- Not persisted
- CompletedSteps = 0
- TurnFile = nil
- -- ### MODULE: misc
- -- ### PATH: ./src/misc.lua
- -- MISC FUNCTIONS --
- function Me()
- ---@diagnostic disable-next-line: undefined-field
- return "TT_" .. os.getComputerID() .. "@" .. (Project.serverAddress or "_")
- end
- function BroadcastError(msg, printError)
- if printError ~= false then
- print("!! " .. msg)
- end
- rednet.broadcast("!! " .. Me() .. ": " .. msg)
- end
- function BroadcastSuccess(msg)
- rednet.broadcast("## " .. Me() .. ": " .. msg)
- end
- function Ensure(fun, expect, errorMessage, resolveMessage)
- local announced = false
- if not not fun() == expect then
- return
- end
- while true do
- for _ = 1, 10 do
- sleep(1)
- if not not fun() == expect then
- if announced and resolveMessage then
- BroadcastSuccess(resolveMessage)
- end
- return
- end
- end
- BroadcastError(errorMessage)
- announced = true
- end
- end
- function BroadcastFatalError(msg, printError)
- for i = 1, 10 do
- BroadcastError(msg, printError)
- sleep(60)
- end
- error(msg)
- end
- -- ### MODULE: movement
- -- ### PATH: ./src/movement.lua
- -- MOVEMENT --
- function Forward()
- YieldForTurtle(TurtleInfront)
- while not turtle.forward() do
- turtle.attack()
- turtle.dig()
- end
- ResetTurnFile()
- end
- function Up()
- YieldForTurtle(TurtleAbove)
- while not turtle.up() do
- turtle.attackUp()
- turtle.digUp()
- end
- ResetTurnFile()
- end
- function Down()
- YieldForTurtle(TurtleBelow)
- while not turtle.down() do
- turtle.attackDown()
- turtle.digDown()
- end
- ResetTurnFile()
- end
- function Right()
- StartTurn()
- turtle.turnRight()
- EndTurn()
- end
- function Left()
- StartTurn()
- turtle.turnLeft()
- EndTurn()
- end
- -- RECORD-KEEPING --
- function ResetTurnFile()
- if TurnFile then
- TurnFile.close()
- TurnFile = nil
- end
- if fs.exists(Filenames.turnFile) then
- fs.delete(Filenames.turnFile)
- end
- end
- function StartTurn()
- if not TurnFile then
- if fs.exists(Filenames.turnFile) then
- fs.delete(Filenames.turnFile)
- end
- TurnFile = fs.open(Filenames.turnFile, "w")
- end
- local any, data = turtle.inspect()
- TurnFile.writeLine(textutils.serialize({
- blockInfront = any and data.name or nil
- }))
- end
- function EndTurn()
- if not TurnFile then
- if fs.exists(Filenames.turnFile) then
- fs.delete(Filenames.turnFile)
- end
- TurnFile = fs.open(Filenames.turnFile, "w")
- end
- TurnFile.writeLine("ok")
- end
- -- ANTI-COLLISION --
- function TurtleInfront()
- local any, data = turtle.inspect()
- return any and data.tags[TurtleBlockTag]
- end
- function TurtleAbove()
- local any, data = turtle.inspectUp()
- return any and data.tags[TurtleBlockTag]
- end
- function TurtleBelow()
- local any, data = turtle.inspectDown()
- return any and data.tags[TurtleBlockTag]
- end
- function YieldForTurtle(checkFunction)
- Ensure(checkFunction, false, "Blocked by other turtle.", "Unblocked.")
- end
- -- ### MODULE: persistence
- -- ### PATH: ./src/persistence.lua
- -- FUNCTIONS --
- function PersistProject()
- local fileHandle = fs.open(Filenames.project, "w")
- fileHandle.write(textutils.serialize(Project))
- fileHandle.close()
- end
- function LoadProject()
- if not fs.exists(Filenames.project) then
- BroadcastFatalError("Cannot load project file.")
- end
- local fileHandle = fs.open(Filenames.project, "r")
- Project = textutils.unserialize(fileHandle.readAll())
- fileHandle.close()
- end
- function PersistState()
- local fileHandle = fs.open(Filenames.state, "w")
- local state = {
- assignedLayer = AssignedLayer,
- initialFuel = InitialFuel,
- activePhase = ActivePhase.name,
- phaseArgs = PhaseArgs
- }
- local serializedState = textutils.serialize(state)
- fileHandle.write(serializedState)
- fileHandle.close()
- end
- function LoadState()
- if not fs.exists(Filenames.state) then
- BroadcastFatalError("Cannot load state.")
- end
- local fileHandle = fs.open(Filenames.state, "r")
- local serializedState = fileHandle.readAll()
- fileHandle.close()
- local state = textutils.unserialize(serializedState)
- AssignedLayer = state.assignedLayer
- InitialFuel = state.initialFuel
- ActivePhase = Phase[state.activePhase]
- PhaseArgs = state.phaseArgs
- end
- function LoadTurnFile()
- local unfinished = nil
- local turns = 0
- if fs.exists(Filenames.turnFile) then
- local handle = fs.open(Filenames.turnFile, "r")
- local line = nil
- repeat
- line = handle.readLine()
- if line == "ok" then
- turns = turns + 1
- unfinished = nil
- elseif line ~= nil then
- unfinished = line
- end
- until line == nil
- handle.close()
- end
- return unfinished and textutils.unserialize(unfinished) or turns
- end
- function LoadFilters()
- local filters = {}
- if not fs.exists(Filenames.filter) then
- return filters
- end
- local handle = fs.open(Filenames.filter, "r")
- local line = nil
- repeat
- line = handle.readLine()
- if line then
- table.insert(filters, line)
- end
- until not line
- handle.close()
- return filters
- end
- -- ### MODULE: phases
- -- ### PATH: ./src/phases.lua
- Phase = {
- inbound = { name = "inbound" },
- outbound = { name = "outbound" },
- working = { name = "working" },
- backtracking = { name = "backtracking" },
- emptyAndRefuel = { name = "emptyAndRefuel" }
- }
- -- GLOBAL FUNCTIONS
- function Resume(steps)
- local moves = InitialFuel - turtle.getFuelLevel()
- local turns = LoadTurnFile()
- local isBlockInfront, blockInfront = turtle.inspect()
- if type(turns) == "table" and isBlockInfront and blockInfront == turns.blockInfront then
- print("I got lost while turning. Please:")
- print("- Place me at the spawn")
- print("- Terminate the program")
- print("- Rejoin the project (server id: " .. Project.serverAddress .. ")")
- local location = "basecamp"
- if ActivePhase.name == Phase.working.name or ActivePhase.name == Phase.backtracking.name then
- location = "layer " .. AssignedLayer
- elseif ActivePhase.name == Phase.inbound or ActivePhase.name == Phase.outbound.name then
- location = "corridor"
- end
- BroadcastFatalError("Lost at " .. location .. ".", false)
- end
- local step = 0
- local foundMoves = 0
- while foundMoves < moves do
- step = step + 1
- if IsMove(steps[step]) then
- foundMoves = foundMoves + 1
- elseif steps[step] == nil then
- BroadcastFatalError("Cannot resume from saved state. Turtle has consumed more fuel than expected.")
- end
- end
- local foundTurns = 0
- while foundTurns < turns do
- step = step + 1
- if IsTurn(steps[step]) then
- foundTurns = foundTurns + 1
- elseif steps[step] == nil or IsMove(steps[step]) then
- BroadcastFatalError("Cannot resume from saved state. Saved state is invalid.")
- end
- end
- return step
- end
- function InitPhase(phase, args)
- ActivePhase = phase
- PhaseArgs = args
- InitialFuel = turtle.getFuelLevel()
- ResetTurnFile()
- PersistState()
- end
- -- HELPERS --
- function IsMove(func)
- return (func == Forward) or (func == Up) or (func == Down)
- end
- function IsTurn(func)
- return (func == Left) or (func == Right)
- end
- -- OUTBOUND --
- function Phase.outbound.generateSteps(args)
- local steps = {}
- if args.from == 0 then
- table.insert(steps, Right)
- table.insert(steps, Right)
- table.insert(steps, Up)
- end
- for _ = args.from + 1, AssignedLayer do
- table.insert(steps, Forward)
- end
- table.insert(steps, Project.workingSide == WorkingSide.right and Right or Left)
- table.insert(steps, Forward)
- table.insert(steps, Down)
- table.insert(steps, function ()
- return Phase.working
- end)
- return steps
- end
- -- WORKING --
- function Phase.working.generateSteps(_)
- local function backtrackToHome()
- return Phase.backtracking, {
- nDoneSteps = CompletedSteps,
- goHome = true
- }
- end
- local steps = {}
- for y = 1, Project.height / 3 do
- for _ = 1, Project.width - 3 do
- table.insert(steps, MineAbove(backtrackToHome))
- table.insert(steps, MineBelow(backtrackToHome))
- table.insert(steps, MineInfront(backtrackToHome))
- table.insert(steps, Forward)
- end
- table.insert(steps, MineAbove(backtrackToHome))
- table.insert(steps, MineInfront(backtrackToHome))
- if y < Project.height / 3 then
- for _ = 1, 3 do
- table.insert(steps, MineBelow(backtrackToHome))
- table.insert(steps, Down)
- table.insert(steps, MineInfront(backtrackToHome))
- end
- table.insert(steps, Left)
- table.insert(steps, Left)
- else
- table.insert(steps, MineBelow(backtrackToHome))
- end
- end
- table.insert(steps, function ()
- return Phase.backtracking, {
- nDoneSteps = CompletedSteps,
- goHome = false
- }
- end)
- return steps
- end
- -- BACKTRACKING --
- function Phase.backtracking.generateSteps(args)
- local steps = {}
- local doneSteps = {}
- for i, step in ipairs(Phase.working.generateSteps()) do
- if i > args.nDoneSteps then
- break
- end
- table.insert(doneSteps, step)
- end
- table.insert(steps, Left)
- table.insert(steps, Left)
- for i = args.nDoneSteps, 1, -1 do
- local step = doneSteps[i]
- if step == Forward then
- table.insert(steps, Forward)
- elseif step == Up then
- table.insert(steps, Down)
- elseif step == Down then
- table.insert(steps, Up)
- elseif step == Left then
- table.insert(steps, Right)
- elseif step == Right then
- table.insert(steps, Left)
- end
- end
- if args.goHome then
- table.insert(steps, Forward)
- table.insert(steps, Project.workingSide == WorkingSide.right and Left or Right)
- table.insert(steps, function ()
- return Phase.inbound
- end)
- else
- local prevLayer = AssignedLayer
- table.insert(steps, function ()
- RequestLayer()
- PersistState()
- end)
- table.insert(steps, Up)
- table.insert(steps, Forward)
- table.insert(steps, Project.workingSide == WorkingSide.right and Right or Left)
- table.insert(steps, function ()
- return Phase.outbound, { from = prevLayer }
- end)
- end
- return steps
- end
- -- INBOUND --
- function Phase.inbound.generateSteps(_)
- local steps = {}
- for i = 1, AssignedLayer do
- steps[i] = Forward
- end
- table.insert(steps, function ()
- return Phase.emptyAndRefuel
- end)
- return steps
- end
- -- EMPTY_AND_REFUEL --
- function Phase.emptyAndRefuel.generateSteps(_)
- local refuelStep = function ()
- for i = 1, 16 do
- turtle.select(i)
- Ensure(function ()
- return turtle.getItemCount(i) == 0 or turtle.dropDown()
- end, true, "Cannot empty.", "Emptied successfully.")
- end
- Refuel(RefuelPosition.home)
- return Phase.outbound, {
- from = 0
- }
- end
- return { refuelStep }
- end
- -- ### MODULE: server
- -- ### PATH: ./src/server.lua
- -- STATE --
- ServerState = nil
- -- FUNCTIONS --
- function RunServer(args)
- local loadedState = nil
- if fs.exists(Filenames.serverState) then
- local fileHandle = fs.open(Filenames.serverState, "r")
- loadedState = textutils.unserialize(fileHandle.readAll())
- fileHandle.close()
- end
- if args then
- local width = tonumber(args[1])
- local height = tonumber(args[2])
- local side = args[3]
- if not width or width <= 0 or not height or height <= 0 or (side ~= WorkingSide.right and side ~= WorkingSide.left) then
- error("Usage: <width> <height> <right|left>")
- return false
- end
- if height % 3 ~= 0 then
- error("Height must be divisible by 3!")
- return false
- end
- ServerState = {
- layers = {},
- turtles = {},
- project = {}
- }
- ServerState.project.serverAddress = os.getComputerID()
- ServerState.project.projectId = os.epoch()
- ServerState.project.width = width
- ServerState.project.height = height
- ServerState.project.workingSide = side
- elseif loadedState then
- ServerState = loadedState
- else
- print("No server session to resume.")
- return false
- end
- ServerState.project.filters = LoadFilters()
- SaveServerState()
- while true do
- local id, msg = rednet.receive(Communication.protocol.request)
- local payload = textutils.unserialize(msg)
- local response = MessageHandlers[payload.message](payload, id)
- sleep(0.1)
- rednet.send(id, textutils.serialize(response), Communication.protocol.response)
- end
- end
- function ClearServer()
- if fs.exists(Filenames.serverState) then
- fs.delete(Filenames.serverState)
- end
- end
- function SaveServerState()
- local fileHandle = fs.open(Filenames.serverState, "w")
- fileHandle.write(textutils.serialize(ServerState))
- fileHandle.close()
- end
- -- MESSAGE HANDLERS --
- MessageHandlers = {
- [Communication.messages.getProject] = function (payload, id)
- return ServerState.project
- end,
- [Communication.messages.requestLayer] = function (payload, id)
- if payload.projectId ~= ServerState.project.projectId then
- return {
- layer = nil
- }
- end
- if payload.previousLayer ~= ServerState.turtles[id] or (not payload.previousLayer and ServerState.turtles[id]) then
- return {
- layer = ServerState.turtles[id]
- }
- end
- local newLayer = #ServerState.layers + 1
- ServerState.layers[newLayer] = id
- if payload.previousLayer then
- ServerState.layers[payload.previousLayer] = false
- end
- ServerState.turtles[id] = newLayer
- SaveServerState()
- return {
- layer = newLayer
- }
- end
- }
- -- ### MODULE: turtle
- -- ### PATH: ./src/turtle.lua
- -- STATE --
- Resuming = false
- -- FUNCTIONS --
- function RunTurtle(arg)
- if not arg then
- LoadProject()
- LoadState()
- CompletedSteps = Resume(ActivePhase.generateSteps(PhaseArgs))
- Resuming = true
- else
- local serverId = tonumber(arg)
- if not serverId then
- return false
- end
- FetchProject(serverId)
- PersistProject()
- RequestLayer()
- Refuel(RefuelPosition.spawn)
- InitPhase(Phase.outbound, { from = -1 })
- end
- while true do
- local steps = ActivePhase.generateSteps(PhaseArgs)
- local nSteps = #steps
- if not Resuming then
- CompletedSteps = 0
- else
- Resuming = false
- end
- while CompletedSteps < nSteps do
- local newPhase, newPhaseArgs = steps[CompletedSteps + 1]()
- if newPhase then
- InitPhase(newPhase, newPhaseArgs)
- print(newPhase.name)
- break
- end
- CompletedSteps = CompletedSteps + 1
- end
- end
- end
- function ClearTurtle()
- local files = {
- Filenames.project,
- Filenames.state,
- Filenames.turnFile
- }
- for _, filename in pairs(files) do
- if fs.exists(filename) then
- fs.delete(filename)
- end
- end
- end
- -- ### MAIN MODULE
- -- ### main.lua
- function Install()
- local programName = shell.getRunningProgram()
- local startupScript = "shell.run(\"" .. programName .. "\")"
- local oppositeNames = {
- ["startup"] = "startup.lua",
- ["startup.lua"] = "startup"
- }
- if programName == "startup" or programName == "startup.lua" then
- if fs.exists(oppositeNames[programName]) then
- fs.move(oppositeNames[programName], os.epoch() .. "_" .. oppositeNames[programName])
- end
- return
- end
- if fs.exists("startup.lua") then
- fs.move("startup.lua", os.epoch() .. "_startup.lua")
- end
- if fs.exists("startup") then
- local readHandle = fs.open("startup", "r")
- local contents = readHandle.readAll()
- readHandle.close()
- if contents == startupScript then
- return
- end
- fs.move("startup", os.epoch() .. "_startup")
- end
- local writeHandle = fs.open("startup", "w")
- writeHandle.write(startupScript)
- writeHandle.close()
- end
- Install()
- for _, side in pairs({
- "top",
- "bottom",
- "left",
- "right",
- "front",
- "back"
- }) do
- if peripheral.hasType(side, "modem") then
- rednet.close(side)
- rednet.open(side)
- break
- end
- end
- Args = { ... }
- if #Args == 0 then
- print("Turtle resuming in 5 seconds...")
- sleep(5)
- parallel.waitForAll(function ()
- RunTurtle(nil)
- end, function ()
- RunServer(nil)
- end)
- elseif #Args == 1 then
- ClearTurtle()
- if tonumber(Args[1]) == os.getComputerID() then
- parallel.waitForAll(function ()
- RunTurtle(os.getComputerID())
- end, function ()
- RunServer(nil)
- end)
- else
- ClearServer()
- RunTurtle(Args[1])
- end
- elseif #Args == 3 then
- ClearTurtle()
- ClearServer()
- parallel.waitForAny(function ()
- RunTurtle(os.getComputerID())
- end, function ()
- RunServer(Args)
- end)
- end
- -- TODO: clear previous state if starting new session, but preserve project id (or generate random each time if possible? math.random?)
Advertisement
Advertisement