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