· 4 years ago · Aug 07, 2021, 11:10 PM
1local apiPath = "apis/lama"
2local stateFile = {
3position = "/.lama-state",
4waypoints = "/.lama-waypoints",
5fuel = "/.lama-fuel",
6move = "/.lama-move-state",
7path = "/.lama-path-state",
8wrap = "/.lama-wrap-state"
9}
10local invalidStateFile = "/.lama-invalid"
11-- This should be an integer value in the interval [0, 99], where lower values
12local startupPriority = 10
13local startupBackupFile = "/.lama-startup-backup"
14local useMinecraftCoordinates = true
15if lama then
16local env = getfenv()
17for k, v in pairs(lama) do
18env[k] = v
19end
20return
21end
22local state, private
23version = "1.4c"
24side = {forward = 0, right = 1, back = 2, left = 3,
25north = 0, east = 1, south = 2, west = 3,
26front = 0,
27["0"] = 0, ["1"] = 1, ["2"] = 2, ["3"] = 3,
28[0] = "north", [1] = "east", [2] = "south", [3] = "west"}
29reason = {
30unknown = "unknown",
31queue_full = "queue_full",
32coroutine = "coroutine",
33fuel = "fuel",
34block = "block",
35turtle = "turtle",
36unbreakable_block = "unbreakable_block",
37entity = "entity",
38invulnerable_entity = "invulnerable_entity"
39}
40function get()
41private.resume()
42local position = state.position
43return private.toMC(position.x, position.y, position.z, position.f)
44end
45function getX()
46local x, _, _, _ = get()
47return x
48end
49function getY()
50local _, y, _, _ = get()
51return y
52end
53function getZ()
54local _, _, z, _ = get()
55return z
56end
57function getPosition()
58return vector.new(get())
59end
60function getFacing()
61local _, _, _, f = get()
62return f
63end
64function set(x, y, z, facing)
65assert(private.isInteger(x), "'x' must be an integral number")
66assert(private.isInteger(y), "'y' must be an integral number")
67assert(private.isInteger(z), "'z' must be an integral number")
68assert(private.isFacing(facing),
69"'facing' must be one of the lama.side constants")
70private.resume(true)
71if private.isLocked() then
72error("Cannot set position while moving or refueling.")
73end
74private.reset()
75x, y, z, facing = private.fromMC(x, y, z, facing)
76local position = state.position
77position.x = x
78position.y = y
79position.z = z
80position.f = facing
81private.save("position")
82return get()
83end
84function forward(tries, aggressive)
85private.resume()
86if private.isLocked() then
87return false, reason.coroutine
88end
89return private.forward(tries, aggressive)
90end
91function back(tries)
92private.resume()
93if private.isLocked() then
94return false, reason.coroutine
95end
96return private.back(tries)
97end
98function up(tries, aggressive)
99private.resume()
100if private.isLocked() then
101return false, reason.coroutine
102end
103return private.up(tries, aggressive)
104end
105function down(tries, aggressive)
106private.resume()
107if private.isLocked() then
108return false, reason.coroutine
109end
110return private.down(tries, aggressive)
111end
112function moveto(x, y, z, facing, tries, aggressive, longestFirst)
113return navigate({{x = x, y = y, z = z, facing = facing}},
114tries, aggressive, longestFirst)
115end
116getfenv()["goto"] = moveto
117function navigate(path, tries, aggressive, longestFirst)
118assert(type(path) == "table", "'path' must be a table")
119assert(tries == nil or private.isInteger(tries),
120"'tries' must be an integral number or omitted")
121assert(aggressive == nil or type(aggressive) == "boolean",
122"'aggressive' must be a boolean or omitted")
123assert(longestFirst == nil or type(longestFirst) == "boolean",
124"'longestFirst' must be a boolean or omitted")
125private.resume()
126if private.isLocked() then
127return false, reason.coroutine
128end
129local absPath = {}
130for k, v in ipairs(path) do
131if type(v) == "string" then
132local x, y, z, f = private.fromMC(waypoint.get(v))
133table.insert(absPath, {x = x, y = y, z = z, f = f})
134elseif type(v) == "table" then
135local x, y, z, f = v.x, v.y, v.z, v.facing
136assert(private.isInteger(x),
137"'x' at index " .. k .. " must be an integral number")
138assert(private.isInteger(y),
139"'y' at index " .. k .. " must be an integral number")
140assert(private.isInteger(z),
141"'z' at index " .. k .. " must be an integral number")
142assert(f == nil or private.isFacing(f),
143"'facing' at index " .. k ..
144" must be one of the lama.side constants or omitted")
145x, y, z, f = private.fromMC(x, y, z, f)
146table.insert(absPath, {x = x, y = y, z = z, f = f})
147else
148error("Invalid path entry at index " .. k)
149end
150end
151if #absPath == 0 then
152return true
153end
154for i = 1, #absPath - 1 do
155absPath[i].f = nil
156end
157state.path = {
158steps = absPath,
159tries = tries or 0,
160aggressive = aggressive or nil,
161longestFirst = longestFirst or nil
162}
163private.save("path")
164return private.navigate()
165end
166function turnRight()
167return turn((getFacing() + 1) % 4)
168end
169function turnLeft()
170return turn((getFacing() - 1) % 4)
171end
172function turnAround()
173return turn((getFacing() + 2) % 4)
174end
175function turn(towards)
176assert(private.isFacing(towards),
177"'towards' must be one of the lama.side constants")
178private.resume()
179if private.isLocked() then
180return false, reason.coroutine
181end
182return private.turn(towards)
183end
184function refuel(count)
185assert(count == nil or (private.isInteger(count) and
186count >= 0 and count <= 64),
187"'count' must be a positive integral number in [0, 64] or omitted")
188private.resume()
189if private.isLocked() then
190return false, reason.coroutine
191end
192local id
193if count then
194id = turtle.native.refuel(count)
195else
196id = turtle.native.refuel()
197end
198if id == -1 then
199return false, reason.queue_full
200end
201state.fuel.id = id
202private.save("fuel")
203local result = private.waitForResponse(id)
204state.fuel.id = nil
205state.fuel.current = turtle.getFuelLevel()
206private.save("fuel")
207return result
208end
209waypoint = {}
210function waypoint.add(name, x, y, z, facing)
211private.resume()
212assert(type(name) == "string" and name ~= "",
213"'name' must be a non-empty string")
214assert(x == nil or private.isInteger(x),
215"'x' must be an integral number or omitted")
216assert(y == nil or private.isInteger(y),
217"'y' must be an integral number or omitted")
218assert(z == nil or private.isInteger(z),
219"'z' must be an integral number or omitted")
220assert(facing == nil or private.isFacing(facing),
221"'facing' must be one of the lama.side constants or omitted")
222x, y, z, facing = private.fromMC(x, y, z, facing)
223local position = state.position
224if x == nil and y == nil and z == nil and facing == nil then
225facing = position.f
226end
227x = x or position.x
228y = y or position.y
229z = z or position.z
230local wasOverwritten = waypoint.exists(name)
231state.waypoints[name] = {
232x = math.floor(x),
233y = math.floor(y),
234z = math.floor(z),
235f = facing
236}
237private.save("waypoints")
238return wasOverwritten
239end
240function waypoint.remove(name)
241private.resume()
242if not waypoint.exists(name) then
243return false
244end
245state.waypoints[name] = nil
246private.save("waypoints")
247return true
248end
249function waypoint.exists(name)
250private.resume()
251assert(type(name) == "string" and name ~= "",
252"'name' must be a non-empty string")
253return state.waypoints[name] ~= nil
254end
255function waypoint.get(name)
256private.resume()
257assert(waypoint.exists(name),
258"no such waypoint, '" .. tostring(name) .. "'")
259local w = state.waypoints[name]
260return private.toMC(w.x, w.y, w.z, w.f)
261end
262function waypoint.iter()
263private.resume()
264local name
265return function()
266local coordinate
267name, coordinate = next(state.waypoints, name)
268if name then
269return name, private.toMC(coordinate.x, coordinate.y,
270coordinate.z, coordinate.f)
271end
272end
273end
274function waypoint.moveto(name, tries, aggressive, longestFirst)
275x, y, z, facing = waypoint.get(name)
276return moveto(x, y, z, facing, tries, aggressive, longestFirst)
277end
278waypoint["goto"] = waypoint.moveto
279function init()
280private.resume()
281end
282function startupResult()
283private.resume()
284if not private.startupResult then
285return true
286end
287return private.startupResult.result, private.startupResult.reason
288end
289function hijackTurtleAPI(restore)
290if restore then
291if not turtle._lama then return end
292turtle.forward = turtle._lama.forward
293turtle.back = turtle._lama.back
294turtle.up = turtle._lama.up
295turtle.down = turtle._lama.down
296turtle.turnRight = turtle._lama.turnRight
297turtle.turnLeft = turtle._lama.turnLeft
298turtle.refuel = turtle._lama.refuel
299turtle._lama = nil
300else
301if turtle._lama then return end
302turtle._lama = {
303forward = turtle.forward,
304back = turtle.back,
305up = turtle.up,
306down = turtle.down,
307turnRight = turtle.turnRight,
308turnLeft = turtle.turnLeft,
309refuel = turtle.refuel
310}
311turtle.forward = function() return forward() ~= false end
312turtle.back = function() return back() ~= false end
313turtle.up = function() return up() ~= false end
314turtle.down = function() return down() ~= false end
315turtle.turnRight = function() return turnRight() ~= false end
316turtle.turnLeft = function() return turnLeft() ~= false end
317turtle.refuel = function() return refuel() ~= false end
318end
319end
320if useMinecraftCoordinates then
321for k, v in pairs(side) do
322if type(v) == "number" then
323side[k] = (v + 2) % 4
324end
325end
326side[0], side[1], side[2], side[3] = side[2], side[3], side[0], side[1]
327end
328do
329local function makeReadonly(table, name)
330setmetatable(table, {
331__index = function(t,k)
332error("Trying to access invalid '" .. name ..
333"' constant '" .. k .. "'.")
334end,
335__newindex = function()
336error("Trying to modify readonly table.")
337end
338})
339end
340makeReadonly(side, "lama.side")
341makeReadonly(reason, "lama.reason")
342end
343private = {}
344private.direction = {
345[1] = "forward", [2] = "back", [3] = "up", [4] = "down",
346forward = 1, back = 2, up = 3, down = 4
347}
348state = {
349position = {x = 0, y = 0, z = 0, f = 0},
350waypoints = {},
351fuel = {current = turtle.getFuelLevel()},
352move = nil,
353path = nil,
354wrap = 0
355}
356private.schema = {}
357private.schema[version] = {
358position = {
359type = "table",
360properties = {
361x = {type = "number", value = private.isInteger},
362y = {type = "number", value = private.isInteger},
363z = {type = "number", value = private.isInteger},
364f = {type = "number", value = private.isFacing}
365}
366},
367waypoints = {
368type = "table",
369entries = {
370type = "table",
371keytype = "string",
372properties = {
373x = {type = "number", value = private.isInteger},
374y = {type = "number", value = private.isInteger},
375z = {type = "number", value = private.isInteger},
376f = {type = "number", value = private.isFacing,
377optional = true}
378}
379}
380},
381fuel = {
382type = "table",
383properties = {
384current = {type = "number", value = private.isInteger},
385id = {type = "number", value = private.isInteger,
386optional = true}
387}
388},
389move = {
390type = "table",
391properties = {
392direction = {
393type = "number",
394value = function(v) return private.direction[v] ~= nil end
395},
396tries = {type = "number", value = private.isInteger},
397aggressive = {type = "boolean", optional = true}
398}
399},
400path = {
401type = "table",
402properties = {
403steps = {
404type = "table",
405entries = {
406type = "table",
407properties = {
408x = {type = "number", value = private.isInteger},
409y = {type = "number", value = private.isInteger},
410z = {type = "number", value = private.isInteger},
411f = {type = "number", value = private.isFacing,
412optional = true}
413}
414}
415},
416tries = {type = "number", value = private.isInteger},
417aggressive = {type = "boolean", optional = true},
418longestFirst = {type = "boolean", optional = true}
419}
420},
421wrap = {type = "number", value = private.isInteger}
422}
423private.schema["1.2"] = {
424move = {
425type = "table",
426optional = true,
427properties = {
428direction = {
429type = "number",
430value = function(v) return private.direction[v] ~= nil end
431},
432preMoveFuel = {type = "number", value = private.isInteger},
433tries = {type = "number", value = private.isInteger},
434aggressive = {type = "boolean"}
435},
436upgrade = function(value)
437state.fuel.current = value.preMoveFuel
438private.save("fuel")
439return {value.direction, value.tries, value.aggressive}
440end
441}
442}
443private.schema["1.0"] = {
444type = "table",
445properties = {
446position = {
447type = "table",
448properties = {
449x = {type = "number", value = private.isInteger},
450y = {type = "number", value = private.isInteger},
451z = {type = "number", value = private.isInteger},
452f = {type = "number", value = private.isFacing}
453}
454},
455moving = {
456type = {"boolean", "string"},
457value = function(v)
458if type(v) == "string" then
459return v == "forward" or
460v == "back" or
461v == "up" or
462v == "down"
463end
464return true
465end
466},
467preMoveFuel = {type = "number", value = private.isInteger},
468tries = {type = "number", value = private.isInteger},
469aggressive = {type = "boolean"}
470}
471}
472function private.validate(value, schema)
473assert(schema ~= nil, "no schema given")
474local function validate(value, schema, path)
475if schema.optional and value == nil then
476return true
477end
478if type(schema.type) == "table" then
479local ok = false
480for _,valueType in pairs(schema.type) do
481if type(value) == valueType then
482ok = true
483break
484end
485end
486if not ok then
487return false, path .. ": invalid type; is " .. type(value) ..
488", should be one of [" ..
489table.concat(schema.type, ", ") .. "]"
490end
491elseif schema.type and type(value) ~= schema.type then
492return false, path .. ": invalid type; is " .. type(value) ..
493", should be " .. schema.type
494end
495if schema.value and not schema.value(value) then
496return false, path .. ": invalid value"
497end
498if schema.properties then
499for property, propertySchema in pairs(schema.properties) do
500local result, location = validate(value[property],
501propertySchema,
502path .. "." .. property)
503if not result then
504return result, location
505end
506end
507end
508if schema.entries then
509for key, entry in pairs(value) do
510if schema.entries.keytype and
511type(key) ~= schema.entries.keytype
512then
513return false, path .. "[" .. key ..
514"]: invalid key type; is " .. type(key) ..
515", should be " .. schema.entries.keytype
516end
517local result, location = validate(entry,
518schema.entries,
519path .. "[" .. key .. "]")
520if not result then
521return result, location
522end
523end
524end
525return true
526end
527return validate(value, schema, "value")
528end
529function private.isInteger(value)
530return type(value) == "number" and
531value == math.floor(value)
532end
533function private.isFacing(value)
534return type(value) == "number" and
535private.isInteger(value) and value >= 0 and value < 4
536end
537function private.save(what)
538local data = textutils.serialize(state[what])
539local file = fs.open(stateFile[what], "w")
540if not file then
541private.invalidate()
542error("Could not opens state file '" .. what .. "' for writing.")
543end
544file.write(data)
545file.close()
546end
547function private.load()
548if private.upgrade() then
549return true
550end
551local function load(section)
552local filename = stateFile[section]
553if not fs.exists(filename) then
554return true
555end
556assert(not fs.isDir(filename),
557"Invalid state filename '" .. filename .. "': it's a folder.")
558local success, result = private.unserialize(filename)
559if success then
560local valid, failureReason = private.validate(result,
561private.schema[version][section])
562if valid then
563state[section] = result
564return true
565elseif private.schema["1.2"][section] and
566private.validate(result, private.schema["1.2"][section])
567then
568state[section] = private.schema["1.2"][section].upgrade(result)
569return true
570else
571print("LAMA: Invalid state file '" .. filename ..
572"' (" .. failureReason .. ").")
573end
574end
575return false
576end
577for section, _ in pairs(stateFile) do
578if not load(section) then
579return false
580end
581end
582return true
583end
584function private.unserialize(filename)
585local file = assert(fs.open(filename, "r"))
586local data = file.readAll()
587file.close()
588local result, _ = loadstring("return " .. data, filename)
589if not result then
590return true, data
591else
592return true, setfenv(result, {["inf"] = math.huge})()
593end
594end
595function private.upgrade()
596if not fs.exists(stateFile.position) or fs.isDir(stateFile.position) then
597return false
598end
599local success, result = private.unserialize(stateFile.position)
600if not success or not private.validate(result, private.schema["1.0"]) then
601return false
602end
603state.position = result.position
604if type(result.moving) == "string" then
605state.fuel.current = result.preMoveFuel
606state.move = {
607direction = private.direction[result.moving],
608tries = result.tries,
609aggressive = result.aggressive
610}
611state.wrap = 1
612end
613private.save("position")
614private.save("move")
615private.save("fuel")
616private.save("wrap")
617return true
618end
619function private.reset()
620state.position.x = 0
621state.position.y = 0
622state.position.z = 0
623state.position.f = 0
624state.fuel = {current = turtle.getFuelLevel()}
625state.move = nil
626state.path = nil
627state.wrap = 0
628state.isInitializing = nil
629state.isInitialized = true
630fs.delete(stateFile.position)
631fs.delete(stateFile.fuel)
632fs.delete(stateFile.move)
633fs.delete(stateFile.path)
634fs.delete(stateFile.wrap)
635fs.delete(invalidStateFile)
636end
637function private.isLocked()
638return state.move or state.path or state.fuel.id
639end
640local startupScript = string.format(
641[[assert(os.loadAPI(%q))
642lama.init()
643lama.hijackTurtleAPI()]], apiPath)
644local startupHandlers = {
645default = {
646init = function()
647assert(type(startupBackupFile) == "string" and
648startupBackupFile ~= "",
649"The setting 'startupBackupFile' must be a non-empty string.")
650end,
651wrap = function()
652local haveStartup = fs.exists("/startup")
653if haveStartup then
654fs.move("/startup", startupBackupFile)
655end
656local f = assert(fs.open("/startup", "w"),
657"Could not open startup script for writing.")
658f.writeLine(startupScript)
659if haveStartup then
660f.writeLine("shell.run('/startup')")
661else
662end
663f.close()
664end,
665unwrap = function()
666fs.delete("/startup")
667if fs.exists(startupBackupFile) then
668fs.move(startupBackupFile, "/startup")
669end
670end,
671test = function()
672return false
673end
674},
675forairan = {
676init = function()
677local priority = type(startupPriority) == "number" and
678string.format("%2d") or
679tostring(startupPriority)
680local path = "/init-scripts/" .. priority .. "_lama"
681local f = assert(fs.open(path, "w"),
682"Could not open startup script for writing.")
683f.write(startupScript)
684f.close()
685end,
686test = function()
687return fs.exists("/init-scripts") and fs.isDir("/init-scripts")
688end
689},
690sangar = {
691init = function()
692startup.remove("lama")
693startup.addString("lama", startupPriority, startupScript)
694end,
695test = function()
696return startup ~= nil and startup.version ~= nil
697end
698}
699}
700function private.initStartup()
701assert(private.isInteger(startupPriority),
702"The setting 'startupPriority' must be an integral number.")
703private.startupHandler = startupHandlers.default
704for _, handler in pairs(startupHandlers) do
705if handler.test() then
706private.startupHandler = handler
707break
708end
709end
710if private.startupHandler.init then
711private.startupHandler.init()
712end
713end
714function private.wrapStartup()
715if state.wrap == 0 then
716if private.startupHandler.wrap then
717private.startupHandler.wrap()
718end
719end
720state.wrap = state.wrap + 1
721private.save("wrap")
722end
723function private.unwrapStartup(force)
724if state.wrap == 1 or
725(force and state.wrap > 0)
726then
727if private.startupHandler.unwrap then
728private.startupHandler.unwrap()
729end
730state.wrap = 0
731fs.delete(stateFile.wrap)
732elseif state.wrap > 0 then
733state.wrap = state.wrap - 1
734private.save("wrap")
735end
736end
737function private.resume(dontCrash)
738if state.isInitialized then
739return
740end
741while state.isInitializing do
742os.sleep(1)
743end
744if fs.exists(invalidStateFile) then
745if dontCrash then
746return
747end
748error("Invalid state. Please reinitialize the turtle's position.")
749end
750assert(turtle, "The lama API can only run on turtles.")
751assert(os.getComputerLabel(), "Turtle has no label, required for state persistence.")
752assert(turtle.getFuelLevel() ~= "unlimited", "Turtles must use fuel for this API to work correctly.")
753if bapil then
754apiPath = bapil.resolveAPI(apiPath)
755else
756local i
757local tryPaths = {
758"/rom/apis/lama",
759"/rom/apis/turtle/lama",
760"/apis/lama",
761"/apis/turtle/lama"
762}
763for i = 1 , #tryPaths do
764if fs.exists(tryPaths[i]) and not fs.isDir(tryPaths[i]) then
765apiPath = tryPaths[i]
766end
767end
768end
769startupScript = string.format(
770[[assert(os.loadAPI(%q))
771lama.init()
772lama.hijackTurtleAPI()]], apiPath)
773assert(type(apiPath) == "string" and apiPath ~= "", "The setting 'apiPath' must be a non-empty string.")
774assert(fs.exists(apiPath) and not fs.isDir(apiPath), "No file found at 'apiPath', please make sure it points to the lama API.")
775state.isInitializing = true
776private.initStartup()
777local valid = private.load()
778local id
779repeat
780os.sleep(0.1)
781id = turtle.native.detect()
782until id ~= -1
783--private.waitForResponse(id)
784private.unwrapStartup(true)
785if state.fuel.id then
786if id > state.fuel.id then
787state.fuel.current = turtle.getFuelLevel()
788else
789valid = false
790end
791elseif state.move then
792valid = valid and turtle.getFuelLevel() == state.fuel.current or
793turtle.getFuelLevel() == state.fuel.current - 1
794else
795valid = valid and turtle.getFuelLevel() == state.fuel.current
796end
797if not valid then
798state.isInitializing = nil
799assert(fs.open(invalidStateFile, "w"),
800"Failed to create invalidation file.").close()
801error("Invalid state. Please reinitialize the turtle's position.")
802end
803if state.move then
804if turtle.getFuelLevel() == state.fuel.current then
805if state.move.tries > 0 then
806local result, failureReason =
807private.move(state.move.direction,
808state.move.tries,
809state.move.aggressive)
810private.startupResult = {
811result = result,
812reason = failureReason
813}
814else
815private.startupResult = {
816result = false,
817reason = private.tryGetReason(state.move.direction)
818}
819private.endMove()
820end
821elseif turtle.getFuelLevel() == state.fuel.current - 1 then
822private.updateState()
823private.endMove()
824else
825assert(false)
826end
827else
828state.fuel.current = turtle.getFuelLevel()
829state.fuel.id = nil
830private.save("fuel")
831end
832if state.path then
833local result, _ = not private.startupResult or
834private.startupResult.result
835if result then
836private.navigate()
837else
838private.endNavigate()
839end
840end
841state.isInitializing = nil
842state.isInitialized = true
843end
844function private.toMC(x, y, z, facing)
845if useMinecraftCoordinates then
846return y, z, -x, facing and (facing + 2) % 4 or nil
847else
848return x, y, z, facing
849end
850end
851function private.fromMC(x, y, z, facing)
852if useMinecraftCoordinates then
853return -z, x, y, facing and (facing + 2) % 4 or nil
854else
855return x, y, z, facing
856end
857end
858function private.waitForResponse(ids)
859if type(ids) ~= "table" then
860ids = {ids}
861elseif #ids == 0 then
862return true
863end
864local success = true
865repeat
866local event, responseID, result = os.pullEvent("turtle_response")
867if event == "turtle_response" then
868for i = 1, #ids do
869if ids[i] == responseID then
870success = success and result
871table.remove(ids, i)
872break
873end
874end
875end
876until #ids == 0
877return success
878end
879function private.tryGetReason(direction)
880local detect = ({
881[private.direction.forward] = turtle.detect,
882[private.direction.up] = turtle.detectUp,
883[private.direction.down] = turtle.detectDown})[direction]
884local sideName = ({
885[private.direction.forward] = "front",
886[private.direction.up] = "top",
887[private.direction.down] = "bottom"})[direction]
888if peripheral.getType(sideName) == "turtle" then
889return reason.turtle
890elseif detect then
891if detect() then
892return reason.block
893else
894return reason.entity
895end
896else
897return reason.unknown
898end
899end
900function private.forward(tries, aggressive)
901return private.move(private.direction.forward, tries, aggressive)
902end
903function private.back(tries)
904return private.move(private.direction.back, tries)
905end
906function private.up(tries, aggressive)
907return private.move(private.direction.up, tries, aggressive)
908end
909function private.down(tries, aggressive)
910return private.move(private.direction.down, tries, aggressive)
911end
912function private.turn(towards)
913local ids, position = {}, state.position
914if useMinecraftCoordinates then
915towards = (towards + 2) % 4
916end
917while position.f ~= towards do
918local id
919if towards == (position.f + 1) % 4 then
920id = turtle.turnRight()
921if id == -1 then
922return false, reason.queue_full
923end
924position.f = (position.f + 1) % 4
925else
926id = turtle.turnLeft()
927if id == -1 then
928return false, reason.queue_full
929end
930position.f = (position.f - 1) % 4
931end
932private.save("position")
933table.insert(ids, id)
934end
935return ids -- private.waitForResponse(ids)
936end
937function private.move(direction, tries, aggressive)
938assert(tries == nil or type(tries) == "number",
939"'tries' must be a number or omitted")
940assert(aggressive == nil or type(aggressive) == "boolean",
941"'aggressive' must be a boolean or omitted")
942if turtle.getFuelLevel() < 1 then
943return false, reason.fuel
944end
945tries = tonumber(tries or 0)
946aggressive = aggressive and true or nil
947local move = ({
948[private.direction.forward] = turtle.native.forward,
949[private.direction.back] = turtle.native.back,
950[private.direction.up] = turtle.native.up,
951[private.direction.down] = turtle.native.down})[direction]
952local detect = ({
953[private.direction.forward] = turtle.detect,
954[private.direction.up] = turtle.detectUp,
955[private.direction.down] = turtle.detectDown})[direction]
956local dig = ({
957[private.direction.forward] = turtle.dig,
958[private.direction.up] = turtle.digUp,
959[private.direction.down] = turtle.digDown})[direction]
960local attack = ({
961[private.direction.forward] = turtle.attack,
962[private.direction.up] = turtle.attackUp,
963[private.direction.down] = turtle.attackDown})[direction]
964local side = ({
965[private.direction.forward] = "front",
966[private.direction.back] = "back",
967[private.direction.up] = "top",
968[private.direction.down] = "bottom"})[direction]
969state.move = {
970direction = direction,
971tries = tries,
972aggressive = aggressive
973}
974while true do
975if peripheral.getType(side) == "turtle" then
976if state.move.tries > 0 then
977private.save("move")
978end
979else
980local moveId = move()
981if moveId == -1 then
982private.endMove()
983return false, reason.queue_full
984end
985private.save("move")
986private.wrapStartup()
987local success = moveId -- private.waitForResponse(moveId)
988private.unwrapStartup()
989if success then
990private.updateState()
991private.endMove()
992return true
993end
994end
995if state.move.tries == 0 then
996private.endMove()
997return false, private.tryGetReason(direction)
998end
999if peripheral.getType(side) == "turtle" then
1000os.sleep(1)
1001elseif dig and dig() then
1002os.sleep(0.5)
1003elseif peripheral.getType(side) == "turtle" then
1004os.sleep(1)
1005elseif aggressive and attack and attack() then
1006elseif peripheral.getType(side) == "turtle" then
1007os.sleep(1)
1008elseif detect then
1009local block = detect()
1010if peripheral.getType(side) == "turtle" then
1011os.sleep(1)
1012elseif block then
1013private.endMove()
1014return false, reason.unbreakable_block
1015else
1016if state.move.tries == 1 then
1017private.endMove()
1018return false, reason.invulnerable_entity
1019end
1020os.sleep(1)
1021end
1022else
1023os.sleep(0.5)
1024end
1025state.move.tries = state.move.tries - 1
1026end
1027end
1028function private.updateState()
1029local position = state.position
1030local direction = private.direction[state.move.direction]
1031local delta = {
1032forward = { 1, 0, 0},
1033right = { 0, 1, 0},
1034back = {-1, 0, 0},
1035left = { 0, -1, 0},
1036up = { 0, 0, 1},
1037down = { 0, 0, -1}
1038}
1039for i=1,position.f do
1040delta.forward, delta.right, delta.back, delta.left =
1041delta.right, delta.back, delta.left, delta.forward
1042end
1043position.x = position.x + delta[direction][1]
1044position.y = position.y + delta[direction][2]
1045position.z = position.z + delta[direction][3]
1046private.save("position")
1047state.fuel.current = turtle.getFuelLevel()
1048private.save("fuel")
1049end
1050function private.endMove()
1051state.move = nil
1052fs.delete(stateFile.move)
1053end
1054function private.navigate()
1055assert(#state.path.steps > 0, "you found a bug")
1056local function travel(axis, distance)
1057if distance == 0 then
1058return true
1059end
1060local directions = ({
1061x = {side.north, side.south},
1062y = {side.east, side.west}})[axis]
1063if directions then
1064local direction = distance > 0 and directions[1] or directions[2]
1065local result, failureReason = private.turn(direction)
1066if not result then
1067return result, failureReason
1068end
1069end
1070local action = ({
1071x = private.forward,
1072y = private.forward,
1073z = distance > 0 and private.up or private.down})[axis]
1074distance = math.abs(distance)
1075local tries, aggressive = state.path.tries, state.path.aggressive
1076while distance > 0 do
1077local result, failureReason = action(tries, aggressive)
1078if not result then
1079return result, failureReason
1080end
1081distance = distance - 1
1082end
1083return true
1084end
1085private.wrapStartup()
1086local function shortestFirstComparator(a, b)
1087return math.abs(a.distance) < math.abs(b.distance)
1088end
1089local function longestFirstComparator(a, b)
1090return math.abs(a.distance) > math.abs(b.distance)
1091end
1092local result, failureReason = true
1093local comparator = state.path.longestFirst and
1094longestFirstComparator or
1095shortestFirstComparator
1096repeat
1097local position = state.position
1098local x, y, z = position.x, position.y, position.z, position.f
1099local step = state.path.steps[1]
1100local dx, dy, dz = step.x - x, step.y - y, step.z - z
1101local axisCount = (dx ~= 0 and 1 or 0) +
1102(dy ~= 0 and 1 or 0) +
1103(dz ~= 0 and 1 or 0)
1104if state.path.longestFirst and axisCount > 1 then
1105local axes = {{axis = 1, distance = dx},
1106{axis = 2, distance = dy},
1107{axis = 3, distance = dz}}
1108table.sort(axes, shortestFirstComparator)
1109local stopover = {step.x, step.y, step.z}
1110for _, entry in ipairs(axes) do
1111stopover[entry.axis] = stopover[entry.axis] - entry.distance
1112if stopover[1] == x and
1113stopover[2] == y and
1114stopover[3] == z
1115then
1116break
1117end
1118local stopoverCopy = {x = stopover[1],
1119y = stopover[2],
1120z = stopover[3]}
1121table.insert(state.path.steps, 1, stopoverCopy)
1122end
1123else
1124local axes = {{axis = "x", distance = dx},
1125{axis = "y", distance = dy},
1126{axis = "z", distance = dz}}
1127table.sort(axes, comparator)
1128for _, entry in ipairs(axes) do
1129result, failureReason = travel(entry.axis, entry.distance)
1130if not result then
1131break
1132end
1133end
1134if result and step.f ~= nil then
1135if useMinecraftCoordinates then
1136step.f = (step.f + 2) % 4
1137end
1138result, failureReason = private.turn(step.f)
1139end
1140table.remove(state.path.steps, 1)
1141end
1142private.save("path")
1143until not result or #state.path.steps == 0
1144private.endNavigate()
1145private.unwrapStartup()
1146return result, failureReason
1147end
1148function private.endNavigate()
1149state.path = nil
1150fs.delete(stateFile.path)
1151end