347 lines
12 KiB
Lua
347 lines
12 KiB
Lua
--[[
|
|
Itemscript - A simplified set of methods for item manipulation.
|
|
|
|
Author: Andrew Lalis <andrewlalisofficial@gmail.com>
|
|
|
|
|
|
]]--
|
|
|
|
-- 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)
|
|
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 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
|
|
|
|
-- 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.
|
|
|
|
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.
|
|
]]--
|
|
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
|
|
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 {
|
|
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
|
|
|
|
|
|
-- Gets the total number of items in the turtle's inventory that match the given expression.
|
|
function itemscript.totalCount(filterExpr)
|
|
local filter = itemscript.filterize(filterExpr)
|
|
local count = 0
|
|
for i = 1, 16 do
|
|
local item = t.getItemDetail(i)
|
|
if filter(item) then
|
|
count = count + item.count
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- 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 = itemscript.filterize(filterExpr)
|
|
for i = 1, 16 do
|
|
local item = turtle.getItemDetail(i)
|
|
if filter(item) then
|
|
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
|
|
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
|
|
|
|
-- Selects an empty slot.
|
|
function itemscript.selectEmpty()
|
|
for i = 1, 16 do
|
|
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
|
|
turtle.select(i)
|
|
dropFunction()
|
|
end
|
|
end
|
|
end
|
|
|
|
function itemscript.dropAll(filterExpr)
|
|
dropFiltered(turtle.drop, filterExpr)
|
|
end
|
|
|
|
function itemscript.dropAllDown(filterExpr)
|
|
dropFiltered(turtle.dropDown, filterExpr)
|
|
end
|
|
|
|
function itemscript.dropAllUp(filterExpr)
|
|
dropFiltered(turtle.dropUp, filterExpr)
|
|
end
|
|
|
|
return itemscript |