From f76c72019a89098b705ec033ef39cf1e049011bc Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 27 Apr 2023 11:02:12 +0200 Subject: [PATCH] Added proper itemscript --- src/itemscript.lua | 359 ++++++++++++++++++++++++++++++++------------- src/movescript.lua | 3 +- tester.lua | 27 +++- 3 files changed, 281 insertions(+), 108 deletions(-) diff --git a/src/itemscript.lua b/src/itemscript.lua index 2d6ff9c..3989660 100644 --- a/src/itemscript.lua +++ b/src/itemscript.lua @@ -5,120 +5,258 @@ Author: Andrew Lalis ]]-- -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, "^[!#]+") - 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. + 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 - 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 + print("Found NOT") + 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)) - 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 \ No newline at end of file diff --git a/src/movescript.lua b/src/movescript.lua index 50768fe..42de964 100644 --- a/src/movescript.lua +++ b/src/movescript.lua @@ -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, diff --git a/tester.lua b/tester.lua index 6ac2e6c..a684470 100644 --- a/tester.lua +++ b/tester.lua @@ -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)