postprocessing.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. local registered_callbacks = {}
  2. simplecrafting_lib.register_postprocessing_callback = function(callback)
  3. table.insert(registered_callbacks, callback)
  4. end
  5. ----------------------------------------------------------------------------------------
  6. -- Run-once code, post server initialization, that purges all uncraftable recipes from the
  7. -- crafting system data.
  8. -- splits a string into an array of substrings based on a delimiter
  9. local function split(str, delimiter)
  10. local result = {}
  11. for match in (str..delimiter):gmatch("(.-)"..delimiter) do
  12. table.insert(result, match)
  13. end
  14. return result
  15. end
  16. local group_examples = {}
  17. local function input_exists(input_item)
  18. if minetest.registered_items[input_item] then
  19. return true
  20. end
  21. if group_examples[input_item] then
  22. return true
  23. end
  24. if not string.match(input_item, ",") then
  25. return false
  26. end
  27. local target_groups = split(input_item, ",")
  28. for item, def in pairs(minetest.registered_items) do
  29. local overall_found = true
  30. for _, target_group in pairs(target_groups) do
  31. local one_group_found = false
  32. for group, _ in pairs(def.groups) do
  33. if group == target_group then
  34. one_group_found = true
  35. break
  36. end
  37. end
  38. if not one_group_found then
  39. overall_found = false
  40. break
  41. end
  42. end
  43. if overall_found then
  44. group_examples[input_item] = item
  45. return true
  46. end
  47. end
  48. return false
  49. end
  50. local function validate_inputs_and_outputs(recipe)
  51. for item, count in pairs(recipe.input) do
  52. if not input_exists(item) then
  53. return item
  54. end
  55. end
  56. if recipe.output then
  57. local output_name = ItemStack(recipe.output):get_name()
  58. if not minetest.registered_items[output_name] then
  59. return output_name
  60. end
  61. end
  62. if recipe.returns then
  63. for item, count in pairs(recipe.returns) do
  64. if not minetest.registered_items[item] then
  65. return item
  66. end
  67. end
  68. end
  69. return true
  70. end
  71. local log_removals = minetest.settings:get_bool("simplecrafting_lib_log_invalid_recipe_removal")
  72. local recipe_to_string = function(recipe)
  73. local out = "{\n"
  74. for k, v in pairs(recipe) do
  75. if k == "output" then
  76. out = out .. "output = \"" .. ItemStack(v):to_string() .."\",\n"
  77. else
  78. out = out .. k .. " = " .. dump(v) .. ",\n"
  79. end
  80. end
  81. out = out .. "}"
  82. return out
  83. end
  84. local merge_item_count = function(list, alias, item_to_merge)
  85. local existing_count = list[alias] or 0 -- in case a recipe mixes various items that all alias to the same item
  86. list[alias] = item_to_merge + existing_count
  87. end
  88. local merge_sublists = function(list, alias, item_to_merge)
  89. local existing_list = list[alias] or {}
  90. for _, recipe in ipairs(item_to_merge) do
  91. table.insert(existing_list, recipe)
  92. end
  93. list[alias] = existing_list
  94. end
  95. local resolve_aliases_in_list = function(aliases, list, action_to_perform)
  96. if list == nil then return end
  97. local items_to_remove = {}
  98. for item, value in pairs(list) do
  99. if item:find(":") then -- only apply this test to non-group items
  100. local alias = aliases[item]
  101. if alias then
  102. table.insert(items_to_remove, item)
  103. action_to_perform(list, alias, value)
  104. end
  105. end
  106. end
  107. for _, item in ipairs(items_to_remove) do
  108. list[item] = nil
  109. end
  110. end
  111. local resolve_aliases = function()
  112. local aliases = minetest.registered_aliases
  113. for craft_type, recs in pairs(simplecrafting_lib.type) do
  114. for _, recipe in ipairs(recs.recipes) do
  115. resolve_aliases_in_list(aliases, recipe.input, merge_item_count)
  116. resolve_aliases_in_list(aliases, recipe.returns, merge_item_count)
  117. local output = ItemStack(recipe.output)
  118. local output_alias = aliases[output:get_name()]
  119. if output_alias then
  120. output:set_name(output_alias)
  121. recipe.output = output
  122. end
  123. end
  124. resolve_aliases_in_list(aliases, recs.recipes_by_in, merge_sublists)
  125. resolve_aliases_in_list(aliases, recs.recipes_by_out, merge_sublists)
  126. end
  127. end
  128. local purge_uncraftable_recipes = function()
  129. for item, def in pairs(minetest.registered_items) do
  130. for group, _ in pairs(def.groups) do
  131. group_examples[group] = item
  132. end
  133. end
  134. for craft_type, _ in pairs(simplecrafting_lib.type) do
  135. local i = 1
  136. local recs = simplecrafting_lib.type[craft_type].recipes
  137. while i <= #simplecrafting_lib.type[craft_type].recipes do
  138. local validation_result = validate_inputs_and_outputs(recs[i])
  139. if validation_result == true then
  140. i = i + 1
  141. else
  142. if log_removals then
  143. if string.match(validation_result, ":") then
  144. 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.")
  145. else
  146. 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.")
  147. end
  148. end
  149. table.remove(recs, i)
  150. end
  151. end
  152. for output, outs in pairs(simplecrafting_lib.type[craft_type].recipes_by_out) do
  153. i = 1
  154. while i <= #outs do
  155. if validate_inputs_and_outputs(outs[i]) == true then
  156. i = i + 1
  157. else
  158. table.remove(outs, i)
  159. end
  160. end
  161. if #outs == 0 then
  162. if log_removals then
  163. minetest.log("error", "[simplecrafting_lib] All recipes that had an output of " .. output
  164. .. " have been purged as uncraftable, this item can not be made by the player.")
  165. end
  166. simplecrafting_lib.type[craft_type].recipes_by_out[output] = nil
  167. end
  168. end
  169. for input, ins in pairs(simplecrafting_lib.type[craft_type].recipes_by_in) do
  170. i = 1
  171. while i <= #ins do
  172. if validate_inputs_and_outputs(ins[i]) == true then
  173. i = i + 1
  174. else
  175. table.remove(ins, i)
  176. end
  177. end
  178. if #ins == 0 then
  179. simplecrafting_lib.type[craft_type].recipes_by_in[input] = nil
  180. end
  181. end
  182. end
  183. group_examples = nil -- don't need this any more.
  184. end
  185. -- Tests for recipies that don't actually do anything (A => A)
  186. -- and for recipes that pointlessly consume input without giving new output
  187. local operative_recipe = function(recipe)
  188. local has_an_effect = false
  189. for in_item, in_count in pairs(recipe.input) do
  190. local out_count = recipe.output[in_item] or 0
  191. local returns_count = 0
  192. if recipe.returns then returns_count = recipe.returns[in_item] or 0 end
  193. if out_count + returns_count ~= in_count then
  194. has_an_effect = true
  195. break
  196. end
  197. end
  198. if not has_an_effect then
  199. return false
  200. end
  201. local new_output
  202. local out_item = recipe.output:get_name()
  203. local out_count = recipe.output:get_count()
  204. if not recipe.input[out_item] or recipe.input[out_item] < out_count then
  205. -- produces something that's not in the input, or produces more of the input item than there was intially.
  206. return true
  207. end
  208. return false
  209. end
  210. -- 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.
  211. -- 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.
  212. local disintermediate = function(craft_type, contents)
  213. local disintermediating_recipes = {}
  214. for _, recipe in pairs(contents.recipes) do
  215. if not recipe.do_not_disintermediate then
  216. for in_item, in_count in pairs(recipe.input) do
  217. -- check if there's recipes in this crafting type that produces the input item
  218. if contents.recipes_by_out[in_item] then
  219. -- find a recipe whose output divides evenly into the input
  220. for _, recipe_producing_in_item in pairs(contents.recipes_by_out[in_item]) do
  221. if not recipe_producing_in_item.do_not_use_for_disintermediation and
  222. (in_count % recipe_producing_in_item.output:get_count() == 0 or recipe_producing_in_item.output:get_count() % in_count == 0) then
  223. local multiplier = in_count / recipe_producing_in_item.output:get_count()
  224. local working_recipe = simplecrafting_lib.deep_copy(recipe)
  225. working_recipe.input[in_item] = nil -- clear the input from the working recipe (soon to be our newly created disintermediated recipe)
  226. if multiplier < 1 then
  227. -- the recipe_producing_in_item produces more than the working recipe requires by a whole multiplier.
  228. -- eg, 1A + 1B => 2C, 1C + 1D => 1E.
  229. -- Multiply working recipe by the inverse of multiplier and then set multiplier to 1.
  230. -- That will get us 1A + 1B + 2D => 2E
  231. local inverse = 1/multiplier
  232. multiplier = 1
  233. for item, count in pairs(working_recipe.input) do
  234. working_recipe.input[item] = count * inverse
  235. end
  236. working_recipe.output:set_count(working_recipe.output:get_count() * inverse)
  237. if working_recipe.returns then
  238. for item, count in pairs(working_recipe.returns) do
  239. working_recipe.returns[item] = count * inverse
  240. end
  241. end
  242. end
  243. -- add the inputs and outputs of the disintermediating recipe
  244. for new_in_item, new_in_count in pairs(recipe_producing_in_item.input) do
  245. if not working_recipe.input[new_in_item] then
  246. working_recipe.input[new_in_item] = new_in_count * multiplier
  247. else
  248. working_recipe.input[new_in_item] = working_recipe.input[new_in_item] + new_in_count * multiplier
  249. end
  250. end
  251. local new_out_item = recipe_producing_in_item.output:get_name()
  252. local new_out_count = recipe_producing_in_item.output:get_count()
  253. if new_out_item ~= in_item then -- this output is what's replacing the input we deleted, so don't add it.
  254. if not working_recipe.output:get_name() == new_out_item then
  255. working_recipe.output = ItemStack(new_out_item .. " " .. tostring(new_out_count * multiplier))
  256. else
  257. working_recipe.output:set_count(working_recipe.output:get_count() + new_out_count * multiplier)
  258. end
  259. end
  260. if recipe_producing_in_item.returns then
  261. for new_returns_item, new_returns_count in pairs(recipe_producing_in_item.returns) do
  262. working_recipe.returns = working_recipe.returns or {}
  263. if not working_recipe.returns[new_returns_item] then
  264. working_recipe.returns[new_returns_item] = new_returns_count * multiplier
  265. else
  266. working_recipe.returns[new_returns_item] = working_recipe.returns[new_returns_item] + new_returns_count * multiplier
  267. end
  268. end
  269. end
  270. if operative_recipe(working_recipe) then
  271. table.insert(disintermediating_recipes, working_recipe)
  272. end
  273. end
  274. end
  275. end
  276. end
  277. end
  278. end
  279. local count = 0
  280. for _, new_recipe in pairs(disintermediating_recipes) do
  281. if simplecrafting_lib.register(craft_type, new_recipe) then
  282. count = count + 1
  283. end
  284. end
  285. return count
  286. end
  287. local postprocess = function()
  288. resolve_aliases()
  289. purge_uncraftable_recipes()
  290. for craft_type, contents in pairs(simplecrafting_lib.type) do
  291. local cycles = contents.disintermediation_cycles or 0
  292. local previous_count = 0
  293. while cycles > 0 do
  294. local new_count = disintermediate(craft_type, contents)
  295. if new_count == 0 then
  296. minetest.log("info", "[simplecrafting_lib] disintermediation loop for crafting type " .. craft_type
  297. .. " exited early due to no need for additional disintermediation recipes.")
  298. cycles = 0
  299. break
  300. elseif previous_count > 0 and new_count > previous_count then
  301. minetest.log("error", "[simplecrafting_lib] potential disintermediation problem: crafting type \""
  302. .. craft_type .. "\" added " .. previous_count .. " disintermediation recipes on the previous cycle "
  303. .. "and " .. new_count .. " disintermediation recipes on the current cycle. This growing recipe "
  304. .. "addition rate could indicate that there's a unbalanced \"loop\" in the recipes defined for "
  305. .. "this craft type, resulting in an ever-increasing number of ways of producing a particular "
  306. .. "output. Examine the recipes for this craft type to see if you can identify the culprit, "
  307. .. "or reduce the value of this craft type's disintermediation_cycles property to prevent "
  308. .. "the recipe growth from getting too bad.")
  309. end
  310. previous_count = new_count
  311. cycles = cycles - 1
  312. end
  313. end
  314. for _, callback in ipairs(registered_callbacks) do
  315. callback()
  316. end
  317. registered_callbacks = nil
  318. end
  319. minetest.after(0, postprocess)