357 lines
12 KiB
Lua
357 lines
12 KiB
Lua
local registered_callbacks = {}
|
|
|
|
simplecrafting_lib.register_postprocessing_callback = function(callback)
|
|
table.insert(registered_callbacks, callback)
|
|
end
|
|
|
|
----------------------------------------------------------------------------------------
|
|
-- Run-once code, post server initialization, that purges all uncraftable recipes from the
|
|
-- crafting system data.
|
|
|
|
-- 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
|
|
|
|
local group_examples = {}
|
|
|
|
local function input_exists(input_item)
|
|
if minetest.registered_items[input_item] then
|
|
return true
|
|
end
|
|
|
|
if group_examples[input_item] then
|
|
return true
|
|
end
|
|
|
|
if not string.match(input_item, ",") then
|
|
return false
|
|
end
|
|
|
|
local target_groups = split(input_item, ",")
|
|
|
|
for item, def in pairs(minetest.registered_items) do
|
|
local overall_found = true
|
|
for _, target_group in pairs(target_groups) do
|
|
local one_group_found = false
|
|
for group, _ in pairs(def.groups) do
|
|
if group == target_group then
|
|
one_group_found = true
|
|
break
|
|
end
|
|
end
|
|
if not one_group_found then
|
|
overall_found = false
|
|
break
|
|
end
|
|
end
|
|
if overall_found then
|
|
group_examples[input_item] = item
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function validate_inputs_and_outputs(recipe)
|
|
for item, count in pairs(recipe.input) do
|
|
if not input_exists(item) then
|
|
return item
|
|
end
|
|
end
|
|
if recipe.output then
|
|
local output_name = ItemStack(recipe.output):get_name()
|
|
if not minetest.registered_items[output_name] then
|
|
return output_name
|
|
end
|
|
end
|
|
if recipe.returns then
|
|
for item, count in pairs(recipe.returns) do
|
|
if not minetest.registered_items[item] then
|
|
return item
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local log_removals = minetest.settings:get_bool("simplecrafting_lib_log_invalid_recipe_removal")
|
|
|
|
local recipe_to_string = function(recipe)
|
|
local out = "{\n"
|
|
for k, v in pairs(recipe) do
|
|
if k == "output" then
|
|
out = out .. "output = \"" .. ItemStack(v):to_string() .."\",\n"
|
|
else
|
|
out = out .. k .. " = " .. dump(v) .. ",\n"
|
|
end
|
|
end
|
|
out = out .. "}"
|
|
|
|
return out
|
|
end
|
|
|
|
local merge_item_count = function(list, alias, item_to_merge)
|
|
local existing_count = list[alias] or 0 -- in case a recipe mixes various items that all alias to the same item
|
|
list[alias] = item_to_merge + existing_count
|
|
end
|
|
|
|
local merge_sublists = function(list, alias, item_to_merge)
|
|
local existing_list = list[alias] or {}
|
|
for _, recipe in ipairs(item_to_merge) do
|
|
table.insert(existing_list, recipe)
|
|
end
|
|
list[alias] = existing_list
|
|
end
|
|
|
|
local resolve_aliases_in_list = function(aliases, list, action_to_perform)
|
|
if list == nil then return end
|
|
local items_to_remove = {}
|
|
for item, value in pairs(list) do
|
|
if item:find(":") then -- only apply this test to non-group items
|
|
local alias = aliases[item]
|
|
if alias then
|
|
table.insert(items_to_remove, item)
|
|
action_to_perform(list, alias, value)
|
|
end
|
|
end
|
|
end
|
|
for _, item in ipairs(items_to_remove) do
|
|
list[item] = nil
|
|
end
|
|
end
|
|
|
|
local resolve_aliases = function()
|
|
local aliases = minetest.registered_aliases
|
|
for craft_type, recs in pairs(simplecrafting_lib.type) do
|
|
for _, recipe in ipairs(recs.recipes) do
|
|
resolve_aliases_in_list(aliases, recipe.input, merge_item_count)
|
|
resolve_aliases_in_list(aliases, recipe.returns, merge_item_count)
|
|
|
|
local output = ItemStack(recipe.output)
|
|
local output_alias = aliases[output:get_name()]
|
|
if output_alias then
|
|
output:set_name(output_alias)
|
|
recipe.output = output
|
|
end
|
|
end
|
|
resolve_aliases_in_list(aliases, recs.recipes_by_in, merge_sublists)
|
|
resolve_aliases_in_list(aliases, recs.recipes_by_out, merge_sublists)
|
|
end
|
|
end
|
|
|
|
local purge_uncraftable_recipes = function()
|
|
for item, def in pairs(minetest.registered_items) do
|
|
for group, _ in pairs(def.groups) do
|
|
group_examples[group] = item
|
|
end
|
|
end
|
|
|
|
for craft_type, _ in pairs(simplecrafting_lib.type) do
|
|
local i = 1
|
|
local recs = simplecrafting_lib.type[craft_type].recipes
|
|
while i <= #simplecrafting_lib.type[craft_type].recipes do
|
|
local validation_result = validate_inputs_and_outputs(recs[i])
|
|
if validation_result == true then
|
|
i = i + 1
|
|
else
|
|
if log_removals then
|
|
if string.match(validation_result, ":") then
|
|
minetest.log("error", "[simplecrafting_lib] Uncraftable recipe purged due to the nonexistent item " .. validation_result .. "\n"..recipe_to_string(recs[i]) .. "\nThis could be due to an error in the mod that defined this recipe, rather than an error in simplecrafting_lib itself.")
|
|
else
|
|
minetest.log("error", "[simplecrafting_lib] Uncraftable recipe purged due to no registered items matching the group requirement " .. validation_result .. "\n"..recipe_to_string(recs[i]) .. "\nThis could be due to an error in the mod that defined this recipe, rather than an error in simplecrafting_lib itself.")
|
|
end
|
|
end
|
|
table.remove(recs, i)
|
|
end
|
|
end
|
|
for output, outs in pairs(simplecrafting_lib.type[craft_type].recipes_by_out) do
|
|
i = 1
|
|
while i <= #outs do
|
|
if validate_inputs_and_outputs(outs[i]) == true then
|
|
i = i + 1
|
|
else
|
|
table.remove(outs, i)
|
|
end
|
|
end
|
|
if #outs == 0 then
|
|
if log_removals then
|
|
minetest.log("error", "[simplecrafting_lib] All recipes that had an output of " .. output
|
|
.. " have been purged as uncraftable, this item can not be made by the player.")
|
|
end
|
|
simplecrafting_lib.type[craft_type].recipes_by_out[output] = nil
|
|
end
|
|
end
|
|
for input, ins in pairs(simplecrafting_lib.type[craft_type].recipes_by_in) do
|
|
i = 1
|
|
while i <= #ins do
|
|
if validate_inputs_and_outputs(ins[i]) == true then
|
|
i = i + 1
|
|
else
|
|
table.remove(ins, i)
|
|
end
|
|
end
|
|
if #ins == 0 then
|
|
simplecrafting_lib.type[craft_type].recipes_by_in[input] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
group_examples = nil -- don't need this any more.
|
|
end
|
|
|
|
-- Tests for recipies that don't actually do anything (A => A)
|
|
-- and for recipes that pointlessly consume input without giving new output
|
|
local operative_recipe = function(recipe)
|
|
local has_an_effect = false
|
|
for in_item, in_count in pairs(recipe.input) do
|
|
local out_count = recipe.output[in_item] or 0
|
|
local returns_count = 0
|
|
if recipe.returns then returns_count = recipe.returns[in_item] or 0 end
|
|
if out_count + returns_count ~= in_count then
|
|
has_an_effect = true
|
|
break
|
|
end
|
|
end
|
|
if not has_an_effect then
|
|
return false
|
|
end
|
|
|
|
local new_output
|
|
local out_item = recipe.output:get_name()
|
|
local out_count = recipe.output:get_count()
|
|
if not recipe.input[out_item] or recipe.input[out_item] < out_count then
|
|
-- produces something that's not in the input, or produces more of the input item than there was intially.
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- This method goes through all of the recipes in a craft type and finds ones that "feed into" each other, creating new recipes that skip those unnecessary intermediate steps.
|
|
-- So for example if there's a recipe that goes A + B => C, and a recipe that goes C + D => E, this method will detect that and create an additional recipe that goes A + B + D => E.
|
|
local disintermediate = function(craft_type, contents)
|
|
local disintermediating_recipes = {}
|
|
for _, recipe in pairs(contents.recipes) do
|
|
if not recipe.do_not_disintermediate then
|
|
for in_item, in_count in pairs(recipe.input) do
|
|
-- check if there's recipes in this crafting type that produces the input item
|
|
if contents.recipes_by_out[in_item] then
|
|
-- find a recipe whose output divides evenly into the input
|
|
for _, recipe_producing_in_item in pairs(contents.recipes_by_out[in_item]) do
|
|
if not recipe_producing_in_item.do_not_use_for_disintermediation and
|
|
(in_count % recipe_producing_in_item.output:get_count() == 0 or recipe_producing_in_item.output:get_count() % in_count == 0) then
|
|
|
|
local multiplier = in_count / recipe_producing_in_item.output:get_count()
|
|
|
|
local working_recipe = simplecrafting_lib.deep_copy(recipe)
|
|
working_recipe.input[in_item] = nil -- clear the input from the working recipe (soon to be our newly created disintermediated recipe)
|
|
|
|
if multiplier < 1 then
|
|
-- the recipe_producing_in_item produces more than the working recipe requires by a whole multiplier.
|
|
-- eg, 1A + 1B => 2C, 1C + 1D => 1E.
|
|
-- Multiply working recipe by the inverse of multiplier and then set multiplier to 1.
|
|
-- That will get us 1A + 1B + 2D => 2E
|
|
local inverse = 1/multiplier
|
|
multiplier = 1
|
|
|
|
for item, count in pairs(working_recipe.input) do
|
|
working_recipe.input[item] = count * inverse
|
|
end
|
|
working_recipe.output:set_count(working_recipe.output:get_count() * inverse)
|
|
if working_recipe.returns then
|
|
for item, count in pairs(working_recipe.returns) do
|
|
working_recipe.returns[item] = count * inverse
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- add the inputs and outputs of the disintermediating recipe
|
|
for new_in_item, new_in_count in pairs(recipe_producing_in_item.input) do
|
|
if not working_recipe.input[new_in_item] then
|
|
working_recipe.input[new_in_item] = new_in_count * multiplier
|
|
else
|
|
working_recipe.input[new_in_item] = working_recipe.input[new_in_item] + new_in_count * multiplier
|
|
end
|
|
end
|
|
|
|
local new_out_item = recipe_producing_in_item.output:get_name()
|
|
local new_out_count = recipe_producing_in_item.output:get_count()
|
|
if new_out_item ~= in_item then -- this output is what's replacing the input we deleted, so don't add it.
|
|
if not working_recipe.output:get_name() == new_out_item then
|
|
working_recipe.output = ItemStack(new_out_item .. " " .. tostring(new_out_count * multiplier))
|
|
else
|
|
working_recipe.output:set_count(working_recipe.output:get_count() + new_out_count * multiplier)
|
|
end
|
|
end
|
|
|
|
if recipe_producing_in_item.returns then
|
|
for new_returns_item, new_returns_count in pairs(recipe_producing_in_item.returns) do
|
|
working_recipe.returns = working_recipe.returns or {}
|
|
if not working_recipe.returns[new_returns_item] then
|
|
working_recipe.returns[new_returns_item] = new_returns_count * multiplier
|
|
else
|
|
working_recipe.returns[new_returns_item] = working_recipe.returns[new_returns_item] + new_returns_count * multiplier
|
|
end
|
|
end
|
|
end
|
|
|
|
if operative_recipe(working_recipe) then
|
|
table.insert(disintermediating_recipes, working_recipe)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local count = 0
|
|
for _, new_recipe in pairs(disintermediating_recipes) do
|
|
if simplecrafting_lib.register(craft_type, new_recipe) then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
local postprocess = function()
|
|
resolve_aliases()
|
|
purge_uncraftable_recipes()
|
|
|
|
for craft_type, contents in pairs(simplecrafting_lib.type) do
|
|
local cycles = contents.disintermediation_cycles or 0
|
|
local previous_count = 0
|
|
while cycles > 0 do
|
|
local new_count = disintermediate(craft_type, contents)
|
|
if new_count == 0 then
|
|
minetest.log("info", "[simplecrafting_lib] disintermediation loop for crafting type " .. craft_type
|
|
.. " exited early due to no need for additional disintermediation recipes.")
|
|
cycles = 0
|
|
break
|
|
elseif previous_count > 0 and new_count > previous_count then
|
|
minetest.log("error", "[simplecrafting_lib] potential disintermediation problem: crafting type \""
|
|
.. craft_type .. "\" added " .. previous_count .. " disintermediation recipes on the previous cycle "
|
|
.. "and " .. new_count .. " disintermediation recipes on the current cycle. This growing recipe "
|
|
.. "addition rate could indicate that there's a unbalanced \"loop\" in the recipes defined for "
|
|
.. "this craft type, resulting in an ever-increasing number of ways of producing a particular "
|
|
.. "output. Examine the recipes for this craft type to see if you can identify the culprit, "
|
|
.. "or reduce the value of this craft type's disintermediation_cycles property to prevent "
|
|
.. "the recipe growth from getting too bad.")
|
|
end
|
|
previous_count = new_count
|
|
cycles = cycles - 1
|
|
end
|
|
end
|
|
|
|
for _, callback in ipairs(registered_callbacks) do
|
|
callback()
|
|
end
|
|
registered_callbacks = nil
|
|
end
|
|
|
|
minetest.after(0, postprocess) |