Compare commits

...

39 Commits

Author SHA1 Message Date
Andrew Lalis c326b4adad Fixed word matching for modded items. 2023-05-08 08:05:38 +02:00
Andrew Lalis a8242c610e Fixed findSlots 2023-05-08 07:31:23 +02:00
Andrew Lalis 0f3e5d9ee2 Fix bug in itemscript. 2023-05-07 21:24:41 +02:00
Andrew Lalis d27320c135 Organized slot functions. 2023-05-05 09:36:29 +02:00
Andrew Lalis e477ba3b42 Added some more documentation. 2023-05-02 10:28:43 +02:00
Andrew Lalis 812e5a8eaf Fix buildscript. 2023-04-27 13:56:47 +02:00
Andrew Lalis 38e34f9a68 Added execute hooks. 2023-04-27 13:37:45 +02:00
Andrew Lalis a7b8bcf1c8 Made placing safe. 2023-04-27 13:19:37 +02:00
Andrew Lalis febe9b8878 Fixed selection logic. 2023-04-27 12:59:32 +02:00
Andrew Lalis affd55fa81 remove version checking. 2023-04-27 12:46:13 +02:00
Andrew Lalis f545e6a930 Fix again. 2023-04-27 12:42:05 +02:00
Andrew Lalis 66ca84fd9f Fix installer. 2023-04-27 12:35:47 +02:00
Andrew Lalis f9a9398c74 Fixed bugs, improved installer. 2023-04-27 12:30:58 +02:00
Andrew Lalis d3da86c95e added installer script to readme. 2023-04-27 12:17:07 +02:00
Andrew Lalis 3560cca7ca Added ms-installer. 2023-04-27 12:15:25 +02:00
Andrew Lalis f7fd8790f7 Try relative links 2023-04-27 11:14:35 +02:00
Andrew Lalis cb0f6b592f Added scripts listing 2023-04-27 11:11:26 +02:00
Andrew Lalis 690841eca1 Fix typo 2023-04-27 11:07:17 +02:00
Andrew Lalis 0ae239ad53 Add docs workflow to triggers 2023-04-27 11:06:02 +02:00
Andrew Lalis b095535c8e Force node 16 for docs 2023-04-27 11:05:09 +02:00
Andrew Lalis f76c72019a Added proper itemscript 2023-04-27 11:02:12 +02:00
Andrew Lalis cfe8cc8a20 Improved buildscript and itemscript. Still thinking about itemscript filters... 2023-04-26 10:37:17 +02:00
Andrew Lalis a1dd7a850a Added buildscript start 2023-04-26 08:41:55 +02:00
Andrew Lalis 78d49f8abc Moved INSTRUCTION_TYPES declaration to front of file. 2022-12-31 11:09:03 +01:00
Andrew Lalis 9a00985a68 Added options and more documentation. 2022-12-23 13:19:44 +01:00
Andrew Lalis 2178004f65 Merge branch 'main' of github.com:andrewlalis/movescript into main 2022-12-23 11:37:46 +01:00
Andrew Lalis 3fdc6c01f0 Added repeated instructions and action options. 2022-12-23 11:37:40 +01:00
Andrew Lalis f0a0fd5789 Fixed refuel error with nil item. 2022-12-23 08:27:45 +01:00
Andrew Lalis a90f9fbd5b Fixed refuel bug. 2022-12-22 21:54:32 +01:00
Andrew Lalis 821a29175d Updated package lock sources. 2022-12-22 13:03:12 +01:00
Andrew Lalis a3294c3c1a Added copy-to-clipboard, and fixed bug in refuel. 2022-12-22 12:45:46 +01:00
Andrew Lalis f0681b77bd Added basic scripts page. 2022-12-22 12:33:25 +01:00
Andrew Lalis dc1450ddd9 Changed to deploy full scripts to docs pages. 2022-12-22 12:32:29 +01:00
Andrew Lalis e84fc835e7 Added shorter links to scripts. 2022-12-22 12:26:04 +01:00
Andrew Lalis 17a8ae6988 Added validate method, and docs. 2022-12-22 12:20:18 +01:00
Andrew Lalis e80a7fd61f Added permissions to workflow. 2022-12-22 12:12:47 +01:00
Andrew Lalis 11d01cb749 Added the workflow for real. 2022-12-22 12:09:52 +01:00
Andrew Lalis 15f9e1a824 Added better workflow. 2022-12-22 12:09:07 +01:00
Andrew Lalis 11bba93309
Merge pull request #5 from andrewlalis/create-pull-request/patch
Changes by create-pull-request action
2022-12-22 10:49:46 +01:00
21 changed files with 975 additions and 254 deletions

View File

