Added proper itemscript

This commit is contained in:
Andrew Lalis 2023-04-27 11:02:12 +02:00
parent cfe8cc8a20
commit f76c72019a
3 changed files with 281 additions and 108 deletions

View File

@ -5,120 +5,258 @@ 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:
"coal" matches only "minecraft:coal" items
"!#wood" matches all items except any that contain the phrase "wood"
"#iron" matches all items that contain the phrase "iron"
"#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, "^[!#]+")
function itemscript.parseFilterExpression(str)
str = str:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace from the beginning and end of the string.
print("Parsing expr: " .. str)
-- 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
print("Found GROUP")
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
print("Found " .. operator.name)
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
print("Found " .. typeName)
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
end
-- Parse NOT operator.
if string.sub(str, 1, 1) == "!" then
print("Found NOT")
return {
type = "NOT",
expr = itemscript.parseFilterExpression(string.sub(str, 2, -1))
}
end
-- Parse fuzzy and plain words.
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
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
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
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)
if item == nil then return false end
local matches = stackMatches(item, expr, fuzzy)
if negated then
matches = not matches
return stackMatches(item, expr.value, expr.fuzzy)
end
return matches
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))
end
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 filter = itemscript.filterize(filterExpr)
local count = 0
for i = 1, 16 do
local item = t.getItemDetail(i)
@ -129,56 +267,81 @@ function itemscript.totalCount(filterExpr)
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 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)
return true
end
end
return false
end
-- Selects a random slot containing a matching item stack.
function itemscript.selectRandom(filterExpr)
local filter = itemscript.filterize(filterExpr)
local eligibleSlots = {}
for i = 1, 16 do
local item = turtle.getItemDetail(i)
if filter(item) then
table.insert(eligibleSlots, i)
end
end
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 minCount (or 1) of an item type matching
-- the given filter expression.
function itemscript.selectOrWait(filterExpr, minCount)
minCount = minCount or 1
while itemscript.totalCount(filterExpr) < minCount do
local filter = itemscript.filterize(filterExpr)
while itemscript.totalCount(filter) < minCount do
print("Couldn't find at least " .. minCount .. " item(s) matching the filter expression: \"" .. filterExpr .. "\". Please add it.")
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)
-- Selects an empty slot.
function itemscript.selectEmpty()
for i = 1, 16 do
local item = t.getItemDetail(i)
local item = turtle.getItemDetail(i)
if item == nil then
turtle.select(i)
return true
end
end
return false
end
-- Helper function to drop items in a flexible way, using a drop function and filtering function.
local function dropFiltered(dropFunction, filterExpr)
local filter = itemscript.filterize(filterExpr)
for i = 1, 16 do
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,8 +7,6 @@ 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 = {
@ -17,6 +15,7 @@ if not t then t = {
-- The movescript module. Functions defined within this table are exported.
local movescript = {}
movescript.VERSION = "0.0.1"
movescript.defaultSettings = {
debug = false,

View File

@ -24,12 +24,23 @@ function print_r (t, name, indent)
-- 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 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 success, result = bs.parseArgs(args, spec)
print(success)
print_r(result)
local matches = filter(item)
print(matches)