Epic/mods/simplecrafting_lib/postprocessing.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)