@ -1,13 +1,15 @@
name: Publish Docs
name: Deploy Docs to Pages
on:
push:
branches: [ "main" ]
paths:
- src/**
- docs/**
- .github/workflows/deploy-docs2.yml
workflow_dispatch:
permissions:
contents: read
pages: write
@ -20,10 +22,38 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Copy scripts to Docs public assets
run: |
script_dir=docs/src/.vuepress/public/scripts
rm -rf $script_dir
mkdir $script_dir
cp src/* $script_dir/
- name: Install Lua
uses: ljmf00/setup-lua@v1.0.0
with:
lua-version: 5.3
install-luarocks: true
- name: Minify Scripts
run: |
script_dir=docs/src/.vuepress/public/scripts
lua minify.lua minify src/movescript.lua > $script_dir/movescript-min.lua
lua minify.lua minify src/itemscript.lua > $script_dir/itemscript-min.lua
lua minify.lua minify src/buildscript.lua > $script_dir/buildscript-min.lua
- name: Clean Up Lua Artifacts
run: |
rm -rf .lua
rm -rf .luarocks
rm -rf .source
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Setup Pages
uses: actions/configure-pages@v2
@ -50,3 +80,4 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

View File

@ -1,39 +0,0 @@
name: Minify Scripts
on:
push:
branches: [ "main" ]
paths:
- src/**
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Lua
uses: ljmf00/setup-lua@v1.0.0
with:
lua-version: 5.3
install-luarocks: true
- name: Minify Scripts
run: |
rm -rf min
mkdir min
lua minify.lua minify src/movescript.lua > min/movescript.lua
lua minify.lua minify src/itemscript.lua > min/itemscript.lua
- name: Clean Up Repository
run: |
rm -rf .lua
rm -rf .luarocks
rm -rf .source
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4

14
docs/package-lock.json generated
View File

@ -8,6 +8,9 @@
"name": "movescript-docs",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"vuepress-plugin-code-copy": "^1.0.6"
},
"devDependencies": {
"vuepress": "^1.5.3"
}
@ -14222,6 +14225,12 @@
"object.getownpropertydescriptors": "^2.0.3"
}
},
"node_modules/vuepress-plugin-code-copy": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vuepress-plugin-code-copy/-/vuepress-plugin-code-copy-1.0.6.tgz",
"integrity": "sha512-FiqwMtlb4rEsOI56O6sSkekcd3SlESxbkR2IaTIQxsMOMoalKfW5R9WlR1Pjm10v6jmU661Ex8MR11k9IzrNUg==",
"license": "GPL-3.0-or-later"
},
"node_modules/vuepress-plugin-container": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/vuepress-plugin-container/-/vuepress-plugin-container-2.1.5.tgz",
@ -26370,6 +26379,11 @@
}
}
},
"vuepress-plugin-code-copy": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vuepress-plugin-code-copy/-/vuepress-plugin-code-copy-1.0.6.tgz",
"integrity": "sha512-FiqwMtlb4rEsOI56O6sSkekcd3SlESxbkR2IaTIQxsMOMoalKfW5R9WlR1Pjm10v6jmU661Ex8MR11k9IzrNUg=="
},
"vuepress-plugin-container": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/vuepress-plugin-container/-/vuepress-plugin-container-2.1.5.tgz",

View File

@ -15,5 +15,8 @@
"license": "MIT",
"devDependencies": {
"vuepress": "^1.5.3"
},
"dependencies": {
"vuepress-plugin-code-copy": "^1.0.6"
}
}

View File

@ -81,5 +81,11 @@ module.exports = {
plugins: [
'@vuepress/plugin-back-to-top',
'@vuepress/plugin-medium-zoom',
['vuepress-plugin-code-copy', {
backgroundTransition: false,
staticIcon: false,
color: '#de9502',
successText: 'Copied to clipboard.'
}]
]
}

View File

@ -1,15 +0,0 @@
---
sidebar: auto
---
# Config
## foo
- Type: `string`
- Default: `/`
## bar
- Type: `string`
- Default: `/`

View File

@ -8,3 +8,8 @@ Movescript is a set of libraries that remove the prohibitive tedium of writing t
| [Itemscript](./itemscript/) | A collection of helper functions that simplify inventory management. |
These modules are intended to be used with the Turtle robots provided by the [CC:Tweaked](https://tweaked.cc/) mod for Minecraft.
You can run an installer script to automatically download the latest versions of all of the movescript family of libraries:
```shell
wget run https://andrewlalis.github.io/movescript/scripts/ms-installer.lua
```

View File

@ -7,7 +7,7 @@ The Itemscript module provides a flexible, powerful interface for managing a tur
To install this module, run the following command from your turtle's console:
```shell
wget https://raw.githubusercontent.com/andrewlalis/movescript/main/min/itemscript.lua
wget https://andrewlalis.github.io/movescript/scripts/itemscript.lua
```
And then use it in a script:

View File

@ -26,6 +26,27 @@ In the case of strings, a **filter expression string** is a string that can be u
The most basic form of an expression string is just an item name, like `"minecraft:dirt"`, or `"create:train_door"`. Most normal items will begin with the `minecraft:` *namespace* prefix. If you don't include such a prefix, and you're not doing a [fuzzy match](#fuzzy-match), itemscript will add `minecraft:` for you.
### Grammar
Filter expressions can be summarized with a BNF-style grammar description.
```
word = %a[%w%-_:]* A whole or substring of an item's name.
number = %d+
expr = word Matches item stacks whose name matches the given word.
= #word Matches item stacks whose name contains the given word.
= (expr) Grouping of a nested expression.
= !expr Matches item stacks that don't match the given expression.
= expr | expr Matches item stacks that match any of the given expressions (OR).
= expr & expr Matches item stacks that match all of the given expressions (AND).
= expr > number Matches item stacks that match the given expression, and have more than N items.
= expr >= number Matches item stacks that match the given expression, and have more than or equal to N items.
= expr < number Matches item stacks that match the given expression, and have less than N items.
= expr <= number Matches item stacks that match the given expression, and have less than or equal to N items.
= expr = number Matches item stacks that match the given expression, and have exactly N items.
= expr != number Matches item stacks that match the given expression, and do not have exactly N items.
```
For example, we can count the number of stone items in our inventory like this:
```lua

View File

@ -7,7 +7,7 @@ The Movescript module provides a simple interface for executing *movescript sour
To install this module, run the following command from your turtle's console:
```shell
wget https://raw.githubusercontent.com/andrewlalis/movescript/main/min/movescript.lua
wget https://andrewlalis.github.io/movescript/scripts/movescript.lua
```
And then use it in a script:

View File

@ -7,14 +7,48 @@ local ms = require("movescript")
ms.run("2F")
```
## `run(script, settings)`
## `parse(script, settings)`
Parses the given `script` string and returns a table containing the parsed instructions to be executed. This is mostly useful for debugging your scripts.
## `executeInstruction(instruction, settings, preExecuteHook, postExecuteHook)`
Executes a single instruction table using the given settings, and if pre- and post-execution hooks are defined, they will be invoked. This is mostly useful for debugging your scripts.
## `run(script, settings, preExecuteHook, postExecuteHook)`
Runs the given `script` string as a movescript, and optionally a `settings` table can be provided. Otherwise, [default settings](settings.md) will be used.
## `runFile(filename, settings)`
For example:
```lua
local ms = require("movescript")
ms.run("3F2R3B2LUD", {debug=true})
```
If you provide a non-nil `preExecuteHook` or `postExecuteHook` function, that function will run before or after each instruction in the script, respectively. This could be used to update other systems as to the robot's status, or to make sure items are selected.
## `runFile(filename, settings, preExecuteHook, postExecuteHook)`
Reads content from the given filename and executes it as a script. Just like with `run`, an optional `settings` table can be provided.
## `validate(script, settings)`
Validates the given `script`, by parsing its instructions in a wrapped [`pcall`](https://www.lua.org/pil/8.4.html). It returns `true` if the script is valid, or `false` and an error message describing why the script is not valid.
For example:
```lua
local ms = require("movescript")
local status, message = ms.validate("not a valid script.")
```
## `mirror(script)`
Mirrors the given `script`. That is, this swaps any `R` (turn right) instructions with `L` (turn left), which effectively mirrors the robot's motion relative to its original facing direction.
Returns the mirrored script which can then be run.
## `defaultSettings`
A table containing the default settings for any script executed by the movescript module.
A table containing the default [settings](./settings.md) for any script executed by the movescript module.

View File

@ -2,7 +2,7 @@
Every movescript must follow the outline defined in this specification.
Each script consists of zero or more **instructions**, separated by zero or more whitespace characters.
Each script consists of zero or more **instructions** or **repeated instructions**, separated by zero or more whitespace characters.
## Instructions
@ -10,29 +10,53 @@ An instruction consists of an optional positive integer number, followed by a re
```lua
-- The regex used to parse instructions.
instruction = string.find(script, "%W*(%d*%u%l*)%W*")
instruction = string.find(script, "%s*(%d*%u%l*)%s*")
```
Each instruction can be split into two parts: the **action**, and the **count**. The action is the textual part of the instruction, and maps to a turtle behavior. The count is the optional numerical part of the instruction, and defaults to `1` if no number is provided.
Here are some examples of valid instructions: `3F`, `U`, `1R`
Some instructions may allow you to specify additional options. These can be defined as key-value pairs in parentheses after the action part.
For example: `4A(delay=0.25, file=tmp.txt)`
## Repeated Instructions
A repeated instruction is a grouping of instructions that are repeated a specified number of times. It's denoted as a positive integer number, followed by a series of [instructions](#instructions) within parentheses.
For example: `22(AF)` - We execute the instructions `A` and `F` 22 times.
## Actions
The following table lists all actions that are available in Movescript. Attempting to invoke an action not listed here will result in an error that will terminate your script.
| Action | Description | Needs Fuel |
| ------ | ------------------------------------------------ | ---------- |
| `U` | Move up. | ✅ |
| `D` | Move down. | ✅ |
| `L` | Turn left. | ❌ |
| `R` | Turn right. | ❌ |
| `F` | Move forward. | ✅ |
| `B` | Move backward. | ✅ |
| `P` | Place the selected item in front of the turtle. | ❌ |
| `Pu` | Place the selected item above the turtle. | ❌ |
| `Pd` | Place the selected item below the turtle. | ❌ |
| `A` | Attack in front of the turtle. | ❌ |
| `Au` | Attack above the turtle. | ❌ |
| `Ad` | Attack below the turtle. | ❌ |
| Action | Description | Options |
| ------ | ------------------------------------------------ | ------------------------------------------ |
| `U` | Move up. |
| `D` | Move down. |
| `L` | Turn left. |
| `R` | Turn right. |
| `F` | Move forward. |
| `B` | Move backward. |
| `P` | Place the selected item in front of the turtle. | `text: string` - Text to use if placing a sign. |
| `Pu` | Place the selected item above the turtle. | `text: string` - Text to use if placing a sign. |
| `Pd` | Place the selected item below the turtle. | `text: string` - Text to use if placing a sign. |
| `A` | Attack in front of the turtle. | `side: string` - The tool side to use (left or right). |
| `Au` | Attack above the turtle. | `side: string` - The tool side to use (left or right). |
| `Ad` | Attack below the turtle. | `side: string` - The tool side to use (left or right). |
| `Dg` | Dig in front of the turtle. | `side: string` - The tool side to use (left or right). |
| `Dgu` | Dig above the turtle. | `side: string` - The tool side to use (left or right). |
| `Dgd` | Dig below the turtle. | `side: string` - The tool side to use (left or right). |
| `S` | Suck items from in front of the turtle. | `count: number` - The number of items to suck. |
| `Su` | Suck items from above the turtle. | `count: number` - The number of items to suck. |
| `Sd` | Suck items from below the turtle. | `count: number` - The number of items to suck. |
| `Eqr` | Equip the selected item to the right side. |
| `Eql` | Equip the selected item to the left side. |
| `Sel` | Selects slot 1, or the specified slot. | `slot: number` - The slot to select. |
| `Dr` | Drops the selected items in front of the turtle. | `count: number` - The number of items to drop. |
| `Dru` | Drops the selected items above the turtle. | `count: number` - The number of items to drop. |
| `Drd` | Drops the selected items below the turtle. | `count: number` - The number of items to drop. |
For example, if we want our turtle to go forward 3 times, instead of writing `turtle.forward()` 3 times, we can just do the following:
@ -55,6 +79,6 @@ The following snippets show a few example scripts, along with a description of w
`3F2U1L` - Move forward 3 blocks, then up 2 blocks, and turn left.
`B2RAd` - Move back 2 blocks, then turn right twice, and then attack downward.
`B2RAd` - Move back, then turn right twice, and then attack downward.
`4(U2RD)` - 4 times in a row, move up, twice right, and back down.

View File

@ -0,0 +1,10 @@
# Scripts
This directory contains all scripts published under **Movescript**.
- [movescript](./movescript.lua)
- [movescript-min](./movescript-min.lua)
- [itemscript](./itemscript.lua)
- [itemscript-min](./itemscript-min.lua)
- [buildscript](./buildscript.lua)
- [buildscript-min](./buildscript-min.lua)

View File

@ -1,24 +0,0 @@
--[[
Installation script for installing all libraries.
Run `wget run https://raw.githubusercontent.com/andrewlalis/movescript/main/install.lua`
to run the installer on your device.
]]--
BASE_URL = "https://raw.githubusercontent.com/andrewlalis/movescript/main/"
SCRIPTS = {
"movescript.lua",
"itemscript.lua"
}
-- Create a local executable to re-install, instead of having to run this file via wget.
local f = io.open("install-movescript.lua", "w")
for _, script in pairs(SCRIPTS) do
url = BASE_URL .. script
cmd = "wget " .. url .. " " .. script
shell.run(cmd)
f:write("if fs.exists(\"" .. script .. "\") then fs.delete(\"" .. script .. "\") end")
f:write("shell.run(\"" .. cmd .. "\")")
end
f:close()

View File

@ -1 +0,0 @@
a="0.0.1"local b=turtle local c={}local function d(k,l,m)return k~=nil and((not m and k.name==l)or string.find(k.name,l))end local function e(k)return function(l)return not k(l)end end local function f(k)return function(l)for m,n in pairs(k)do if not n(l)then return false end end return true end end local function g(k)return function(l)for m,n in pairs(k)do if n(l)then return true end end return false end end local function h(k)local l,m=string.find(k,"^[!#]+")local n=false local o=false if l~=nil then for q=l,m do local r=string.sub(k,q,q)if r=="!"then o=true elseif r=="#"then n=true end end k=string.sub(k,m+1,string.len(k))end local p=string.find(k,":")if p==nil and not n then k="minecraft:"..k end return function(q)if q==nil then return false end local r=d(q,k,n)if o then r=not r end return r end end local function i(k)if type(k)=="table"and#k>0 and type(k[1])=="string"then local l={}for m,n in pairs(k)do table.insert(l,h(n))end return g(l)elseif type(k)=="string"then return h(k)elseif type(k)=="function"then return k else error("Unsupported filter type: "..type(k))end end function c.totalCount(k)local l=i(k)local m=0 for n=1,16 do local o=b.getItemDetail(n)if l(o)then m=m+o.count end end return m end function c.select(k)local l=i(k)for m=1,16 do local n=b.getItemDetail(m)if l(n)then b.select(m)return true end end return false end local function j(k,l)for m=1,16 do local n=b.getItemDetail(m)if l(n)then b.select(m)k()end end end function c.dropAll(k)j(b.drop,i(k))end function c.dropAllDown(k)j(b.dropDown,i(k))end function c.dropAllUp(k)j(b.dropUp,i(k))end function c.organize()error("Not yet implemented.")end return c

View File

@ -1 +0,0 @@
a="0.0.1"local d=turtle local e={}e.defaultSettings={debug=false,safe=true,destructive=false,fuels={"minecraft:coal","minecraft:charcoal"}}local function f(t,u)if u and u.debug then print("[MS] "..t)end end function d.digBack(t)d.turnRight()d.turnRight()d.dig(t)d.turnRight()d.turnRight()end function d.detectBack()d.turnRight()d.turnRight()local t=d.detect()d.turnRight()d.turnRight()return t end local function g(t,u,v,w)w=w or e.defaultSettings b=w.safe or e.defaultSettings.safe c=w.destructive or e.defaultSettings.destructive local x=t()if not b then return end while not x do f("Unable to move.",w)if c and v()then f("Detected a block in the way; attempting to remove it.",w)u()end x=t()end end local function h(t)f("Moving up.",t)g(d.up,d.digUp,d.detectUp,t)end local function j(t)f("Moving down.",t)g(d.down,d.digDown,d.detectDown,t)end local function k(t)f("Moving forward.",t)g(d.forward,d.dig,d.detect,t)end local function l(t)f("Moving back.",t)g(d.back,d.digBack,d.detectBack,t)end local function m(t)f("Turning right.",t)d.turnRight()end local function n(t)f("Turning left.",t)d.turnLeft()end local o={["U"]={f=h,needsFuel=true},["D"]={f=j,needsFuel=true},["L"]={f=n,needsFuel=false},["R"]={f=m,needsFuel=false},["F"]={f=k,needsFuel=true},["B"]={f=l,needsFuel=true},["P"]={f=d.place,needsFuel=false},["Pu"]={f=d.placeUp,needsFuel=false},["Pd"]={f=d.placeDown,needsFuel=false},["A"]={f=d.attack,needsFuel=false},["Au"]={f=d.attackUp,needsFuel=false},["Ad"]={f=d.attackDown,needsFuel=false}}local function p(t)f("Refueling...",t)local u=t.fuels or e.defaultSettings.fuels local v=false for w=1,16 do local x=d.getItemDetail(w)if x~=nil then for y,z in pairs(u)do if x.name==z then d.select(i)if d.refuel(x.count)then v=true end break end end end end return v end local function q(t,u)p(u)while d.getFuelLevel<t do print("[MS] Fuel level is too low. Level: "..d.getFuelLevel()..". Required: "..t..". Please add some of the following fuels:")local v=u.fuels or e.defaultSettings.fuels for x,y in pairs(v)do print(" - "..y)end local w=false while not w do os.pullEvent("turtle_inventory")w=p()end end end local function r(t,u)local v=o[t.action]if v then f("Executing action \""..t.action.."\" "..t.count.." times.",u)local w=((u.safe or true)and(v.needsFuel)and(t.count>d.getFuelLevel()))if w then local x=t.count q(x,u)end for x=1,t.count do v.f()end end end local function s(t,u)local v={}for w in string.gfind(t,"%W*(%d*%u%l*)%W*")do local x,y=string.find(w,"%d+")local z,A=string.find(w,"%u%l*")local B=1 if x~=nil then B=tonumber(string.sub(w,x,y))end local C=string.sub(w,z,A)if B<1 or B>d.getFuelLimit()then error("Instruction at index "..z.." has an invalid count of "..B..". It should be >= 1 and <= "..d.getFuelLimit())end if o[C]==nil then error("Instruction at index "..z..", \""..C.."\", does not refer to a valid action.")end table.insert(v,{action=C,count=B})f("Parsed instruction: "..w,u)end return v end function e.run(t,u)u=u or e.defaultSettings t=t or""f("Executing script: "..t,u)local v=s(t,u)for w,x in pairs(v)do r(x,u)end end function e.runFile(t,u)local v=fs.open(t,"r")local w=v.readAll()v.close()e.run(w,u)end return e

119
src/buildscript.lua Normal file
View File

@ -0,0 +1,119 @@
--[[
Buildscript - A unified set of tools that make repetitive building tasks easier
with ComputerCraft robots.
Author: Andrew Lalis <andrewlalisofficial@gmail.com>
This module depends upon both Movescript and Itemscript.
]]--
local movescript = require("movescript")
local itemscript = require("itemscript")
-- The buildscript module.
local buildscript = {}
buildscript.VERSION = "0.0.1"
-- Runs a movescript script, while ensuring that a given item is always selected.
function buildscript.runWithItem(ms_script, filterExpr, settings)
movescript.run(ms_script, settings, function() itemscript.selectOrWait(filterExpr) end)
end
-- Runs a movescript script, while selecting random items that match a filter.
function buildscript.runWithRandomItems(ms_script, filterExpr, settings)
movescript.run(ms_script, settings, function() itemscript.selectRandomOrWait(filterExpr) end)
end
-- Parses a value for an argument specification from a raw value.
local function parseArgValue(argSpec, arg)
if argSpec.required and (not arg or #arg < 1) then
return false, "Missing required value."
end
if argSpec.type == "string" then
return true, arg
elseif argSpec.type == "number" then
local num = tonumber(arg)
if not num and argSpec.required then
return false, "Invalid number."
end
return true, num
elseif argSpec.type == "bool" then
local txt = string.lower(arg)
if txt == "true" or txt == "t" or txt == "yes" or txt == "y" then
return true, true
else
return true, false
end
else
return false, "Unknown type: " .. argSpec.type
end
end
-- Parses arguments according to a specification table, for common building
-- scripts, and returns a table with key-value pairs for each arg.
-- The specification table should be formatted like so:
-- {
-- argName = { type = "string", required = true, idx = 1 },
-- namedArg = { name = "-f", required = true, type = "bool" }
-- }
-- Supported types: string, number, bool
function buildscript.parseArgs(args, spec)
for name, argSpec in pairs(spec) do
if argSpec.idx ~= nil then
if type(argSpec.idx) ~= "number" or argSpec.idx < 1 then
return false, "Invalid argument specification: " .. name .. " does not have a valid numeric index."
end
elseif argSpec.name ~= nil then
if type(argSpec.name) ~= "string" or #argSpec.name < 3 then
return false, "Invalid argument specification: " .. name .. " does not have a valid string name."
end
else
return false, "Invalid argument specification: " .. name .. " doesn't have idx or name."
end
if not argSpec.type then argSpec.type = "string" end
end
local results = {}
-- Iterate over each argument specification, and try and find a value for it.
for name, argSpec in pairs(spec) do
if argSpec.idx then
-- Parse a positional argument.
if argSpec.idx > #args and argSpec.required then
return false, "Missing required positional argument " .. name .. " at index " .. argSpec.idx
end
if argSpec.idx > #args then
results[name] = nil
else
local success, value = parseArgValue(argSpec, args[argSpec.idx])
if not success then
return false, "Failed to parse value for argument " .. name .. ": " .. value
end
results[name] = value
end
else
-- Parse a named argument by iterating over all args until we find one matching the name.
local valueFound = false
for idx, arg in pairs(args) do
if arg == argSpec.name then
if idx >= #args and argSpec.required then
return false, "Missing value for required argument " .. name
end
local success, value = parseArgValue(argSpec, args[idx + 1])
if not success then
return false, "Failed to parse value for argument " .. name .. ": " .. value
end
results[name] = value
valueFound = true
break
end
end
if argSpec.required and not valueFound then
return false, "Missing argument: " .. name
end
end
end
return true, results
end
return buildscript

View File

@ -5,164 +5,362 @@ Author: Andrew Lalis <andrewlalisofficial@gmail.com>
]]--
VERSION = "0.0.1"
local t = turtle
-- The itemscript module. Functions defined within this table are exported.
local itemscript = {}
itemscript.VERSION = "0.0.1"
-- Determines if an item stack matches the given name.
-- If fuzzy, then the item name will be matched against the given name.
local function stackMatches(itemStack, name, fuzzy)
return itemStack ~= nil and
(
(not fuzzy and itemStack.name == name) or
string.find(itemStack.name, name)
)
if itemStack == nil or itemStack.name == nil then return false end
if fuzzy then return string.find(itemStack.name, name) ~= nil end
return itemStack.name == name
end
local function notFilter(filter)
return function(item)
return not filter(item)
local function splitString(str, sep)
if sep == nil then sep = "%s" end
local result = {}
for s in string.gmatch(str, "([^"..sep.."]+)") do
table.insert(result, s)
end
return result
end
local function andFilter(filters)
return function(item)
for _, filter in pairs(filters) do
if not filter(item) then
return false
end
end
return true
end
end
local function orFilter(filters)
return function(item)
for _, filter in pairs(filters) do
if filter(item) then
return true
end
end
return false
end
end
-- Parses a filter expression string and returns a filter that implements it.
-- Parses a filter expression string and returns a table representing the syntax tree.
-- An error is thrown if compilation fails.
--[[
Item Filter Expressions:
A filter expression is a way to define a complex method of matching item
stacks.
Prepending ! will match any item stack whose name does not match.
Prepending # will do a fuzzy match using string.find.
Grammar:
word = %a[%w%-_:]* A whole or substring of an item's name.
number = %d+
expr = word Matches item stacks whose name matches the given word.
= #word Matches item stacks whose name contains the given word.
= (expr)
= !expr Matches item stacks that don't match the given expression.
= expr | expr Matches item stacks that match any of the given expressions (OR).
= expr & expr Matches item stacks that match all of the given expressions (AND).
= expr > %d Matches item stacks that match the given expression, and have more than N items.
= expr >= %d Matches item stacks that match the given expression, and have more than or equal to N items.
= expr < %d Matches item stacks that match the given expression, and have less than N items.
= expr <= %d Matches item stacks that match the given expression, and have less than or equal to N items.
= expr = %d Matches item stacks that match the given expression, and have exactly N items.
= expr != %d Matches item stacks that match the given expression, and do not have exactly N items.
Examples:
"#log > 10" matches any items containing the word "log", that have more than 10 items in the stack.
"10% coal, 90% iron_ore" matches coal 10% of the time, and iron_ore 90% of the time.
]]--
local function parseItemFilterExpression(expr)
local prefixIdx, prefixIdxEnd = string.find(expr, "^[!#]+")
local fuzzy = false
local negated = false
if prefixIdx ~= nil then
for i = prefixIdx, prefixIdxEnd do
local char = string.sub(expr, i, i)
if char == "!" then
negated = true
elseif char == "#" then
fuzzy = true
function itemscript.parseFilterExpression(str)
str = str:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace from the beginning and end of the string.
-- Parse group constructs
local ignoreRange = nil
if string.sub(str, 1, 1) == "(" then
local idx1, idx2 = string.find(str, "%b()")
if idx1 == nil then
error("Invalid group construct: \"" .. str .. "\".")
end
-- If the group is the whole expression, parse it. Otherwise, defer parsing to later.
if idx2 == #str then
return itemscript.parseFilterExpression(string.sub(str, idx1 + 1, idx2 - 1))
else
ignoreRange = {idx1, idx2}
end
end
-- Parse logical group operators (OR and AND)
local logicalGroupOperators = {
{ name = "OR", token = "|" },
{ name = "AND", token = "&" }
}
for _, operator in pairs(logicalGroupOperators) do
local idx = string.find(str, operator.token)
if idx ~= nil and (ignoreRange == nil or idx < ignoreRange[1] or idx > ignoreRange[2]) then
return {
type = operator.name,
children = {
itemscript.parseFilterExpression(string.sub(str, 1, idx - 1)),
itemscript.parseFilterExpression(string.sub(str, idx + 1, -1))
}
}
end
end
-- Parse item count arithmetic operators
local arithmeticOperators = {
["LESS_THAN"] = "<",
["LESS_THAN_OR_EQUAL_TO"] = "<=",
["GREATER_THAN"] = ">",
["GREATER_THAN_OR_EQUAL_TO"] = ">=",
["EQUALS"] = "=",
["NOT_EQUALS"] = "!="
}
for typeName, token in pairs(arithmeticOperators) do
local idx = string.find(str, token)
if idx ~= nil and (ignoreRange == nil or idx < ignoreRange[1] or idx > ignoreRange[2]) then
local subExpr = itemscript.parseFilterExpression(string.sub(str, 1, idx - 1))
local numberExprIdx1, numberExprIdx2 = string.find(str, "%d+", idx + 1)
if numberExprIdx1 == nil then
error("Could not find number expression (%d+) in string: \"" .. string.sub(str, idx + 1, -1) .. "\".")
end
local numberValue = tonumber(string.sub(str, numberExprIdx1, numberExprIdx2))
if numberValue == nil then
error("Could not parse number from string: \"" .. string.sub(str, numberExprIdx1, numberExprIdx2) .. "\".")
end
return {
type = typeName,
expr = subExpr,
value = numberValue
}
end
expr = string.sub(expr, prefixIdxEnd + 1, string.len(expr))
end
local namespaceSeparatorIdx = string.find(expr, ":")
if namespaceSeparatorIdx == nil and not fuzzy then
expr = "minecraft:" .. expr
-- Parse NOT operator.
if string.sub(str, 1, 1) == "!" then
return {
type = "NOT",
expr = itemscript.parseFilterExpression(string.sub(str, 2, -1))
}
end
return function(item)
if item == nil then return false end
local matches = stackMatches(item, expr, fuzzy)
if negated then
matches = not matches
-- Parse fuzzy and plain words.
local fuzzy = false
if string.sub(str, 1, 1) == "#" then
fuzzy = true
str = string.sub(str, 2, -1)
end
local wordIdx1, wordIdx2 = string.find(str, "%a[%w%-_:]*")
if wordIdx1 ~= nil then
local value = string.sub(str, wordIdx1, wordIdx2)
if not fuzzy and string.find(value, ":") == nil then
value = "minecraft:" .. value
end
return matches
return {
type = "WORD",
value = value,
fuzzy = fuzzy
}
end
error("Invalid filter expression syntax: " .. str)
end
-- Compiles a filter function from a filter expression syntax tree.
function itemscript.compileFilter(expr)
if expr.type == "WORD" then
return function(item)
return stackMatches(item, expr.value, expr.fuzzy)
end
end
if expr.type == "NOT" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return not subFilter(item)
end
end
if expr.type == "LESS_THAN" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count < expr.value
end
end
if expr.type == "GREATER_THAN" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count > expr.value
end
end
if expr.type == "LESS_THAN_OR_EQUAL_TO" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count <= expr.value
end
end
if expr.type == "GREATER_THAN_OR_EQUAL_TO" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count >= expr.value
end
end
if expr.type == "EQUALS" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count == expr.value
end
end
if expr.type == "NOT_EQUALS" then
local subFilter = itemscript.compileFilter(expr.expr)
return function (item)
return subFilter(item) and item.count ~= expr.value
end
end
if expr.type == "AND" then
local subFilters = {}
for _, childExpr in pairs(expr.children) do
table.insert(subFilters, itemscript.compileFilter(childExpr))
end
return function (item)
for _, subFilter in pairs(subFilters) do
if not subFilter(item) then return false end
end
return true
end
end
if expr.type == "OR" then
local subFilters = {}
for _, childExpr in pairs(expr.children) do
table.insert(subFilters, itemscript.compileFilter(childExpr))
end
return function (item)
for _, subFilter in pairs(subFilters) do
if subFilter(item) then return true end
end
return false
end
end
error("Invalid filter expression syntax tree item: " .. expr.type)
end
--[[
Converts an arbitrary value to a filter function that can be applied to item
stacks for filtering operations. The following types are supported:
- strings are parsed and compiled to filter functions.
- functions are assumed to be filter functions that take an item stack as
a single parameter, and return true for a match, and false otherwise.
- tables are assumed to be pre-parsed filter expression syntax trees.
]]--
function itemscript.filterize(value)
if type(value) == "string" then
return itemscript.compileFilter(itemscript.parseFilterExpression(value))
elseif type(value) == "table" then
return itemscript.compileFilter(value)
elseif type(value) == "function" then
return value
else
error("Invalid filterizable value. Expected filter expression string, syntax tree table, or filter function.")
end
end
-- Converts an arbitrary variable into a filter; useful for any function that's public, so users can supply any filter.
-- It converts the following:
-- filter function tables directly.
-- strings and lists of strings are translated into an item names filter.
-- Functions are added with default fuzzy and whitelist parameters.
local function convertToFilter(var)
if type(var) == "table" and #var > 0 and type(var[1]) == "string" then
local filters = {}
for _, expr in pairs(var) do
table.insert(filters, parseItemFilterExpression(expr))
end
return orFilter(filters)
elseif type(var) == "string" then
return parseItemFilterExpression(var)
elseif type(var) == "function" then
return var
else
error("Unsupported filter type: " .. type(var))
-- Finds the first matching slot for the given filter expression.
function itemscript.findSlot(filterExpr)
local filter = itemscript.filterize(filterExpr)
for i = 1, 16 do
local item = turtle.getItemDetail(i)
if filter(item) then return i end
end
return nil
end
-- Gets a list of all inventory slots that match the given filter expression.
function itemscript.findSlots(filterExpr)
local filter = itemscript.filterize(filterExpr)
local slots = {}
for i = 1, 16 do
local item = turtle.getItemDetail(i)
if filter(item) then
table.insert(slots, i)
end
end
return slots
end
-- Gets the total number of items in the turtle's inventory that match the given expression.
function itemscript.totalCount(filterExpr)
local filter = convertToFilter(filterExpr)
local count = 0
for i = 1, 16 do
local item = t.getItemDetail(i)
if filter(item) then
count = count + item.count
end
for _, slot in pairs(itemscript.findSlots(filterExpr)) do
local item = turtle.getItemDetail(slot)
count = count + item.count
end
return count
end
-- Selects a slot containing at least one of the given item type.
-- Select the first slot containing a matching item stack for a filter.
-- Returns a boolean indicating whether we could find and select the item.
function itemscript.select(filterExpr)
local filter = convertToFilter(filterExpr)
local slot = itemscript.findSlot(filterExpr)
if slot ~= nil then
turtle.select(slot)
return true
end
return false
end
-- Selects a random slot containing a matching item stack.
function itemscript.selectRandom(filterExpr)
local eligibleSlots = itemscript.findSlots(filterExpr)
if #eligibleSlots == 0 then return false end
local slot = eligibleSlots[math.random(1, #eligibleSlots)]
turtle.select(slot)
return true
end
-- Selects a slot containing at least 1 of an item type matching
-- the given filter expression.
function itemscript.selectOrWait(filterExpr)
local filter = itemscript.filterize(filterExpr)
while not itemscript.select(filter) do
print("Couldn't find at least 1 item matching the filter expression: \"" .. filterExpr .. "\". Please add it.")
os.pullEvent("turtle_inventory")
end
end
function itemscript.selectRandomOrWait(filterExpr)
local filter = itemscript.filterize(filterExpr)
while not itemscript.select(filter) do
print("Couldn't find at least 1 item matching the filter expression: \"" .. filterExpr .. "\". Please add it.")
os.pullEvent("turtle_inventory")
end
end
-- Selects the first empty slot, if there is one. Returns true if an empty slot could be selected.
function itemscript.selectEmpty()
for i = 1, 16 do
local item = t.getItemDetail(i)
if filter(item) then
t.select(i)
local item = turtle.getItemDetail(i)
if item == nil then
turtle.select(i)
return true
end
end
return false
end
-- Selects the first empty slot, or prompts the user to remove items so that an empty slot can be selected.
function itemscript.selectEmptyOrWait()
while not itemscript.selectEmpty() do
print("Couldn't find an empty slot. Please remove some items.")
os.pullEvent("turtle_inventory")
end
end
-- Helper function to drop items in a flexible way, using a drop function and filtering function.
local function dropFiltered(dropFunction, filter)
local function dropFiltered(dropFunction, filterExpr)
local filter = itemscript.filterize(filterExpr)
for i = 1, 16 do
local item = t.getItemDetail(i)
local item = turtle.getItemDetail(i)
if filter(item) then
t.select(i)
turtle.select(i)
dropFunction()
end
end
end
function itemscript.dropAll(filterExpr)
dropFiltered(t.drop, convertToFilter(filterExpr))
dropFiltered(turtle.drop, filterExpr)
end
function itemscript.dropAllDown(filterExpr)
dropFiltered(t.dropDown, convertToFilter(filterExpr))
dropFiltered(turtle.dropDown, filterExpr)
end
function itemscript.dropAllUp(filterExpr)
dropFiltered(t.dropUp, convertToFilter(filterExpr))
end
-- Cleans up the turtle's inventory by compacting all stacks of items.
function itemscript.organize()
error("Not yet implemented.")
dropFiltered(turtle.dropUp, filterExpr)
end
return itemscript

View File

@ -7,12 +7,15 @@ Movescript provides a simpler, conciser way to program "turtles" (robots), so
that you don't need to get tired of typing "turtle.forward()" over and over.
]]--
VERSION = "0.0.1"
local t = turtle
-- For testing purposes, if the turtle API is not present, we inject our own.
if not t then t = {
getFuelLimit = function() return 1000000000 end
} end
-- The movescript module. Functions defined within this table are exported.
local movescript = {}
movescript.VERSION = "0.0.1"
movescript.defaultSettings = {
debug = false,
@ -21,6 +24,11 @@ movescript.defaultSettings = {
fuels = {"minecraft:coal", "minecraft:charcoal"}
}
local INSTRUCTION_TYPES = {
repeated = 1,
instruction = 2
}
local function debug(msg, settings)
if settings and settings.debug then
print("[MS] " .. msg)
@ -62,36 +70,160 @@ local function goDirection(dirFunction, digFunction, detectFunction, settings)
end
end
local function goUp(settings)
local function goUp(options, settings)
debug("Moving up.", settings)
goDirection(t.up, t.digUp, t.detectUp, settings)
end
local function goDown(settings)
local function goDown(options, settings)
debug("Moving down.", settings)
goDirection(t.down, t.digDown, t.detectDown, settings)
end
local function goForward(settings)
local function goForward(options, settings)
debug("Moving forward.", settings)
goDirection(t.forward, t.dig, t.detect, settings)
end
local function goBack(settings)
local function goBack(options, settings)
debug("Moving back.", settings)
goDirection(t.back, t.digBack, t.detectBack, settings)
end
local function goRight(settings)
local function goRight(options, settings)
debug("Turning right.", settings)
t.turnRight()
end
local function goLeft(settings)
local function goLeft(options, settings)
debug("Turning left.", settings)
t.turnLeft()
end
local function placeFunction(func, digFunction, detectFunction, settings, options)
settings = settings or movescript.defaultSettings
safe = settings.safe or movescript.defaultSettings.safe
destructive = settings.destructive or movescript.defaultSettings.destructive
local success = func(options.text)
if not safe then return end
while not success do
debug("Unable to place.", settings)
if destructive and detectFunction() then
debug("Detected a block in the way; attempting to remove it.", settings)
digFunction()
end
success = func(options.text)
end
end
local function place(options, settings)
debug("Placing.", settings)
placeFunction(t.place, t.dig, t.detect, settings, options)
end
local function placeUp(options, settings)
debug("Placing up.", settings)
placeFunction(t.placeUp, t.digUp, t.detectUp, settings, options)
end
local function placeDown(options, settings)
debug("Placing down.", settings)
placeFunction(t.placeDown, t.digDown, t.detectDown, settings, options)
end
local function attack(options, settings)
debug("Attacking.", settings)
t.attack(options.side)
end
local function attackUp(options, settings)
debug("Attacking up.", settings)
t.attackUp(options.side)
end
local function attackDown(options, settings)
debug("Attacking down.", settings)
t.attackDown(options.side)
end
local function dig(options, settings)
debug("Digging.", settings)
t.dig(options.side)
end
local function digUp(options, settings)
debug("Digging up.", settings)
t.digUp(options.side)
end
local function digDown(options, settings)
debug("Digging down.", settings)
t.digDown(options.side)
end
local function suck(options, settings)
debug("Sucking.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.suck(count)
end
local function suckUp(options, settings)
debug("Sucking up.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.suckUp(count)
end
local function suckDown(options, settings)
debug("Sucking down.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.suckDown(count)
end
local function selectSlot(options, settings)
local slot = 1
if options.slot ~= nil then
slot = tonumber(options.slot)
end
debug("Selecting slot " .. slot .. ".", settings)
t.select(slot)
end
local function drop(options, settings)
debug("Dropping.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.drop(count)
end
local function dropUp(options, settings)
debug("Dropping up.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.dropUp(count)
end
local function dropDown(options, settings)
debug("Dropping down.", settings)
local count = nil
if options.count ~= nil then
count = tonumber(options.count)
end
t.dropDown(count)
end
local actionMap = {
["U"] = {f = goUp, needsFuel = true},
["D"] = {f = goDown, needsFuel = true},
@ -99,12 +231,24 @@ local actionMap = {
["R"] = {f = goRight, needsFuel = false},
["F"] = {f = goForward, needsFuel = true},
["B"] = {f = goBack, needsFuel = true},
["P"] = {f = t.place, needsFuel = false},
["Pu"] = {f = t.placeUp, needsFuel = false},
["Pd"] = {f = t.placeDown, needsFuel = false},
["A"] = {f = t.attack, needsFuel = false},
["Au"] = {f = t.attackUp, needsFuel = false},
["Ad"] = {f = t.attackDown, needsFuel = false}
["P"] = {f = place, needsFuel = false},
["Pu"] = {f = placeUp, needsFuel = false},
["Pd"] = {f = placeDown, needsFuel = false},
["A"] = {f = attack, needsFuel = false},
["Au"] = {f = attackUp, needsFuel = false},
["Ad"] = {f = attackDown, needsFuel = false},
["Dg"] = {f = dig, needsFuel = false},
["Dgu"] = {f = digUp, needsFuel = false},
["Dgd"] = {f = digDown, needsFuel = false},
["S"] = {f = suck, needsFuel = false},
["Su"] = {f = suckUp, needsFuel = false},
["Sd"] = {f = suckDown, needsFuel = false},
["Eqr"] = {f = t.equipRight, needsFuel = false},
["Eql"] = {f = t.equipLeft, needsFuel = false},
["Sel"] = {f = selectSlot, needsFuel = false},
["Dr"] = {f = drop, needsFuel = false},
["Dru"] = {f = dropUp, needsFuel = false},
["Drd"] = {f = dropDown, needsFuel = false}
}
-- Tries to refuel the turtle from all slots that contain a valid fuel.
@ -118,7 +262,7 @@ local function refuelAll(settings)
if item ~= nil then
for _, fuelName in pairs(fuels) do
if item.name == fuelName then
t.select(i)
t.select(slot)
if t.refuel(item.count) then refueled = true end
break
end
@ -131,7 +275,7 @@ end
-- Blocks until the turtle's fuel level is at least at the required level.
local function refuelToAtLeast(requiredLevel, settings)
refuelAll(settings)
while t.getFuelLevel < requiredLevel do
while t.getFuelLevel() < requiredLevel do
print(
"[MS] Fuel level is too low. Level: " .. t.getFuelLevel() .. ". Required: " .. requiredLevel ..
". Please add some of the following fuels:"
@ -143,68 +287,187 @@ local function refuelToAtLeast(requiredLevel, settings)
local fuelUpdated = false
while not fuelUpdated do
os.pullEvent("turtle_inventory")
fuelUpdated = refuelAll()
fuelUpdated = refuelAll(settings)
end
end
end
-- Executes a single instruction. An instruction is a table with an "action"
-- and some attributes, such as if it needs fuel or not.
local function executeInstruction(instruction, settings)
local action = actionMap[instruction.action]
if action then
debug("Executing action \"" .. instruction.action .. "\" " .. instruction.count .. " times.", settings)
local shouldRefuel = (
(settings.safe or true) and
(action.needsFuel) and
(instruction.count > t.getFuelLevel())
)
if shouldRefuel then
local fuelRequired = instruction.count
refuelToAtLeast(fuelRequired, settings)
function movescript.executeInstruction(instruction, settings, preExecuteHook, postExecuteHook)
if settings == nil then settings = movescript.defaultSettings end
if instruction.type == INSTRUCTION_TYPES.repeated then
debug("Executing repeated instruction " .. instruction.count .. " times.", settings)
for i = 1, instruction.count do
for _, nestedInstruction in pairs(instruction.instructions) do
movescript.executeInstruction(nestedInstruction, settings, preExecuteHook, postExecuteHook)
end
end
elseif instruction.type == INSTRUCTION_TYPES.instruction then
local action = actionMap[instruction.action]
if action then
debug("Executing action \"" .. instruction.action .. "\" " .. instruction.count .. " times.", settings)
local shouldRefuel = (
((settings ~= nil and settings.safe) or true) and
(action.needsFuel) and
(instruction.count > t.getFuelLevel())
)
if shouldRefuel then
local fuelRequired = instruction.count
refuelToAtLeast(fuelRequired, settings)
end
for i = 1, instruction.count do
if preExecuteHook ~= nil then preExecuteHook() end
action.f(instruction.options, settings)
if postExecuteHook ~= nil then postExecuteHook() end
end
end
for i = 1, instruction.count do action.f() end
end
end
local function parseInstructionOptions(text, settings)
local idx, endIdx = string.find(text, "%b()")
if idx == nil or endIdx - idx < 4 then return nil end
local optionPairsText = string.sub(text, idx, endIdx)
debug("Parsing instruction options: " .. optionPairsText, settings)
local options = {}
local nextIdx = 1
while nextIdx < string.len(optionPairsText) do
idx, endIdx = string.find(optionPairsText, "%w+=[%w_-%.]+", nextIdx)
if idx == nil then break end
local pairText = string.sub(optionPairsText, idx, endIdx)
local keyIdx, keyEndIdx = string.find(pairText, "%w+")
local key = string.sub(pairText, keyIdx, keyEndIdx)
local valueIdx, valueEndIdx = string.find(pairText, "[%w_-%.]+", keyEndIdx + 2)
local value = string.sub(pairText, valueIdx, valueEndIdx)
options[key] = value
debug(" Found option: key = " .. key .. ", value = " .. value, settings)
nextIdx = endIdx + 2
end
return options
end
local function parseRepeatedInstruction(match, settings)
debug("Parsing repeated instruction: " .. match, settings)
local instruction = {}
instruction.type = INSTRUCTION_TYPES.repeated
local countIdx, countEndIdx = string.find(match, "%d+")
instruction.count = tonumber(string.sub(match, countIdx, countEndIdx))
if instruction.count < 0 then
error("Repeated instruction cannot have a negative count.")
end
local innerScriptIdx, innerScriptEndIdx = string.find(match, "%b()", countEndIdx + 1)
local innerScript = string.sub(match, innerScriptIdx + 1, innerScriptEndIdx - 1)
instruction.instructions = movescript.parse(innerScript, settings)
return instruction
end
local function parseInstruction(match, settings)
debug("Parsing instruction: " .. match, settings)
local instruction = {}
instruction.type = INSTRUCTION_TYPES.instruction
local countIdx, countEndIdx = string.find(match, "%d+")
instruction.count = 1
if countIdx ~= nil then
instruction.count = tonumber(string.sub(match, countIdx, countEndIdx))
end
if instruction.count < 1 or instruction.count > t.getFuelLimit() then
error("Instruction at index " .. actionIdx .. " has an invalid count of " .. instruction.count .. ". It should be >= 1 and <= " .. t.getFuelLimit())
end
local actionIdx, actionEndIdx = string.find(match, "%u%l*")
instruction.action = string.sub(match, actionIdx, actionEndIdx)
if actionMap[instruction.action] == nil then
error("Instruction at index " .. actionIdx .. ", \"" .. instruction.action .. "\", does not refer to a valid action.")
end
return instruction
end
-- Parses a movescript script into a series of instruction tables.
local function parseScript(script, settings)
--[[
Movescript Grammar:
block: instruction | repeatedInstructions
repeatedInstructions: count '(' {instruction | repeatedInstructions} ')'
regex: %d+%s*%b()
instruction: [count] action [actionOptions] <- Not yet implemented.
regex: %d*%u%l*
count: %d+
action: %u%l*
actionOptions: '(' {optionPair ','} ')'
regex: %b()
optionPair: optionKey '=' optionValue
optionKey: %w+
optionValue: [%w_-]+
]]--
function movescript.parse(script, settings)
local instructions = {}
for instruction in string.gfind(script, "%W*(%d*%u%l*)%W*") do
local countIdx, countIdxEnd = string.find(instruction, "%d+")
local actionIdx, actionIdxEnd = string.find(instruction, "%u%l*")
local count = 1
if countIdx ~= nil then
count = tonumber(string.sub(instruction, countIdx, countIdxEnd))
local scriptIdx = 1
while scriptIdx <= string.len(script) do
local instruction = {}
local repeatedMatchStartIdx, repeatedMatchEndIdx = string.find(script, "%d+%s*%b()", scriptIdx)
local instructionMatchStartIdx, instructionMatchEndIdx = string.find(script, "%d*%u%l*", scriptIdx)
-- Parse the first occurring matched pattern.
if repeatedMatchStartIdx ~= nil and (instructionMatchStartIdx == nil or repeatedMatchStartIdx < instructionMatchStartIdx) then
-- Parse repeated instructions.
local match = string.sub(script, repeatedMatchStartIdx, repeatedMatchEndIdx)
table.insert(instructions, parseRepeatedInstruction(match, settings))
scriptIdx = repeatedMatchEndIdx + 1
elseif instructionMatchStartIdx ~= nil and (repeatedMatchStartIdx == nil or instructionMatchStartIdx < repeatedMatchStartIdx) then
-- Parse single instruction.
local match = string.sub(script, instructionMatchStartIdx, instructionMatchEndIdx)
local instruction = parseInstruction(match, settings)
local optionsIdx, optionsEndIdx = string.find(script, "%s*%b()", instructionMatchEndIdx + 1)
if optionsIdx ~= nil then
-- Check that there's nothing but empty space between the instruction and the options text.
if not string.find(string.sub(script, instructionMatchEndIdx + 1, optionsIdx - 1), "%S+") then
local optionsText = string.sub(script, optionsIdx, optionsEndIdx)
instruction.options = parseInstructionOptions(optionsText, settings)
end
end
if instruction.options == nil then instruction.options = {} end
table.insert(instructions, instruction)
scriptIdx = instructionMatchEndIdx + 1
else
error("Invalid script characters found at index " .. scriptIdx)
end
local action = string.sub(instruction, actionIdx, actionIdxEnd)
if count < 1 or count > t.getFuelLimit() then
error("Instruction at index " .. actionIdx .. " has an invalid count of " .. count .. ". It should be >= 1 and <= " .. t.getFuelLimit())
end
if actionMap[action] == nil then
error("Instruction at index " .. actionIdx .. ", \"" .. action .. "\", does not refer to a valid action.")
end
table.insert(instructions, {action = action, count = count})
debug("Parsed instruction: " .. instruction, settings)
end
return instructions
end
function movescript.run(script, settings)
function movescript.run(script, settings, preExecuteHook, postExecuteHook)
settings = settings or movescript.defaultSettings
script = script or ""
debug("Executing script: " .. script, settings)
local instructions = parseScript(script, settings)
local instructions = movescript.parse(script, settings)
for idx, instruction in pairs(instructions) do
executeInstruction(instruction, settings)
movescript.executeInstruction(instruction, settings, preExecuteHook, postExecuteHook)
end
end
function movescript.runFile(filename, settings)
function movescript.runFile(filename, settings, preExecuteHook, postExecuteHook)
local f = fs.open(filename, "r")
local script = f.readAll()
f.close()
movescript.run(script, settings)
movescript.run(script, settings, preExecuteHook, postExecuteHook)
end
function movescript.validate(script, settings)
return pcall(function () movescript.parse(script, settings) end)
end
-- "Mirrors" a movescript; that is, swaps any "turn right" instructions for "turn left", and vice versa.
-- Note that it does not mirror "equip right" and "equip left" instructions.
function movescript.mirror(script)
local template = string.gsub(script, "L", "__LEFT__")
template = string.gsub(template, "R", "__RIGHT__")
local result = string.gsub(template, "__LEFT__", "R")
return string.gsub(result, "__RIGHT__", "L")
end
return movescript

27
src/ms-installer.lua Normal file
View File

@ -0,0 +1,27 @@
--[[
An installation script that manages installing all movescript libraries easily.
]]--
local libs = {
"movescript.lua",
"itemscript.lua",
"buildscript.lua"
}
local BASE_URL = "https://andrewlalis.github.io/movescript/scripts/"
print("Running Movescript installer")
print("----------------------------")
for _, lib in pairs(libs) do
if fs.exists(lib) then
fs.delete(lib)
print("Deleted " .. lib)
end
local success = shell.run("wget", BASE_URL .. lib)
if not success then
error("Failed to install " .. lib)
end
print("Downloaded " .. lib)
end
print("----------------------------")
print("Done!")

46
tester.lua Normal file
View File

@ -0,0 +1,46 @@
-- http://lua-users.org/wiki/TableSerialization
function print_r (t, name, indent)
local tableList = {}
function table_r (t, name, indent, full)
local serial=string.len(full) == 0 and name
or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']'
io.write(indent,serial,' = ')
if type(t) == "table" then
if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n')
else
tableList[t]=full..serial
if next(t) then -- Table not empty
io.write('{\n')
for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end
io.write(indent,'};\n')
else io.write('{};\n') end
end
else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"'
or tostring(t),';\n') end
end
table_r(t,name or '__unnamed__',indent or '','')
end
-- local ms = require("src/movescript")
-- print_r(ms.parse("35(2F(safe=false)R 3(L(delay=0.25, file=file.txt)UB))", {debug=true}))
-- local bs = require("src/buildscript")
-- local args = {...}
-- local spec = {
-- num = { type = "number", required = true, idx = 1 },
-- name = { name = "name", type = "bool", required = true }
-- }
-- local success, result = bs.parseArgs(args, spec)
-- print(success)
-- print_r(result)
local is = require("src/itemscript")
local t = is.parseFilterExpression("!log")
print_r(t, "filter_expression_syntax_tree", " ")
local filter = is.compileFilter(t)
local item = {
name = "minecraft:oak_log",
count = 54
}
local matches = filter(item)
print(matches)