util.lua 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. --------------------------------------------------------------------------------------------------------------------
  2. -- Local functions
  3. -- Turns an item list (as returned by inv:get_list) into a form more easily used by crafting functions
  4. local function itemlist_to_countlist(itemlist)
  5. local count_list = {}
  6. for _, stack in ipairs(itemlist) do
  7. if not stack:is_empty() then
  8. local name = stack:get_name()
  9. count_list[name] = (count_list[name] or 0) + stack:get_count()
  10. -- alias its groups to the item
  11. if minetest.registered_items[name] then
  12. for group, _ in pairs(minetest.registered_items[name].groups or {}) do
  13. if not count_list[group] then count_list[group] = {} end
  14. count_list[group][name] = true -- using names as keys makes this act as a set
  15. end
  16. end
  17. end
  18. end
  19. return count_list
  20. end
  21. -- splits a string into an array of substrings based on a delimiter
  22. local function split(str, delimiter)
  23. local result = {}
  24. for match in (str..delimiter):gmatch("(.-)"..delimiter) do
  25. table.insert(result, match)
  26. end
  27. return result
  28. end
  29. -- I apologise for this function.
  30. -- From the items in groupset, checks input_list to find the item
  31. -- with the highest count and adds it to required_input
  32. local function get_highest_count_item_for_group(groupset, input_list, required_input, count)
  33. local highest_item_name
  34. local highest_item_count = 0
  35. for group_item, _ in pairs(groupset) do
  36. if input_list[group_item] > highest_item_count then
  37. highest_item_count = input_list[group_item]
  38. highest_item_name = group_item
  39. end
  40. end
  41. if highest_item_count == 0 then
  42. return false
  43. end
  44. required_input[highest_item_name] = (required_input[highest_item_name] or 0) + count
  45. return true
  46. end
  47. -- returns the number of times the recipe can be crafted from the given input_list,
  48. -- and also a copy of the recipe with groups substituted for the most common item
  49. -- in the input_list that matches them
  50. local function get_craft_count(input_list, recipe)
  51. -- Recipe without groups (most common node in group instead)
  52. local work_recipe = simplecrafting_lib.deep_copy(recipe)
  53. if recipe.output then
  54. work_recipe.output = ItemStack(recipe.output)
  55. end
  56. work_recipe.input = {}
  57. local required_input = work_recipe.input
  58. for item, count in pairs(recipe.input) do
  59. if string.find(item, ",") then -- special syntax used to require an item that belongs to multiple groups
  60. local groups = split(item, ",")
  61. -- This unfortunate block of code builds up an intersection
  62. -- of the items belonging to each group in the list of groups
  63. -- that this recipe item slot requires.
  64. local multigroup_itemset
  65. for _, group in pairs(groups) do
  66. if not input_list[group] then
  67. return 0
  68. end
  69. if not multigroup_itemset then
  70. multigroup_itemset = {}
  71. for multigroup_item, _ in pairs(input_list[group]) do
  72. multigroup_itemset[multigroup_item] = true
  73. end
  74. else
  75. local intersect = {}
  76. for multigroup_item, _ in pairs(input_list[group]) do
  77. if multigroup_itemset[multigroup_item] then
  78. intersect[multigroup_item] = true
  79. end
  80. end
  81. multigroup_itemset = intersect
  82. end
  83. end
  84. if not get_highest_count_item_for_group(multigroup_itemset, input_list, required_input, count) then
  85. return 0
  86. end
  87. else
  88. if not input_list[item] then
  89. return 0
  90. end
  91. -- Groups are a string alias to most common member item
  92. if type(input_list[item]) == "table" then
  93. -- find group item with highest count
  94. if not get_highest_count_item_for_group(input_list[item], input_list, required_input, count) then
  95. return 0
  96. end
  97. else
  98. required_input[item] = (required_input[item] or 0) + count
  99. end
  100. end
  101. end
  102. local number = math.huge
  103. for ingredient, count in pairs(required_input) do
  104. local max = input_list[ingredient] / count
  105. if max < 1 then
  106. return 0
  107. elseif max < number then
  108. number = max
  109. end
  110. end
  111. -- Return number of possible crafts as integer
  112. return math.floor(number), work_recipe
  113. end
  114. -- Used for alphabetizing an array of itemstacks by description
  115. local function compare_stacks_by_desc(stack1, stack2)
  116. local item1 = stack1:get_name()
  117. local item2 = stack2:get_name()
  118. local def1 = minetest.registered_items[item1]
  119. local def2 = minetest.registered_items[item2]
  120. return def1.description < def2.description
  121. end
  122. --------------------------------------------------------------------------------------------------------------------
  123. -- Public API
  124. -- Note that a circular table reference will result in a crash, TODO: guard against that.
  125. -- Unlikely to be needed, though - it'd take a lot of work for users to get into this bit of trouble.
  126. simplecrafting_lib.deep_copy = function(recipe_in)
  127. local recipe_out = {}
  128. for index, value in pairs(recipe_in) do
  129. if type(value) == "table" then
  130. recipe_out[index] = simplecrafting_lib.deep_copy(value)
  131. elseif type(value) == "userdata" and index == "output" then
  132. recipe_out[index] = ItemStack(value)
  133. else
  134. recipe_out[index] = value
  135. end
  136. end
  137. return recipe_out
  138. end
  139. simplecrafting_lib.get_crafting_info = function(craft_type)
  140. -- ensure the destination tables exist
  141. simplecrafting_lib.type[craft_type] = simplecrafting_lib.type[craft_type] or {}
  142. simplecrafting_lib.type[craft_type].recipes = simplecrafting_lib.type[craft_type].recipes or {}
  143. simplecrafting_lib.type[craft_type].recipes_by_out = simplecrafting_lib.type[craft_type].recipes_by_out or {}
  144. simplecrafting_lib.type[craft_type].recipes_by_in = simplecrafting_lib.type[craft_type].recipes_by_in or {}
  145. return simplecrafting_lib.type[craft_type]
  146. end
  147. simplecrafting_lib.set_description = function(craft_type, desc)
  148. simplecrafting_lib.get_crafting_info(craft_type).description = desc
  149. end
  150. simplecrafting_lib.get_description = function(craft_type)
  151. return simplecrafting_lib.get_crafting_info(craft_type).description
  152. end
  153. simplecrafting_lib.set_disintermediation_cycles = function(craft_type, cycle_count)
  154. simplecrafting_lib.get_crafting_info(craft_type).disintermediation_cycles = tonumber(cycle_count)
  155. end
  156. -- returns a fuel definition for the item if it is fuel, nil otherwise
  157. -- note: will always return the last-registered definition for a particular item
  158. -- or group.
  159. simplecrafting_lib.is_fuel = function(craft_type, item)
  160. local fuels = simplecrafting_lib.get_crafting_info(craft_type).recipes_by_in
  161. -- First check if the item has been explicitly registered as fuel
  162. if fuels[item] then
  163. return fuels[item][#fuels[item]]
  164. end
  165. -- Failing that, check its groups.
  166. local def = minetest.registered_items[item]
  167. if def and def.groups then
  168. local max = -1
  169. local fuel_group
  170. for group, _ in pairs(def.groups) do
  171. if fuels[group] then
  172. local last_fuel_def = fuels[group][#fuels[group]]
  173. local last_fuel_burntime
  174. if last_fuel_def.output and last_fuel_def.output:get_name() == "simplecrafting_lib:heat" then
  175. last_fuel_burntime = last_fuel_def.output:get_count()
  176. else
  177. last_fuel_burntime = 0
  178. end
  179. if last_fuel_burntime > max then
  180. fuel_group = last_fuel_def -- track whichever is the longest-burning group
  181. max = last_fuel_burntime
  182. end
  183. end
  184. end
  185. if fuel_group then
  186. return fuel_group
  187. end
  188. end
  189. return nil
  190. end
  191. -- Returns a list of all fuel recipes whose ingredients can be satisfied by the item_list
  192. simplecrafting_lib.get_fuels = function(craft_type, item_list)
  193. local count_list = itemlist_to_countlist(item_list)
  194. local burnable = {}
  195. for item, count in pairs(count_list) do
  196. local recipe = simplecrafting_lib.is_fuel(craft_type, item)
  197. if recipe then
  198. table.insert(burnable, recipe)
  199. end
  200. end
  201. return burnable
  202. end
  203. -- Returns a list of all recipes whose ingredients can be satisfied by the item_list
  204. simplecrafting_lib.get_craftable_recipes = function(craft_type, item_list)
  205. local count_list = itemlist_to_countlist(item_list)
  206. local craftable = {}
  207. local recipes = simplecrafting_lib.type[craft_type].recipes
  208. for i = 1, #recipes do
  209. local number, recipe = get_craft_count(count_list, recipes[i])
  210. if number > 0 then
  211. table.insert(craftable, recipe)
  212. end
  213. end
  214. return craftable
  215. end
  216. -- Returns a list of all the possible item stacks that could be crafted from the provided item list
  217. -- if max_craftable is true the returned stacks will have as many items in them as possible to craft,
  218. -- if max_craftable is false or nil the returned stacks will have only the minimum output
  219. -- if alphabetize is true then the items will be sorted alphabetically by description
  220. -- if alphabetize is false or nil the items will be left in default order
  221. simplecrafting_lib.get_craftable_items = function(craft_type, item_list, max_craftable, alphabetize)
  222. local count_list = itemlist_to_countlist(item_list)
  223. local craftable_count_list = {}
  224. local craftable_stacks = {}
  225. local chosen_recipe = {}
  226. local recipes = simplecrafting_lib.type[craft_type].recipes
  227. for i = 1, #recipes do
  228. local number, recipe = get_craft_count(count_list, recipes[i])
  229. if number > 0 then
  230. if not max_craftable then number = 1 end
  231. local output_name = recipe.output:get_name()
  232. local output_count = recipe.output:get_count()
  233. if craftable_count_list[output_name] and output_count*number > craftable_count_list[output_name] then
  234. craftable_count_list[output_name] = output_count*number
  235. chosen_recipe[output_name] = recipe
  236. elseif not craftable_count_list[output_name] and output_count*number > 0 then
  237. craftable_count_list[output_name] = output_count*number
  238. chosen_recipe[output_name] = recipe
  239. end
  240. end
  241. end
  242. -- Limit stacks to stack limit
  243. for item, count in pairs(craftable_count_list) do
  244. local stack = ItemStack(item)
  245. local max = stack:get_stack_max()
  246. if count > max then
  247. count = max - max % chosen_recipe[item].output:get_count()
  248. end
  249. stack:set_count(count)
  250. simplecrafting_lib.execute_pre_craft(craft_type, chosen_recipe[item], stack, item_list)
  251. table.insert(craftable_stacks, stack)
  252. end
  253. if alphabetize then
  254. table.sort(craftable_stacks, compare_stacks_by_desc)
  255. end
  256. return craftable_stacks
  257. end
  258. -- Returns true if the item name is an input for at least one
  259. -- recipe belonging to the given craft type
  260. simplecrafting_lib.is_possible_input = function(craft_type, item_name)
  261. local info = simplecrafting_lib.get_crafting_info(craft_type)
  262. if info.recipes_by_in[item_name] then
  263. return true -- that was easy.
  264. end
  265. -- Now for the group checks. :(
  266. local item_def = minetest.registered_items[item_name]
  267. if not item_def then return false end -- undefined item
  268. local groups = item_def.groups or {}
  269. for _, recipe in pairs(info.recipes) do
  270. --the multi-group flower/dye type inputs are complicated to check for
  271. for input_item, _ in pairs(recipe.input) do
  272. if not string.match(input_item, ":") then
  273. local input_grouplist = split(input_item, ",") -- most of the time we'll have a list of one.
  274. local input_matches = true
  275. for _, needed_group in pairs(input_grouplist) do -- for each group in that list of input groups
  276. if not groups[needed_group] then -- if the test item doesn't have that group
  277. input_matches = false -- it's not a match.
  278. break
  279. end
  280. end
  281. if input_matches then return true end
  282. end
  283. end
  284. end
  285. return false
  286. end
  287. -- Returns true if the item is a possible output for at least
  288. -- one recipe belonging to the given craft type
  289. simplecrafting_lib.is_possible_output = function(craft_type, item_name)
  290. return simplecrafting_lib.type[craft_type].recipes_by_out[item_name] ~= nil
  291. end
  292. -- adds two count lists together, returns a new count list with the sum of the parameters' contents
  293. simplecrafting_lib.count_list_add = function(list1, list2)
  294. local out_list = {}
  295. for item, count in pairs(list1) do
  296. out_list[item] = count
  297. end
  298. if list2 == nil then return out_list end
  299. for item, count in pairs(list2) do
  300. if type(count) == "table" then
  301. -- item is actually a group name, it has a set of items associated with it.
  302. -- Perform a union with existing set.
  303. out_list[item] = out_list[item] or {}
  304. for group_item, _ in pairs(count) do
  305. out_list[item][group_item] = true
  306. end
  307. else
  308. out_list[item] = (out_list[item] or 0) + count
  309. end
  310. end
  311. return out_list
  312. end
  313. -- Returns a recipe with the inputs and outputs multiplied to match the requested
  314. -- quantity of ouput items in the crafted stack. Note that the output could
  315. -- actually be larger than crafted_stack if an exactly matching recipe can't be found.
  316. -- returns nil if crafting is impossible with the given source inventory
  317. simplecrafting_lib.get_crafting_result = function(craft_type, input_list, request_stack)
  318. local input_count = itemlist_to_countlist(input_list)
  319. local request_name = request_stack:get_name()
  320. local request_count = request_stack:get_count()
  321. local recipes = simplecrafting_lib.type[craft_type].recipes_by_out[request_name]
  322. local smallest_remainder = math.huge
  323. local smallest_remainder_output_count = 0
  324. local smallest_remainder_recipe = nil
  325. for i = 1, #recipes do
  326. local number, recipe = get_craft_count(input_count, recipes[i])
  327. if number > 0 then
  328. local output_count = recipe.output:get_count()
  329. if (request_count % output_count) <= smallest_remainder and output_count > smallest_remainder_output_count then
  330. smallest_remainder = request_count % output_count
  331. smallest_remainder_output_count = output_count
  332. smallest_remainder_recipe = recipe
  333. end
  334. end
  335. end
  336. if smallest_remainder_recipe then
  337. local multiple = math.ceil(request_count / smallest_remainder_recipe.output:get_count())
  338. for input_item, quantity in pairs(smallest_remainder_recipe.input) do
  339. smallest_remainder_recipe.input[input_item] = multiple * quantity
  340. end
  341. smallest_remainder_recipe.output:set_count(smallest_remainder_recipe.output:get_count() * multiple)
  342. if smallest_remainder_recipe.returns then
  343. for returned_item, quantity in pairs(smallest_remainder_recipe.returns) do
  344. smallest_remainder_recipe.returns[returned_item] = multiple * quantity
  345. end
  346. end
  347. return smallest_remainder_recipe
  348. end
  349. return nil
  350. end
  351. local pre_craft = {}
  352. local post_craft = {}
  353. simplecrafting_lib.register_pre_craft = function(callback)
  354. table.insert(pre_craft, callback)
  355. end
  356. simplecrafting_lib.register_post_craft = function(callback)
  357. table.insert(post_craft, callback)
  358. end
  359. simplecrafting_lib.execute_pre_craft = function(craft_type, recipe, output_stack, source_item_list)
  360. for k, callback in ipairs(pre_craft) do
  361. callback(craft_type, recipe, output_stack, source_item_list)
  362. end
  363. end
  364. simplecrafting_lib.execute_post_craft = function(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
  365. for k, callback in ipairs(post_craft) do
  366. callback(craft_type, recipe, output_stack, source_inv, source_listname, destination_inv, destination_listname)
  367. end
  368. end