guide.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. local MP = minetest.get_modpath(minetest.get_current_modname())
  2. local S, NS = dofile(MP.."/intllib.lua")
  3. -- TODO: support to put a guide formspec inside a tab that's part of a larger set of formspecs
  4. simplecrafting_lib.guide = {}
  5. simplecrafting_lib.guide.playerdata = {}
  6. simplecrafting_lib.guide.groups = {}
  7. simplecrafting_lib.guide.guide_def = {}
  8. local default_width = 10
  9. local default_height = 6
  10. local default_recipes_per_page = 4
  11. local function get_guide_def(craft_type)
  12. return simplecrafting_lib.guide.guide_def[craft_type] or {}
  13. end
  14. -- Explicitly set examples for some common input item groups
  15. -- Other mods can also add explicit items like this if they wish
  16. -- Groups list isn't populated with "guessed" examples until
  17. -- after initialization, when all other mods are already loaded
  18. if minetest.get_modpath("default") then
  19. simplecrafting_lib.guide.groups["wood"] = "default:wood"
  20. simplecrafting_lib.guide.groups["stick"] = "default:stick"
  21. simplecrafting_lib.guide.groups["tree"] = "default:tree"
  22. simplecrafting_lib.guide.groups["stone"] = "default:stone"
  23. simplecrafting_lib.guide.groups["sand"] = "default:sand"
  24. simplecrafting_lib.guide.groups["soil"] = "default:dirt"
  25. end
  26. if minetest.get_modpath("wool") then
  27. simplecrafting_lib.guide.groups["wool"] = "wool:white"
  28. end
  29. -- internationalization boilerplate
  30. local MP = minetest.get_modpath(minetest.get_current_modname())
  31. local S, NS = dofile(MP.."/intllib.lua")
  32. local function initialize_group_examples()
  33. -- finds an example item for every group that does not already have one defined
  34. for item, def in pairs(minetest.registered_items) do
  35. for group, _ in pairs(def.groups) do
  36. if not simplecrafting_lib.guide.groups[group] then
  37. simplecrafting_lib.guide.groups[group] = item
  38. end
  39. end
  40. end
  41. end
  42. simplecrafting_lib.register_postprocessing_callback(initialize_group_examples) -- run once after server has loaded all other mods
  43. -- splits a string into an array of substrings based on a delimiter
  44. local function split(str, delimiter)
  45. local result = {}
  46. for match in (str..delimiter):gmatch("(.-)"..delimiter) do
  47. table.insert(result, match)
  48. end
  49. return result
  50. end
  51. local function find_multi_group(multigroup)
  52. if simplecrafting_lib.guide.groups[multigroup] then
  53. return simplecrafting_lib.guide.groups[multigroup]
  54. end
  55. local target_groups = split(multigroup, ",")
  56. for item, def in pairs(minetest.registered_items) do
  57. local overall_found = true
  58. for _, target_group in pairs(target_groups) do
  59. local one_group_found = false
  60. for group, _ in pairs(def.groups) do
  61. if group == target_group then
  62. one_group_found = true
  63. break
  64. end
  65. end
  66. if not one_group_found then
  67. overall_found = false
  68. break
  69. end
  70. end
  71. if overall_found then
  72. simplecrafting_lib.guide.groups[multigroup] = item
  73. return item
  74. end
  75. end
  76. return nil
  77. end
  78. -- Used for alphabetizing an array of items by description
  79. local function compare_items_by_desc(item1, item2)
  80. local def1 = minetest.registered_items[item1]
  81. local def2 = minetest.registered_items[item2]
  82. return def1.description < def2.description
  83. end
  84. local function get_output_list(craft_type, player_name, search_filter)
  85. local guide_def = get_guide_def(craft_type)
  86. local is_recipe_included = guide_def.is_recipe_included
  87. local outputs = {}
  88. for item, recipes in pairs(simplecrafting_lib.type[craft_type].recipes_by_out) do
  89. -- if the item is not excluded from the crafting guide entirely by group membership
  90. for _, recipe in ipairs(recipes) do
  91. -- and there is no is_recipe_included callback, or at least one recipe passes the is_recipe_included callback
  92. if ((is_recipe_included == nil) or (is_recipe_included(recipe, player_name)))
  93. and (search_filter == "" or string.find(item, search_filter))
  94. then
  95. -- then this output is included in this guide
  96. table.insert(outputs, item)
  97. break
  98. end
  99. end
  100. end
  101. -- TODO: sorting option
  102. table.sort(outputs)
  103. return outputs
  104. end
  105. local function get_playerdata(craft_type, player_name)
  106. if not simplecrafting_lib.guide.playerdata[craft_type] then
  107. simplecrafting_lib.guide.playerdata[craft_type] = {}
  108. end
  109. if simplecrafting_lib.guide.playerdata[craft_type][player_name] then
  110. return simplecrafting_lib.guide.playerdata[craft_type][player_name]
  111. end
  112. simplecrafting_lib.guide.playerdata[craft_type][player_name] = {["input_page"] = 0, ["output_page"] = 0, ["selection"] = 0, ["search"] = ""}
  113. return simplecrafting_lib.guide.playerdata[craft_type][player_name]
  114. end
  115. simplecrafting_lib.make_guide_formspec = function(craft_type, player_name)
  116. local guide_def = get_guide_def(craft_type)
  117. local width = guide_def.output_width or default_width
  118. local height = guide_def.output_height or default_height
  119. local recipes_per_page = guide_def.recipes_per_page or default_recipes_per_page
  120. local is_recipe_included = guide_def.is_recipe_included
  121. local groups = simplecrafting_lib.guide.groups
  122. local playerdata = get_playerdata(craft_type, player_name)
  123. local outputs = get_output_list(craft_type, player_name, playerdata.search)
  124. local description = simplecrafting_lib.get_crafting_info(craft_type).description
  125. local displace_y = 0
  126. if description then
  127. displace_y = 0.5
  128. end
  129. local size = "size[" .. width .. "," .. height + recipes_per_page + 0.7 + displace_y .."]"
  130. local formspec = {
  131. size,
  132. }
  133. if description then
  134. -- title of the page
  135. table.insert(formspec, "label[" .. width/2-0.5 .. ",0;"..description.."]")
  136. end
  137. if minetest.get_modpath("default") then
  138. table.insert(formspec, default.gui_bg .. default.gui_bg_img .. default.gui_slots)
  139. end
  140. local buttons_per_page = width*height
  141. -- products that this craft guide can show recipes for
  142. for i = 1, buttons_per_page do
  143. local current_item_index = i + playerdata.output_page * buttons_per_page
  144. local current_item = outputs[current_item_index]
  145. if current_item then
  146. table.insert(formspec, "item_image_button[" ..
  147. (i-1)%width .. "," .. math.floor((i-1)/width) + displace_y ..
  148. ";1,1;" .. current_item .. ";product_" .. current_item_index ..
  149. ";]")
  150. else
  151. table.insert(formspec, "item_image_button[" ..
  152. (i-1)%width .. "," .. math.floor((i-1)/width) + displace_y ..
  153. ";1,1;;;]")
  154. end
  155. end
  156. local middle_buttons_height = height + displace_y
  157. -- search bar
  158. table.insert(formspec,
  159. "field_close_on_enter[search_filter;false]"
  160. .."field[".. 0.3 ..",".. middle_buttons_height+0.25 ..";2.5,1;search_filter;;"..minetest.formspec_escape(playerdata.search).."]"
  161. .."image_button[".. 2.5 ..",".. middle_buttons_height ..";0.8,0.8;crafting_guide_search.png;apply_search;]"
  162. .."tooltip[search_filter;"..S("Enter substring to search item identifiers for").."]"
  163. .."tooltip[apply_search;"..S("Apply search to outputs").."]"
  164. )
  165. -- If there are more possible outputs that can be displayed at once, show next/previous buttons for the output list
  166. if #outputs > buttons_per_page then
  167. table.insert(formspec,
  168. "image_button[".. 3.3 ..",".. middle_buttons_height ..";0.8,0.8;simplecrafting_lib_prev.png;previous_output;]"
  169. .."label[" .. 3.95 .. "," .. middle_buttons_height .. ";".. playerdata.output_page + 1 .."]"
  170. .."image_button[".. 4.1 ..",".. middle_buttons_height ..";0.8,0.8;simplecrafting_lib_next.png;next_output;]"
  171. .."tooltip[next_output;"..S("Next page of outputs").."]"
  172. .."tooltip[previous_output;"..S("Previous page of outputs").."]"
  173. )
  174. end
  175. if playerdata.selection <= 0 or playerdata.selection > #outputs then
  176. -- No output selected
  177. table.insert(formspec, "item_image[" .. 5 .. "," .. middle_buttons_height .. ";1,1;]")
  178. playerdata.selection = 0
  179. else
  180. -- Output selected, show an image of it
  181. table.insert(formspec, "item_image[" .. 5 .. "," .. middle_buttons_height .. ";1,1;" ..
  182. outputs[playerdata.selection] .. "]")
  183. end
  184. -- Everything below here is for displaying recipes for the selected output
  185. local recipes
  186. if playerdata.selection > 0 then
  187. -- Get a list of the recipes we'll want to display
  188. if is_recipe_included then
  189. recipes = {}
  190. for _, recipe in ipairs(simplecrafting_lib.type[craft_type].recipes_by_out[outputs[playerdata.selection]]) do
  191. if is_recipe_included(recipe) then
  192. table.insert(recipes, recipe)
  193. end
  194. end
  195. else
  196. recipes = simplecrafting_lib.type[craft_type].recipes_by_out[outputs[playerdata.selection]]
  197. end
  198. end
  199. if recipes == nil then
  200. -- No recipes to display.
  201. return table.concat(formspec)
  202. end
  203. local last_page = math.floor((#recipes-1)/recipes_per_page)
  204. local next_input = "next_input"
  205. if playerdata.input_page >= last_page then
  206. playerdata.input_page = last_page
  207. end
  208. if playerdata.input_page == last_page then
  209. next_input = "" -- disable the next_input button, we're on the last page.
  210. end
  211. if #recipes > recipes_per_page then
  212. table.insert(formspec,
  213. "image_button[".. width-1.6 ..",".. middle_buttons_height ..";0.8,0.8;simplecrafting_lib_prev.png;previous_input;]"
  214. .."label[" .. width-0.95 .. "," .. middle_buttons_height .. ";".. playerdata.input_page + 1 .."]"
  215. .."image_button[".. width-0.8 ..",".. middle_buttons_height ..";0.8,0.8;simplecrafting_lib_next.png;next_input;]"
  216. .."tooltip[next_input;"..S("Next page of recipes for this output").."]"
  217. .."tooltip[previous_input;"..S("Previous page of recipes for this output").."]"
  218. )
  219. end
  220. local x_out = 0
  221. local y_out = middle_buttons_height + 1
  222. local recipe_button_count = 1
  223. for i = 1, recipes_per_page do
  224. local recipe = recipes[i + playerdata.input_page * recipes_per_page]
  225. if not recipe then break end
  226. local recipe_formspec = {}
  227. -------------------------------- Inputs
  228. for input, count in pairs(recipe.input) do
  229. if string.match(input, ":") then
  230. local itemdef = minetest.registered_items[input]
  231. local itemdesc = input
  232. if itemdef then
  233. itemdesc = itemdef.description
  234. end
  235. table.insert(recipe_formspec, "item_image_button["..x_out..","..y_out..";1,1;"..input..";recipe_button_"..recipe_button_count..";\n\n "..count.."]"
  236. .."tooltip[recipe_button_"..recipe_button_count..";"..count.." "..itemdesc.."]")
  237. elseif not string.match(input, ",") then
  238. local itemdesc = "Group: "..input
  239. table.insert(recipe_formspec, "item_image_button["..x_out..","..y_out..";1,1;"..groups[input]..";recipe_button_"..recipe_button_count..";\n G\n "..count.."]"
  240. .."tooltip[recipe_button_"..recipe_button_count..";"..count.." "..itemdesc.."]")
  241. else
  242. -- it's one of those weird multi-group items, like dyes.
  243. local multimatch = find_multi_group(input)
  244. local itemdesc = "Groups: "..input
  245. table.insert(recipe_formspec, "item_image_button["..x_out..","..y_out..";1,1;"..multimatch..";recipe_button_"..recipe_button_count..";\n G\n "..count.."]"
  246. .."tooltip[recipe_button_"..recipe_button_count..";"..count.." "..itemdesc.."]")
  247. end
  248. recipe_button_count = recipe_button_count + 1
  249. x_out = x_out + 1
  250. end
  251. -------------------------------- Outputs
  252. x_out = width - 1
  253. local output_name = recipe.output:get_name()
  254. local output_count = recipe.output:get_count()
  255. local itemdesc = minetest.registered_items[output_name].description -- we know this item exists otherwise a recipe wouldn't have been found
  256. table.insert(recipe_formspec, "item_image_button["..x_out..","..y_out..";1,1;"..output_name..";recipe_button_"..recipe_button_count..";\n\n "..output_count.."]"
  257. .."tooltip[recipe_button_"..recipe_button_count..";"..output_count.." "..itemdesc.."]")
  258. recipe_button_count = recipe_button_count + 1
  259. x_out = x_out - 1
  260. if recipe.returns then
  261. for returns, count in pairs(recipe.returns) do
  262. local itemdef = minetest.registered_items[returns]
  263. local itemdesc = returns
  264. if itemdef then
  265. itemdesc = itemdef.description
  266. end
  267. table.insert(recipe_formspec, "item_image_button["..x_out..","..y_out..";1,1;"..returns..";recipe_button_"..recipe_button_count..";\n\n "..count.."]"
  268. .."tooltip[recipe_button_"..recipe_button_count..";"..count.." "..itemdesc.."]")
  269. recipe_button_count = recipe_button_count + 1
  270. x_out = x_out - 1
  271. end
  272. end
  273. if minetest.get_modpath("default") then
  274. table.insert(recipe_formspec, "image["..x_out..","..y_out..";1,1;gui_furnace_arrow_bg.png^[transformR270]")
  275. else
  276. table.insert(recipe_formspec, "label["..x_out..","..y_out..";=>]")
  277. end
  278. x_out = 0
  279. y_out = y_out + 1
  280. for _, button in pairs(recipe_formspec) do
  281. table.insert(formspec, button)
  282. end
  283. end
  284. if guide_def.append_to_formspec then
  285. table.insert(formspec, guide_def.append_to_formspec)
  286. end
  287. return table.concat(formspec), size
  288. end
  289. simplecrafting_lib.handle_guide_receive_fields = function(craft_type, player, fields)
  290. local guide_def = get_guide_def(craft_type)
  291. local width = guide_def.output_width or default_width
  292. local height = guide_def.output_height or default_height
  293. local player_name = player:get_player_name()
  294. local playerdata = get_playerdata(craft_type, player_name)
  295. local outputs = get_output_list(craft_type, player_name, playerdata.search)
  296. local stay_in_formspec = false
  297. for field, value in pairs(fields) do
  298. if field == "previous_output" and playerdata.output_page > 0 then
  299. playerdata.output_page = playerdata.output_page - 1
  300. minetest.sound_play("paperflip2", {to_player=player:get_player_name(), gain = 1.0})
  301. stay_in_formspec = true
  302. elseif field == "next_output" and playerdata.output_page < #outputs/(width*height)-1 then
  303. playerdata.output_page = playerdata.output_page + 1
  304. minetest.sound_play("paperflip1", {to_player=player:get_player_name(), gain = 1.0})
  305. stay_in_formspec = true
  306. elseif field == "previous_input" and playerdata.input_page > 0 then
  307. playerdata.input_page = playerdata.input_page - 1
  308. minetest.sound_play("paperflip2", {to_player=player:get_player_name(), gain = 1.0})
  309. stay_in_formspec = true
  310. elseif field == "next_input" then -- we don't know how many recipes there are, let make_formspec sanitize this
  311. playerdata.input_page = playerdata.input_page + 1
  312. minetest.sound_play("paperflip1", {to_player=player:get_player_name(), gain = 1.0})
  313. stay_in_formspec = true
  314. elseif string.sub(field, 1, 8) == "product_" then
  315. playerdata.input_page = 0
  316. playerdata.selection = tonumber(string.sub(field, 9))
  317. minetest.sound_play("paperflip1", {to_player=player:get_player_name(), gain = 1.0})
  318. stay_in_formspec = true
  319. elseif field == "search_filter" then
  320. value = string.lower(value)
  321. if playerdata.search ~= value then
  322. playerdata.search = value
  323. playerdata.output_page = 0
  324. playerdata.input_page = 0
  325. playerdata.selection = 0
  326. end
  327. elseif field == "apply_search" or fields.key_enter_field == "search_filter"then
  328. stay_in_formspec = true
  329. elseif field == "quit" then
  330. if playerdata.on_exit then
  331. playerdata.on_exit()
  332. end
  333. return false
  334. end
  335. end
  336. return stay_in_formspec
  337. end
  338. minetest.register_on_player_receive_fields(function(player, formname, fields)
  339. if string.sub(formname, 1, 30) ~= "simplecrafting_lib:craftguide_" then return false end
  340. local craft_type = string.sub(formname, 31)
  341. if simplecrafting_lib.handle_guide_receive_fields(craft_type, player, fields) then
  342. minetest.show_formspec(player:get_player_name(),
  343. "simplecrafting_lib:craftguide_"..craft_type,
  344. simplecrafting_lib.make_guide_formspec(craft_type,player:get_player_name())
  345. )
  346. end
  347. return true
  348. end)
  349. simplecrafting_lib.show_crafting_guide = function(craft_type, user, on_exit)
  350. if simplecrafting_lib.type[craft_type] then
  351. get_playerdata(craft_type, user:get_player_name()).on_exit = on_exit
  352. minetest.show_formspec(user:get_player_name(),
  353. "simplecrafting_lib:craftguide_"..craft_type,
  354. simplecrafting_lib.make_guide_formspec(craft_type, user:get_player_name())
  355. )
  356. else
  357. minetest.chat_send_player(user:get_player_name(), "Unable to show crafting guide for " .. craft_type .. ", it has no recipes registered.")
  358. end
  359. end
  360. -- defines some parameters regarding how the formspec of the guide for a given craft_type is displayed.
  361. -- guide_def
  362. -- {
  363. -- output_width = 10
  364. -- output_height = 6
  365. -- recipes_per_page = 4
  366. -- append_to_formspec = string
  367. -- is_recipe_included = function(recipe, player_name) -- return true to include this recipe in the guide, if not defined then all recipes are included
  368. -- }
  369. simplecrafting_lib.set_crafting_guide_def = function(craft_type, guide_def)
  370. simplecrafting_lib.guide.guide_def[craft_type] = guide_def
  371. end
  372. -- creates a basic crafting guide item
  373. -- guide_item_def has many options.
  374. -- {
  375. -- description = string description the item will get. Defaults to "<description of craft type> Recipes"
  376. -- inventory_image = inventory image to be used with this item. Defaults to the book texture included with simplecrafting_lib
  377. -- guide_color = ColorString. If defined, the inventory image will be tinted with this color.
  378. -- wield_image = image to be used when wielding this item. Defaults to inventory image.
  379. -- groups = groups this item will belong to. Defaults to {book = 1}
  380. -- stack_max = maximum stack size. Defaults to 1.
  381. -- wield_scale = scale of wield_image, defaults to nil (same as standard craftitem def)
  382. -- copy_item_to_book = an item name string (eg, "workshops:smelter"). If the default mod is installed, a recipe will be generated that combines a default:book with copy_item_to_book and returns this guide and copy_item_to_book. In this manner the player can only get a handy portable reference guide if they are already in possession of the thing that the guide is used with. If copy_item_to_book is not defined then no crafting recipe is generated for this guide.
  383. -- }
  384. simplecrafting_lib.register_crafting_guide_item = function(item_name, craft_type, guide_item_def)
  385. local description
  386. if guide_item_def.description then
  387. description = guide_item_def.description
  388. elseif simplecrafting_lib.get_crafting_info(craft_type).description then
  389. description = S("@1 Recipes", simplecrafting_lib.get_crafting_info(craft_type).description)
  390. else
  391. description = S("@1 Recipes", craft_type)
  392. end
  393. local inventory_image
  394. if guide_item_def.inventory_image then
  395. inventory_image = guide_item_def.inventory_image
  396. if guide_item_def.guide_color then
  397. inventory_image = inventory_image .. "^[multiply:" .. guide_item_def.guide_color
  398. end
  399. elseif guide_item_def.guide_color then
  400. inventory_image = "crafting_guide_cover.png^[multiply:" .. guide_item_def.guide_color .. "^crafting_guide_contents.png"
  401. else
  402. inventory_image = "crafting_guide_cover.png^crafting_guide_contents.png"
  403. end
  404. minetest.register_craftitem(item_name, {
  405. description = description,
  406. inventory_image = inventory_image,
  407. wield_image = guide_item_def.wield_image or inventory_image,
  408. wield_scale = guide_item_def.wield_scale,
  409. stack_max = guide_item_def.stack_max or 1,
  410. groups = guide_item_def.groups or {book = 1},
  411. on_use = function(itemstack, user)
  412. simplecrafting_lib.show_crafting_guide(craft_type, user)
  413. end,
  414. })
  415. if guide_item_def.copy_item_to_book and minetest.get_modpath("default") then
  416. minetest.register_craft({
  417. output = item_name,
  418. type = "shapeless",
  419. recipe = {guide_item_def.copy_item_to_book, "default:book"},
  420. replacements = {{guide_item_def.copy_item_to_book, guide_item_def.copy_item_to_book}}
  421. })
  422. end
  423. end