APIs and examples and more item moving logic

master
zaners123 2022-01-03 17:21:06 -08:00
parent 0cb5264e28
commit 0fc59a8d42
16 changed files with 898 additions and 315 deletions

84
API.md Normal file
View File

@ -0,0 +1,84 @@
# ComputerTest API
This api is two parts, with notes/documentation above, and functions below
## Init Function
See README.md for basic setup.
Once you have a turtle, you need an init function such as:
```lua
function init(turtle)
turtle:moveForward()
end
```
## Directions
Many commands have directions. These come in sets like:
```lua
turtle:moveForward()
turtle:moveBackward()
turtle:moveRight()
turtle:moveLeft()
turtle:moveUp()
turtle:moveDown()
```
This will be referred to as `turtle:move[direction]()`
Up and down are always the same, but orientation is changed with
```lua
turtle:turnLeft()
turtle:turnRight()
```
## The turtle's inventory
The turtle has 16 'turtleslots' numbered 1-16.
## Config
Edit init.lua in the config object at the top to change the config.
Actions take effect on restart/reload.
## Action Functions (do things)
### `turtle:yield(reason)`
- Yields
Many functions yield to prevent lag, balance the mod, and to prevent hanging.
You MUST have a yield in all time-consuming processes.
If you don't yield, and your turtle takes too long, the entire server will lag/halt.
The reason can be any string, and is visible in debug.txt (can turn on/off debugging in config)
###`turtle:mine[direction]()`
- Yields
- Uses Fuel
The turtle will mine the block next to it and take the items of the block. It will take items from chests, too
They return true if the block was removed
## Inventory/Fuel functions
### `turtle:useFuel()`
- Uses Fuel
Burns fuel. Once you run out of fuel, your turtle is stuck.
### `turtle:getFuel()`
Returns the number actions you can take until needing to refuel.
If this is zero, you won't be able to do much except refuel.
### `turtle:itemRefuel(turtleslot)`
Takes fuel from this turtleslot and increases the turtle's fuel
###`turtle:move[direction]()`
- Yields
- Uses Fuel
Moves one block in that direction, yields, and consumes fuel
## Info functions
###`turtle:setName(name)`
Sets the turtle's name
###`turtle:debug(string)`
Prints string to debug.txt (can be disabled in config)
## Other Functions
I might be missing some, check out turtle.lua for more!
All functions that start with TurtleEntity are callable (although some shouldn't be such as the ones marked as helper functions)

View File

@ -11,6 +11,38 @@ A ComputerCraft-inspired mod for Minetest!
- Turtles take time to do actions such as moving,mining,turning,etc
- A "computertest" privilege, to limit turtle usage to trusted users (so only users with this privilege can use/edit turtles)
## An example of how to use this mod
1. Install the ComputerTest mod
2. Get a turtle block using either
- The command `/giveme computertest:turtle`
- The creative menu
- The recipe
```
III
ICI
IMI
I = Steel Ingot , C = Chest , M = Mesa Block
```
3. Place the turtle
4. Right click it and click "Upload Code"
5. Paste this into the large field (sometimes Minetest requires you to paste twice) and click "Upload Code"
```lua
function init(turtle)
while true do
turtle:moveForward()
turtle:turnRight()
end
end
```
6. Watch as it spins around!
## Other Information
The API.md contains important documentation for programming.
EXAMPLES.md contains some fun examples
## Changes are Welcome!
Anyone interested in adding these features can try in entity/turtle.lua, I'd be interested in any great working pull requests!
@ -18,6 +50,5 @@ Anyone interested in adding these features can try in entity/turtle.lua, I'd be
### Features to Add
- Add dumping into chests to create fully-auto mining
- Add fuel, so everything consumes fuel until it runs out or refuels
- Inventory management commands, such as crafting, sorting, and dropping
- The turtle code isn't sandboxed, so turtles could call dangerous functions. This has been mitigated by the "computertest" privilege, but proper sandboxing would work best.
- The turtle code isn't sandboxed, so turtles could call dangerous functions. This has been mitigated by the "computertest" privilege, but proper sandboxing would work best.

View File

@ -13,6 +13,12 @@ minetest.register_node("computertest:turtle", {
},
groups = {oddly_breakable_by_hand=2},
paramtype2 = "facedir",
after_place_node = function(pos, placer)
if placer and placer:is_player() then
local meta = minetest.get_meta(pos)
meta:set_string("owner", placer:get_player_name())
end
end,
on_construct = function(pos)
local turtle = minetest.add_entity(pos,"computertest:turtle")
turtle = turtle:get_luaentity()

View File

@ -8,7 +8,9 @@ local FORM_NOPRIV = "size[9,1;]label[0,0;You do not have the 'computertest' priv
local TURTLE_INVENTORYSIZE = 4*4
---@returns TurtleEntity of that ID
local function getTurtle(id) return computertest.turtles[id] end
---@returns true
local function isValidInventoryIndex(index) return 0 < index and index <= TURTLE_INVENTORYSIZE end
local function has_computertest_priv(player) return minetest.check_player_privs(player,"computertest") end
@ -28,12 +30,16 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
return not turtle:upload_code_to_turtle(player,"",false)
end
elseif isForm(FORMNAME_TURTLE_TERMINAL) then
if (fields.terminal_out ~= nil) then return true end
if (fields.terminal_out ~= nil) then
return true
end
local id = tonumber(string.sub(formname,1+string.len(FORMNAME_TURTLE_TERMINAL)))
local turtle = getTurtle(id)
turtle.lastCommandRan = fields.terminal_in
local command = fields.terminal_in
if command==nil or command=="" then return nil end
if command==nil or command=="" then
return nil
end
command = "function init(turtle) return "..command.." end"
local commandResult = turtle:upload_code_to_turtle(player,command, true)
if (commandResult==nil) then
@ -45,7 +51,9 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
minetest.show_formspec(player:get_player_name(),FORMNAME_TURTLE_TERMINAL..id,turtle:get_formspec_terminal());
elseif isForm(FORMNAME_TURTLE_UPLOAD) then
local id = tonumber(string.sub(formname,1+string.len(FORMNAME_TURTLE_UPLOAD)))
if (fields.button_upload == nil or fields.upload == nil) then return true end
if (fields.button_upload == nil or fields.upload == nil) then
return true
end
local turtle = getTurtle(id)
return not turtle:upload_code_to_turtle(player,fields.upload,false)
else
@ -58,16 +66,20 @@ end)
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if (timer >= computertest.config.turtle_tick) then
while (timer >= computertest.config.turtle_tick) do
for _,turtle in pairs(computertest.turtles) do
if turtle.coroutine then
if coroutine.status(turtle.coroutine)=="suspended" then
--TODO check for fuel here
local status, result = coroutine.resume(turtle.coroutine)
minetest.log("coroutine stat "..dump(status).." said "..dump(result))
--elseif coroutine.status(turtle.coroutine)=="dead" then
--minetest.log("turtle #"..id.." has coroutine, but it's already done running")
if (turtle.fuel>0) then
local status, result = coroutine.resume(turtle.coroutine)
turtle:debug("coroutine stat "..dump(status).." said "..dump(result))
turtle:debug("fuel="..turtle.fuel)
else
turtle:debug("No Fuel in turtle")
end
end
--elseif coroutine.status(turtle.coroutine)=="dead" then
--minetest.log("turtle #"..id.." has coroutine, but it's already done running")
elseif turtle.code then
--minetest.log("turtle #"..id.." has no coroutine but has code! Making coroutine...")
--TODO add some kinda timeout into coroutine
@ -83,9 +95,10 @@ minetest.register_globalstep(function(dtime)
end
end)
--Code responsible for generating turtle entity and turtle interface
minetest.register_entity("computertest:turtle", {
local TurtleEntity = {
initial_properties = {
hp_max = 1,
hp_max = 20,
is_visible = true,
makes_footstep_sound = false,
physical = true,
@ -101,260 +114,416 @@ minetest.register_entity("computertest:turtle", {
"computertest_front.png",
},
automatic_rotate = 0,
id = -1,
id = -1
},
}
--MAIN TURTLE USER INTERFACE------------------------------------------
get_formspec_inventory = function(turtle)
return "size[12,5;]"
.."button[0,0;2,1;open_terminal;Open Terminal]"
.."button[2,0;2,1;upload_code;Upload Code]"
.."button[4,0;2,1;factory_reset;Factory Reset]"
.."set_focus[open_terminal;true]"
.."list[".. turtle.inv_fullname..";main;8,1;4,4;]"
.."background[8,1;1,1;computertest_inventory.png]"
.."list[current_player;main;0,1;8,4;]";
end,
get_formspec_terminal = function(turtle)
local previous_answers = turtle.previous_answers
local parsed_output = "";
for i=1, #previous_answers do parsed_output = parsed_output .. minetest.formspec_escape(previous_answers[i]).."," end
--local saved_output = "";
--for i=1, #previous_answers do saved_output = saved_output .. minetest.formspec_escape(previous_answers[i]).."\n" end
return
"size[12,9;]"
.."field_close_on_enter[terminal_in;false]"
.."field[0,0;12,1;terminal_in;;"..minetest.formspec_escape(turtle.lastCommandRan or "").."]"
.."set_focus[terminal_in;true]"
.."textlist[0,1;12,8;terminal_out;"..parsed_output.."]";
end,
get_formspec_upload = function(turtle)
--TODO could indicate if code is already uploaded
return
"size[12,9;]"
.."button[0,0;2,1;button_upload;Upload Code to #"..turtle.id.."]"
.."field_close_on_enter[upload;false]"
.."textarea[0,1;12,8;upload;;"..minetest.formspec_escape(turtle.codeUncompiled or "").."]"
.."set_focus[upload;true]";
end,
---
---@return true on success
---
upload_code_to_turtle = function(turtle,player, code_string,run_for_result)
--Check permissions
if (not has_computertest_priv(player)) then
--minetest.debug(player:get_player_name().." does not have computertest priv")
--MAIN TURTLE HELPER FUNCTIONS------------------------------------------
function TurtleEntity:getTurtleslot(turtleslot)
if not isValidInventoryIndex(turtleslot) then return nil end
return self.inv:get_stack("main",turtleslot)
end
function TurtleEntity:setTurtleslot(turtleslot, stack)
if not isValidInventoryIndex(turtleslot) then return false end
self.inv:set_stack("main", turtleslot,stack)
return true
end
function TurtleEntity:move(nodeLocation)
--Verify new pos is empty
if (nodeLocation == nil or minetest.get_node(nodeLocation).name~="air") then
self:yield("Moving")
return false
end
--Take Action
self.object:set_pos(nodeLocation)
self:yield("Moving",true)
return true
end
function TurtleEntity:mine(nodeLocation)
if nodeLocation == nil then
return false
end
local node = minetest.get_node(nodeLocation)
if (node.name=="air") then
return false
end
--Try sucking the inventory (in case it's a chest)
self:itemSuck(nodeLocation)
local drops = minetest.get_node_drops(node)
--TODO NOTE This violates spawn protection, but I know of no way to mine that abides by spawn protection AND picks up all items and contents (dig_node drops items and I don't know how to pick them up)
minetest.remove_node(nodeLocation)
for _, iteminfo in pairs(drops) do
local stack = ItemStack(iteminfo)
if self.inv:room_for_item("main",stack) then
self.inv:add_item("main",stack)
end
end
self:yield("Mining",true)
return true
end
function TurtleEntity:build(nodeLocation, turtleslot)
if nodeLocation == nil then return false end
if not isValidInventoryIndex(turtleslot) then return false end
local node = minetest.get_node(nodeLocation)
if node.name~="air" then return false end
--Build and consume item
local stack = self:getTurtleslot("main", turtleslot)
if stack:is_empty() then return false end
local newstack, position_placed = minetest.item_place_node(stack, nil, { type="node", under=nodeLocation, above=self:get_pos()})
self.inv:set_stack("main", turtleslot,newstack)
if position_placed == nil then
self:yield("Building")
return false
end
self:yield("Building",true)
return true
end
function TurtleEntity:scan(nodeLocation) return minetest.get_node(nodeLocation) end
--[[ Sucks inventory (chest, node, furnace, etc) at nodeLocation into turtle
@returns true if it sucked everything up]]
--function TurtleEntity:suckBlock(nodeLocation)
-- local suckedEverything = true
-- local nodeInventory = minetest.get_inventory({type="node", pos=nodeLocation})
-- if not nodeInventory then
-- return true --No node inventory, nothing left to suck
-- end
-- for listname,listStacks in pairs(nodeInventory:get_lists()) do
-- for stackI,itemStack in pairs(listStacks) do
-- if self.inv:room_for_item("main",itemStack) then
-- local remainingItemStack = self.inv:add_item("main",itemStack)
-- nodeInventory:set_stack(listname, stackI, remainingItemStack)
-- else
-- suckedEverything = false
-- end
-- end
-- end
-- return suckedEverything
--end
---Takes everything from block that matches list
--- @param filterlist - Something like {"default:stone","default:dirt"}
--- @param isWhitelist - If true, only take things in list.
--- If false, take everything EXCEPT the items in the list
--- @param listname - take only from specific listname. If nil, take from every list
--- @return boolean true unless any items can't fit
function TurtleEntity:itemSuck(nodeLocation, filterlist, isWhitelist, listname)
filterlist = filterlist or {}
local suckedEverything = true
local nodeInventory = minetest.get_inventory({type="node", pos=nodeLocation})
if not nodeInventory then
return true --No node inventory, nothing left to suck
end
local function suckList(listname, listStacks)
local function intable(item)
for _,x in pairs(filterlist) do
if item==x then return true end
end
return false
end
local function sandbox(code)
--TODO sandbox this!
--Currently returns function that defines init and loop. In the future, this should probably just initialize it using some callbacks
if (code =="") then return nil end
return loadstring(code)
end
turtle.codeUncompiled = code_string
turtle.coroutine = nil
turtle.code = sandbox(turtle.codeUncompiled)
if (run_for_result) then
--TODO run subroutine once, if it returns a value, return that here
return "Ran"
end
return turtle.code ~= nil
end,
--MAIN END TURTLE USER INTERFACE------------------------------------------
--- From 0 to 3
set_heading = function(turtle,heading)
heading = (tonumber(heading) or 0)%4
if turtle.heading ~= heading then
turtle.heading = heading
turtle.object:set_yaw(turtle.heading * 3.14159265358979323/2)
if (coroutine.running() == turtle.coroutine) then turtle:yield("Turning",true) end
end
end,
get_heading = function(self)
return self.heading
end,
on_activate = function(turtle, staticdata, dtime_s)
--TODO use staticdata to load previous state, such as inventory and whatnot
--Give ID
computertest.num_turtles = computertest.num_turtles+1
turtle.id = computertest.num_turtles
turtle.heading = 0
turtle.previous_answers = {}
turtle.coroutine = nil
turtle.fuel = 100
--Give her an inventory
turtle.inv_name = "computertest:turtle:".. turtle.id
turtle.inv_fullname = "detached:".. turtle.inv_name
local inv = minetest.create_detached_inventory(turtle.inv_name,{})
if inv == nil or inv == false then error("Could not spawn inventory")end
inv:set_size("main", TURTLE_INVENTORYSIZE)
if turtle.inv ~= nil then inv.set_lists(turtle.inv) end
turtle.inv = inv
-- Add to turtle list
computertest.turtles[turtle.id] = turtle
end,
on_rightclick = function(self, clicker)
if not clicker or not clicker:is_player() then return end
if (not has_computertest_priv(clicker)) then
minetest.show_formspec(clicker:get_player_name(),FORMNAME_TURTLE_NOPRIV,FORM_NOPRIV);
return
for stackI,itemStack in pairs(listStacks) do
if intable(itemStack:get_name()) == isWhitelist then
local remainingItemStack = self.inv:add_item("main",itemStack)
nodeInventory:set_stack(listname, stackI, remainingItemStack)
suckedEverything = suckedEverything and remainingItemStack:is_empty()
end
end
end
if listname then
suckList(listname, nodeInventory:get_list(listname))
else
for listname,listStacks in pairs(nodeInventory:get_lists()) do
suckList(listname, listStacks)
end
end
return suckedEverything
end
function TurtleEntity:itemSuckForward(filterlist, isWhitelist, listname) return self:itemSuck(self:getLocForward(), filterlist, isWhitelist, listname) end
---Takes everything from block that matches list
--- @param filterlist - Something like {"default:stone","default:dirt"}
--- @param isWhitelist - If true, only take things in list.
--- If false, take everything EXCEPT the items in the list
--- @param listname - take only from specific listname. If nil, take from every list
--- @return boolean true if stack completely pushed
function TurtleEntity:itemPush(nodeLocation, turtleslot, filterlist, isWhitelist, listname)
listname = listname or "main"
local nodeInventory = minetest.get_inventory({type="node", pos=nodeLocation})--InvRef
if not nodeInventory then
return false --No node inventory (Ex: Not a chest)
end
--Try putting my stack somewhere
local toPush = self:getTurtleslot(turtleslot)
local remainingItemStack = nodeInventory:add_item(listname,toPush)
self:setTurtleslot(turtleslot, remainingItemStack)
return remainingItemStack:is_empty()
end
---
---@returns true on success
---
function TurtleEntity:upload_code_to_turtle(player, code_string,run_for_result)
--Check permissions
if (not has_computertest_priv(player)) then
--minetest.debug(player:get_player_name().." does not have computertest priv")
return false
end
local function sandbox(code)
--TODO sandbox this!
--Currently returns function that defines init and loop. In the future, this should probably just initialize it using some callbacks
if (code =="") then return nil end
return loadstring(code)
end
self.codeUncompiled = code_string
self.coroutine = nil
self.code = sandbox(self.codeUncompiled)
if (run_for_result) then
--TODO run subroutine once, if it returns a value, return that here
return "Ran"
end
return self.code ~= nil
end
--MAIN TURTLE USER INTERFACE------------------------------------------
function TurtleEntity:get_formspec_inventory()
return "size[12,5;]"
.."button[0,0;2,1;open_terminal;Open Terminal]"
.."button[2,0;2,1;upload_code;Upload Code]"
.."button[4,0;2,1;factory_reset;Factory Reset]"
.."set_focus[open_terminal;true]"
.."list[".. self.inv_fullname..";main;8,1;4,4;]"
.."background[8,1;1,1;computertest_inventory.png]"
.."list[current_player;main;0,1;8,4;]";
end
function TurtleEntity:get_formspec_terminal()
local previous_answers = self.previous_answers
local parsed_output = "";
for i=1, #previous_answers do
parsed_output = parsed_output .. minetest.formspec_escape(previous_answers[i])..","
end
return
"size[12,9;]"
.."field_close_on_enter[terminal_in;false]"
.."field[0,0;12,1;terminal_in;;"..minetest.formspec_escape(self.lastCommandRan or "").."]"
.."set_focus[terminal_in;true]"
.."textlist[0,1;12,8;terminal_out;"..parsed_output.."]";
end
function TurtleEntity:get_formspec_upload()
--TODO could indicate if code is already uploaded
return
"size[12,9;]"
.."button[0,0;3,1;button_upload;Upload Code to '"..self.name.."']"
.."field_close_on_enter[upload;false]"
.."textarea[0,1;12,8;upload;;"..minetest.formspec_escape(self.codeUncompiled or "").."]"
.."set_focus[upload;true]";
end
--MAIN TURTLE ENTITY FUNCTIONS------------------------------------------
function TurtleEntity:on_activate(staticdata, dtime_s)
--TODO use staticdata to load previous state, such as inventory and whatnot
--Give ID
computertest.num_turtles = computertest.num_turtles+1
self.id = computertest.num_turtles
self.name = "Unnamed #"..self.id
--self.owner = minetest.get_meta(pos):get_string("owner")
self.heading = 0
self.previous_answers = {}
self.coroutine = nil
self.fuel = computertest.config.fuel_initial
--Give her an inventory
self.inv_name = "computertest:turtle:".. self.id
self.inv_fullname = "detached:".. self.inv_name
local inv = minetest.create_detached_inventory(self.inv_name,{})
if inv == nil or inv == false then error("Could not spawn inventory")end
inv:set_size("main", TURTLE_INVENTORYSIZE)
if self.inv ~= nil then inv.set_lists(self.inv) end
self.inv = inv
-- Add to turtle list
computertest.turtles[self.id] = self
end
function TurtleEntity:on_rightclick(clicker)
if not clicker or not clicker:is_player() then
return
end
if (not has_computertest_priv(clicker))then
minetest.show_formspec(clicker:get_player_name(),FORMNAME_TURTLE_NOPRIV,FORM_NOPRIV);
return
end
minetest.show_formspec(clicker:get_player_name(), FORMNAME_TURTLE_INVENTORY.. self.id,
self:get_formspec_inventory())
end
function TurtleEntity:get_staticdata()
minetest.debug("Deleting/Forgetting all data of turtle "..self.name)
local static = {}
minetest.show_formspec(clicker:get_player_name(), FORMNAME_TURTLE_INVENTORY.. self.id,
self:get_formspec_inventory())
end,
get_staticdata = function(self)
-- TODO convert inventory and internal code to string and back somehow, or else it'll be deleted every time the entity gets unloaded
minetest.debug("Deleting all data of turtle")
end,
turtle_move_withHeading = function (turtle,numForward,numRight,numUp)
local new_pos = turtle:get_nearby_pos(numForward,numRight,numUp)
--Verify new pos is empty
if (new_pos == nil or minetest.get_node(new_pos).name~="air") then
turtle:yield("Moving",true)
return false
end
--Take Action
turtle.object:set_pos(new_pos)
turtle:yield("Moving",true)
return true
end,
get_nearby_pos = function(turtle, numForward, numRight, numUp)
local pos = turtle:get_pos()
if pos==nil then return nil end -- To prevent unloaded turtles from trying to load things
local new_pos = vector.new(pos)
if turtle:get_heading()%4==0 then new_pos.z=pos.z-numForward;new_pos.x=pos.x-numRight; end
if turtle:get_heading()%4==1 then new_pos.x=pos.x+numForward;new_pos.z=pos.z-numRight; end
if turtle:get_heading()%4==2 then new_pos.z=pos.z+numForward;new_pos.x=pos.x+numRight; end
if turtle:get_heading()%4==3 then new_pos.x=pos.x-numForward;new_pos.z=pos.z+numRight; end
new_pos.y = pos.y + (numUp or 0)
return new_pos
end,
mine = function(turtle, nodeLocation)
if nodeLocation == nil then return false end
local node = minetest.get_node(nodeLocation)
if (node.name=="air") then return false end
--Try sucking the inventory (in case it's a chest)
turtle:suckBlock(nodeLocation)
local drops = minetest.get_node_drops(node)
--NOTE This violates spawn protection, but I know of no way to mine that abides by spawn protection AND picks up all items and contents (dig_node drops items and I don't know how to pick them up)
minetest.remove_node(nodeLocation)
for _, iteminfo in pairs(drops) do
local stack = ItemStack(iteminfo)
if turtle.inv:room_for_item("main",stack) then
turtle.inv:add_item("main",stack)
end
end
turtle:yield("Mining",true)
return true
end,
useFuel = function(turtle)
if turtle.fuel > 0 then
turtle.fuel = turtle.fuel - 1;
end
end,
-- MAIN TURTLE INTERFACE ---------------------------------------
yield = function(turtle,reason,useFuel)
-- Yield at least once
if (coroutine.running() == turtle.coroutine) then
coroutine.yield(reason)
end
--Use a fuel if requested
if useFuel then turtle:useFuel() end
end,
--static = self -- Would this work..? no.
moveForward = function(turtle) return turtle:turtle_move_withHeading( 1, 0, 0) end,
moveBackward = function(turtle) return turtle:turtle_move_withHeading(-1, 0, 0) end,
moveRight = function(turtle) return turtle:turtle_move_withHeading( 0, 1, 0) end,
moveLeft = function(turtle) return turtle:turtle_move_withHeading( 0,-1, 0) end,
moveUp = function(turtle) return turtle:turtle_move_withHeading( 0, 0, 1) end,
moveDown = function(turtle) return turtle:turtle_move_withHeading( 0, 0,-1) end,
turnLeft = function(turtle) return turtle:set_heading(turtle:get_heading()+1) end,
turnRight = function(turtle) return turtle:set_heading(turtle:get_heading()-1) end,
static.complete = true
return minetest.serialize(static)
end
--MAIN PLAYER INTERFACE (CALL THESE)------------------------------------------
function TurtleEntity:getLoc() return self.object:get_pos() end
function TurtleEntity:getLocRelative(numForward,numUp,numRight)
local pos = self:getLoc()
if pos==nil then
return nil -- To prevent unloaded turtles from trying to load things
end
local new_pos = vector.new(pos)
if self:get_heading()%4==0 then new_pos.z=pos.z-numForward;new_pos.x=pos.x-numRight; end
if self:get_heading()%4==1 then new_pos.x=pos.x+numForward;new_pos.z=pos.z-numRight; end
if self:get_heading()%4==2 then new_pos.z=pos.z+numForward;new_pos.x=pos.x+numRight; end
if self:get_heading()%4==3 then new_pos.x=pos.x-numForward;new_pos.z=pos.z+numRight; end
new_pos.y = pos.y + (numUp or 0)
return new_pos
end
function TurtleEntity:getLocForward() return self:getLocRelative( 1,0,0) end
function TurtleEntity:getLocBackward() return self:getLocRelative(-1,0,0) end
function TurtleEntity:getLocUp() return self:getLocRelative(0, 1,0) end
function TurtleEntity:getLocDown() return self:getLocRelative(0,-1,0) end
function TurtleEntity:getLocRight() return self:getLocRelative(0,0, 1) end
function TurtleEntity:getLocLeft() return self:getLocRelative(0,0,-1) end
---Consumes a fuel point
function TurtleEntity:useFuel()
if self.fuel > 0 then
self.fuel = self.fuel - 1;
end
end
--- From 0 to 3
function TurtleEntity:set_heading(heading)
heading = (tonumber(heading) or 0)%4
if self.heading ~= heading then
self.heading = heading
self.object:set_yaw(self.heading * 3.14159265358979323/2)
if (coroutine.running() == self.coroutine) then self:yield("Turning",true) end
end
end
function TurtleEntity:get_heading() return self.heading end
function TurtleEntity:turnLeft() return self:set_heading(self:get_heading()+1) end
function TurtleEntity:turnRight() return self:set_heading(self:get_heading()-1) end
mineForward = function(turtle) return turtle:mine(turtle:get_nearby_pos (1,0,0)) end,
mineBackward = function(turtle) return turtle:mine(turtle:get_nearby_pos (-1,0,0)) end,
mineRight = function(turtle) return turtle:mine(turtle:get_nearby_pos (0,1,0)) end,
mineLeft = function(turtle) return turtle:mine(turtle:get_nearby_pos (0,-1,0)) end,
mineUp = function(turtle) return turtle:mine(turtle:get_nearby_pos (0,0,1)) end,
mineDown = function(turtle) return turtle:mine(turtle:get_nearby_pos (0,0,-1)) end,
function TurtleEntity:moveForward() return self:move(self:getLocForward()) end
function TurtleEntity:moveBackward() return self:move(self:getLocBackward()) end
function TurtleEntity:moveUp() return self:move(self:getLocUp()) end
function TurtleEntity:moveDown() return self:move(self:getLocDown()) end
function TurtleEntity:moveRight() return self:move(self:getLocRight()) end
function TurtleEntity:moveLeft() return self:move(self:getLocLeft()) end
get_pos = function(turtle) return turtle.object:get_pos() end,
get_fuel = function(turtle) return turtle.fuel end,
function TurtleEntity:mineForward() return self:mine(self:getLocForward()) end
function TurtleEntity:mineBackward() return self:mine(self:getLocBackward()) end
function TurtleEntity:mineUp() return self:mine(self:getLocUp()) end
function TurtleEntity:mineDown() return self:mine(self:getLocDown()) end
function TurtleEntity:mineRight() return self:mine(self:getLocRight()) end
function TurtleEntity:mineLeft() return self:mine(self:getLocLeft()) end
--[[ Sucks inventory (chest, node, furnace, etc) at nodeLocation into turtle
@returns true if it sucked everything up]]
suckBlock = function(turtle, nodeLocation)
local suckedEverything = true
local nodeInventory = minetest.get_inventory({type="node", pos=nodeLocation})
if not nodeInventory then
return false --No node inventory
end
for listName,listStacks in pairs(nodeInventory:get_lists()) do
for stackI,itemStack in pairs(listStacks) do
if turtle.inv:room_for_item("main",itemStack) then
local remainingItemStack = turtle.inv:add_item("main",itemStack)
nodeInventory:set_stack(listName, stackI, remainingItemStack)
else
suckedEverything = false
end
end
end
return suckedEverything
end,
function TurtleEntity:buildForward(turtleslot) return self:build(self:getLocForward(),turtleslot) end
function TurtleEntity:buildBackward(turtleslot) return self:build(self:getLocBackward(),turtleslot) end
function TurtleEntity:buildUp(turtleslot) return self:build(self:getLocUp(),turtleslot) end
function TurtleEntity:buildDown(turtleslot) return self:build(self:getLocDown(),turtleslot) end
function TurtleEntity:buildRight(turtleslot) return self:build(self:getLocRight(),turtleslot) end
function TurtleEntity:buildLeft(turtleslot) return self:build(self:getLocLeft(),turtleslot) end
-- MAIN INVENTORY COMMANDS--------------------------
--- TODO drops item onto ground
itemDrop = function(turtle,itemslot)
end,
--- TODO Returns ItemStack on success or nil on failure
---Ex: turtle:itemGet(3):get_name() -> "default:stone"
itemGet = function(turtle,itemslot)
if isValidInventoryIndex(itemslot) then
return turtle.inv:get_stack("main",itemslot)
end
return nil
end,
--- Swaps itemstacks in slots A and B
itemMove = function(turtle, itemslotA, itemslotB)
if (not isValidInventoryIndex(itemslotA)) or (not isValidInventoryIndex(itemslotB)) then
turtle:yield("Inventorying")
return false
end
---Scan gets the info of this node
function TurtleEntity:scanForward() return self:scan(self:getLocForward()) end
function TurtleEntity:scanBackward() return self:scan(self:getLocBackward()) end
function TurtleEntity:scanUp() return self:scan(self:getLocUp()) end
function TurtleEntity:scanDown() return self:scan(self:getLocDown()) end
function TurtleEntity:scanRight() return self:scan(self:getLocRight()) end
function TurtleEntity:scanLeft() return self:scan(self:getLocLeft()) end
local stackA = turtle.inv:get_stack("main",itemslotA)
local stackB = turtle.inv:get_stack("main",itemslotB)
function TurtleEntity:yield(reason,useFuel)
-- Yield at least once
if (coroutine.running() == self.coroutine) then
coroutine.yield(reason)
end
--Use a fuel if requested
if useFuel then self:useFuel() end
end
minetest.debug(dump(stackA:to_string()))
minetest.debug(dump(stackB:to_string()))
--Inventory Interface
-- MAIN INVENTORY COMMANDS--------------------------
--- TODO drops item onto ground
function TurtleEntity:itemDrop(itemslot)
turtle.inv:set_stack("main",itemslotA,stackB)
turtle.inv:set_stack("main",itemslotB,stackA)
end
--- TODO Returns ItemStack on success or nil on failure
---Ex: turtle:itemGet(3):get_name() -> "default:stone"
function TurtleEntity:itemGet(turtleslot)
return self:getTurtleslot(turtleslot)
end
--- Swaps itemstacks in slots A and B
function TurtleEntity:itemMove(turtleslotA, turtleslotB)
if (not isValidInventoryIndex(turtleslotA)) or (not isValidInventoryIndex(turtleslotB)) then
self:yield("Inventorying")
return false
end
turtle:yield("Inventorying")
return true
end,
--- TODO Pushes item into forward-facing chest
--- TODO after getting this working, add a general function with whitelists
itemPush = function(turtle, itemslot, listname)
listname = listname or "main"
end,
--- TODO craft using top right 3x3 grid, and put result in itemslotResult
itemCraft = function(turtle,itemslotResult)
end,
itemRefuel = function(turtle,itemslot)
local burntime = 0--TODO get burntime
-- If fuels are defined like this, how do I get the burntime back, given an item?
--[[ minetest.register_craft("",{
type = "fuel",
recipe = "bucket:bucket_lava",
burntime = 60,
replacements = {{"bucket:bucket_lava", "bucket:bucket_empty"}},
})]]
local stackA = self:getTurtleslot(turtleslotA)
local stackB = self:getTurtleslot(turtleslotB)
turtle.fuel = turtle.fuel + burntime * computertest.config.fuel_multiplier
end,
-- MAIN TURTLE INTERFACE END---------------------------------------
})
self:setTurtleslot(turtleslotA,stackB)
self:setTurtleslot(turtleslotB,stackA)
self:yield("Inventorying")
return true
end
function TurtleEntity:itemPushForward(turtleslot, listname) return self:itemPush(turtleslot, self:getLocForward(), listname) end
function TurtleEntity:itemPushBackward(turtleslot, listname) return self:itemPush(turtleslot, self:getLocBackward(), listname) end
function TurtleEntity:itemPushUp(turtleslot, listname) return self:itemPush(turtleslot, self:getLocUp(), listname) end
function TurtleEntity:itemPushDown(turtleslot, listname) return self:itemPush(turtleslot, self:getLocDown(), listname) end
function TurtleEntity:itemPushRight(turtleslot, listname) return self:itemPush(turtleslot, self:getLocRight(), listname) end
function TurtleEntity:itemPushLeft(turtleslot, listname) return self:itemPush(turtleslot, self:getLocLeft(), listname) end
--- TODO craft using top right 3x3 grid, and put result in itemslotResult
function TurtleEntity:itemCraft(itemslotResult)
-- Use minetest.get_craft_result
end
--- @returns True if fuel was consumed. False if itemslot did not have fuel.
function TurtleEntity:itemRefuel(turtleslot)
if (not isValidInventoryIndex(turtleslot)) then
return false
end
local stack = self:getTurtleslot(turtleslot)
local burntime = 0
local quantity = stack:get_count()
--TODO how do you get burntime?
--minetest.debug(dump(minetest.get_all_craft_recipes("default:coal_lump")))
if (stack:get_name() == "default:coal_lump") then burntime = 40 end
if (burntime <= 0) then
return false
end
self:setTurtleslot(turtleslot,nil)
self.fuel = self.fuel + quantity * burntime * computertest.config.fuel_multiplier
self:yield("Fueling")
return true
end
function TurtleEntity:getFuel() return self.fuel end
function TurtleEntity:setName(name) self.name = minetest.formspec_escape(name) end
function TurtleEntity:debug(string)
if computertest.config.debug then
minetest.debug("computertest turtle #"..self.id..": "..string)
end
end
function TurtleEntity:dump(object) return dump(object) end
-- MAIN TURTLE INTERFACE END---------------------------------------
-- MAIN TURTLE INTERFACE END---------------------------------------
-- MAIN TURTLE INTERFACE END---------------------------------------
minetest.register_entity("computertest:turtle", TurtleEntity)

53
examples/0-api.lua Normal file
View File

@ -0,0 +1,53 @@
-- Generally speaking, "do action" functions return true on success and false on failure
-- See example #1 for information on how to run code
function init(turtle)
---set_name can be used in identification
turtle:set_name("Mining Turtle")
---Move commands move the turtle relative to its heading, and take one "turtle_tick"
---They return true if the turtle successfully moved
turtle:moveForward()
turtle:moveBackward()
turtle:moveRight()
turtle:moveLeft()
turtle:moveUp()
turtle:moveDown()
---Turn commands turn the turtle 90 degrees, and take one "turtle_tick"
turtle:turnLeft()
turtle:turnRight()
---Mine commands break and take the items of the block, take one "turtle_tick", and use fuel
---They return true if the block was removed
turtle:mineForward()
turtle:mineBackward()
turtle:mineRight()
turtle:mineLeft()
turtle:mineUp()
turtle:mineDown()
---Informational commands happen instantly (don't yield)
turtle:get_pos()--Returns something like {x = -277,y = 6,z = -1558}
---More turtle commands can be viewed in entity/turtle.lua (only use the ones in the interface)
---For inventory commands, remember that lua starts from one (itemslots go from 1-16)
turtle:itemMove(1,2) -- Swap item in slot 1 with item in slot two
turtle:itemRefuel(5) -- Takes fuel from slot five. The turtle needs fuel to do actions
--- Build commands take an Itemstack and place it as a Node. Give them an inventory itemnumber (1-16)
--- Build commands yield and use fuel
turtle:buildForward(1) -- Places the block in slot one infront of it
turtle:buildBackward(1)
turtle:buildLeft(5) -- Places the block in slot five (under slot one) to the left of it
turtle:buildRight(1)
turtle:buildUp(1)
turtle:buildDown(1)
---IMPORTANT:
---Yield waits one "turtle_tick" (the time this takes can be changed in the config). It does not use fuel.
---You MUST yield in anything that could be an infinite loop, or else your turtle will deadlock and die
--- Infinite loops, such as "while true do end", MUST be replaced with "while true do turtle:yield() end"
--- or else the server thread will freeze (This is VERY bad, the server would need restarting)
turtle:yield(reason_string) -- Reason string can be something like "Ran out of Fuel"
end

View File

@ -1,17 +1,11 @@
--This simple example will make the turtle spin clockwise forever (or until it hits an obstacle)
--1. Install the ComputerTest mod
--2. Get a turtle block (craftable from one dirt)
--3. Place the block
--4. Click "Upload Code"
--5. Paste this into the large field (sometimes Minetest requires you to paste twice) and click "Upload Code"
--6. Watch as it digs a hole!
---The turtle calls init(turtle) on startup
---@param turtle self This is the turtle, and has many functions (see example 3 for more)
---@param turtle self This is the turtle, and has many functions (see the API example for more)
function init(turtle)
while true do
--All actions (moving, turning, mining, etc) take one "turtle_tick"
-- to run to prevent destruction and increase realism
--Many actions (moving, turning, mining, etc) take one "turtle_tick"
-- to run to prevent destruction and increase realism. They also use fuel.
turtle:moveForward()
turtle:turnRight()
end

View File

@ -0,0 +1,66 @@
--WARNING this is the answers to example #2, look at example #2 first!
--WARNING this is the answers to example #2, look at example #2 first!
--WARNING this is the answers to example #2, look at example #2 first!
--WARNING this is the answers to example #2, look at example #2 first!
--WARNING this is the answers to example #2, look at example #2 first!
--One solution is to do the same thing, just with more loops
local function quarry_with_loops(turtle, size)
local x,y,z = 0,0,0
while true do
while true do
while x<size-1 do
turtle:mineForward()
if turtle:moveForward() then x=x+1 end
end
while x>0 do
if turtle:moveBackward() then x=x-1 end
end
if z==size then break end
turtle:mineRight()
if turtle:moveRight() then z=z+1 end
end
while z>0 do
if turtle:moveLeft() then z=z-1 end
end
if y==size then break end
turtle:mineDown()
if turtle:moveDown() then y=y+1 end
end
while y>0 do
if turtle:moveUp() then y=y-1 end
end
end
local function quarry_with_lambdas(turtle, size)
local function quarryLine(moveIn,moveOut,lineEnd)
for _ = 1, size-1 do moveIn() end
lineEnd()
for _ = 1, size-1 do moveOut() end
end
local quarryX = (function()
quarryLine(
(function() turtle:mineForward() turtle:moveForward() end),
(function()turtle:moveBackward()end),
(function()end))
end)
local quarryY = (function()
quarryLine(
(function() quarryX()turtle:mineRight();turtle:moveRight();end),
(function()turtle:moveLeft()end),
(function()quarryX()end))
end)
local quarryZ = (function()
quarryLine(
(function() quarryY() turtle:mineDown() turtle:moveDown() end),
(function() turtle:moveUp() end),
(function()quarryY()end))
end )
quarryZ()
end
function init(turtle)
quarry_with_lambdas(turtle,3)
turtle:turnRight()
turtle:turnRight()
quarry_with_loops(turtle,3)
end

View File

@ -3,29 +3,21 @@
--See example #1 on how to upload code
function init(turtle)
local size = 3
local y=0
while y<size do
local z=0
while z <size do
local x=1
while x<size do
turtle:mineForward()
if turtle:moveForward() then x=x+1 end
end
while x>0 do
if turtle:moveBackward() then x=x-1 end
end
turtle:mineRight()
if turtle:moveRight() then z = z +1 end
local size = 5
local z =0
while z <size do
local x=0
while x<size do
turtle:mineForward()
if turtle:moveForward() then x=x+1 end
end
while z >0 do
if turtle:moveLeft() then z = z -1 end
while x>0 do
if turtle:moveBackward() then x=x-1 end
end
turtle:mineDown()
if turtle:moveDown() then y=y+1 end
turtle:mineRight()
if turtle:moveRight() then z = z +1 end
end
while y>0 do
if turtle:moveUp() then y=y-1 end
while z >0 do
if turtle:moveLeft() then z = z -1 end
end
end

View File

@ -1,31 +0,0 @@
-- Generally speaking, "do action" functions return true on success and false on failure
function init(turtle)
---Yield waits one "turtle_tick"
turtle:yield()
---Move commands move the turtle relative to its heading, and take one "turtle_tick"
---They return true if the turtle successfully moved
turtle:moveForward()
turtle:moveBackward()
turtle:moveRight()
turtle:moveLeft()
turtle:moveUp()
turtle:moveDown()
---Turn commands turn the turtle 90 degrees, and take one "turtle_tick"
turtle:turnLeft()
turtle:turnRight()
---Mine commands break and take the items of the block, and take one "turtle_tick"
---They return true if the block was removed
turtle:mineForward()
turtle:mineBackward()
turtle:mineRight()
turtle:mineLeft()
turtle:mineUp()
turtle:mineDown()
---Informational commands happen instantly (don't yield)
turtle:get_pos()--Returns something like {x = -277,y = 6,z = -1558}
---More turtle commands can be viewed in entity/turtle.lua (only use the ones in the interface)
---For inventory commands, remember that lua starts from one (itemslots go from 1-16)
end

View File

@ -1,5 +1,20 @@
--If you're low on fuel, grab fuel from somewhere
local function checkFuel(turtle)
while turtle:getFuel() < 10 do
for i = 1, 16 do
turtle:itemRefuel(i)
end
--If you're completely out of fuel, then wait
--(If you don't do this, the turtle will deadlock all turtles)
if turtle:getFuel() < 10 then turtle:yield("No fuel") end
end
end
function init(turtle)
while true do
checkFuel(turtle)
turtle:itemMove(1,2)
turtle:turnRight()
turtle:buildForward(4)
end
end

View File

@ -0,0 +1,29 @@
function buildcube(turtle, size)
local function buildLine(moveIn,moveOut,each)
for _ = 1, size-1 do moveIn() end
for _ = size-1,1,-1 do
each()
moveOut()
end
each()
end
local buildX = (function()
buildLine((function()turtle:moveForward()end),
(function()turtle:moveBackward()end),
(function()turtle:buildForward(1)end))
end)
local buildY = (function()
buildLine((function()turtle:moveRight()end),
(function()turtle:moveLeft()end),
(function()buildX()end))
end)
local buildZ = (function()
buildLine((function()turtle:moveUp()end),
(function()turtle:moveDown()end),
(function()buildY()end))
end)
buildZ()
end
function init(turtle)
buildcube(turtle,3)
end

View File

@ -0,0 +1,51 @@
local function buildAny(turtle)
local ret = false
for i = 1, 16 do
ret = ret or turtle:buildForward(i)
end
return ret
end
---Builds a sphere centered on the turtle
local function buildSphere(turtle, radius)
local x,y,z = 0,0,0
local function shouldBuildSphere(x,y,z) return x*x + y*y + z*z <= radius * radius end
local function moreLeftToBuild()
for x2=x,radius do
if shouldBuildSphere(x2,y,z,radius) then return true end
end
return false
end
turtle:moveBackward()
for _=1,radius do
turtle:moveLeft();z=z-1
turtle:moveBackward();x=x-1
turtle:moveDown();y=y-1
end
while true do
while true do
while x<=radius and moreLeftToBuild() do
if turtle:moveForward() then x=x+1 end
end
while x>-radius do
if turtle:moveBackward() then x=x-1 end
if shouldBuildSphere(x,y,z) then
while not buildAny(turtle) do
turtle:yield("NO BLOCKS")
end
end
end
if z==radius then break end
if turtle:moveRight() then z=z+1 end
end
if y==radius then break end
while z>-radius do
if turtle:moveLeft() then z=z-1 end
end
if turtle:moveUp() then y=y+1 end
end
end
function init(turtle)
for _=1,21 do turtle:moveUp() end
buildSphere(turtle,20)
end

77
examples/7-lumber.lua Normal file
View File

@ -0,0 +1,77 @@
--- Mines an entire forest
local x,y,z = 0,0,0
local turtle = nil
function dumpItems()
local sx,sy,sz = x,y,z -- save
moveTo(0,0,0)
turtle:turnLeft()
turtle:turnLeft()
for i = 1, 16 do turtle:itemPushForward(i) end
turtle:turnLeft()
turtle:turnLeft()
moveTo(sx,sy,sz)
end
function moveTo(tox,toy,toz)
while y<toy do turtle:mineUp() ; turtle:moveUp() ; y=y+1 end
while x>tox do turtle:mineBackward() ; turtle:moveBackward() ; x=x-1 end
while x<tox do turtle:mineForward() ; turtle:moveForward() ; x=x+1 end
while z>toz do turtle:mineLeft() ; turtle:moveLeft() ; z=z-1 end
while z<toz do turtle:mineRight() ; turtle:moveRight() ; z=z+1 end
while y>toy do turtle:mineDown() ; turtle:moveDown() ; y=y-1 end
x=tox; y=toy; z=toz
end
function scan()
local scanlist = {}
table.insert(scanlist,{0,0,0})
local function processScan(point,name)
local names = {'default:tree', 'default:leaves',
'default:jungletree','default:jungleleaves',
'default:pine_tree','default:pine_needles',
'default:acacia_tree','default:acacia_leaves',
'default:aspen_tree','default:aspen_leaves'}
local function has(array,value)
for k,v in pairs(array) do
if value==v then
return true
end
end
return false
end
if has(names,name) then
turtle:debug("name: "..turtle:dump(name))
table.insert(scanlist,point)
end
end
local function mineHere()
local point = table.remove(scanlist,#scanlist)
moveTo(point[1],point[2],point[3])
processScan({point[1]-1,point[2],point[3]},turtle:scanBackward().name)
processScan({point[1]+1,point[2],point[3]},turtle:scanForward() .name)
processScan({point[1],point[2]-1,point[3]},turtle:scanDown() .name)
processScan({point[1],point[2]+1,point[3]},turtle:scanUp() .name)
processScan({point[1],point[2],point[3]-1},turtle:scanLeft() .name)
processScan({point[1],point[2],point[3]+1},turtle:scanRight() .name)
table.sort(scanlist,(function(a,b) return a[1] < b[1] end))
end
while next(scanlist) do
mineHere()
end
moveTo(0,0,0)
end
function init(turtleL)
turtle = turtleL
while true do
scan()
turtle:yield("Done Scanning")
end
end

View File

@ -0,0 +1,29 @@
---Dumps everything from the turtle forward if it's in the list
--- @param list - Something like {"default:stone","default:dirt"}
--- @param isWhitelist - If true, only dump things in list.
--- If false, dump everything EXCEPT the items in the list
--- @param listname @see itemPush
--- @return boolean true unless any items can't fit
local function itemPushForwardFiltered(turtle, list, isWhitelist, listname)
local success = true
local function intable(item)
for _,x in pairs(list) do
if item==x then return true end
end
return false
end
for turtleslot = 1, 16 do
if intable(turtle:itemGet(turtleslot):get_name()) == isWhitelist then
success = success and turtle:itemPushForward(turtleslot,listname)
end
end
return success
end
function init(turtle)
while true do
itemPushForwardFiltered(turtle,{"default:cobble","default:sand"}, true, "src")
itemPushForwardFiltered(turtle,{"default:coal_lump"}, true, "fuel")
turtle:itemSuckForward({"default:stone","default:glass"},true,"dst")
turtle:yield("Waiting")
end
end

13
examples/EXAMPLES.md Normal file
View File

@ -0,0 +1,13 @@
# Examples
See README.md for how to run examples.
# 0-api.lua
The 0-api.lua contains all of the things a turtle can do!
# 2-quarry.lua
This contains a practice on learning how to write turtle code.
See 2-quarry-answers.lua when you're done!
# 7-lumber.lua will walk down trees and mine them

View File

@ -1,9 +1,14 @@
computertest = {
config = {
--Turtles are yielded after calling long events, and resumed this often (in seconds)
turtle_tick = 0.5,
--This is how long timed turtle actions take, such as mining, moving, and placing
turtle_tick = .5,--Default: 0.5 seconds
--Fuel is measured in burntime seconds. A 1 second fuel allows this many actions
fuel_multiplier = 50,
fuel_multiplier = 50,--Default: 50 actions per fuel second
--Fuel inside of turtles when they spawn
fuel_initial = 10000000000,--Default: 1000 actions
--Allows turtle:debug(string) to go to debug.txt
debug = true,
},
turtles = {},
num_turtles = 0,