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