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