An extensible inventory mod which allows searching crafting and browsing for recipes in the same dialogue. https://content.minetest.net/packages/RealBadAngel/unified_inventory/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

410 lines
10KB

  1. -- match_craft.lua
  2. -- Find and automatically move inventory items to the crafting grid
  3. -- according to the recipe.
  4. --[[
  5. Retrieve items from inventory lists and calculate their total count.
  6. Return a table of "item name" - "total count" pairs.
  7. Arguments:
  8. inv: minetest inventory reference
  9. lists: names of inventory lists to use
  10. Example usage:
  11. -- Count items in "main" and "craft" lists of player inventory
  12. unified_inventory.count_items(player_inv_ref, {"main", "craft"})
  13. Example output:
  14. {
  15. ["default:pine_wood"] = 2,
  16. ["default:acacia_wood"] = 4,
  17. ["default:chest"] = 3,
  18. ["default:axe_diamond"] = 2, -- unstackable item are counted too
  19. ["wool:white"] = 6
  20. }
  21. ]]--
  22. function unified_inventory.count_items(inv, lists)
  23. local counts = {}
  24. for i = 1, #lists do
  25. local name = lists[i]
  26. local size = inv:get_size(name)
  27. local list = inv:get_list(name)
  28. for j = 1, size do
  29. local stack = list[j]
  30. if not stack:is_empty() then
  31. local item = stack:get_name()
  32. local count = stack:get_count()
  33. counts[item] = (counts[item] or 0) + count
  34. end
  35. end
  36. end
  37. return counts
  38. end
  39. --[[
  40. Retrieve craft recipe items and their positions in the crafting grid.
  41. Return a table of "craft item name" - "set of positions" pairs.
  42. Note that if craft width is not 3 then positions are recalculated as
  43. if items were placed on a 3x3 grid. Also note that craft can contain
  44. groups of items with "group:" prefix.
  45. Arguments:
  46. craft: minetest craft recipe
  47. Example output:
  48. -- Bed recipe
  49. {
  50. ["wool:white"] = {[1] = true, [2] = true, [3] = true}
  51. ["group:wood"] = {[4] = true, [5] = true, [6] = true}
  52. }
  53. --]]
  54. function unified_inventory.count_craft_positions(craft)
  55. local positions = {}
  56. local craft_items = craft.items
  57. local craft_type = unified_inventory.registered_craft_types[craft.type]
  58. or unified_inventory.craft_type_defaults(craft.type, {})
  59. local display_width = craft_type.dynamic_display_size
  60. and craft_type.dynamic_display_size(craft).width
  61. or craft_type.width
  62. local craft_width = craft_type.get_shaped_craft_width
  63. and craft_type.get_shaped_craft_width(craft)
  64. or display_width
  65. local i = 0
  66. for y = 1, 3 do
  67. for x = 1, craft_width do
  68. i = i + 1
  69. local item = craft_items[i]
  70. if item ~= nil then
  71. local pos = 3 * (y - 1) + x
  72. local set = positions[item]
  73. if set ~= nil then
  74. set[pos] = true
  75. else
  76. positions[item] = {[pos] = true}
  77. end
  78. end
  79. end
  80. end
  81. return positions
  82. end
  83. --[[
  84. For every craft item find all matching inventory items.
  85. - If craft item is a group then find all inventory items that matches
  86. this group.
  87. - If craft item is not a group (regular item) then find only this item.
  88. If inventory doesn't contain needed item then found set is empty for
  89. this item.
  90. Return a table of "craft item name" - "set of matching inventory items"
  91. pairs.
  92. Arguments:
  93. inv_items: table with items names as keys
  94. craft_items: table with items names or groups as keys
  95. Example output:
  96. {
  97. ["group:wood"] = {
  98. ["default:pine_wood"] = true,
  99. ["default:acacia_wood"] = true
  100. },
  101. ["wool:white"] = {
  102. ["wool:white"] = true
  103. }
  104. }
  105. --]]
  106. function unified_inventory.find_usable_items(inv_items, craft_items)
  107. local get_group = minetest.get_item_group
  108. local result = {}
  109. for craft_item in pairs(craft_items) do
  110. local group = craft_item:match("^group:(.+)")
  111. local found = {}
  112. if group ~= nil then
  113. for inv_item in pairs(inv_items) do
  114. if get_group(inv_item, group) > 0 then
  115. found[inv_item] = true
  116. end
  117. end
  118. else
  119. if inv_items[craft_item] ~= nil then
  120. found[craft_item] = true
  121. end
  122. end
  123. result[craft_item] = found
  124. end
  125. return result
  126. end
  127. --[[
  128. Match inventory items with craft grid positions.
  129. For every position select the matching inventory item with maximum
  130. (total_count / (times_matched + 1)) value.
  131. If for some position matching item cannot be found or match count is 0
  132. then return nil.
  133. Return a table of "matched item name" - "set of craft positions" pairs
  134. and overall match count.
  135. Arguments:
  136. inv_counts: table of inventory items counts from "count_items"
  137. craft_positions: table of craft positions from "count_craft_positions"
  138. Example output:
  139. match_table = {
  140. ["wool:white"] = {[1] = true, [2] = true, [3] = true}
  141. ["default:acacia_wood"] = {[4] = true, [6] = true}
  142. ["default:pine_wood"] = {[5] = true}
  143. }
  144. match_count = 2
  145. --]]
  146. function unified_inventory.match_items(inv_counts, craft_positions)
  147. local usable = unified_inventory.find_usable_items(inv_counts, craft_positions)
  148. local match_table = {}
  149. local match_count
  150. local matches = {}
  151. for craft_item, pos_set in pairs(craft_positions) do
  152. local use_set = usable[craft_item]
  153. for pos in pairs(pos_set) do
  154. local pos_item
  155. local pos_count
  156. for use_item in pairs(use_set) do
  157. local count = inv_counts[use_item]
  158. local times_matched = matches[use_item] or 0
  159. local new_pos_count = math.floor(count / (times_matched + 1))
  160. if pos_count == nil or pos_count < new_pos_count then
  161. pos_item = use_item
  162. pos_count = new_pos_count
  163. end
  164. end
  165. if pos_item == nil or pos_count == 0 then
  166. return nil
  167. end
  168. local set = match_table[pos_item]
  169. if set ~= nil then
  170. set[pos] = true
  171. else
  172. match_table[pos_item] = {[pos] = true}
  173. end
  174. matches[pos_item] = (matches[pos_item] or 0) + 1
  175. end
  176. end
  177. for match_item, times_matched in pairs(matches) do
  178. local count = inv_counts[match_item]
  179. local item_count = math.floor(count / times_matched)
  180. if match_count == nil or item_count < match_count then
  181. match_count = item_count
  182. end
  183. end
  184. return match_table, match_count
  185. end
  186. --[[
  187. Remove item from inventory lists.
  188. Return stack of actually removed items.
  189. This function replicates the inv:remove_item function but can accept
  190. multiple lists.
  191. Arguments:
  192. inv: minetest inventory reference
  193. lists: names of inventory lists
  194. stack: minetest item stack
  195. --]]
  196. function unified_inventory.remove_item(inv, lists, stack)
  197. local removed = ItemStack(nil)
  198. local leftover = ItemStack(stack)
  199. for i = 1, #lists do
  200. if leftover:is_empty() then
  201. break
  202. end
  203. local cur_removed = inv:remove_item(lists[i], leftover)
  204. removed:add_item(cur_removed)
  205. leftover:take_item(cur_removed:get_count())
  206. end
  207. return removed
  208. end
  209. --[[
  210. Add item to inventory lists.
  211. Return leftover stack.
  212. This function replicates the inv:add_item function but can accept
  213. multiple lists.
  214. Arguments:
  215. inv: minetest inventory reference
  216. lists: names of inventory lists
  217. stack: minetest item stack
  218. --]]
  219. function unified_inventory.add_item(inv, lists, stack)
  220. local leftover = ItemStack(stack)
  221. for i = 1, #lists do
  222. if leftover:is_empty() then
  223. break
  224. end
  225. leftover = inv:add_item(lists[i], leftover)
  226. end
  227. return leftover
  228. end
  229. --[[
  230. Move items from source list to destination list if possible.
  231. Skip positions specified in exclude set.
  232. Arguments:
  233. inv: minetest inventory reference
  234. src_list: name of source list
  235. dst_list: name of destination list
  236. exclude: set of positions to skip
  237. --]]
  238. function unified_inventory.swap_items(inv, src_list, dst_list, exclude)
  239. local size = inv:get_size(src_list)
  240. local empty = ItemStack(nil)
  241. for i = 1, size do
  242. if exclude == nil or exclude[i] == nil then
  243. local stack = inv:get_stack(src_list, i)
  244. if not stack:is_empty() then
  245. inv:set_stack(src_list, i, empty)
  246. local leftover = inv:add_item(dst_list, stack)
  247. if not leftover:is_empty() then
  248. inv:set_stack(src_list, i, leftover)
  249. end
  250. end
  251. end
  252. end
  253. end
  254. --[[
  255. Move matched items to the destination list.
  256. If destination list position is already occupied with some other item
  257. then function tries to (in that order):
  258. 1. Move it to the source list
  259. 2. Move it to some other unused position in destination list itself
  260. 3. Drop it to the ground if nothing else is possible.
  261. Arguments:
  262. player: minetest player object
  263. src_list: name of source list
  264. dst_list: name of destination list
  265. match_table: table of matched items
  266. amount: amount of items per every position
  267. --]]
  268. function unified_inventory.move_match(player, src_list, dst_list, match_table, amount)
  269. local inv = player:get_inventory()
  270. local item_drop = minetest.item_drop
  271. local src_dst_list = {src_list, dst_list}
  272. local dst_src_list = {dst_list, src_list}
  273. local needed = {}
  274. local moved = {}
  275. -- Remove stacks needed for craft
  276. for item, pos_set in pairs(match_table) do
  277. local stack = ItemStack(item)
  278. local stack_max = stack:get_stack_max()
  279. local bounded_amount = math.min(stack_max, amount)
  280. stack:set_count(bounded_amount)
  281. for pos in pairs(pos_set) do
  282. needed[pos] = unified_inventory.remove_item(inv, dst_src_list, stack)
  283. end
  284. end
  285. -- Add already removed stacks
  286. for pos, stack in pairs(needed) do
  287. local occupied = inv:get_stack(dst_list, pos)
  288. inv:set_stack(dst_list, pos, stack)
  289. if not occupied:is_empty() then
  290. local leftover = unified_inventory.add_item(inv, src_dst_list, occupied)
  291. if not leftover:is_empty() then
  292. inv:set_stack(dst_list, pos, leftover)
  293. local oversize = unified_inventory.add_item(inv, src_dst_list, stack)
  294. if not oversize:is_empty() then
  295. item_drop(oversize, player, player:get_pos())
  296. end
  297. end
  298. end
  299. moved[pos] = true
  300. end
  301. -- Swap items from unused positions to src (moved positions excluded)
  302. unified_inventory.swap_items(inv, dst_list, src_list, moved)
  303. end
  304. --[[
  305. Find craft match and move matched items to the destination list.
  306. If match cannot be found or match count is smaller than the desired
  307. amount then do nothing.
  308. If amount passed is -1 then amount is defined by match count itself.
  309. This is used to indicate "craft All" case.
  310. Arguments:
  311. player: minetest player object
  312. src_list: name of source list
  313. dst_list: name of destination list
  314. craft: minetest craft recipe
  315. amount: desired amount of output items
  316. --]]
  317. function unified_inventory.craftguide_match_craft(player, src_list, dst_list, craft, amount)
  318. local inv = player:get_inventory()
  319. local src_dst_list = {src_list, dst_list}
  320. local counts = unified_inventory.count_items(inv, src_dst_list)
  321. local positions = unified_inventory.count_craft_positions(craft)
  322. local match_table, match_count = unified_inventory.match_items(counts, positions)
  323. if match_table == nil or match_count < amount then
  324. return
  325. end
  326. if amount == -1 then
  327. amount = match_count
  328. end
  329. unified_inventory.move_match(player, src_list, dst_list, match_table, amount)
  330. end