123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- --------------------------------------------------------------------------------------------------------------------
- -- 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
|