· 4 years ago · Dec 26, 2020, 08:02 PM
1--[[
2 LAMA - Location Aware Movement API - 2013 Sangar
3
4 This program is licensed under the MIT license.
5 http://opensource.org/licenses/mit-license.php
6
7 The API will keep track of the turtle's position and facing, even across
8 multiple games: the state is persistent. In particular it is very robust,
9 i.e. even if the turtle is currently moving when it is forced to shut down
10 because its chunk is unloaded, the coordinates will always be correct after
11 it resumes. In theory, anyways. It's the only actually working approach I'm
12 aware of, using the turtle's fuel level to check if it moved in such cases.
13
14 The API is relatively basic, in that it is purely focused on movement. It's
15 essentially a rich drop-in replacement for the the original navigational
16 functions in the turtle API:
17 turtle.forward -> lama.forward
18 turtle.back -> lama.back
19 turtle.up -> lama.up
20 turtle.down -> lama.down
21 turtle.turnRight -> lama.turnRight
22 turtle.turnLeft -> lama.turnLeft
23
24 When using this API, you must not use any other functions that alter
25 the turtle's position or facing. In particular:
26
27 DO NOT USE turtle.forward, turtle.back, turtle.up, turtle.down,
28 turtle.turnRight or turtle.turnLeft NOR THEIR NATIVE EQUIVALENTS.
29
30 Any other external force changing the turtle's position will also
31 invalidate the coordinates, of course (such as the player pickaxing the
32 turtle and placing it somewhere else or RP2 frames).
33
34 The utility function lama.hijackTurtleAPI() can be used to override the
35 original turtle API functions, to make it easier to integrate this API into
36 existing programs.
37 When starting a new program it is recommended using the functions directly
38 though, to make full use of their capabilities (in particular automatically
39 clearing the way). See the function's documentation for more information.
40]]
41
42-------------------------------------------------------------------------------
43-- Config --
44-------------------------------------------------------------------------------
45
46-- The absolute path to this file. This is used for generating startup logic
47-- which initializes the API by loading it into the OS.
48local apiPath = "apis/lama"
49
50-- This is the name of the file in which we store our state, i.e. the position
51-- and facing of the turtle, as well as whether it is currently moving or not.
52-- We split this up into several files to keep file i/o while moving minimal.
53-- You may want to change this if it collides with another program or API.
54local stateFile = {
55 position = "/.lama-state",
56 waypoints = "/.lama-waypoints",
57 fuel = "/.lama-fuel",
58 move = "/.lama-move-state",
59 path = "/.lama-path-state",
60 wrap = "/.lama-wrap-state"
61}
62
63-- The file used to mark the API state as invalid. If this file exists we will
64-- not allow any interaction besides lama.set(), but always throw an error
65-- instead. This is to ensure that turtles don't do weird things after the
66-- position cannot be guaranteed to be correct anymore (e.g. due to hard server
67-- crashes resulting in a rollback).
68local invalidStateFile = "/.lama-invalid"
69
70-- If this computer uses a multi-startup script (e.g. Forairan's or mine) this
71-- determins the 'priority' with which the API is initialized after a reboot.
72-- This should be an integer value in the interval [0, 99], where lower values
73-- represent a higher priority.
74local startupPriority = 10
75
76-- The filename of the file to backup any original startup file to when
77-- creating the startup file used to finish any running moves in case the
78-- turtle is forced to shut down during the move. This is only used if no
79-- multi-startup-script system is found on the computer.
80-- You may want to change this if it collides with another program or API.
81local startupBackupFile = "/.lama-startup-backup"
82
83-- Whether to use the same coordinate system Minecraft uses internally. This
84-- only influences how the API works from the outside; the state file will
85-- always use the internal coordinate system, to provide compatibility.
86local useMinecraftCoordinates = true
87
88-------------------------------------------------------------------------------
89-------------------------------------------------------------------------------
90-- API variables and methods follow; do not change them. --
91-------------------------------------------------------------------------------
92-------------------------------------------------------------------------------
93
94-- If this API was loaded before, reuse it to avoid unnecessarily reloading the
95-- state and whatnot. In particular if some coroutine gets funny ideas like
96-- reloading it while we're performing a move...
97if lama then
98 local env = getfenv()
99 for k, v in pairs(lama) do
100 env[k] = v
101 end
102 return
103end
104
105-- Internal forward declarations. They have to be declared here so that
106-- functions can access them.
107local state, private
108
109-------------------------------------------------------------------------------
110-- Constants --
111-------------------------------------------------------------------------------
112
113-- The current version of the API.
114version = "1.4c"
115
116-- Constants for a turtle's facing, used for turn() and get()/set(). Note that
117-- these are adjusted in the initialization of the API if the setting
118-- useMinecraftCoordinates is set to true.
119side = {forward = 0, right = 1, back = 2, left = 3,
120 north = 0, east = 1, south = 2, west = 3,
121 front = 0,
122 ["0"] = 0, ["1"] = 1, ["2"] = 2, ["3"] = 3,
123 -- Reverse mapping for name lookup based on the used coordinate system.
124 [0] = "north", [1] = "east", [2] = "south", [3] = "west"}
125
126-- Reasons for movement failure. One of these is returned as a second value by
127-- the movement functions (forward, back, up, down) if they fail.
128reason = {
129 -- We could not determine what was blocking us. Mostly used when moving
130 -- backwards, since we cannot detect anything in that direction.
131 unknown = "unknown",
132
133 -- The VM's event queue is full, meaning no further commands can be issued
134 -- until some of them are processed first.
135 queue_full = "queue_full",
136
137 -- Another corouting is still waiting for a move to finish.
138 coroutine = "coroutine",
139
140 -- The fuel's empty so we cannot move at all.
141 fuel = "fuel",
142
143 -- Some block is in the way. If we had multiple tries, this means the block
144 -- respawned/regenerated, so we either have a stack of sand/gravel that is
145 -- higher than the number of tries was, or a cobblestone generator or
146 -- something similar.
147 block = "block",
148
149 -- Another turtle got in our way. If we had multiple tries, this means the
150 -- turtle did not move out of the way, so it's either not moving or wants
151 -- to move to where this turtle is (direct opposite move direction).
152 turtle = "turtle",
153
154 -- Some unbreakable block is in the way. This can only be determined when
155 -- we have multiple tries, in which case this is returned if the dig()
156 -- command returns false.
157 unbreakable_block = "unbreakable_block",
158
159 -- Some entity is in our way. This is the case when we're blocked but no
160 -- block is detected in the desired direction.
161 entity = "entity",
162
163 -- Some invulnerable entity is our way. This can only be determined when we
164 -- have multiple tries and we're aggressive, in which case this is returned
165 -- if the attack() command fails.
166 invulnerable_entity = "invulnerable_entity"
167}
168
169-----------------------------
170-- State related functions --
171-----------------------------
172
173--[[
174Note: all coordinates are initially relative to the turtle's origin,
175 i.e. to where it has been placed and this API was first used.
176 If the turtle is moved in some way other than via the functions
177 of this API (pickaxed by player and placed somewhere else, RP2
178 frames, ...) the coordinates will refer to somewhere else in
179 world space, since the origin has changed!
180]]
181
182--[[
183 Get the position and facing of the turtle.
184
185 @return a tuple (x, y, z, facing).
186]]
187function get()
188 private.resume()
189 local position = state.position
190 return private.toMC(position.x, position.y, position.z, position.f)
191end
192
193--[[
194 Get the current X coordinate of the turtle.
195
196 @return the turtle's current X position.
197]]
198function getX()
199 local x, _, _, _ = get()
200 return x
201end
202
203--[[
204 Get the current Y coordinate of the turtle.
205
206 @return the turtle's current Y position.
207]]
208function getY()
209 local _, y, _, _ = get()
210 return y
211end
212
213--[[
214 Get the current Z coordinate of the turtle.
215
216 @return the turtle's current Z position.
217]]
218function getZ()
219 local _, _, z, _ = get()
220 return z
221end
222
223--[[
224 Get the current X,Y,Z coordinates of the turtle as a vector.
225
226 @return a vector instance representing the turtle's position.
227]]
228function getPosition()
229 return vector.new(get())
230end
231
232--[[
233 Get the direction the turtle is currently facing.
234
235 @return the current orientation of the turtle.
236 @see lama.side
237]]
238function getFacing()
239 local _, _, _, f = get()
240 return f
241end
242
243--[[
244 Sets the position and facing of the turtle.
245
246 This can be useful to calibrate the turtle to a new origin, e.g. after
247 placing it, to match the actual world coordinates. The facing must be one
248 of the lama.side constants.
249
250 @param x the X coordinate to set the position to.
251 @param y the Y coordinate to set the position to.
252 @param z the Z coordinate to set the position to.
253 @param facing the facing to set.
254 @return the new position and facing of the turtle (like lama.get()).
255]]
256function set(x, y, z, facing)
257 -- Type checks.
258 assert(private.isInteger(x), "'x' must be an integral number")
259 assert(private.isInteger(y), "'y' must be an integral number")
260 assert(private.isInteger(z), "'z' must be an integral number")
261 assert(private.isFacing(facing),
262 "'facing' must be one of the lama.side constants")
263
264 -- Thread safety.
265 private.resume(true)
266 if private.isLocked() then
267 error("Cannot set position while moving or refueling.")
268 end
269
270 -- Reset the complete state before applying our new position.
271 private.reset()
272
273 -- Convert coordinates.
274 x, y, z, facing = private.fromMC(x, y, z, facing)
275
276 local position = state.position
277 position.x = x
278 position.y = y
279 position.z = z
280 position.f = facing
281 private.save("position")
282 return get()
283end
284
285--------------------------------
286-- Movement related functions --
287--------------------------------
288
289--[[
290 Try to move the turtle forward.
291
292 @param tries how often to try to move. If this larger than zero, the turtle
293 will try to dig its way through any obstructions as many times (e.g. to
294 get through stacks of sand or gravel).
295 @param aggressive if set to true, will also try to attack to clear its way
296 when obstructed.
297 @return true if the turtle moved successfully, (false, reason) if it failed
298 and is still in the same position.
299]]
300function forward(tries, aggressive)
301 private.resume()
302 if private.isLocked() then
303 return false, reason.coroutine
304 end
305 return private.forward(tries, aggressive)
306end
307
308--[[
309 Try to move the turtle backward.
310
311 Note that this does not have the 'tries' and 'aggressive' parameters, since
312 the turtle would have to turn around first in order to dig or attack.
313
314 @param tries how often to try to move. If this larger than zero, the turtle
315 will try to wait for any obstructions it hits to go away as many times.
316 As opposed to the other movement functions this will not dig nor attack
317 and only wait for other turtle to pass. In the other cases it will
318 immediately return false.
319 @return true if the turtle moved successfully, (false, reason) if it failed
320 and is still in the same position.
321]]
322function back(tries)
323 private.resume()
324 if private.isLocked() then
325 return false, reason.coroutine
326 end
327 return private.back(tries)
328end
329
330--[[
331 Try to move the turtle up.
332
333 @param tries how often to try to move. If this larger than zero, the turtle
334 will try to dig its way through any obstructions as many times (e.g. to
335 get through stacks of sand or gravel).
336 @param aggressive if set to true, will also try to attack to clear its way
337 when obstructed.
338 @return true if the turtle moved successfully, (false, reason) if it failed
339 and is still in the same position.
340]]
341function up(tries, aggressive)
342 private.resume()
343 if private.isLocked() then
344 return false, reason.coroutine
345 end
346 return private.up(tries, aggressive)
347end
348
349--[[
350 Try to move the turtle down.
351
352 @param tries how often to try to move. If this larger than zero, the turtle
353 will try to dig its way through any obstructions as many times (e.g. to
354 get through stacks of sand or gravel).
355 @param aggressive if set to true, will also try to attack to clear its way
356 when obstructed.
357 @return true if the turtle moved successfully, (false, reason) if it failed
358 and is still in the same position.
359]]
360function down(tries, aggressive)
361 private.resume()
362 if private.isLocked() then
363 return false, reason.coroutine
364 end
365 return private.down(tries, aggressive)
366end
367
368--[[
369 Moves the turtle to the specified location and turns it to face in the
370 specified direction if any.
371
372 If the turtle is shut down while moving to the specified target coordinates
373 it will continue moving after booting up again. After it finished moving,
374 either because it reached its target or because it encountered a problem,
375 the remaining startup scripts will be executed and the result of the move
376 command can be queried via lama.startupResult().
377
378 This is actually just a shortcut for
379 lama.navigate({x, y, z, facing}, tries, aggressive)
380
381 @param x the target X position.
382 @param x the target Y position.
383 @param x the target Z position.
384 @param facing the final facing, after reaching the target position. This
385 parameter is options; if omitted the turtle will remain in the
386 orientation it arrived in.
387 @param tries how often to try to move for each single move. If this larger
388 than zero, the turtle will try to dig its way through any obstructions
389 as many times (e.g. to get through stacks of sand or gravel).
390 @param aggressive if set to true, will also try to attack to clear its way
391 when obstructed.
392 @param longestFirst whether to move along the longer axes first. This an be
393 used to control how the turtle moves along its path. For example, when
394 moving from (0, 0, 0) to (1, 0, 3), when this is true the turtle will
395 move along the Z-axis first, then along the X-axis, if it is false the
396 other way around.
397 @return true if the turtle moved successfully, (false, reason) if it failed
398 and stopped somewhere along the way.
399]]
400function moveto(x, y, z, facing, tries, aggressive, longestFirst)
401 return navigate({{x = x, y = y, z = z, facing = facing}},
402 tries, aggressive, longestFirst)
403end
404
405-- Alias for moveto, usable as long as it's not a keyword, so why not.
406getfenv()["goto"] = moveto
407
408--[[
409 Moves the turtle along the specified path.
410
411 Each path entry can either be a waypoint or a set of coordinates. Note that
412 waypoints are resolved once in the beginning, so if a waypoint is changed
413 asynchronously (e.g. coroutine or interrupted due to forced shut down) that
414 change will not influence how the turtle will move.
415
416 If the turtle is shut down while moving along the specified path it will
417 continue moving after booting up again. After it finished moving, either
418 because it reached its final target or because it encountered a problem,
419 the remaining startup scripts will be executed and the result of the move
420 command can be queried via lama.startupResult().
421
422 Note that all facings for intermediate steps will be ignored, only the
423 facing in the final waypoint will be applied after the turtle arrived.
424
425 @param path the list of coordinates or waypoints to move along. Note that
426 only array entries are considered (i.e. with a numerical key) because
427 record entries (arbitrary key) have no guaranteed order.
428 @param tries how often to try to move for each single move. If this larger
429 than zero, the turtle will try to dig its way through any obstructions
430 as many times (e.g. to get through stacks of sand or gravel).
431 @param aggressive if set to true, will also try to attack to clear its way
432 when obstructed.
433 @param longestFirst whether to move along the longer axes first. This an be
434 used to control how the turtle moves along its path. For example, when
435 moving from (0, 0, 0) to (1, 0, 3), when this is true the turtle will
436 move along the Z-axis first, then along the X-axis, if it is false the
437 other way around.
438 @return true if the turtle moved successfully, (false, reason) if it failed
439 and stopped somewhere along the way.
440 @throw if the path contains an unknown waypoint or an invalid entry.
441]]
442function navigate(path, tries, aggressive, longestFirst)
443 -- Type checks.
444 assert(type(path) == "table", "'path' must be a table")
445 assert(tries == nil or private.isInteger(tries),
446 "'tries' must be an integral number or omitted")
447 assert(aggressive == nil or type(aggressive) == "boolean",
448 "'aggressive' must be a boolean or omitted")
449 assert(longestFirst == nil or type(longestFirst) == "boolean",
450 "'longestFirst' must be a boolean or omitted")
451
452 -- Thread safety.
453 private.resume()
454 if private.isLocked() then
455 return false, reason.coroutine
456 end
457
458 -- Resolve path to be all absolute and validate coordinates.
459 local absPath = {}
460 for k, v in ipairs(path) do
461 if type(v) == "string" then
462 -- It's a waypoint. Add the coordinate to the list.
463 local x, y, z, f = private.fromMC(waypoint.get(v))
464 table.insert(absPath, {x = x, y = y, z = z, f = f})
465 elseif type(v) == "table" then
466 -- It's a coordinate. Do some type checking.
467 local x, y, z, f = v.x, v.y, v.z, v.facing
468 assert(private.isInteger(x),
469 "'x' at index " .. k .. " must be an integral number")
470 assert(private.isInteger(y),
471 "'y' at index " .. k .. " must be an integral number")
472 assert(private.isInteger(z),
473 "'z' at index " .. k .. " must be an integral number")
474 assert(f == nil or private.isFacing(f),
475 "'facing' at index " .. k ..
476 " must be one of the lama.side constants or omitted")
477
478 -- Convert coordinates.
479 x, y, z, f = private.fromMC(x, y, z, f)
480
481 -- Add the coordinate to the list.
482 table.insert(absPath, {x = x, y = y, z = z, f = f})
483 else
484 error("Invalid path entry at index " .. k)
485 end
486 end
487
488 -- If we have no steps at all we can stop right here.
489 if #absPath == 0 then
490 return true
491 end
492
493 -- Strip facings for all except the last entry (because those don't matter
494 -- and will only slow us down).
495 for i = 1, #absPath - 1 do
496 absPath[i].f = nil
497 end
498
499 -- Set our new target and start moving.
500 state.path = {
501 steps = absPath,
502 tries = tries or 0,
503 aggressive = aggressive or nil,
504 longestFirst = longestFirst or nil
505 }
506 private.save("path")
507 return private.navigate()
508end
509
510-----------------------------------
511-- Orientation related functions --
512-----------------------------------
513
514--[[
515 Turn the turtle right.
516
517 @return true if the turtle turned successfully, false otherwise.
518]]
519function turnRight()
520 return turn((getFacing() + 1) % 4)
521end
522
523--[[
524 Turn the turtle left.
525
526 @return true if the turtle turned successfully, false otherwise.
527]]
528function turnLeft()
529 return turn((getFacing() - 1) % 4)
530end
531
532--[[
533 Turn the turtle around.
534
535 @return true if the turtle turned successfully, (false, reason) if it
536 failed - in this case the turtle may also have turned around halfway.
537 Only fails if the event queue is full.
538]]
539function turnAround()
540 return turn((getFacing() + 2) % 4)
541end
542
543--[[
544 Turn the turtle to face the specified direction.
545
546 @param towards the direction in which the turtle should face.
547 @return true if the turtle turned successfully, (false, reason) if it
548 failed - in this case the turtle may already have turned partially
549 towards the specified facing. Only fails if the event queue is full.
550 @see lama.side
551]]
552function turn(towards)
553 -- Type check, ensuring it's in bounds.
554 assert(private.isFacing(towards),
555 "'towards' must be one of the lama.side constants")
556
557 -- Thread safety.
558 private.resume()
559 if private.isLocked() then
560 return false, reason.coroutine
561 end
562
563 return private.turn(towards)
564end
565
566---------------
567-- Refueling --
568---------------
569
570--[[
571 Uses the items in the currently select slot to refuel the turtle.
572
573 @param count the number of items to consume; complete stack if omitted.
574 @return true if the item was successfully consumed; false otherwise.
575]]
576function refuel(count)
577 -- Type check.
578 assert(count == nil or (private.isInteger(count) and
579 count >= 0 and count <= 64),
580 "'count' must be a positive integral number in [0, 64] or omitted")
581
582 -- Thread safety.
583 private.resume()
584 if private.isLocked() then
585 return false, reason.coroutine
586 end
587
588 -- Issue the command, remember we're currently refueling and wait.
589 local id
590 -- We have to split it like this because otherwise count being nil is still
591 -- counted as an argument, leading to an "Expected a number" error.
592 if count then
593 id = turtle.native.refuel(count)
594 else
595 id = turtle.native.refuel()
596 end
597 if id == -1 then
598 return false, reason.queue_full
599 end
600 state.fuel.id = id
601 private.save("fuel")
602 local result = private.waitForResponse(id)
603 state.fuel.id = nil
604 state.fuel.current = turtle.getFuelLevel()
605 private.save("fuel")
606 return result
607end
608
609---------------
610-- Waypoints --
611---------------
612
613-- Namespace for waypoint related functions.
614waypoint = {}
615
616--[[
617 Adds a new waypoint to the list of known waypoints.
618
619 Note that the coordiantes are optional. If they are not specified,
620 the current coordiantes will be used. If all three coordinates are omitted
621 the facing of the waypoint will be set to the current facing; otherwise the
622 waypoint's facing will be unspecified.
623
624 @param name the name of the waypoint.
625 @param x the X coordinate of the waypoint.
626 @param y the Y coordinate of the waypoint.
627 @param z the Z coordinate of the waypoint.
628 @param facing the optional facing of the waypoint.
629 @return true if a waypoint of that name already existed and was
630 overwritten; false otherwise.
631]]
632function waypoint.add(name, x, y, z, facing)
633 private.resume()
634
635 -- Type checking.
636 assert(type(name) == "string" and name ~= "",
637 "'name' must be a non-empty string")
638 assert(x == nil or private.isInteger(x),
639 "'x' must be an integral number or omitted")
640 assert(y == nil or private.isInteger(y),
641 "'y' must be an integral number or omitted")
642 assert(z == nil or private.isInteger(z),
643 "'z' must be an integral number or omitted")
644 assert(facing == nil or private.isFacing(facing),
645 "'facing' must be one of the lama.side constants or omitted")
646
647 -- Convert coordinates.
648 x, y, z, facing = private.fromMC(x, y, z, facing)
649
650 -- Default to current position; also take facing if we use the exact
651 -- coordinates, i.e. we wouldn't have to move to reach the waypoint.
652 local position = state.position
653 if x == nil and y == nil and z == nil and facing == nil then
654 facing = position.f
655 end
656 x = x or position.x
657 y = y or position.y
658 z = z or position.z
659
660 local wasOverwritten = waypoint.exists(name)
661 state.waypoints[name] = {
662 x = math.floor(x),
663 y = math.floor(y),
664 z = math.floor(z),
665 f = facing
666 }
667 private.save("waypoints")
668 return wasOverwritten
669end
670
671--[[
672 Removes a waypoint from the list of known waypoints.
673
674 @param name the name of the waypoint to remove.
675 @return true if the waypoint was removed; false if there was no such
676 waypoint.
677]]
678function waypoint.remove(name)
679 private.resume()
680 if not waypoint.exists(name) then
681 return false
682 end
683 state.waypoints[name] = nil
684 private.save("waypoints")
685 return true
686end
687
688--[[
689 Checks if a waypoint with the specified name exists.
690
691 @param name the name of the waypoint to test for.
692 @return true if a waypoint of that name exists; false otherwise.
693]]
694function waypoint.exists(name)
695 private.resume()
696 assert(type(name) == "string" and name ~= "",
697 "'name' must be a non-empty string")
698 return state.waypoints[name] ~= nil
699end
700
701--[[
702 Get the coordinates of the waypoint with the specified name.
703
704 @param the name of the waypoint to get.
705 @return (x, y, z, facing) or nil if there is no such waypoint.
706 @throw if there is no waypoint with the specified name.
707]]
708function waypoint.get(name)
709 private.resume()
710 assert(waypoint.exists(name),
711 "no such waypoint, '" .. tostring(name) .. "'")
712 local w = state.waypoints[name]
713 return private.toMC(w.x, w.y, w.z, w.f)
714end
715
716--[[
717 Returns an iterator function to be used in a for loop.
718
719 Usage: for name, x, y, z, facing in lama.waypoint.iter() do ... end
720 Note that the facing may be nil.
721
722 @return an iterator over all known waypoints.
723]]
724function waypoint.iter()
725 private.resume()
726 local name
727 return function()
728 local coordinate
729 name, coordinate = next(state.waypoints, name)
730 if name then
731 return name, private.toMC(coordinate.x, coordinate.y,
732 coordinate.z, coordinate.f)
733 end
734 end
735end
736
737--[[
738 Moves the turtle to the specified waypoint.
739
740 If the turtle is shut down while moving to the specified target coordinates
741 it will continue moving after booting up again. After it finished moving,
742 either because it reached its target or because it encountered a problem,
743 the remaining startup scripts will be executed and the result of the move
744 command can be queried via lama.startupResult().
745
746 This just calls lama.moveto() with the waypoint's coordinates, which in
747 turn is an alias for lama.navigate() for single length paths.
748
749 @param name the name of the waypoint to move to.
750 @param tries how often to try to move for each single move. If this larger
751 than zero, the turtle will try to dig its way through any obstructions
752 as many times (e.g. to get through stacks of sand or gravel).
753 @param aggressive if set to true, will also try to attack to clear its way
754 when obstructed.
755 @param longestFirst whether to move along the longer axes first. This an be
756 used to control how the turtle moves along its path. For example, when
757 moving from (0, 0, 0) to (1, 0, 3), when this is true the turtle will
758 move along the Z-axis first, then along the X-axis, if it is false the
759 other way around.
760 @return true if the turtle moved successfully, (false, reason) if it failed
761 and stopped somewhere along the way.
762 @throw if there is no waypoint with the specified name.
763]]
764function waypoint.moveto(name, tries, aggressive, longestFirst)
765 x, y, z, facing = waypoint.get(name)
766 return moveto(x, y, z, facing, tries, aggressive, longestFirst)
767end
768
769-- Alias for waypoint.moveto, usable as long as it's not a keyword, so why not.
770waypoint["goto"] = waypoint.moveto
771
772-----------------------
773-- Utility functions --
774-----------------------
775
776--[[
777 This function can be called to fully initialize the API.
778
779 This entrails loading any previous state and resuming any incomplete
780 movement orders (including paths, so it can take a while for this function
781 to return the first time it is called).
782
783 This function is idempotent.
784]]
785function init()
786 private.resume()
787end
788
789--[[
790 Gets the result of resuming a move on startup.
791
792 This can be used to query whether a move that was interrupted and continued
793 on startup finished successfully or not, and if not for what reason.
794
795 For example: imagine you issue lama.forward() and the program stops. Your
796 program restores its state and knows it last issued the forward command,
797 but further execution depends on whether that move was successful or not.
798 To check this after resuming across unloading you'd use this function.
799
800 Note that this will return true in case the startup did not continue a move
801 (program was not interrupted during a move).
802
803 @return true or a tuple (result, reason) based on the startup move result.
804]]
805function startupResult()
806 private.resume()
807 if not private.startupResult then
808 return true
809 end
810 return private.startupResult.result, private.startupResult.reason
811end
812
813--[[
814 Replaces the movement related functions in the turtle API.
815
816 This makes it easier to integrate this API into existing programs.
817 This does NOT touch the native methods.
818 The injected functions will NOT be the same as calling the API function
819 directly, to avoid changes in existing programs when this is dropped in.
820
821 For example: a call to turtle.forward(1) will return false if the turtle is
822 blocked, whereas lama.forward(1) would try to destroy the block, and then
823 move. The function replacing turtle.forward() will behave the same as the
824 old one, in that the parameter will be ignored. This follows the principle
825 of least astonishment.
826
827 @param restore whether to restore the original turtle API functions.
828]]
829function hijackTurtleAPI(restore)
830 -- Wrap methods to avoid accidentally passing parameters along. This is
831 -- done to make sure behavior is the same even if the functions are
832 -- called with (unused/invalid) parameters.
833 if restore then
834 if not turtle._lama then return end
835 turtle.forward = turtle._lama.forward
836 turtle.back = turtle._lama.back
837 turtle.up = turtle._lama.up
838 turtle.down = turtle._lama.down
839 turtle.turnRight = turtle._lama.turnRight
840 turtle.turnLeft = turtle._lama.turnLeft
841 turtle.refuel = turtle._lama.refuel
842 turtle._lama = nil
843 else
844 if turtle._lama then return end
845 turtle._lama = {
846 forward = turtle.forward,
847 back = turtle.back,
848 up = turtle.up,
849 down = turtle.down,
850 turnRight = turtle.turnRight,
851 turnLeft = turtle.turnLeft,
852 refuel = turtle.refuel
853 }
854 turtle.forward = function() return forward() ~= false end
855 turtle.back = function() return back() ~= false end
856 turtle.up = function() return up() ~= false end
857 turtle.down = function() return down() ~= false end
858 turtle.turnRight = function() return turnRight() ~= false end
859 turtle.turnLeft = function() return turnLeft() ~= false end
860 turtle.refuel = function() return refuel() ~= false end
861 end
862end
863
864-------------------------------------------------------------------------------
865-------------------------------------------------------------------------------
866-- Internal methods follow; you cannot call these, so you can probably stop --
867-- reading right here, unless you're interested in implementation details. --
868-------------------------------------------------------------------------------
869-------------------------------------------------------------------------------
870
871-- Adjust constants depending on coordinate system.
872if useMinecraftCoordinates then
873 -- Constants.
874 for k, v in pairs(side) do
875 if type(v) == "number" then
876 side[k] = (v + 2) % 4
877 end
878 end
879 -- Reverse lookup.
880 side[0], side[1], side[2], side[3] = side[2], side[3], side[0], side[1]
881end
882
883-- Raise error when trying to access invalid key in the constant tables. This
884-- is meant to make it easier to track down typos.
885do
886 local function makeReadonly(table, name)
887 setmetatable(table, {
888 __index = function(t,k)
889 error("Trying to access invalid '" .. name ..
890 "' constant '" .. k .. "'.")
891 end,
892 __newindex = function()
893 error("Trying to modify readonly table.")
894 -- Yes, existing entries can be overwritten like this, but I
895 -- prefer to keep the table enumerable via pairs(). And this is
896 -- just a utility "feature" anyway.
897 end
898 })
899 end
900 makeReadonly(side, "lama.side")
901 makeReadonly(reason, "lama.reason")
902end
903
904-- Namespace for private functions.
905private = {}
906
907-- Movement directions, used for move state, as a bidirectional map.
908private.direction = {
909 [1] = "forward", [2] = "back", [3] = "up", [4] = "down",
910 forward = 1, back = 2, up = 3, down = 4
911}
912
913-- Initialize state variable with defaults.
914state = {
915 -- Coordinates and facing.
916 position = {x = 0, y = 0, z = 0, f = 0},
917
918 -- List of registered waypoints.
919 waypoints = {},
920
921 -- The last known fuel state. This is used to ensure our coordinates are
922 -- still valid (might be broken due to invalid API use or server rollback).
923 -- Also stores command ID when currently refueling (to detect rollback).
924 fuel = {current = turtle.getFuelLevel()},
925
926 -- The current movement state, i.e. the direction we're moving in, the fuel
927 -- level before the move started, the remaining tries and whether we're
928 -- moving aggressively or not.
929 move = nil,
930
931 -- The path we're currently moving along when performing a multi-block
932 -- movement (via lama.navigate() or lama.moveto()/lama.waypoint.moveto()).
933 path = nil,
934
935 -- This is used to keep track of how many times we tried to wrap the
936 -- startup file in a nested fashion. We need to store this in the
937 -- persistent state in case the API is loaded again (which would reset
938 -- normal variables) and it helps us to check whether we need to unwrap
939 -- the startup file when resuming after a reboot.
940 wrap = 0
941}
942
943-- Schemas for the state variable. This defines the expected value types and
944-- value presence in the state, which we use to validate a loaded state.
945private.schema = {}
946private.schema[version] = {
947 position = {
948 type = "table",
949 properties = {
950 x = {type = "number", value = private.isInteger},
951 y = {type = "number", value = private.isInteger},
952 z = {type = "number", value = private.isInteger},
953 f = {type = "number", value = private.isFacing}
954 }
955 },
956 waypoints = {
957 type = "table",
958 entries = {
959 type = "table",
960 keytype = "string",
961 properties = {
962 x = {type = "number", value = private.isInteger},
963 y = {type = "number", value = private.isInteger},
964 z = {type = "number", value = private.isInteger},
965 f = {type = "number", value = private.isFacing,
966 optional = true}
967 }
968 }
969 },
970 fuel = {
971 type = "table",
972 properties = {
973 current = {type = "number", value = private.isInteger},
974 id = {type = "number", value = private.isInteger,
975 optional = true}
976 }
977 },
978 move = {
979 type = "table",
980 properties = {
981 direction = {
982 type = "number",
983 value = function(v) return private.direction[v] ~= nil end
984 },
985 tries = {type = "number", value = private.isInteger},
986 aggressive = {type = "boolean", optional = true}
987 }
988 },
989 path = {
990 type = "table",
991 properties = {
992 steps = {
993 type = "table",
994 entries = {
995 type = "table",
996 properties = {
997 x = {type = "number", value = private.isInteger},
998 y = {type = "number", value = private.isInteger},
999 z = {type = "number", value = private.isInteger},
1000 f = {type = "number", value = private.isFacing,
1001 optional = true}
1002 }
1003 }
1004 },
1005 tries = {type = "number", value = private.isInteger},
1006 aggressive = {type = "boolean", optional = true},
1007 longestFirst = {type = "boolean", optional = true}
1008 }
1009 },
1010 wrap = {type = "number", value = private.isInteger}
1011}
1012
1013-- Schema delta for version 1.2 to 1.3 state files (used for upgrading).
1014private.schema["1.2"] = {
1015 move = {
1016 type = "table",
1017 optional = true,
1018 properties = {
1019 direction = {
1020 type = "number",
1021 value = function(v) return private.direction[v] ~= nil end
1022 },
1023 preMoveFuel = {type = "number", value = private.isInteger},
1024 tries = {type = "number", value = private.isInteger},
1025 aggressive = {type = "boolean"}
1026 },
1027 upgrade = function(value)
1028 state.fuel.current = value.preMoveFuel
1029 private.save("fuel")
1030 return {value.direction, value.tries, value.aggressive}
1031 end
1032 }
1033}
1034
1035-- Schema for version 1.0 state files (used for upgrading).
1036private.schema["1.0"] = {
1037 type = "table",
1038 properties = {
1039 position = {
1040 type = "table",
1041 properties = {
1042 x = {type = "number", value = private.isInteger},
1043 y = {type = "number", value = private.isInteger},
1044 z = {type = "number", value = private.isInteger},
1045 f = {type = "number", value = private.isFacing}
1046 }
1047 },
1048 moving = {
1049 type = {"boolean", "string"},
1050 value = function(v)
1051 if type(v) == "string" then
1052 return v == "forward" or
1053 v == "back" or
1054 v == "up" or
1055 v == "down"
1056 end
1057 return true
1058 end
1059 },
1060 preMoveFuel = {type = "number", value = private.isInteger},
1061 tries = {type = "number", value = private.isInteger},
1062 aggressive = {type = "boolean"}
1063 }
1064}
1065
1066-------------------------------------------------------------------------------
1067-- Schema based value validation --
1068-------------------------------------------------------------------------------
1069
1070--[[
1071 Validates a value based on a schema.
1072
1073 This checks if the value fits the specified schema (i.e. types are correct)
1074 which is used when loading states, to avoid loading a corrupted state.
1075
1076 @param value the value to validate.
1077 @param schema the schema to use to validate the value.
1078 @return true if the value fits the schema; (false, reason) otherwise.
1079 @private
1080]]
1081function private.validate(value, schema)
1082 assert(schema ~= nil, "no schema given")
1083 local function validate(value, schema, path)
1084 -- Is the value optional? We do this first because we still want to
1085 -- return false if the type mismatches if the value is optional but not
1086 -- nil.
1087 if schema.optional and value == nil then
1088 return true
1089 end
1090
1091 -- Is the value type correct?
1092 if type(schema.type) == "table" then
1093 -- Value may have multiple types, check if any one fits.
1094 local ok = false
1095 for _,valueType in pairs(schema.type) do
1096 if type(value) == valueType then
1097 ok = true
1098 break
1099 end
1100 end
1101 if not ok then
1102 return false, path .. ": invalid type; is " .. type(value) ..
1103 ", should be one of [" ..
1104 table.concat(schema.type, ", ") .. "]"
1105 end
1106 elseif schema.type and type(value) ~= schema.type then
1107 return false, path .. ": invalid type; is " .. type(value) ..
1108 ", should be " .. schema.type
1109 end
1110
1111 -- See if we have a custom validator function.
1112 if schema.value and not schema.value(value) then
1113 return false, path .. ": invalid value"
1114 end
1115
1116 -- Recursively check properties of the value.
1117 if schema.properties then
1118 for property, propertySchema in pairs(schema.properties) do
1119 local result, location = validate(value[property],
1120 propertySchema,
1121 path .. "." .. property)
1122 if not result then
1123 return result, location
1124 end
1125 end
1126 end
1127
1128 -- Recursively check entries of a table.
1129 if schema.entries then
1130 for key, entry in pairs(value) do
1131 if schema.entries.keytype and
1132 type(key) ~= schema.entries.keytype
1133 then
1134 return false, path .. "[" .. key ..
1135 "]: invalid key type; is " .. type(key) ..
1136 ", should be " .. schema.entries.keytype
1137 end
1138 local result, location = validate(entry,
1139 schema.entries,
1140 path .. "[" .. key .. "]")
1141 if not result then
1142 return result, location
1143 end
1144 end
1145 end
1146
1147 -- No issues.
1148 return true
1149 end
1150 return validate(value, schema, "value")
1151end
1152
1153--[[
1154 Checks if the specified number is integral.
1155
1156 @param value the number to check.
1157 @return true if the number is integral; false otherwise.
1158 @private
1159]]
1160function private.isInteger(value)
1161 return type(value) == "number" and
1162 value == math.floor(value)
1163end
1164
1165--[[
1166 Checks if the specified number is a valid facing.
1167
1168 @param value the number to check.
1169 @return true if the number is a valid facing; false otherwise.
1170 @private
1171]]
1172function private.isFacing(value)
1173 return type(value) == "number" and
1174 private.isInteger(value) and value >= 0 and value < 4
1175end
1176
1177-------------------------------------------------------------------------------
1178-- State saving/loading --
1179-------------------------------------------------------------------------------
1180
1181--[[
1182 Saves the specified state section to its corresponding file.
1183
1184 @param the name of the section to save.
1185 @private
1186]]
1187function private.save(what)
1188 -- Serialize before opening the file, just in case.
1189 local data = textutils.serialize(state[what])
1190 local file = fs.open(stateFile[what], "w")
1191 if not file then
1192 private.invalidate()
1193 error("Could not opens state file '" .. what .. "' for writing.")
1194 end
1195 file.write(data)
1196 file.close()
1197end
1198
1199--[[
1200 Restores the complete state.
1201
1202 @return true if the state was restored successfully; false otherwise.
1203 @private
1204]]
1205function private.load()
1206 -- Check if we may have upgraded and the state file is of an old version.
1207 if private.upgrade() then
1208 return true
1209 end
1210
1211 -- Utility function for loading single sections.
1212 local function load(section)
1213 local filename = stateFile[section]
1214 if not fs.exists(filename) then
1215 return true
1216 end
1217 assert(not fs.isDir(filename),
1218 "Invalid state filename '" .. filename .. "': it's a folder.")
1219
1220 local success, result = private.unserialize(filename)
1221 if success then
1222 -- Validate the read state.
1223 local valid, failureReason = private.validate(result,
1224 private.schema[version][section])
1225 if valid then
1226 -- All green, keep the new state.
1227 state[section] = result
1228 return true
1229 elseif private.schema["1.2"][section] and
1230 private.validate(result, private.schema["1.2"][section])
1231 then
1232 -- We can upgrade this section. Let's.
1233 state[section] = private.schema["1.2"][section].upgrade(result)
1234 return true
1235 else
1236 print("LAMA: Invalid state file '" .. filename ..
1237 "' (" .. failureReason .. ").")
1238 end
1239 end
1240 return false
1241 end
1242
1243 -- Read all state sections one by one.
1244 for section, _ in pairs(stateFile) do
1245 if not load(section) then
1246 return false
1247 end
1248 end
1249
1250 -- Everything is fine.
1251 return true
1252end
1253
1254--[[
1255 Utility function for unserializing data from a file.
1256
1257 @param filename the name of the file to deserialize the data from.
1258 @return (true, result) if the file exists, false otherwise.
1259 @private
1260]]
1261function private.unserialize(filename)
1262 -- Read the text data from the file.
1263 local file = assert(fs.open(filename, "r"))
1264 local data = file.readAll()
1265 file.close()
1266
1267 -- Unserialize after closing the file, just in case. I don't fully
1268 -- trust CC Lua's GC in this regard because I had to actually close
1269 -- MC once or twice because some files stayed locked even though I
1270 -- already returned to the menu.
1271
1272 -- Custom implementation of textutils.unserialize() that properly handles
1273 -- serialized math.huge values...
1274 local result, _ = loadstring("return " .. data, filename)
1275 if not result then
1276 return true, data
1277 else
1278 return true, setfenv(result, {["inf"] = math.huge})()
1279 end
1280end
1281
1282--[[
1283 Utility function for trying to upgrade a state file from previous versions.
1284
1285 @return true if a state file was upgraded; false otherwise.
1286 @private
1287]]
1288function private.upgrade()
1289 -- Skip if no state file of the expected old name exists.
1290 if not fs.exists(stateFile.position) or fs.isDir(stateFile.position) then
1291 return false
1292 end
1293
1294 -- Try to parse the 'main' state file.
1295 local success, result = private.unserialize(stateFile.position)
1296 if not success or not private.validate(result, private.schema["1.0"]) then
1297 return false
1298 end
1299
1300 -- It's a version 1.0 format state file. Convert to current
1301 -- format and return.
1302 state.position = result.position
1303 if type(result.moving) == "string" then
1304 state.fuel.current = result.preMoveFuel
1305 state.move = {
1306 direction = private.direction[result.moving],
1307 tries = result.tries,
1308 aggressive = result.aggressive
1309 }
1310 -- We did not track this explicitly in version 1.0, but
1311 -- when in a moving state we definitely had the startup
1312 -- file wrapped.
1313 state.wrap = 1
1314 end
1315
1316 -- Write back the new format.
1317 private.save("position")
1318 private.save("move")
1319 private.save("fuel")
1320 private.save("wrap")
1321
1322 return true
1323end
1324
1325--[[
1326 Resets our internal state.
1327
1328 This is used if we run into any unexpected errors, for example invalid
1329 state files or an unexpected fuel level on startup.
1330
1331 @private
1332]]
1333function private.reset()
1334 state.position.x = 0
1335 state.position.y = 0
1336 state.position.z = 0
1337 state.position.f = 0
1338 -- No state.waypoints = {}. Keep waypoints intact.
1339 state.fuel = {current = turtle.getFuelLevel()}
1340 state.move = nil
1341 state.path = nil
1342 state.wrap = 0
1343 state.isInitializing = nil
1344 state.isInitialized = true
1345
1346 fs.delete(stateFile.position)
1347 -- No fs.delete(stateFile.waypoints). Keep the waypoints intact.
1348 fs.delete(stateFile.fuel)
1349 fs.delete(stateFile.move)
1350 fs.delete(stateFile.path)
1351 fs.delete(stateFile.wrap)
1352
1353 fs.delete(invalidStateFile)
1354end
1355
1356--[[
1357 Checks whether the API is currently locked (because we're moving e.g.).
1358
1359 @return true if we're locked; false otherwise.
1360]]
1361function private.isLocked()
1362 return state.move or state.path or state.fuel.id
1363end
1364
1365-------------------------------------------------------------------------------
1366-- Resume command on startup stuff --
1367-------------------------------------------------------------------------------
1368
1369-- The script we use for automatically initializing the API after a reboot.
1370local startupScript = string.format(
1371[[assert(os.loadAPI(%q))
1372lama.init()
1373lama.hijackTurtleAPI()]], apiPath)
1374
1375-- List of 'handlers'. This makes it easy to add support for different startup
1376-- script implementations (e.g. from different OSs or utility scripts).
1377--
1378-- The logic works like this: the active handler is determined by looking for
1379-- the first implementation that returns true from its 'test' function, else
1380-- the default handler is used. For the selected handler 'init' is called once
1381-- when the API is loaded. After that, wrap and unwrap are called like so:
1382-- move command -> wrap -> actual move -> unwrap
1383-- multimove -> wrap -> single moves (nested wraps!) -> unwrap
1384-- startup -> unwrap(true)
1385local startupHandlers = {
1386 -- Default implementation creates a wrapper startup script and moves the
1387 -- original startup script, if any, to a backup location to be restored
1388 -- when the startup script is run. This has rather bad performance because
1389 -- it adds one file write, one deletion and two moves to each move command.
1390 default = {
1391 init = function()
1392 assert(type(startupBackupFile) == "string" and
1393 startupBackupFile ~= "",
1394 "The setting 'startupBackupFile' must be a non-empty string.")
1395 end,
1396 wrap = function()
1397 local haveStartup = fs.exists("/startup")
1398 if haveStartup then
1399 fs.move("/startup", startupBackupFile)
1400 end
1401
1402 local f = assert(fs.open("/startup", "w"),
1403 "Could not open startup script for writing.")
1404 f.writeLine(startupScript)
1405 if haveStartup then
1406 f.writeLine("shell.run('/startup')")
1407 else
1408 end
1409 f.close()
1410 end,
1411 unwrap = function()
1412 fs.delete("/startup")
1413 if fs.exists(startupBackupFile) then
1414 fs.move(startupBackupFile, "/startup")
1415 end
1416 end,
1417 test = function()
1418 -- Ignore when determining handler.
1419 return false
1420 end
1421 },
1422
1423 -- Implementation for using Forairan's init-script startup program. This
1424 -- will only create a startup script once, which has no side effects if
1425 -- there were no pending moves, so performance impact is minimal.
1426 forairan = {
1427 init = function()
1428 -- Overwrite the startup file since the apiPath or the startup
1429 -- script's priority may have changed.
1430 local priority = type(startupPriority) == "number" and
1431 string.format("%2d") or
1432 tostring(startupPriority)
1433 local path = "/init-scripts/" .. priority .. "_lama"
1434 local f = assert(fs.open(path, "w"),
1435 "Could not open startup script for writing.")
1436 f.write(startupScript)
1437 f.close()
1438 end,
1439 test = function()
1440 -- Assume we use Forairan's startup logic if the init-scripts
1441 -- folder exists.
1442 return fs.exists("/init-scripts") and fs.isDir("/init-scripts")
1443 end
1444 },
1445
1446 -- Implementation for my own little startup API. This will only create
1447 -- a startup script once, which has no side effects if there were no
1448 -- pending moves, so performance impact is minimal.
1449 sangar = {
1450 init = function()
1451 -- Overwrite the startup file since the apiPath or the startup
1452 -- script's priority may have changed.
1453 startup.remove("lama")
1454 startup.addString("lama", startupPriority, startupScript)
1455 end,
1456 test = function()
1457 -- Check if the startup API is loaded.
1458 return startup ~= nil and startup.version ~= nil
1459 end
1460 }
1461}
1462
1463--[[
1464 Initializes startup script management.
1465 @private
1466]]
1467function private.initStartup()
1468 -- Validate configuration related to startup.
1469 assert(private.isInteger(startupPriority),
1470 "The setting 'startupPriority' must be an integral number.")
1471
1472 -- Detect which handler to use, initialized to default as fallback.
1473 private.startupHandler = startupHandlers.default
1474 for _, handler in pairs(startupHandlers) do
1475 if handler.test() then
1476 private.startupHandler = handler
1477 break
1478 end
1479 end
1480
1481 -- Run handler's init code.
1482 if private.startupHandler.init then
1483 private.startupHandler.init()
1484 end
1485end
1486
1487--[[
1488 Actual behavior depends on the active handler.
1489
1490 This allows for nested calling, where only the first nesting level performs
1491 an actual call to the wrap implementation.
1492
1493 @private
1494]]
1495function private.wrapStartup()
1496 if state.wrap == 0 then
1497 if private.startupHandler.wrap then
1498 private.startupHandler.wrap()
1499 end
1500 end
1501 state.wrap = state.wrap + 1
1502 private.save("wrap")
1503end
1504
1505--[[
1506 Actual behavior depends on the active handler.
1507
1508 This allows for nested calling, where only the first nesting level performs
1509 an actual call to the unwrap implementation.
1510
1511 @param force whether to force the unwrapping, used in init.
1512 @private
1513]]
1514function private.unwrapStartup(force)
1515 if state.wrap == 1 or
1516 (force and state.wrap > 0)
1517 then
1518 if private.startupHandler.unwrap then
1519 private.startupHandler.unwrap()
1520 end
1521 state.wrap = 0
1522 fs.delete(stateFile.wrap)
1523 elseif state.wrap > 0 then
1524 state.wrap = state.wrap - 1
1525 private.save("wrap")
1526 end
1527end
1528
1529--[[
1530 Initializes the API by restoring a previous state and finishing any pending
1531 moves.
1532
1533 This is used for lazy initialization, to avoid blocking when actually
1534 loading the API (i.e. when calling os.loadAPI("lama")). The first call to
1535 any function of the API will trigger this logic. An alternative "no-op"
1536 function to get() (which isn't very clear when reading) is the init()
1537 function, which should make the purpose of the call clear.
1538
1539 @param dontCrash don't throw an error if the API is in an invalid state.
1540 @private
1541]]
1542function private.resume(dontCrash)
1543 if state.isInitialized then
1544 -- Already initialized successfully, nothing to do here.
1545 return
1546 end
1547 while state.isInitializing do
1548 -- Already being initialized by another thread, wait for it to finish.
1549 os.sleep(1)
1550 end
1551 if fs.exists(invalidStateFile) then
1552 -- API is in an invalid state, don't allow use of any functions.
1553 if dontCrash then
1554 -- Except those that explicitly allow it, which is set() for now.
1555 return
1556 end
1557 error("Invalid state. Please reinitialize the turtle's position.")
1558 end
1559
1560 -------------------------------------------------------------------------------
1561 -- Environment checking --
1562 -------------------------------------------------------------------------------
1563
1564 -- MT: Moved assertions here from end of API file because assert failures were
1565 -- preventing the API from loading when stored in ROM (e.g. when included in a
1566 -- resource pack
1567
1568 assert(turtle, "The lama API can only run on turtles.")
1569 assert(os.getComputerLabel(), "Turtle has no label, required for state persistence.")
1570 assert(turtle.getFuelLevel() ~= "unlimited", "Turtles must use fuel for this API to work correctly.")
1571
1572 if bapil then
1573 apiPath = bapil.resolveAPI(apiPath)
1574 else
1575 -- MT: Allow locally installed versions on each turtle to override the ROM version
1576 local i
1577 local tryPaths = {
1578 "/rom/apis/lama",
1579 "/rom/apis/turtle/lama",
1580 "/apis/lama",
1581 "/apis/turtle/lama"
1582 }
1583 for i = 1 , #tryPaths do
1584 if fs.exists(tryPaths[i]) and not fs.isDir(tryPaths[i]) then
1585 apiPath = tryPaths[i]
1586 end
1587 end
1588 end
1589
1590 -- MT: Re-set startupScript to include the chosen apiPath
1591 startupScript = string.format(
1592[[assert(os.loadAPI(%q))
1593lama.init()
1594lama.hijackTurtleAPI()]], apiPath)
1595
1596 assert(type(apiPath) == "string" and apiPath ~= "", "The setting 'apiPath' must be a non-empty string.")
1597 assert(fs.exists(apiPath) and not fs.isDir(apiPath), "No file found at 'apiPath', please make sure it points to the lama API.")
1598
1599 -- Thread safety: engaged!
1600 state.isInitializing = true
1601
1602 -- Initialize startup script management.
1603 private.initStartup()
1604
1605 -- Load state, if any.
1606 local valid = private.load()
1607
1608 -- Process event queue (blocks until processed) and get the ID which is
1609 -- indicative of how long the turtle has lived (since command IDs grow
1610 -- steadily over the lifetime of a computer). This is used for the validity
1611 -- checks below.
1612 local id
1613 repeat
1614 os.sleep(0.1)
1615 id = turtle.native.detect()
1616 until id ~= -1
1617 private.waitForResponse(id)
1618
1619 -- Force unwrapping if we have at least one level of startup wrapper.
1620 private.unwrapStartup(true)
1621
1622 -- Ensure state validity using some tricks...
1623 if state.fuel.id then
1624 -- If the ID we just got from our detect() call was larger than the one
1625 -- we got when refueling we're OK. If it wasn't we can be sure there
1626 -- was a rollback, in which case we can't say where we are anymore.
1627 if id > state.fuel.id then
1628 state.fuel.current = turtle.getFuelLevel()
1629 else
1630 valid = false
1631 end
1632 elseif state.move then
1633 -- If we're moving it must match the stored one or be one less.
1634 valid = valid and turtle.getFuelLevel() == state.fuel.current or
1635 turtle.getFuelLevel() == state.fuel.current - 1
1636 else
1637 -- If we're not moving our fuel state must match the stored one.
1638 valid = valid and turtle.getFuelLevel() == state.fuel.current
1639 end
1640
1641 -- If any validity check failed lock down the API.
1642 if not valid then
1643 -- This should not be possible if the API is used correctly, i.e. no
1644 -- other movement functions of the turtle are used. Another possibility
1645 -- why this can happen is a hard server crash (rollback).
1646 state.isInitializing = nil
1647 assert(fs.open(invalidStateFile, "w"),
1648 "Failed to create invalidation file.").close()
1649 error("Invalid state. Please reinitialize the turtle's position.")
1650 end
1651
1652 -- Check if we performed a single move and finish any pending multi-try
1653 -- single moves.
1654 if state.move then
1655 -- We can use that fuel state to check whether we moved successfully or
1656 -- not: if we were moving, the fuel can only be equal to the previous
1657 -- level, or one less. Equal meaning we didn't move, one less meaning
1658 -- we did move.
1659 if turtle.getFuelLevel() == state.fuel.current then
1660 -- No fuel was used, so we didn't move. If we have some tries left,
1661 -- continue trying.
1662 if state.move.tries > 0 then
1663 local result, failureReason =
1664 private.move(state.move.direction,
1665 state.move.tries,
1666 state.move.aggressive)
1667 private.startupResult = {
1668 result = result,
1669 reason = failureReason
1670 }
1671 else
1672 private.startupResult = {
1673 result = false,
1674 reason = private.tryGetReason(state.move.direction)
1675 }
1676 -- We do this after trying to get the reason, because that can
1677 -- yield (detect) so we might lose the result when we're reset
1678 -- in that phase.
1679 private.endMove()
1680 end
1681 elseif turtle.getFuelLevel() == state.fuel.current - 1 then
1682 -- We used one fuel so we made our move! As with the case above,
1683 -- this can only be wrong if refueling is involved somewhere,
1684 -- which, again, can only happen using coroutines.
1685 private.updateState()
1686 private.endMove()
1687 else
1688 -- Other cases handled in validation above!
1689 assert(false)
1690 end
1691 else
1692 -- We can assume we're in a valid non-moving state, so update our fuel.
1693 -- This is used to avoid having to save the fuel state again before
1694 -- each attempt to move and to finalize refuel operations.
1695 state.fuel.current = turtle.getFuelLevel()
1696 state.fuel.id = nil
1697 private.save("fuel")
1698 end
1699
1700 -- If we're currently traveling towards a target location, continue doing
1701 -- so after resolving possibly active single moves above.
1702 if state.path then
1703 -- See if the previous move was successful. This isn't *really*
1704 -- necessary, because we could just try to continue moving, which would
1705 -- then result in the same failure, again. But we can avoid repeating
1706 -- work, so let's do it like this.
1707 local result, _ = not private.startupResult or
1708 private.startupResult.result
1709 if result then
1710 -- Everything is still OK, continue moving.
1711 private.navigate()
1712 else
1713 -- Something went wrong, exit travel mode. We don't have to touch
1714 -- the startup result, it's obviously already set.
1715 private.endNavigate()
1716 end
1717 end
1718
1719 -- Done, don't run again, release thread lock!
1720 state.isInitializing = nil
1721 state.isInitialized = true
1722end
1723
1724-------------------------------------------------------------------------------
1725-- Coordinate conversion --
1726-------------------------------------------------------------------------------
1727
1728--[[
1729 Converts a coordinate from the internal system to Minecraft's system.
1730
1731 This does nothing if useMinecraftCoordinates is false.
1732
1733 @param x the X coordinate.
1734 @param y the Y coordinate.
1735 @param z the Z coordinate.
1736 @param facing the optional facing.
1737 @private
1738]]
1739function private.toMC(x, y, z, facing)
1740 if useMinecraftCoordinates then
1741 return y, z, -x, facing and (facing + 2) % 4 or nil
1742 else
1743 return x, y, z, facing
1744 end
1745end
1746
1747--[[
1748 Converts a coordinate from Minecraft's system to the internal system.
1749
1750 This does nothing if useMinecraftCoordinates is false.
1751
1752 @param x the X coordinate.
1753 @param y the Y coordinate.
1754 @param z the Z coordinate.
1755 @param facing the optional facing.
1756 @private
1757]]
1758function private.fromMC(x, y, z, facing)
1759 if useMinecraftCoordinates then
1760 return -z, x, y, facing and (facing + 2) % 4 or nil
1761 else
1762 return x, y, z, facing
1763 end
1764end
1765
1766-------------------------------------------------------------------------------
1767-- Movement implementation --
1768-------------------------------------------------------------------------------
1769
1770--[[
1771 Waits for queued commands to finish.
1772
1773 @param ids the response ID or list of IDs of the commands to wait for.
1774 @return true if the commands were executed successfully, false otherwise.
1775 @private
1776]]
1777function private.waitForResponse(ids)
1778 if type(ids) ~= "table" then
1779 ids = {ids}
1780 elseif #ids == 0 then
1781 return true
1782 end
1783 local success = true
1784 repeat
1785 local event, responseID, result = os.pullEvent("turtle_response")
1786 if event == "turtle_response" then
1787 for i = 1, #ids do
1788 if ids[i] == responseID then
1789 success = success and result
1790 table.remove(ids, i)
1791 break
1792 end
1793 end
1794 end
1795 until #ids == 0
1796 return success
1797end
1798
1799--[[
1800 Figures out why a turtle cannot move in the specified direction.
1801
1802 @param direction the direction to check for.
1803 @return one of the lama.reason constants.
1804 @private
1805]]
1806function private.tryGetReason(direction)
1807 local detect = ({
1808 [private.direction.forward] = turtle.detect,
1809 [private.direction.up] = turtle.detectUp,
1810 [private.direction.down] = turtle.detectDown})[direction]
1811 local sideName = ({
1812 [private.direction.forward] = "front",
1813 [private.direction.up] = "top",
1814 [private.direction.down] = "bottom"})[direction]
1815
1816 -- Check for turtles first, because it's non-yielding.
1817 if peripheral.getType(sideName) == "turtle" then
1818 -- A turtle is blocking us.
1819 return reason.turtle
1820 elseif detect then
1821 if detect() then
1822 -- Some other block is in our way.
1823 return reason.block
1824 else
1825 -- Not a block, so we can assume it's some entity.
1826 return reason.entity
1827 end
1828 else
1829 -- Cannot determine what's blocking us.
1830 return reason.unknown
1831 end
1832end
1833
1834--[[
1835 Internal forward() implementation, used to ignore lock while navigating.
1836
1837 @private
1838]]
1839function private.forward(tries, aggressive)
1840 return private.move(private.direction.forward, tries, aggressive)
1841end
1842
1843--[[
1844 Internal back() implementation, used to ignore lock while navigating.
1845
1846 @private
1847]]
1848function private.back(tries)
1849 return private.move(private.direction.back, tries)
1850end
1851
1852--[[
1853 Internal up() implementation, used to ignore lock while navigating.
1854
1855 @private
1856]]
1857function private.up(tries, aggressive)
1858 return private.move(private.direction.up, tries, aggressive)
1859end
1860
1861--[[
1862 Internal down() implementation, used to ignore lock while navigating.
1863
1864 @private
1865]]
1866function private.down(tries, aggressive)
1867 return private.move(private.direction.down, tries, aggressive)
1868end
1869
1870--[[
1871 Internal turn() implementation, used to ignore lock while navigating.
1872
1873 @private
1874]]
1875function private.turn(towards)
1876 -- Turn towards the target facing.
1877 local ids, position = {}, state.position
1878 if useMinecraftCoordinates then
1879 towards = (towards + 2) % 4
1880 end
1881 while position.f ~= towards do
1882 -- We do not use the turnLeft() and turnRight() functions, because we
1883 -- want full control: we push all native events in one go and then wait
1884 -- for all of them to finish. This way we can stick to the pattern of
1885 -- immediately returning (non-yielding) if the turn fails due to a full
1886 -- event queue.
1887 local id
1888 if towards == (position.f + 1) % 4 then
1889 -- Special case for turning clockwise, to avoid turning three times
1890 -- when once is enough, in particular for the left -> forward case,
1891 -- where we wrap around (from 3 -> 0).
1892 id = turtle.native.turnRight()
1893 if id == -1 then
1894 return false, reason.queue_full
1895 end
1896 position.f = (position.f + 1) % 4
1897 else
1898 id = turtle.native.turnLeft()
1899 if id == -1 then
1900 return false, reason.queue_full
1901 end
1902 position.f = (position.f - 1) % 4
1903 end
1904 private.save("position")
1905 table.insert(ids, id)
1906 end
1907 return private.waitForResponse(ids)
1908end
1909
1910--[[
1911 Tries to move the turtle in the specified direction.
1912
1913 If it doesn't work. checks whether we should try harder: if a number of
1914 tries is specified we'll dig (and if aggressive is set attack) as many
1915 times in the hope of getting somewhere.
1916
1917 Use math.huge for infinite tries.
1918
1919 @param direction the direction in which to move, i.e. forward, back, up or
1920 down. The appropriate functions are selected based on this value.
1921 @param tries if specified, the number of times to retry the move after
1922 trying to remove obstacles in our way. We may want to try more than
1923 once for stacks of sand/gravel or enemy entities.
1924 @param aggressive whether to allow attacking in addition to digging when
1925 trying to remove obstacles (only used if tries larger than zero).
1926 @param true if the move was successful, false otherwise.
1927 @private
1928]]
1929function private.move(direction, tries, aggressive)
1930 -- Type checks.
1931 assert(tries == nil or type(tries) == "number",
1932 "'tries' must be a number or omitted")
1933 assert(aggressive == nil or type(aggressive) == "boolean",
1934 "'aggressive' must be a boolean or omitted")
1935
1936 -- Check our fuel.
1937 if turtle.getFuelLevel() < 1 then
1938 return false, reason.fuel
1939 end
1940
1941 -- Clean up arguments.
1942 tries = tonumber(tries or 0)
1943 aggressive = aggressive and true or nil
1944
1945 -- Mapping for functions based on movement direction.
1946 local move = ({
1947 [private.direction.forward] = turtle.native.forward,
1948 [private.direction.back] = turtle.native.back,
1949 [private.direction.up] = turtle.native.up,
1950 [private.direction.down] = turtle.native.down})[direction]
1951 local detect = ({
1952 [private.direction.forward] = turtle.detect,
1953 [private.direction.up] = turtle.detectUp,
1954 [private.direction.down] = turtle.detectDown})[direction]
1955 local dig = ({
1956 [private.direction.forward] = turtle.dig,
1957 [private.direction.up] = turtle.digUp,
1958 [private.direction.down] = turtle.digDown})[direction]
1959 local attack = ({
1960 [private.direction.forward] = turtle.attack,
1961 [private.direction.up] = turtle.attackUp,
1962 [private.direction.down] = turtle.attackDown})[direction]
1963 local side = ({
1964 [private.direction.forward] = "front",
1965 [private.direction.back] = "back",
1966 [private.direction.up] = "top",
1967 [private.direction.down] = "bottom"})[direction]
1968
1969 -- Set up our move state. This is cleared if we fail for any reason, and
1970 -- only saved to disk if we actually start moving.
1971 state.move = {
1972 direction = direction,
1973 tries = tries,
1974 aggressive = aggressive
1975 }
1976
1977 -- Try to move until we're out of tries (or successful).
1978 while true do
1979 -- Check if there's a turtle in our way. We do this first because it's
1980 -- non-yielding. If we didn't there's a very (very!) tiny chance for a
1981 -- turtle to block us but move away in the same tick, leading us to
1982 -- wrongly believe what blocked us was actually an invulnerable entity.
1983 if peripheral.getType(side) == "turtle" then
1984 -- There really is a turtle in our way. Reuse failure handling
1985 -- logic from below and just save the move state. Don't waste I/O.
1986 if state.move.tries > 0 then
1987 private.save("move")
1988 end
1989 else
1990 -- Initialize the move by calling the native function.
1991 local moveId = move()
1992 if moveId == -1 then
1993 private.endMove()
1994 return false, reason.queue_full
1995 end
1996 private.save("move")
1997
1998 -- Wait for the result while having our startup file active.
1999 private.wrapStartup()
2000 local success = private.waitForResponse(moveId)
2001 private.unwrapStartup()
2002
2003 -- Update state and flush it to disk if we actually moved.
2004 if success then
2005 private.updateState()
2006 private.endMove()
2007 return true
2008 end
2009 end
2010
2011 -- If something went wrong check whether we should try again.
2012 if state.move.tries == 0 then
2013 private.endMove()
2014 return false, private.tryGetReason(direction)
2015 end
2016
2017 -- See what seems to be blocking us. We do a peripheral check after
2018 -- each yielding function because I'm pretty sure that turtles can move
2019 -- into our way inbetween those yielding calls, which could lead us to
2020 -- believe that there's an indestructible block or invulnerable entity
2021 -- in front of us, not a turtle.
2022 -- This might actually happen with *any* moving... thing. Turtles are
2023 -- just the most likely, and really the only thing we can properly
2024 -- check for, so... yeah.
2025 if peripheral.getType(side) == "turtle" then
2026 -- It's a turtle. Wait and hope it goes away.
2027 os.sleep(1)
2028 elseif dig and dig() then
2029 -- We got rid of some block in our way. Wait a little to allow
2030 -- sand/gravel to drop down. I've had cases where the turtle
2031 -- would get into a weird state in which it moved below a
2032 -- dropping sand block, causing bad behavior.
2033 os.sleep(0.5)
2034 elseif peripheral.getType(side) == "turtle" then
2035 -- A turtle moved in while we dug... more waiting...
2036 os.sleep(1)
2037 elseif aggressive and attack and attack() then
2038 -- We successfully attacked something. Try again immediately!
2039 elseif peripheral.getType(side) == "turtle" then
2040 -- A turtle moved in while we attacked! Wait for it to leave.
2041 os.sleep(1)
2042 elseif detect then
2043 -- See if we can try to detect something.
2044 local block = detect()
2045 if peripheral.getType(side) == "turtle" then
2046 -- A turtle moved in... you know the deal.
2047 os.sleep(1)
2048 elseif block then
2049 -- Some block we can't dig. Stop right here, there's
2050 -- nothing we can do about it. Well, in theory it might
2051 -- go away due to it being moved by a frame/carriage,
2052 -- but we really don't want to count on that...
2053 private.endMove()
2054 return false, reason.unbreakable_block
2055 else
2056 -- Not a block but nothing we can/may attack. Unlike
2057 -- for unbreakable blocks, we'll keep trying even if
2058 -- we have infinite tries, because entities might just
2059 -- move.
2060 if state.move.tries == 1 then
2061 private.endMove()
2062 return false, reason.invulnerable_entity
2063 end
2064 os.sleep(1)
2065 end
2066 else
2067 -- We cannot determine what's in our way. Keep going until
2068 -- we're out of tries...
2069 os.sleep(0.5)
2070 end
2071
2072 -- Let's try again. Doin' it right. Dat bass.
2073 state.move.tries = state.move.tries - 1
2074 end
2075end
2076
2077--[[
2078 Finishes a movement.
2079
2080 Based on whether it was successful or not it adjusts and then saves the new
2081 persistent state.
2082
2083 @private
2084]]
2085function private.updateState()
2086 -- Shortcuts.
2087 local position = state.position
2088 local direction = private.direction[state.move.direction]
2089
2090 -- Yes, update our state. Build a table with the displacement we'd
2091 -- have to apply in our identity state.
2092 local delta = {
2093 forward = { 1, 0, 0},
2094 right = { 0, 1, 0},
2095 back = {-1, 0, 0},
2096 left = { 0, -1, 0},
2097 up = { 0, 0, 1},
2098 down = { 0, 0, -1}
2099 }
2100
2101 -- Apply our facing.
2102 for i=1,position.f do
2103 delta.forward, delta.right, delta.back, delta.left =
2104 delta.right, delta.back, delta.left, delta.forward
2105 end
2106
2107 -- Finally, apply the actual displacement, based on the movement
2108 -- direction. This means we may do some extra work when moving
2109 -- up or down (where the facing doesn't matter), but it's not that
2110 -- bad, considering how long one move takes.
2111 position.x = position.x + delta[direction][1]
2112 position.y = position.y + delta[direction][2]
2113 position.z = position.z + delta[direction][3]
2114
2115 -- Save new state.
2116 private.save("position")
2117
2118 -- Also update our fuel level.
2119 state.fuel.current = turtle.getFuelLevel()
2120 private.save("fuel")
2121end
2122
2123--[[
2124 Cleans up the movement state.
2125
2126 This is really just for better readability.
2127
2128 @private
2129]]
2130function private.endMove()
2131 state.move = nil
2132 fs.delete(stateFile.move)
2133end
2134
2135--[[
2136 Makes a turtle move to the position currently set as the target.
2137
2138 @return true if the turtle successfully reached the target; (false, reason)
2139 otherwise.
2140 @private
2141]]
2142function private.navigate()
2143 -- Validate state. This function should only be called when the list of
2144 -- steps is not empty; if it is it's a bug.
2145 assert(#state.path.steps > 0, "you found a bug")
2146
2147 -- Utility function for moving a specified distance along a single axis.
2148 local function travel(axis, distance)
2149 -- If we make no moves along this axis we can skip the rest.
2150 if distance == 0 then
2151 return true
2152 end
2153
2154 -- Turn to face the axis if necessary, in a way that we can move
2155 -- forwards (we don't want to move backwards because we cannot dig or
2156 -- attack when moving backwards). This is unnecessary for the z axis,
2157 -- because facing doesn't matter there.
2158 local directions = ({
2159 x = {side.north, side.south},
2160 y = {side.east, side.west}})[axis]
2161 if directions then
2162 local direction = distance > 0 and directions[1] or directions[2]
2163 local result, failureReason = private.turn(direction)
2164 if not result then
2165 return result, failureReason
2166 end
2167 end
2168
2169 -- Move in the appropriate direction as often as necessary.
2170 local action = ({
2171 x = private.forward,
2172 y = private.forward,
2173 z = distance > 0 and private.up or private.down})[axis]
2174 distance = math.abs(distance)
2175 local tries, aggressive = state.path.tries, state.path.aggressive
2176 while distance > 0 do
2177 local result, failureReason = action(tries, aggressive)
2178 if not result then
2179 return result, failureReason
2180 end
2181 distance = distance - 1
2182 end
2183
2184 -- All green.
2185 return true
2186 end
2187
2188 -- Wrap startup in case we break down while not actually moving (e.g. while
2189 -- turning) so that we still resume moving.
2190 private.wrapStartup()
2191
2192 -- Used for determining which way to go first.
2193 local function shortestFirstComparator(a, b)
2194 return math.abs(a.distance) < math.abs(b.distance)
2195 end
2196 local function longestFirstComparator(a, b)
2197 return math.abs(a.distance) > math.abs(b.distance)
2198 end
2199
2200 -- Process all remaining waypoints.
2201 local result, failureReason = true
2202 local comparator = state.path.longestFirst and
2203 longestFirstComparator or
2204 shortestFirstComparator
2205 repeat
2206 -- Figure out how far we have to move along each axis.
2207 local position = state.position
2208 local x, y, z = position.x, position.y, position.z, position.f
2209 local step = state.path.steps[1]
2210 local dx, dy, dz = step.x - x, step.y - y, step.z - z
2211 local axisCount = (dx ~= 0 and 1 or 0) +
2212 (dy ~= 0 and 1 or 0) +
2213 (dz ~= 0 and 1 or 0)
2214
2215 -- If we move along several axes and should move along the longest axis
2216 -- first, we split the move into several moves, one for each axis. This
2217 -- is to ensure that we move the same way even if we're interrupted. If
2218 -- we didn't do this and we were to be interrupted while moving along
2219 -- the longest axis, it could become shorter than one of the other axes
2220 -- and thus lead to us suddenly changing direction. We do this on the
2221 -- fly instead of once in the beginning to keep the state file small.
2222 if state.path.longestFirst and axisCount > 1 then
2223 -- Build the one (or two) intermediate points.
2224 local axes = {{axis = 1, distance = dx},
2225 {axis = 2, distance = dy},
2226 {axis = 3, distance = dz}}
2227 table.sort(axes, shortestFirstComparator)
2228 local stopover = {step.x, step.y, step.z}
2229 for _, entry in ipairs(axes) do
2230 stopover[entry.axis] = stopover[entry.axis] - entry.distance
2231 if stopover[1] == x and
2232 stopover[2] == y and
2233 stopover[3] == z
2234 then
2235 break
2236 end
2237 -- Copy it so as not to change ones we already inserted and to
2238 -- get it in the right format (named keys).
2239 local stopoverCopy = {x = stopover[1],
2240 y = stopover[2],
2241 z = stopover[3]}
2242 table.insert(state.path.steps, 1, stopoverCopy)
2243 end
2244 else
2245 -- Then move that distance along each axis.
2246 local axes = {{axis = "x", distance = dx},
2247 {axis = "y", distance = dy},
2248 {axis = "z", distance = dz}}
2249 table.sort(axes, comparator)
2250 for _, entry in ipairs(axes) do
2251 result, failureReason = travel(entry.axis, entry.distance)
2252 if not result then
2253 break
2254 end
2255 end
2256
2257 -- Finally, adjust our facing.
2258 if result and step.f ~= nil then
2259 if useMinecraftCoordinates then
2260 step.f = (step.f + 2) % 4
2261 end
2262 result, failureReason = private.turn(step.f)
2263 end
2264
2265 -- Done, we reached this waypoint so we can remove it from the list.
2266 table.remove(state.path.steps, 1)
2267 end
2268 private.save("path")
2269 until not result or #state.path.steps == 0
2270
2271 -- Clear the state so we don't try to continue moving next startup.
2272 private.endNavigate()
2273
2274 -- Unwrap the startup to restore the previous startup file, if any.
2275 private.unwrapStartup()
2276
2277 -- And we're done.
2278 return result, failureReason
2279end
2280
2281--[[
2282 Cleans up the navigation state.
2283
2284 This is really just for better readability.
2285
2286 @private
2287]]
2288function private.endNavigate()
2289 state.path = nil
2290 fs.delete(stateFile.path)
2291end
2292
2293-------------------------------------------------------------------------------
2294-- Environment checking --
2295-------------------------------------------------------------------------------
2296
2297-- MT: Environment checking moved to the private.resume() method because assert failures were
2298-- preventing the API from loading when stored in ROM (e.g. when included in a resource pack