legacy.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. local log_removals = minetest.settings:get_bool("simplecrafting_lib_log_invalid_recipe_removal")
  2. local register_heat = function()
  3. if minetest.registered_craftitems["simplecrafting_lib:heat"] == nil then
  4. minetest.register_craftitem(":simplecrafting_lib:heat", {
  5. description = "Heat",
  6. groups = {simplecrafting_lib_intermediate=1, not_in_creative_inventory=1,},
  7. inventory_image = "simplecrafting_lib_heat.png",
  8. stack_max = 9999,
  9. })
  10. end
  11. end
  12. local function create_recipe(legacy)
  13. local items = legacy.items
  14. local has_items = false
  15. for _, item in pairs(items) do
  16. has_items = true
  17. break
  18. end
  19. if not has_items then return end
  20. local recipe = {}
  21. recipe.output = ItemStack(legacy.output)
  22. recipe.input = {}
  23. recipe.returns = legacy.returns
  24. for _, item in pairs(items) do
  25. if item ~= "" then
  26. recipe.input[item] = (recipe.input[item] or 0) + 1
  27. end
  28. end
  29. return recipe
  30. end
  31. -- It's possible to have a recipe with a replacements pair that gives back more than what's being replaced,
  32. -- eg in cottages the straw mat recipe replaces 1 default:stone with 3 farming:seed_wheats.
  33. -- This parses out that possibility
  34. local function get_item_and_quantity(item_string)
  35. local string_split_list = {}
  36. for v in string.gmatch(item_string, "%S+") do
  37. table.insert(string_split_list, v)
  38. end
  39. if #string_split_list == 1 then
  40. return item_string, 1 -- no number provided
  41. else
  42. return string_split_list[1], tonumber(string_split_list[#string_split_list])
  43. end
  44. end
  45. local function get_replacement_group_haver(item_name, item_counts, recipe)
  46. if item_counts[item_name] == nil then
  47. -- If the item to be replaced is a group identifier, and there's an item that matches it in the recipe, substitute that.
  48. if string.sub(item_name, 1, 6) == "group:" then
  49. local groupname = string.sub(item_name, 7)
  50. for k, v in pairs(item_counts) do
  51. if minetest.get_item_group(k, groupname) > 0 then
  52. return k
  53. end
  54. end
  55. return false, "[simplecrafting_lib] recipe has a group replacement " .. item_name
  56. .. " but an input belonging to that group can't be found."
  57. end
  58. -- If the item to be replaced doesn't exist in the recipe but there are groups in the recipe, see if the item has one of those groups
  59. for input, _ in pairs(item_counts) do
  60. if string.sub(input, 1, 6) == "group:" and
  61. minetest.get_item_group(item_name, string.sub(input, 7)) > 0 then
  62. return input
  63. end
  64. end
  65. return false, "[simplecrafting_lib] recipe has a replacement target " .. item_name ..
  66. " but an input matching it can't be found."
  67. end
  68. return item_name
  69. end
  70. local function process_replacements(recipe, count, legacy_returns)
  71. for _,pair in pairs(recipe.replacements) do
  72. local item_name, item_quantity = get_item_and_quantity(pair[2])
  73. local replace_input, err = get_replacement_group_haver(pair[1], count, recipe)
  74. if replace_input == false then
  75. return false, err
  76. end
  77. local input_count = count[replace_input]
  78. if input_count == nil then
  79. return false, "Replacement input not found: " .. replace_input
  80. end
  81. legacy_returns[item_name] = input_count * item_quantity
  82. end
  83. return true
  84. end
  85. local function process_shaped_recipe(recipe)
  86. local legacy = {items={},output=recipe.output}
  87. local count = {}
  88. for _,row in pairs(recipe.recipe) do
  89. for _, item in pairs(row) do
  90. legacy.items[#legacy.items+1] = item
  91. count[item] = (count[item] or 0) + 1
  92. end
  93. end
  94. if recipe.replacements then
  95. legacy.returns = {}
  96. local success, err = process_replacements(recipe, count, legacy.returns)
  97. if success == false then
  98. return false, err
  99. end
  100. end
  101. return create_recipe(legacy)
  102. end
  103. local function process_shapeless_recipe(recipe)
  104. local legacy = {items={},output=recipe.output}
  105. if recipe.replacements then
  106. legacy.returns = {}
  107. local count = {}
  108. for _, item in pairs(recipe.recipe) do
  109. count[item] = (count[item] or 0) + 1
  110. end
  111. local success, err = process_replacements(recipe, count, legacy.returns)
  112. if success == false then
  113. return false, err
  114. end
  115. end
  116. legacy.items = recipe.recipe
  117. return create_recipe(legacy)
  118. end
  119. local function process_cooking_recipe(recipe)
  120. local legacy = {input={}}
  121. legacy.output = recipe.output
  122. legacy.input[recipe.recipe] = 1
  123. legacy.input["simplecrafting_lib:heat"] = recipe.cooktime or 3
  124. if recipe.replacements then
  125. legacy.returns = {}
  126. local count = {}
  127. count[recipe.recipe] = 1
  128. local success, err = process_replacements(recipe, count, legacy.returns)
  129. if success == false then
  130. return false, err
  131. end
  132. end
  133. return legacy
  134. end
  135. local function process_fuel_recipe(recipe)
  136. local legacy = {input={}}
  137. legacy.input[recipe.recipe] = 1
  138. legacy.output = ItemStack({name="simplecrafting_lib:heat", count=recipe.burntime})
  139. if recipe.replacements then
  140. legacy.returns = {}
  141. for _,pair in pairs(recipe.replacements) do
  142. local item_name, item_quantity = get_item_and_quantity(pair[2])
  143. legacy.returns[item_name] = item_quantity
  144. end
  145. end
  146. return legacy
  147. end
  148. local already_cleared_processed = {} -- contains recipes suitable for re-registering
  149. -- once we're done initializing, throw these tables away. They're not needed after that.
  150. minetest.after(10, function()
  151. already_cleared_processed = nil
  152. end)
  153. -- This is necessary because the format of recipes returned by
  154. -- get_all_crafts is completely different from the format required by clear_craft.
  155. -- https://github.com/minetest/minetest/issues/5962
  156. -- https://github.com/minetest/minetest/issues/5790
  157. -- https://github.com/minetest/minetest/issues/7429
  158. local function safe_clear_craft(recipe_to_clear)
  159. local parameter_recipe = {}
  160. if recipe_to_clear.method == nil or recipe_to_clear.method == "normal" then
  161. if recipe_to_clear.width == 0 then
  162. parameter_recipe.type="shapeless"
  163. parameter_recipe.recipe = recipe_to_clear.items
  164. elseif recipe_to_clear.width == 1 then
  165. parameter_recipe.width = 1
  166. parameter_recipe.recipe = {
  167. {recipe_to_clear.items[1] or ""},
  168. {recipe_to_clear.items[2] or ""},
  169. {recipe_to_clear.items[3] or ""},
  170. }
  171. elseif recipe_to_clear.width == 2 then
  172. parameter_recipe.width = 2
  173. parameter_recipe.recipe = {
  174. {recipe_to_clear.items[1] or "", recipe_to_clear.items[2] or ""},
  175. {recipe_to_clear.items[3] or "", recipe_to_clear.items[4] or ""},
  176. {recipe_to_clear.items[5] or "", recipe_to_clear.items[6] or ""},
  177. }
  178. elseif recipe_to_clear.width == 3 then
  179. parameter_recipe.width = 3
  180. parameter_recipe.recipe = {
  181. {recipe_to_clear.items[1] or "", recipe_to_clear.items[2] or "", recipe_to_clear.items[3] or ""},
  182. {recipe_to_clear.items[4] or "", recipe_to_clear.items[5] or "", recipe_to_clear.items[6] or ""},
  183. {recipe_to_clear.items[7] or "", recipe_to_clear.items[8] or "", recipe_to_clear.items[9] or ""},
  184. }
  185. end
  186. elseif recipe_to_clear.type == "cooking" then
  187. parameter_recipe.type = "cooking"
  188. parameter_recipe.recipe = recipe_to_clear.items[1]
  189. else
  190. minetest.log("error", "[simplecrafting_lib] safe_clear_craft was unable to parse recipe "..dump(recipe_to_clear))
  191. return false
  192. end
  193. -- https://github.com/minetest/minetest/issues/6513
  194. local success, err = pcall(function()
  195. if not minetest.clear_craft(parameter_recipe) then
  196. minetest.log("warning", "[simplecrafting_lib] failed to clear recipe " .. dump(recipe_to_clear) .. "\nas parameter\n" .. dump(parameter_recipe))
  197. end
  198. end)
  199. if not success and err ~= "No crafting specified for input" then
  200. minetest.log("error", "[simplecrafting_lib] minetest.clear_craft failed with error \"" ..err.. "\" while attempting to clear craft " ..dump(recipe_to_clear))
  201. return false
  202. elseif success == true and err == false then
  203. minetest.log("warning", "[simplecrafting_lib] minetest.clear_craft wasn't able to find inputs for " .. dump(recipe_to_clear))
  204. return false
  205. end
  206. return true
  207. end
  208. local function register_legacy_recipe(legacy_recipe)
  209. local clear_recipe = false
  210. for _, filter in ipairs(simplecrafting_lib.import_filters) do
  211. local working_recipe = simplecrafting_lib.deep_copy(legacy_recipe)
  212. local craft_type, clear_this = filter(working_recipe)
  213. if craft_type then
  214. if (working_recipe.input["simplecrafting_lib:heat"]) or
  215. (working_recipe.output and ItemStack(working_recipe.output):get_name() == "simplecrafting_lib:heat") then
  216. register_heat()
  217. end
  218. simplecrafting_lib.register(craft_type, working_recipe)
  219. end
  220. clear_recipe = clear_this or clear_recipe
  221. end
  222. return clear_recipe
  223. end
  224. -- import_legacy_recipes overrides minetest.register_craft so that subsequently registered
  225. -- crafting recipes will be put into this system. If you wish to register a craft
  226. -- the old way without it being put into this system, use this method.
  227. simplecrafting_lib.minetest_register_craft = minetest.register_craft
  228. local function log_failure(err, recipe)
  229. if log_removals then
  230. if err ~= nil then
  231. minetest.log("error", err .. "\n" .. dump(recipe))
  232. else
  233. minetest.log("error", "[simplecrafting_lib] Shapeless recipe could not be processed, likely due to nonexistent replacement items:\n" .. dump(recipe))
  234. end
  235. end
  236. end
  237. -- This replaces the core register_craft method so that any crafts
  238. -- registered after this one will be added to the new system.
  239. minetest.register_craft = function(recipe)
  240. local clear = false
  241. local new_recipe
  242. if not recipe.type then
  243. local err
  244. new_recipe, err = process_shaped_recipe(recipe)
  245. if new_recipe == false then
  246. log_failure(err, recipe)
  247. return
  248. end
  249. clear = register_legacy_recipe(new_recipe)
  250. elseif recipe.type == "shapeless" then
  251. local err
  252. new_recipe, err = process_shapeless_recipe(recipe)
  253. if new_recipe == false then
  254. log_failure(err, recipe)
  255. return
  256. end
  257. clear = register_legacy_recipe(new_recipe)
  258. elseif recipe.type == "cooking" then
  259. new_recipe = process_cooking_recipe(recipe)
  260. clear = register_legacy_recipe(new_recipe)
  261. elseif recipe.type == "fuel" then
  262. new_recipe = process_fuel_recipe(recipe)
  263. clear = register_legacy_recipe(new_recipe)
  264. end
  265. if not clear then
  266. return simplecrafting_lib.minetest_register_craft(recipe)
  267. else
  268. table.insert(already_cleared_processed, new_recipe)
  269. end
  270. end
  271. local function import_legacy_recipes()
  272. -- if any recipes have been cleared by previous runs of import_legacy_recipes, let this run have the opportunity to look at them.
  273. for _, recipe in pairs(already_cleared_processed) do
  274. register_legacy_recipe(recipe)
  275. end
  276. -- This loop goes through all recipes that have already been registered and
  277. -- converts them
  278. for item,_ in pairs(minetest.registered_items) do
  279. local crafts = minetest.get_all_craft_recipes(item)
  280. if crafts and item ~= "" then
  281. for _,legacy_recipe in pairs(crafts) do
  282. if legacy_recipe.method == nil or legacy_recipe.method == "normal" then
  283. -- get_all_craft_recipes output recipes omit replacements, need to find those experimentally
  284. -- https://github.com/minetest/minetest/issues/4901
  285. local output, decremented_input = minetest.get_craft_result(legacy_recipe)
  286. -- until https://github.com/minetest/minetest_game/commit/ae7206c0064cbb5c0e5434c19893d4bf3fa2b388
  287. -- the dye:red + dye:green -> dye:brown recipe was broken here - there were two
  288. -- red+green recipes, one producing dark grey and one producing brown dye, and when one gets
  289. -- cleared from the crafting system by safe_clear_craft the other goes too and this craft attempt
  290. -- fails.
  291. -- This brokenness manifests by returning their input items and no output, so check if an output
  292. -- was actually made before counting the returns as actual returns.
  293. -- This is not an ideal solution since it may result in recipes losing their replacements,
  294. -- but at this point I'm solving edge cases for edge cases and I need to sleep.
  295. if output.item:get_count() > 0 then
  296. for _, returned_item in pairs(decremented_input.items) do
  297. if returned_item:get_count() > 0 then
  298. legacy_recipe.returns = legacy_recipe.returns or {}
  299. legacy_recipe.returns[returned_item:get_name()] = (legacy_recipe.returns[returned_item:get_name()] or 0) + returned_item:get_count()
  300. end
  301. end
  302. end
  303. local new_recipe = create_recipe(legacy_recipe)
  304. if register_legacy_recipe(new_recipe) then
  305. if not safe_clear_craft(legacy_recipe) then
  306. minetest.log("warning", "[simplecrafting_lib] minetest.clear_craft wasn't able to find inputs for " .. dump(legacy_recipe) .. " for output " .. item)
  307. end
  308. table.insert(already_cleared_processed, new_recipe)
  309. end
  310. elseif legacy_recipe.method == "cooking" then
  311. local new_recipe = {input={}}
  312. new_recipe.output = legacy_recipe.output
  313. new_recipe.input[legacy_recipe.items[1]] = 1
  314. local cooked = minetest.get_craft_result({method = "cooking", width = 1, items = {legacy_recipe.items[1]}})
  315. new_recipe.input["simplecrafting_lib:heat"] = cooked.time
  316. if register_legacy_recipe(new_recipe) then
  317. if not safe_clear_craft(legacy_recipe) then
  318. minetest.log("warning", "[simplecrafting_lib] minetest.clear_craft wasn't able to find inputs for " .. dump(legacy_recipe) .. " for cooking output " .. item)
  319. end
  320. table.insert(already_cleared_processed, new_recipe)
  321. end
  322. else
  323. minetest.log("error", "[simplecrafting_lib] Unrecognized crafting method for legacy recipe " .. dump(legacy_recipe))
  324. end
  325. end
  326. end
  327. -- Fuel recipes aren't returned by get_all_craft_recipes, need to find those experimentally
  328. -- https://github.com/minetest/minetest/issues/5745
  329. local fuel, afterfuel = minetest.get_craft_result({method="fuel",width=1,items={item}})
  330. if fuel.time ~= 0 then
  331. local new_recipe = {}
  332. new_recipe.input = {}
  333. new_recipe.input[item] = 1
  334. new_recipe.output = ItemStack({name="simplecrafting_lib:heat", count = fuel.time})
  335. for _, afteritem in pairs(afterfuel.items) do
  336. if afteritem:get_count() > 0 then
  337. new_recipe.returns = new_recipe.returns or {}
  338. new_recipe.returns[afteritem:get_name()] = (new_recipe.returns[afteritem:get_name()] or 0) + afteritem:get_count()
  339. end
  340. end
  341. if register_legacy_recipe(new_recipe) then
  342. minetest.clear_craft({type="fuel", recipe=item})
  343. table.insert(already_cleared_processed, new_recipe)
  344. end
  345. end
  346. end
  347. end
  348. simplecrafting_lib.import_filters = {}
  349. simplecrafting_lib.register_recipe_import_filter = function(filter_function)
  350. table.insert(simplecrafting_lib.import_filters, filter_function)
  351. import_legacy_recipes()
  352. end