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. -- The itemscript module. Functions defined within this table are exported.
local itemscript = {} local itemscript = {}
itemscript.VERSION = "0.0.1"
-- Determines if an item stack matches the given name. -- Determines if an item stack matches the given name.
-- If fuzzy, then the item name will be matched against the given name. -- If fuzzy, then the item name will be matched against the given name.
local function stackMatches(itemStack, name, fuzzy) local function stackMatches(itemStack, name, fuzzy)
return itemStack ~= nil and if itemStack == nil or itemStack.name == nil then return false end
( if fuzzy then return string.find(itemStack.name, name) ~= nil end
(not fuzzy and itemStack.name == name) or return itemStack.name == name
string.find(itemStack.name, name)
)
end end
local function notFilter(filter) local function splitString(str, sep)
return function(item) if sep == nil then sep = "%s" end
return not filter(item) local result = {}
for s in string.gmatch(str, "([^"..sep.."]+)") do
table.insert(result, s)
end end
return result
end end
local function andFilter(filters) -- Parses a filter expression string and returns a table representing the syntax tree.
return function(item) -- An error is thrown if compilation fails.
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.
--[[ --[[
Item Filter Expressions: Item Filter Expressions:
A filter expression is a way to define a complex method of matching item A filter expression is a way to define a complex method of matching item
stacks. stacks.
Prepending ! will match any item stack whose name does not match. Grammar:
Prepending # will do a fuzzy match using string.find.
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: Examples:
"coal" matches only "minecraft:coal" items "#log > 10" matches any items containing the word "log", that have more than 10 items in the stack.
"!#wood" matches all items except any that contain the phrase "wood" "10% coal, 90% iron_ore" matches coal 10% of the time, and iron_ore 90% of the time.
"#iron" matches all items that contain the phrase "iron"
]]-- ]]--
local function parseItemFilterExpression(expr) function itemscript.parseFilterExpression(str)
local prefixIdx, prefixIdxEnd = string.find(expr, "^[!#]+") str = str:gsub("^%s*(.-)%s*$", "%1") -- Trim whitespace from the beginning and end of the string.
local fuzzy = false print("Parsing expr: " .. str)
local negated = false
if prefixIdx ~= nil then -- Parse group constructs
for i = prefixIdx, prefixIdxEnd do local ignoreRange = nil
local char = string.sub(expr, i, i) if string.sub(str, 1, 1) == "(" then
if char == "!" then local idx1, idx2 = string.find(str, "%b()")
negated = true if idx1 == nil then
elseif char == "#" then error("Invalid group construct: \"" .. str .. "\".")
fuzzy = true 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 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
expr = string.sub(expr, prefixIdxEnd + 1, string.len(expr))
end end
local namespaceSeparatorIdx = string.find(expr, ":")
if namespaceSeparatorIdx == nil and not fuzzy then -- Parse NOT operator.
expr = "minecraft:" .. expr if string.sub(str, 1, 1) == "!" then
print("Found NOT")
return {
type = "NOT",
expr = itemscript.parseFilterExpression(string.sub(str, 2, -1))
}
end end
return function(item)
if item == nil then return false end -- Parse fuzzy and plain words.
local matches = stackMatches(item, expr, fuzzy) local fuzzy = false
if negated then if string.sub(str, 1, 1) == "#" then
matches = not matches 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 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
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. -- Gets the total number of items in the turtle's inventory that match the given expression.
function itemscript.totalCount(filterExpr) function itemscript.totalCount(filterExpr)
local filter = convertToFilter(filterExpr) local filter = itemscript.filterize(filterExpr)
local count = 0 local count = 0
for i = 1, 16 do for i = 1, 16 do
local item = t.getItemDetail(i) local item = t.getItemDetail(i)
@ -129,56 +267,81 @@ function itemscript.totalCount(filterExpr)
return count return count
end 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. -- Returns a boolean indicating whether we could find and select the item.
function itemscript.select(filterExpr) function itemscript.select(filterExpr)
local filter = convertToFilter(filterExpr) local filter = itemscript.filterize(filterExpr)
for i = 1, 16 do for i = 1, 16 do
local item = t.getItemDetail(i) local item = turtle.getItemDetail(i)
if filter(item) then if filter(item) then
t.select(i) turtle.select(i)
return true return true
end end
end end
return false return false
end 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 -- Selects a slot containing at least minCount (or 1) of an item type matching
-- the given filter expression. -- the given filter expression.
function itemscript.selectOrWait(filterExpr, minCount) function itemscript.selectOrWait(filterExpr, minCount)
minCount = minCount or 1 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.") print("Couldn't find at least " .. minCount .. " item(s) matching the filter expression: \"" .. filterExpr .. "\". Please add it.")
os.pullEvent("turtle_inventory") os.pullEvent("turtle_inventory")
end end
end end
-- Helper function to drop items in a flexible way, using a drop function and filtering function. -- Selects an empty slot.
local function dropFiltered(dropFunction, filter) function itemscript.selectEmpty()
for i = 1, 16 do 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 if filter(item) then
t.select(i) turtle.select(i)
dropFunction() dropFunction()
end end
end end
end end
function itemscript.dropAll(filterExpr) function itemscript.dropAll(filterExpr)
dropFiltered(t.drop, convertToFilter(filterExpr)) dropFiltered(turtle.drop, filterExpr)
end end
function itemscript.dropAllDown(filterExpr) function itemscript.dropAllDown(filterExpr)
dropFiltered(t.dropDown, convertToFilter(filterExpr)) dropFiltered(turtle.dropDown, filterExpr)
end end
function itemscript.dropAllUp(filterExpr) function itemscript.dropAllUp(filterExpr)
dropFiltered(t.dropUp, convertToFilter(filterExpr)) dropFiltered(turtle.dropUp, filterExpr)
end
-- Cleans up the turtle's inventory by compacting all stacks of items.
function itemscript.organize()
error("Not yet implemented.")
end end
return itemscript 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. that you don't need to get tired of typing "turtle.forward()" over and over.
]]-- ]]--
VERSION = "0.0.1"
local t = turtle local t = turtle
-- For testing purposes, if the turtle API is not present, we inject our own. -- For testing purposes, if the turtle API is not present, we inject our own.
if not t then t = { if not t then t = {
@ -17,6 +15,7 @@ if not t then t = {
-- The movescript module. Functions defined within this table are exported. -- The movescript module. Functions defined within this table are exported.
local movescript = {} local movescript = {}
movescript.VERSION = "0.0.1"
movescript.defaultSettings = { movescript.defaultSettings = {
debug = false, debug = false,

View File

@ -24,12 +24,23 @@ function print_r (t, name, indent)
-- local ms = require("src/movescript") -- 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})) -- 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 bs = require("src/buildscript")
local args = {...} -- local args = {...}
local spec = { -- local spec = {
num = { type = "number", required = true, idx = 1 }, -- num = { type = "number", required = true, idx = 1 },
name = { name = "name", type = "bool", required = true } -- 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) local matches = filter(item)
print(success) print(matches)
print_r(result)