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