123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- 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)
|