api.lua 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. -- Crafting Mod - semi-realistic crafting in minetest
  2. -- Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
  3. --
  4. -- This library is free software; you can redistribute it and/or
  5. -- modify it under the terms of the GNU Lesser General Public
  6. -- License as published by the Free Software Foundation; either
  7. -- version 2.1 of the License, or (at your option) any later version.
  8. --
  9. -- This library is distributed in the hope that it will be useful,
  10. -- but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. -- Lesser General Public License for more details.
  13. --
  14. -- You should have received a copy of the GNU Lesser General Public
  15. -- License along with this library; if not, write to the Free Software
  16. -- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  17. crafting = {
  18. recipes = {},
  19. recipes_by_id = {},
  20. registered_on_crafts = {},
  21. }
  22. function crafting.register_type(name)
  23. crafting.recipes[name] = {}
  24. end
  25. local recipe_counter = 0
  26. function crafting.register_recipe(def)
  27. assert(def.output, "Output needed in recipe definition")
  28. assert(def.type, "Type needed in recipe definition")
  29. assert(def.items, "Items needed in recipe definition")
  30. def.level = def.level or 1
  31. local tab = crafting.recipes[def.type]
  32. assert(tab, "Unknown craft type " .. def.type)
  33. recipe_counter = recipe_counter + 1
  34. def.id = recipe_counter
  35. crafting.recipes_by_id[recipe_counter] = def
  36. tab[#tab + 1] = def
  37. return def.id
  38. end
  39. local unlocked_cache = {}
  40. function crafting.get_unlocked(name)
  41. local player = minetest.get_player_by_name(name)
  42. if not player then
  43. minetest.log("warning", "Crafting doesn't support getting unlocks for offline players")
  44. return {}
  45. end
  46. local retval = unlocked_cache[name]
  47. if not retval then
  48. retval = minetest.parse_json(
  49. player:get_meta():get("crafting:unlocked") or "{}")
  50. unlocked_cache[name] = retval
  51. end
  52. assert(retval)
  53. return retval
  54. end
  55. if minetest then
  56. minetest.register_on_leaveplayer(function(player)
  57. unlocked_cache[player:get_player_name()] = nil
  58. end)
  59. end
  60. local function write_json_dictionary(value)
  61. if next(value) then
  62. return minetest.write_json(value)
  63. else
  64. return "{}"
  65. end
  66. end
  67. function crafting.lock_all(name)
  68. local player = minetest.get_player_by_name(name)
  69. if not player then
  70. minetest.log("warning", "Crafting doesn't support setting unlocks for offline players")
  71. return {}
  72. end
  73. local unlocked = crafting.get_unlocked(name)
  74. for key, _ in pairs(unlocked) do
  75. unlocked[key] = nil
  76. end
  77. unlocked_cache[name] = unlocked
  78. player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked))
  79. end
  80. function crafting.unlock(name, output)
  81. local player = minetest.get_player_by_name(name)
  82. if not player then
  83. minetest.log("warning", "Crafting doesn't support setting unlocks for offline players")
  84. return {}
  85. end
  86. local unlocked = crafting.get_unlocked(name)
  87. if type(output) == "table" then
  88. for i=1, #output do
  89. unlocked[output[i]] = true
  90. minetest.chat_send_player(name, "You've unlocked " .. output[i])
  91. end
  92. else
  93. unlocked[output] = true
  94. minetest.chat_send_player(name, "You've unlocked " .. output)
  95. end
  96. unlocked_cache[name] = unlocked
  97. player:get_meta():set_string("crafting:unlocked", write_json_dictionary(unlocked))
  98. end
  99. function crafting.get_recipe(id)
  100. return crafting.recipes_by_id[id]
  101. end
  102. function crafting.get_all(type, level, item_hash, unlocked)
  103. assert(crafting.recipes[type], "No such craft type!")
  104. local results = {}
  105. for _, recipe in pairs(crafting.recipes[type]) do
  106. local craftable = true
  107. if recipe.level <= level and (recipe.always_known or unlocked[recipe.output]) then
  108. -- Check all ingredients are available
  109. local items = {}
  110. for _, item in pairs(recipe.items) do
  111. item = ItemStack(item)
  112. local needed_count = item:get_count()
  113. local available_count = item_hash[item:get_name()] or 0
  114. if available_count < needed_count then
  115. craftable = false
  116. end
  117. items[#items + 1] = {
  118. name = item:get_name(),
  119. have = available_count,
  120. need = needed_count,
  121. }
  122. end
  123. results[#results + 1] = {
  124. recipe = recipe,
  125. items = items,
  126. craftable = craftable,
  127. }
  128. end
  129. end
  130. return results
  131. end
  132. function crafting.set_item_hashes_from_list(inv, listname, item_hash)
  133. for _, stack in pairs(inv:get_list(listname)) do
  134. if not stack:is_empty() then
  135. local itemname = stack:get_name()
  136. item_hash[itemname] = (item_hash[itemname] or 0) + stack:get_count()
  137. local def = minetest.registered_items[itemname]
  138. if def and def.groups then
  139. for groupname, _ in pairs(def.groups) do
  140. local group = "group:" .. groupname
  141. item_hash[group] = (item_hash[group] or 0) + stack:get_count()
  142. end
  143. end
  144. end
  145. end
  146. end
  147. function crafting.get_all_for_player(player, type, level)
  148. local unlocked = crafting.get_unlocked(player:get_player_name())
  149. -- Get items hashed
  150. local item_hash = {}
  151. crafting.set_item_hashes_from_list(player:get_inventory(), "main", item_hash)
  152. return crafting.get_all(type, level, item_hash, unlocked)
  153. end
  154. function crafting.can_craft(name, type, level, recipe)
  155. local unlocked = crafting.get_unlocked(name)
  156. return recipe.type == type and recipe.level <= level and
  157. (recipe.always_known or unlocked[recipe.output])
  158. end
  159. local function give_all_to_player(inv, list)
  160. for _, item in pairs(list) do
  161. inv:add_item("main", item)
  162. end
  163. end
  164. function crafting.find_required_items(inv, listname, recipe)
  165. local items = {}
  166. for _, item in pairs(recipe.items) do
  167. item = ItemStack(item)
  168. local itemname = item:get_name()
  169. if item:get_name():sub(1, 6) == "group:" then
  170. local groupname = itemname:sub(7, #itemname)
  171. local required = item:get_count()
  172. -- Find stacks in group
  173. for i = 1, inv:get_size(listname) do
  174. local stack = inv:get_stack(listname, i)
  175. -- Is it in group?
  176. local def = minetest.registered_items[stack:get_name()]
  177. if def and def.groups and def.groups[groupname] then
  178. stack = ItemStack(stack)
  179. if stack:get_count() > required then
  180. stack:set_count(required)
  181. end
  182. items[#items + 1] = stack
  183. required = required - stack:get_count()
  184. if required == 0 then
  185. break
  186. end
  187. end
  188. end
  189. if required > 0 then
  190. return nil
  191. end
  192. else
  193. if inv:contains_item(listname, item) then
  194. items[#items + 1] = item
  195. else
  196. return nil
  197. end
  198. end
  199. end
  200. return items
  201. end
  202. function crafting.has_required_items(inv, listname, recipe)
  203. return crafting.find_required_items(inv, listname, recipe) ~= nil
  204. end
  205. function crafting.register_on_craft(func)
  206. table.insert(crafting.registered_on_crafts, func)
  207. end
  208. function crafting.perform_craft(name, inv, listname, outlistname, recipe)
  209. local items = crafting.find_required_items(inv, listname, recipe)
  210. if not items then
  211. return false
  212. end
  213. -- Take items
  214. local taken = {}
  215. for _, item in pairs(items) do
  216. item = ItemStack(item)
  217. local took = inv:remove_item(listname, item)
  218. taken[#taken + 1] = took
  219. if took:get_count() ~= item:get_count() then
  220. minetest.log("error", "Unexpected lack of items in inventory")
  221. give_all_to_player(inv, taken)
  222. return false
  223. end
  224. end
  225. for i=1, #crafting.registered_on_crafts do
  226. crafting.registered_on_crafts[i](name, recipe)
  227. end
  228. -- Add output
  229. inv:add_item(outlistname, recipe.output)
  230. return true
  231. end
  232. local function to_hex(str)
  233. return (str:gsub('.', function (c)
  234. return string.format('%02X', string.byte(c))
  235. end))
  236. end
  237. function crafting.calc_inventory_list_hash(inv, listname)
  238. local str = ""
  239. for _, stack in pairs(inv:get_list(listname)) do
  240. str = str .. stack:get_name() .. stack:get_count()
  241. end
  242. return minetest.sha1(to_hex(str))
  243. end