Epic/mods/simplecrafting_lib/util.lua

399 lines
14 KiB
Lua

--------------------------------------------------------------------------------------------------------------------
-- Local functions
-- Turns an item list (as returned by inv:get_list) into a form more easily used by crafting functions
local function itemlist_to_countlist(itemlist)
local count_list = {}
for _, stack in ipairs(itemlist) do
if not stack:is_empty() then
local name = stack:get_name()
count_list[name] = (count_list[name] or 0) + stack:get_count()
-- alias its groups to the item
if minetest.registered_items[name] then
for group, _ in pairs(minetest.registered_items[name].groups or {}) do
if not count_list[group] then count_list[group] = {} end
count_list[group][name] = true -- using names as keys makes this act as a set
end
end
end
end
return count_list
end
-- splits a string into an array of substrings based on a delimiter
local function split(str, delimiter)
local result = {}
for match in (str..delimiter):gmatch("(.-)"..delimiter) do
table.insert(result, match)
end
return result
end
-- I apologise for this function.
-- From the items in groupset, checks input_list to find the item
-- with the highest count and adds it to required_input
local function get_highest_count_item_for_group(groupset, input_list, required_input, count)
local highest_item_name
local highest_item_count = 0
for group_item, _ in pairs(groupset) do
if input_list[group_item] > highest_item_count then
highest_item_count = input_list[group_item]
highest_item_name = group_item
end
end
if highest_item_count == 0 then
return false
end
required_input[highest_item_name] = (required_input[highest_item_name] or 0) + count
return true
end
-- returns the number of times the recipe can be crafted from the given input_list,
-- and also a copy of the recipe with groups substituted for the most common item
-- in the input_list that matches them
local function get_craft_count(input_list, recipe)
-- Recipe without groups (most common node in group instead)
local work_recipe = simplecrafting_lib.deep_copy(recipe)
if recipe.output then
work_recipe.output = ItemStack(recipe.output)
end
work_recipe.input = {}
local required_input = work_recipe.input
for item, count in pairs(recipe.input) do
if string.find(item, ",") then -- special syntax used to require an item that belongs to multiple groups
local groups = split(item, ",")
-- This unfortunate block of code builds up an intersection
-- of the items belonging to each group in the list of groups
-- that this recipe item slot requires.
local multigroup_itemset
for _, group in pairs(groups) do
if not input_list[group] then
return 0
end
if not multigroup_itemset then
multigroup_itemset = {}
for multigroup_item, _ in pairs(input_list[group]) do
multigroup_itemset[multigroup_item] = true
end
else
local intersect = {}
for multigroup_item, _ in pairs(input_list[group]) do
if multigroup_itemset[multigroup_item] then
intersect[multigroup_item] = true
end
end
multigroup_itemset = intersect
end
end
if not get_highest_count_item_for_group(multigroup_itemset, input_list, required_input, count) then
return 0
end
else
if not input_list[item] then
return 0
end
-- Groups are a string alias to most common member item
if type(input_list[item]) == "table" then
-- find group item with highest count
if not get_highest_count_item_for_group(input_list[item], input_list, required_input, count) then
return 0
end
else
required_input[item] = (required_input[item] or 0) + count
end
end
end
local number = math.huge
for ingredient, count in pairs(required_input) do
local max = input_list[ingredient] / count
if max < 1 then
return 0
elseif max < number then
number = max
end
end
-- Return number of possible crafts as integer
return math.floor(number), work_recipe
end
-- Used for alphabetizing an array of itemstacks by description
local function compare_stacks_by_desc(stack1, stack2)
local item1 = stack1:get_name()
local item2 = stack2:get_name()
local def1 = minetest.registered_items[item1]
local def2 = minetest.registered_items[item2]
return def1.description < def2.description
end
--------------------------------------------------------------------------------------------------------------------
-- Public API
-- Note that a circular table reference will result in a crash, TODO: guard against that.
-- Unlikely to be needed, though - it'd take a lot of work for users to get into this bit of trouble.
simplecrafting_lib.deep_copy = function(recipe_in)
local recipe_out = {}
for index, value in pairs(recipe_in) do
if type(value) == "table" then
recipe_out[index] = simplecrafting_lib.deep_copy(value)
elseif type(value) == "userdata" and index == "output" then
recipe_out[index] = ItemStack(value)
else
recipe_out[index] = value
end
end
return recipe_out
end
simplecrafting_lib.get_crafting_info = function(craft_type)
-- ensure the destination tables exist
simplecrafting_lib.type[craft_type] = simplecrafting_lib.type[craft_type] or {}
simplecrafting_lib.type[craft_type].recipes = simplecrafting_lib.type[craft_type].recipes or {}
simplecrafting_lib.type[craft_type].recipes_by_out = simplecrafting_lib.type[craft_type].recipes_by_out or {}
simplecrafting_lib.type[craft_type].recipes_by_in = simplecrafting_lib.type[craft_type].recipes_by_in or {}
return simplecrafting_lib.type[craft_type]
end
simplecrafting_lib.set_description = function(craft_type, desc)
simplecrafting_lib.get_crafting_info(craft_type).description = desc
end
simplecrafting_lib.get_description = function(craft_type)
return simplecrafting_lib.get_crafting_info(craft_type).description
end
simplecrafting_lib.set_disintermediation_cycles = function(craft_type, cycle_count)
simplecrafting_lib.get_crafting_info(craft_type).disintermediation_cycles = tonumber(cycle_count)
end
-- returns a fuel definition for the item if it is fuel, nil otherwise
-- note: will always return the last-registered definition for a particular item
-- or group.
simplecrafting_lib.is_fuel = function(craft_type, item)
local fuels = simplecrafting_lib.get_crafting_info(craft_type).recipes_by_in
-- First check if the item has been explicitly registered as fuel
if fuels[item] then
return fuels[item][#fuels[item]]
end
-- Failing that, check its groups.
local def = minetest.registered_items[item]
if def and def.groups then
local max = -1
local fuel_group
for group, _ in pairs(def.groups) do
if fuels[group] then
local last_fuel_def = fuels[group][#fuels[group]]
local last_fuel_burntime
if last_fuel_def.output and last_fuel_def.output:get_name() == "simplecrafting_lib:heat" then
last_fuel_burntime = last_fuel_def.output:get_count()
else
last_fuel_burntime = 0
end
if last_fuel_burntime > max then
fuel_group = last_fuel_def -- track whichever is the longest-burning group
max = last_fuel_burntime
end
end
end
if fuel_group then
return fuel_group
end
end
return nil
end
-- Returns a list of all fuel recipes whose ingredients can be satisfied by the item_list
simplecrafting_lib.get_fuels = function(craft_type, item_list)
local count_list = itemlist_to_countlist(item_list)
local burnable = {}
for item, count in pairs(count_list) do
local recipe = simplecrafting_lib.is_fuel(craft_type, item)
if recipe then
table.insert(burnable, recipe)
end
end
return burnable
end
-- Returns a list of all recipes whose ingredients can be satisfied by the item_list
simplecrafting_lib.get_craftable_recipes = function(craft_type, item_list)
local count_list = itemlist_to_countlist(item_list)
local craftable = {}
local recipes = simplecrafting_lib.type[craft_type].recipes
for i = 1, #recipes do
local number, recipe = get_craft_count(count_list, recipes[i])
if number > 0 then
table.insert(craftable, recipe)
end
end
return craftable
end
-- Returns a list of all the possible item stacks that could be crafted from the provided item list
-- if max_craftable is true the returned stacks will have as many items in them as possible to craft,
-- if max_craftable is false or nil the returned stacks will have only the minimum output
-- if alphabetize is true then the items will be sorted alphabetically by description
-- if alphabetize is false or nil the items will be left in default order
simplecrafting_lib.get_craftable_items = function(craft_type, item_list, max_craftable, alphabetize)
local count_list = itemlist_to_countlist(item_list)
local craftable_count_list = {}
local craftable_stacks = {}
local chosen_recipe = {}
local recipes = simplecrafting_lib.type[craft_type].recipes
for i = 1, #recipes do
local number, recipe = get_craft_count(count_list, recipes[i])
if number > 0 then
if not max_craftable then number = 1 end
local output_name = recipe.output:get_name()
local output_count = recipe.output:get_count()
if craftable_count_list[output_name] and output_count*number > craftable_count_list[output_name] then
craftable_count_list[output_name] = output_count*number
chosen_recipe[output_name] = recipe
elseif not craftable_count_list[output_name] and output_count*number > 0 then
craftable_count_list[output_name] = output_count*number
chosen_recipe[output_name] = recipe
end
end
end
-- Limit stacks to stack limit
for item, count in pairs(craftable_count_list) do
local stack = ItemStack(item)
local max = stack:get_stack_max()
if count > max then
count = max - max % chosen_recipe[item].output:get_count()
end
stack:set_count(count)
simplecrafting_lib.execute_pre_craft(craft_type, chosen_recipe[item], stack, item_list)
table.insert(craftable_stacks, stack)
end
if alphabetize then
table.sort(craftable_stacks, compare_stacks_by_desc)
end
return craftable_stacks
end
-- Returns true if the item name is an input for at least one
-- recipe belonging to the given craft type
simplecrafting_lib.is_possible_input = function(craft_type, item_name)
local info = simplecrafting_lib.get_crafting_info(craft_type)
if info.recipes_by_in[item_name] then
return true -- that was easy.
end
-- Now for the group checks. :(
local item_def = minetest.registered_items[item_name]
if not item_def then return false end -- undefined item
local groups = item_def.groups or {}
for _, recipe in pairs(info.recipes) do
--the multi-group flower/dye type inputs are complicated to check for
for input_item, _ in pairs(recipe.input) do
if not string.match(input_item, ":") then
local input_grouplist = split(input_item, ",") -- most of the time we'll have a list of one.
local input_matches = true
for _, needed_group in pairs(input_grouplist) do -- for each group in that list of input groups
if not groups[needed_group] then -- if the test item doesn't have that group
input_matches = false -- it's not a match.
break
end
end
if input_matches then return true end
end
end
end
return false
end
-- Returns true if the item is a possible output for at least
-- one recipe belonging to the given craft type
simplecrafting_lib.is_possible_output = function(craft_type, item_name)
return simplecrafting_lib.type[craft_type].recipes_by_out[item_name] ~= nil
end
-- adds two count lists together, returns a new count list with the sum of the parameters' contents
simplecrafting_lib.count_list_add = function(list1, list2)
local out_list = {}
for item, count in pairs(list1) do
out_list[item] = count
end
if list2 == nil then return out_list end
for item, count in pairs(list2) do
if type(count) == "table" then
-- item is actually a group name, it has a set of items associated with it.
-- Perform a union with existing set.
out_list[item] = out_list[item] or {}
for group_item, _ in pairs(count) do
out_list[item][group_item] = true
end
else
out_list[item] = (out_list[item] or 0) + count
end
end
return out_list
end
-- Returns a recipe with the inputs and outputs multiplied to match the requested
-- quantity of ouput items in the crafted stack. Note that the output could
-- actually be larger than crafted_stack if an exactly matching recipe can't be found.
-- returns nil if crafting is impossible with the given source inventory
simplecrafting_lib.get_crafting_result = function(craft_type, input_list, request_stack)
local input_count = itemlist_to_countlist(input_list)
local request_name = request_stack:get_name()
local request_count = request_stack:get_count()
local recipes = simplecrafting_lib.type[craft_type].recipes_by_out[request_name]
local smallest_remainder = math.huge
local smallest_remainder_output_count = 0
local smallest_remainder_recipe = nil
for i = 1, #recipes do
local number, recipe = get_craft_count(input_count, recipes[i])
if number > 0 then
local output_count = recipe.output:get_count()
if (request_count % output_count) <= smallest_remainder and output_count > smallest_remainder_output_count then
smallest_remainder = request_count % output_count
smallest_remainder_output_count = output_count
smallest_remainder_recipe = recipe
end
end
end
if smallest_remainder_recipe then
local multiple = math.ceil(request_count / smallest_remainder_recipe.output:get_count())
for input_item, quantity in pairs(smallest_remainder_recipe.input) do
smallest_remainder_recipe.input[input_item] = multiple * quantity
end
smallest_remainder_recipe.output:set_count(smallest_remainder_recipe.output:get_count() * multiple)
if smallest_remainder_recipe.returns then
for returned_item, quantity in pairs(smallest_remainder_recipe.returns) do
smallest_remainder_recipe.returns[returned_item] = multiple * quantity
end
end
return smallest_remainder_recipe
end
return nil
end
local pre_craft = {}
local post_craft = {}
simplecrafting_lib.register_pre_craft = function(callback)
table.insert(pre_craft, callback)
end
simplecrafting_lib.register_post_craft = function(callback)
table.insert(post_craft, callback)
end
simplecrafting_lib.execute_pre_craft = function(craft_type, recipe, output_stack, source_item_list)
for k, callback in ipairs(pre_craft) do
callback(craft_type, recipe, output_stack, source_item_list)
end
end
simplecrafting_lib.execute_post_craft = function(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
for k, callback in ipairs(post_craft) do
callback(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
end
end