The most comprehensive Crafting Guide on Minetest https://content.minetest.net/packages/jp/craftguide/
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.

2120 lines
48KB

  1. craftguide = {}
  2. -- Caches
  3. local pdata = {}
  4. local init_items = {}
  5. local searches = {}
  6. local recipes_cache = {}
  7. local usages_cache = {}
  8. local fuel_cache = {}
  9. local toolrepair
  10. local progressive_mode = core.settings:get_bool "craftguide_progressive_mode"
  11. local sfinv_only = core.settings:get_bool "craftguide_sfinv_only" and rawget(_G, "sfinv")
  12. local autocache = core.settings:get_bool "craftguide_autocache"
  13. local http = core.request_http_api()
  14. local storage = core.get_mod_storage()
  15. local singleplayer = core.is_singleplayer()
  16. local reg_items = core.registered_items
  17. local reg_tools = core.registered_tools
  18. local reg_aliases = core.registered_aliases
  19. local log = core.log
  20. local after = core.after
  21. local clr = core.colorize
  22. local parse_json = core.parse_json
  23. local write_json = core.write_json
  24. local chat_send = core.chat_send_player
  25. local show_formspec = core.show_formspec
  26. local globalstep = core.register_globalstep
  27. local on_shutdown = core.register_on_shutdown
  28. local get_players = core.get_connected_players
  29. local get_craft_result = core.get_craft_result
  30. local on_joinplayer = core.register_on_joinplayer
  31. local get_all_recipes = core.get_all_craft_recipes
  32. local register_command = core.register_chatcommand
  33. local get_player_by_name = core.get_player_by_name
  34. local slz, dslz = core.serialize, core.deserialize
  35. local on_mods_loaded = core.register_on_mods_loaded
  36. local on_leaveplayer = core.register_on_leaveplayer
  37. local get_player_info = core.get_player_information
  38. local on_receive_fields = core.register_on_player_receive_fields
  39. local ESC = core.formspec_escape
  40. local S = core.get_translator "craftguide"
  41. local ES = function(...)
  42. return ESC(S(...))
  43. end
  44. local maxn, sort, concat, copy, insert, remove =
  45. table.maxn, table.sort, table.concat, table.copy,
  46. table.insert, table.remove
  47. local fmt, find, gmatch, match, sub, split, upper, lower =
  48. string.format, string.find, string.gmatch, string.match,
  49. string.sub, string.split, string.upper, string.lower
  50. local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
  51. local pairs, next, type, tostring, unpack = pairs, next, type, tostring, unpack
  52. local vec_add, vec_mul = vector.add, vector.multiply
  53. local FORMSPEC_MINIMAL_VERSION = 3
  54. local ROWS = 9
  55. local LINES = sfinv_only and 5 or 9
  56. local IPP = ROWS * LINES
  57. local WH_LIMIT = 8
  58. local XOFFSET = sfinv_only and 3.83 or 11.2
  59. local YOFFSET = sfinv_only and 4.9 or 1
  60. local PNG = {
  61. bg = "craftguide_bg.png",
  62. bg_full = "craftguide_bg_full.png",
  63. search = "craftguide_search_icon.png",
  64. clear = "craftguide_clear_icon.png",
  65. prev = "craftguide_next_icon.png^\\[transformFX",
  66. next = "craftguide_next_icon.png",
  67. arrow = "craftguide_arrow.png",
  68. fire = "craftguide_fire.png",
  69. fire_anim = "craftguide_fire_anim.png",
  70. book = "craftguide_book.png",
  71. sign = "craftguide_sign.png",
  72. nothing = "craftguide_no.png",
  73. selected = "craftguide_selected.png",
  74. furnace_anim = "craftguide_furnace_anim.png",
  75. search_hover = "craftguide_search_icon_hover.png",
  76. clear_hover = "craftguide_clear_icon_hover.png",
  77. prev_hover = "craftguide_next_icon_hover.png^\\[transformFX",
  78. next_hover = "craftguide_next_icon_hover.png",
  79. }
  80. local FMT = {
  81. box = "box[%f,%f;%f,%f;%s]",
  82. label = "label[%f,%f;%s]",
  83. image = "image[%f,%f;%f,%f;%s]",
  84. button = "button[%f,%f;%f,%f;%s;%s]",
  85. tooltip = "tooltip[%f,%f;%f,%f;%s]",
  86. item_image = "item_image[%f,%f;%f,%f;%s]",
  87. image_button = "image_button[%f,%f;%f,%f;%s;%s;%s]",
  88. animated_image = "animated_image[%f,%f;%f,%f;;%s;%u;%u]",
  89. item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]",
  90. arrow = "image_button[%f,%f;0.8,0.8;%s;%s;;;false;%s]",
  91. }
  92. local function get_fs_version(name)
  93. local info = get_player_info(name)
  94. return info and info.formspec_version or 1
  95. end
  96. local function outdated(name)
  97. local fs = fmt([[
  98. size[6.6,1.3]
  99. image[0,0;1,1;%s]
  100. label[1,0;%s]
  101. button_exit[2.8,0.8;1,1;;OK]
  102. ]],
  103. PNG.book,
  104. "Your Minetest client is outdated.\n" ..
  105. "Get the latest version on minetest.net to use the Crafting Guide.")
  106. return show_formspec(name, "craftguide", fs)
  107. end
  108. local function mul_elem(elem, n)
  109. local fstr, elems = "", {}
  110. for i = 1, n do
  111. fstr = fstr .. "%s"
  112. elems[i] = elem
  113. end
  114. return fmt(fstr, unpack(elems))
  115. end
  116. craftguide.group_stereotypes = {
  117. dye = "dye:white",
  118. wool = "wool:white",
  119. wood = "default:wood",
  120. tree = "default:tree",
  121. coal = "default:coal_lump",
  122. vessel = "vessels:glass_bottle",
  123. flower = "flowers:dandelion_yellow",
  124. water_bucket = "bucket:bucket_water",
  125. mesecon_conductor_craftable = "mesecons:wire_00000000_off",
  126. }
  127. local group_names = {
  128. coal = S"Any coal",
  129. wool = S"Any wool",
  130. wood = S"Any wood planks",
  131. sand = S"Any sand",
  132. stick = S"Any stick",
  133. stone = S"Any kind of stone block",
  134. tree = S"Any tree",
  135. vessel = S"Any vessel",
  136. ["color_red,flower"] = S"Any red flower",
  137. ["color_blue,flower"] = S"Any blue flower",
  138. ["color_black,flower"] = S"Any black flower",
  139. ["color_white,flower"] = S"Any white flower",
  140. ["color_green,flower"] = S"Any green flower",
  141. ["color_orange,flower"] = S"Any orange flower",
  142. ["color_yellow,flower"] = S"Any yellow flower",
  143. ["color_violet,flower"] = S"Any violet flower",
  144. ["color_red,dye"] = S"Any red dye",
  145. ["color_blue,dye"] = S"Any blue dye",
  146. ["color_grey,dye"] = S"Any grey dye",
  147. ["color_pink,dye"] = S"Any pink dye",
  148. ["color_cyan,dye"] = S"Any cyan dye",
  149. ["color_black,dye"] = S"Any black dye",
  150. ["color_white,dye"] = S"Any white dye",
  151. ["color_brown,dye"] = S"Any brown dye",
  152. ["color_green,dye"] = S"Any green dye",
  153. ["color_orange,dye"] = S"Any orange dye",
  154. ["color_yellow,dye"] = S"Any yellow dye",
  155. ["color_violet,dye"] = S"Any violet dye",
  156. ["color_magenta,dye"] = S"Any magenta dye",
  157. ["color_dark_grey,dye"] = S"Any dark grey dye",
  158. ["color_dark_green,dye"] = S"Any dark green dye",
  159. }
  160. local function err(str)
  161. return log("error", str)
  162. end
  163. local function msg(name, str)
  164. return chat_send(name, fmt("[craftguide] %s", str))
  165. end
  166. local function is_str(x)
  167. return type(x) == "string"
  168. end
  169. local function true_str(str)
  170. return is_str(str) and str ~= ""
  171. end
  172. local function is_table(x)
  173. return type(x) == "table"
  174. end
  175. local function is_func(x)
  176. return type(x) == "function"
  177. end
  178. local function is_group(item)
  179. return sub(item, 1, 6) == "group:"
  180. end
  181. local function clean_name(item)
  182. if sub(item, 1, 1) == ":" then
  183. item = sub(item, 2)
  184. end
  185. return item
  186. end
  187. local function array_diff(t1, t2)
  188. local hash = {}
  189. for i = 1, #t1 do
  190. local v = t1[i]
  191. hash[v] = true
  192. end
  193. for i = 1, #t2 do
  194. local v = t2[i]
  195. hash[v] = nil
  196. end
  197. local diff, c = {}, 0
  198. for i = 1, #t1 do
  199. local v = t1[i]
  200. if hash[v] then
  201. c = c + 1
  202. diff[c] = v
  203. end
  204. end
  205. return diff
  206. end
  207. local function table_eq(T1, T2)
  208. local avoid_loops = {}
  209. local function recurse(t1, t2)
  210. if type(t1) ~= type(t2) then return end
  211. if not is_table(t1) then
  212. return t1 == t2
  213. end
  214. if avoid_loops[t1] then
  215. return avoid_loops[t1] == t2
  216. end
  217. avoid_loops[t1] = t2
  218. local t2k, t2kv = {}, {}
  219. for k in pairs(t2) do
  220. if is_table(k) then
  221. insert(t2kv, k)
  222. end
  223. t2k[k] = true
  224. end
  225. for k1, v1 in pairs(t1) do
  226. local v2 = t2[k1]
  227. if type(k1) == "table" then
  228. local ok
  229. for i = 1, #t2kv do
  230. local tk = t2kv[i]
  231. if table_eq(k1, tk) and recurse(v1, t2[tk]) then
  232. remove(t2kv, i)
  233. t2k[tk] = nil
  234. ok = true
  235. break
  236. end
  237. end
  238. if not ok then return end
  239. else
  240. if v2 == nil then return end
  241. t2k[k1] = nil
  242. if not recurse(v1, v2) then return end
  243. end
  244. end
  245. if next(t2k) then return end
  246. return true
  247. end
  248. return recurse(T1, T2)
  249. end
  250. local function table_merge(t1, t2, hash)
  251. t1 = t1 or {}
  252. t2 = t2 or {}
  253. if hash then
  254. for k, v in pairs(t2) do
  255. t1[k] = v
  256. end
  257. else
  258. local c = #t1
  259. for i = 1, #t2 do
  260. c = c + 1
  261. t1[c] = t2[i]
  262. end
  263. end
  264. return t1
  265. end
  266. local function table_replace(t, val, new)
  267. for k, v in pairs(t) do
  268. if v == val then
  269. t[k] = new
  270. end
  271. end
  272. end
  273. local craft_types = {}
  274. function craftguide.register_craft_type(name, def)
  275. if not true_str(name) then
  276. return err "craftguide.register_craft_type(): name missing"
  277. end
  278. if not is_str(def.description) then
  279. def.description = ""
  280. end
  281. if not is_str(def.icon) then
  282. def.icon = ""
  283. end
  284. craft_types[name] = def
  285. end
  286. function craftguide.register_craft(def)
  287. local width, c = 0, 0
  288. if true_str(def.url) then
  289. if not http then
  290. return err(fmt([[craftguide.register_craft(): Unable to reach %s.
  291. No HTTP support for this mod: add it to the `secure.http_mods` or
  292. `secure.trusted_mods` setting.]], def.url))
  293. end
  294. http.fetch({url = def.url}, function(result)
  295. if result.succeeded then
  296. local t = parse_json(result.data)
  297. if is_table(t) then
  298. return craftguide.register_craft(t)
  299. end
  300. end
  301. end)
  302. return
  303. end
  304. if not is_table(def) or not next(def) then
  305. return err "craftguide.register_craft(): craft definition missing"
  306. end
  307. if #def > 1 then
  308. for _, v in pairs(def) do
  309. craftguide.register_craft(v)
  310. end
  311. return
  312. end
  313. if def.result then
  314. def.output = def.result -- Backward compatibility
  315. def.result = nil
  316. end
  317. if not true_str(def.output) then
  318. return err "craftguide.register_craft(): output missing"
  319. end
  320. if not is_table(def.items) then
  321. def.items = {}
  322. end
  323. if def.grid then
  324. if not is_table(def.grid) then
  325. def.grid = {}
  326. end
  327. if not is_table(def.key) then
  328. def.key = {}
  329. end
  330. local cp = copy(def.grid)
  331. sort(cp, function(a, b)
  332. return #a > #b
  333. end)
  334. width = #cp[1]
  335. for i = 1, #def.grid do
  336. while #def.grid[i] < width do
  337. def.grid[i] = def.grid[i] .. " "
  338. end
  339. end
  340. for symbol in gmatch(concat(def.grid), ".") do
  341. c = c + 1
  342. def.items[c] = def.key[symbol]
  343. end
  344. else
  345. local items, len = def.items, #def.items
  346. def.items = {}
  347. for i = 1, len do
  348. items[i] = items[i]:gsub(",", ", ")
  349. local rlen = #split(items[i], ",")
  350. if rlen > width then
  351. width = rlen
  352. end
  353. end
  354. for i = 1, len do
  355. while #split(items[i], ",") < width do
  356. items[i] = items[i] .. ", "
  357. end
  358. end
  359. for name in gmatch(concat(items, ","), "[%s%w_:]+") do
  360. c = c + 1
  361. def.items[c] = match(name, "%S+")
  362. end
  363. end
  364. local output = match(def.output, "%S+")
  365. recipes_cache[output] = recipes_cache[output] or {}
  366. def.custom = true
  367. def.width = width
  368. insert(recipes_cache[output], def)
  369. end
  370. local recipe_filters = {}
  371. function craftguide.add_recipe_filter(name, f)
  372. if not true_str(name) then
  373. return err "craftguide.add_recipe_filter(): name missing"
  374. elseif not is_func(f) then
  375. return err "craftguide.add_recipe_filter(): function missing"
  376. end
  377. recipe_filters[name] = f
  378. end
  379. function craftguide.set_recipe_filter(name, f)
  380. if not is_str(name) then
  381. return err "craftguide.set_recipe_filter(): name missing"
  382. elseif not is_func(f) then
  383. return err "craftguide.set_recipe_filter(): function missing"
  384. end
  385. recipe_filters = {[name] = f}
  386. end
  387. function craftguide.remove_recipe_filter(name)
  388. recipe_filters[name] = nil
  389. end
  390. function craftguide.get_recipe_filters()
  391. return recipe_filters
  392. end
  393. local function apply_recipe_filters(recipes, player)
  394. for _, filter in pairs(recipe_filters) do
  395. recipes = filter(recipes, player)
  396. end
  397. return recipes
  398. end
  399. local search_filters = {}
  400. function craftguide.add_search_filter(name, f)
  401. if not true_str(name) then
  402. return err "craftguide.add_search_filter(): name missing"
  403. elseif not is_func(f) then
  404. return err "craftguide.add_search_filter(): function missing"
  405. end
  406. search_filters[name] = f
  407. end
  408. function craftguide.remove_search_filter(name)
  409. search_filters[name] = nil
  410. end
  411. function craftguide.get_search_filters()
  412. return search_filters
  413. end
  414. local function item_has_groups(item_groups, groups)
  415. for i = 1, #groups do
  416. local group = groups[i]
  417. if (item_groups[group] or 0) == 0 then return end
  418. end
  419. return true
  420. end
  421. local function extract_groups(str)
  422. return split(sub(str, 7), ",")
  423. end
  424. local function item_in_recipe(item, recipe)
  425. local clean_item = reg_aliases[item] or item
  426. for _, recipe_item in pairs(recipe.items) do
  427. local clean_recipe_item = reg_aliases[recipe_item] or recipe_item
  428. if clean_recipe_item == clean_item then
  429. return true
  430. end
  431. end
  432. end
  433. local function groups_item_in_recipe(item, recipe)
  434. local def = reg_items[item]
  435. if not def then return end
  436. local item_groups = def.groups
  437. for _, recipe_item in pairs(recipe.items) do
  438. if is_group(recipe_item) then
  439. local groups = extract_groups(recipe_item)
  440. if item_has_groups(item_groups, groups) then
  441. local usage = copy(recipe)
  442. table_replace(usage.items, recipe_item, item)
  443. return usage
  444. end
  445. end
  446. end
  447. end
  448. local function get_filtered_items(player, data)
  449. local items, known, c = {}, 0, 0
  450. for i = 1, #init_items do
  451. local item = init_items[i]
  452. local recipes = recipes_cache[item]
  453. local usages = usages_cache[item]
  454. recipes = #apply_recipe_filters(recipes or {}, player)
  455. usages = #apply_recipe_filters(usages or {}, player)
  456. if recipes > 0 or usages > 0 then
  457. c = c + 1
  458. items[c] = item
  459. if data then
  460. known = known + recipes + usages
  461. end
  462. end
  463. end
  464. if data then
  465. data.known_recipes = known
  466. end
  467. return items
  468. end
  469. local function get_usages(item)
  470. local usages, c = {}, 0
  471. for _, recipes in pairs(recipes_cache) do
  472. for i = 1, #recipes do
  473. local recipe = recipes[i]
  474. if item_in_recipe(item, recipe) then
  475. c = c + 1
  476. usages[c] = recipe
  477. else
  478. recipe = groups_item_in_recipe(item, recipe)
  479. if recipe then
  480. c = c + 1
  481. usages[c] = recipe
  482. end
  483. end
  484. end
  485. end
  486. if fuel_cache[item] then
  487. usages[#usages + 1] = {
  488. type = "fuel",
  489. items = {item},
  490. replacements = fuel_cache.replacements[item],
  491. }
  492. end
  493. return usages
  494. end
  495. local function get_burntime(item)
  496. return get_craft_result{method = "fuel", items = {item}}.time
  497. end
  498. local function cache_fuel(item)
  499. local burntime = get_burntime(item)
  500. if burntime > 0 then
  501. fuel_cache[item] = burntime
  502. end
  503. end
  504. local function cache_usages(item)
  505. local usages = get_usages(item)
  506. if #usages > 0 then
  507. usages_cache[item] = table_merge(usages, usages_cache[item] or {})
  508. end
  509. end
  510. local function cache_recipes(output)
  511. local recipes = get_all_recipes(output) or {}
  512. if #recipes > 0 then
  513. recipes_cache[output] = recipes
  514. end
  515. end
  516. local function get_recipes(item, data, player)
  517. local clean_item = reg_aliases[item] or item
  518. local recipes = recipes_cache[clean_item]
  519. local usages = usages_cache[clean_item]
  520. if recipes then
  521. recipes = apply_recipe_filters(recipes, player)
  522. end
  523. local no_recipes = not recipes or #recipes == 0
  524. if no_recipes and not usages then
  525. return
  526. elseif sfinv_only then
  527. if usages and no_recipes then
  528. data.show_usages = true
  529. elseif recipes and not usages then
  530. data.show_usages = nil
  531. end
  532. end
  533. if not sfinv_only or (sfinv_only and data.show_usages) then
  534. usages = apply_recipe_filters(usages, player)
  535. end
  536. local no_usages = not usages or #usages == 0
  537. return not no_recipes and recipes or nil,
  538. not no_usages and usages or nil
  539. end
  540. local function groups_to_items(groups, get_all)
  541. if not get_all and #groups == 1 then
  542. local group = groups[1]
  543. local def_gr = "default:" .. group
  544. local stereotypes = craftguide.group_stereotypes
  545. local stereotype = stereotypes and stereotypes[group]
  546. if stereotype then
  547. return stereotype
  548. elseif reg_items[def_gr] then
  549. return def_gr
  550. end
  551. end
  552. local names = {}
  553. for name, def in pairs(reg_items) do
  554. if item_has_groups(def.groups, groups) then
  555. if get_all then
  556. names[#names + 1] = name
  557. else
  558. return name
  559. end
  560. end
  561. end
  562. return get_all and names or ""
  563. end
  564. local function repairable(tool)
  565. local def = reg_tools[tool]
  566. return toolrepair and def and def.groups and def.groups.disable_repair ~= 1
  567. end
  568. local function is_fav(data)
  569. local fav, i
  570. for j = 1, #data.favs do
  571. if data.favs[j] == data.query_item then
  572. fav = true
  573. i = j
  574. break
  575. end
  576. end
  577. return fav, i
  578. end
  579. local function check_newline(def)
  580. return def and def.description and find(def.description, "\n")
  581. end
  582. local function get_desc(name)
  583. if sub(name, 1, 1) == "_" then
  584. name = sub(name, 2)
  585. end
  586. local def = reg_items[name]
  587. return def and (match(def.description, "%)([%w%s]*)") or def.description) or
  588. (def and match(name, ":.*"):gsub("%W%l", upper):sub(2):gsub("_", " ") or
  589. S("Unknown Item (@1)", name))
  590. end
  591. local function get_tooltip(name, info)
  592. local tooltip
  593. if info.groups then
  594. sort(info.groups)
  595. tooltip = group_names[concat(info.groups, ",")]
  596. if not tooltip then
  597. local groupstr, c = {}, 0
  598. for i = 1, #info.groups do
  599. c = c + 1
  600. groupstr[c] = clr("#ff0", info.groups[i])
  601. end
  602. groupstr = concat(groupstr, ", ")
  603. tooltip = S("Any item belonging to the group(s): @1", groupstr)
  604. end
  605. else
  606. tooltip = get_desc(name)
  607. end
  608. local function add(str)
  609. return fmt("%s\n%s", tooltip, str)
  610. end
  611. if info.cooktime then
  612. tooltip = add(S("Cooking time: @1", clr("#ff0", info.cooktime)))
  613. end
  614. if info.burntime then
  615. tooltip = add(S("Burning time: @1", clr("#ff0", info.burntime)))
  616. end
  617. if info.replace then
  618. local desc = clr("#ff0", get_desc(info.replace))
  619. if info.cooktime then
  620. tooltip = add(S("Replaced by @1 on smelting", desc))
  621. elseif info.burntime then
  622. tooltip = add(S("Replaced by @1 on burning", desc))
  623. else
  624. tooltip = add(S("Replaced by @1 on crafting", desc))
  625. end
  626. end
  627. if info.repair then
  628. tooltip = add(S("Repairable by step of @1", clr("#ff0", toolrepair .. "%")))
  629. end
  630. if info.rarity then
  631. local chance = (1 / info.rarity) * 100
  632. tooltip = add(S("@1 of chance to drop", clr("#ff0", chance .. "%")))
  633. end
  634. return fmt("tooltip[%s;%s]", name, ESC(tooltip))
  635. end
  636. local function get_output_fs(data, fs, L)
  637. local custom_recipe = craft_types[L.recipe.type]
  638. if custom_recipe or L.shapeless or L.recipe.type == "cooking" then
  639. local icon = custom_recipe and custom_recipe.icon or
  640. L.shapeless and "shapeless" or "furnace"
  641. if not custom_recipe then
  642. icon = fmt("craftguide_%s.png^[resize:16x16", icon)
  643. end
  644. local pos_x = L.rightest + L.btn_size + 0.1
  645. local pos_y = YOFFSET + (sfinv_only and 0.25 or -0.45) + L.spacing
  646. if sub(icon, 1, 18) == "craftguide_furnace" then
  647. fs[#fs + 1] = fmt(FMT.animated_image,
  648. pos_x, pos_y, 0.5, 0.5, PNG.furnace_anim, 8, 180)
  649. else
  650. fs[#fs + 1] = fmt(FMT.image, pos_x, pos_y, 0.5, 0.5, icon)
  651. end
  652. local tooltip = custom_recipe and custom_recipe.description or
  653. L.shapeless and S"Shapeless" or S"Cooking"
  654. fs[#fs + 1] = fmt(FMT.tooltip, pos_x, pos_y, 0.5, 0.5, ESC(tooltip))
  655. end
  656. local arrow_X = L.rightest + (L._btn_size or 1.1)
  657. local output_X = arrow_X + 0.9
  658. local Y = YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing
  659. fs[#fs + 1] = fmt(FMT.image, arrow_X, Y + 0.2, 0.9, 0.7, PNG.arrow)
  660. if L.recipe.type == "fuel" then
  661. fs[#fs + 1] = fmt(FMT.animated_image, output_X, Y, 1.1, 1.1, PNG.fire_anim, 8, 180)
  662. else
  663. local item = L.recipe.output
  664. item = clean_name(item)
  665. local name = match(item, "%S*")
  666. fs[#fs + 1] = fmt(FMT.image, output_X, Y, 1.1, 1.1, PNG.selected)
  667. local _name = sfinv_only and name or fmt("_%s", name)
  668. fs[#fs + 1] = fmt("item_image_button[%f,%f;%f,%f;%s;%s;%s]",
  669. output_X, Y, 1.1, 1.1, item, _name, "")
  670. local def = reg_items[name]
  671. local infos = {
  672. unknown = not def or nil,
  673. burntime = fuel_cache[name],
  674. repair = repairable(name),
  675. rarity = L.rarity,
  676. newline = check_newline(def),
  677. }
  678. if next(infos) then
  679. fs[#fs + 1] = get_tooltip(_name, infos)
  680. end
  681. if infos.burntime then
  682. fs[#fs + 1] = fmt(FMT.image,
  683. output_X + 1, YOFFSET + (sfinv_only and 0.7 or 0.1) + L.spacing,
  684. 0.6, 0.4, PNG.arrow)
  685. fs[#fs + 1] = fmt(FMT.animated_image,
  686. output_X + 1.6, YOFFSET + (sfinv_only and 0.55 or 0) + L.spacing,
  687. 0.6, 0.6, PNG.fire_anim, 8, 180)
  688. end
  689. end
  690. end
  691. local function get_grid_fs(data, fs, rcp, spacing)
  692. local width = rcp.width or 1
  693. local replacements = rcp.replacements
  694. local rarity = rcp.rarity
  695. local rightest, btn_size, _btn_size = 0, 1.1
  696. local cooktime, shapeless
  697. if rcp.type == "cooking" then
  698. cooktime, width = width, 1
  699. elseif width == 0 and not rcp.custom then
  700. shapeless = true
  701. local n = #rcp.items
  702. width = (n < 5 and n > 1) and 2 or min(3, max(1, n))
  703. end
  704. local rows = ceil(maxn(rcp.items) / width)
  705. if width > WH_LIMIT or rows > WH_LIMIT then
  706. fs[#fs + 1] = fmt(FMT.label,
  707. XOFFSET + (sfinv_only and -1.5 or -1.6),
  708. YOFFSET + (sfinv_only and 0.5 or spacing),
  709. ES("Recipe's too big to be displayed (@1x@2)", width, rows))
  710. return concat(fs)
  711. end
  712. local large_recipe = width > 3 or rows > 3
  713. if large_recipe then
  714. fs[#fs + 1] = "style_type[item_image_button;border=true]"
  715. end
  716. for i = 1, width * rows do
  717. local item = rcp.items[i] or ""
  718. item = clean_name(item)
  719. local name = match(item, "%S*")
  720. local X = ceil((i - 1) % width - width) + XOFFSET
  721. local Y = ceil(i / width) + YOFFSET - min(2, rows) + spacing
  722. if large_recipe then
  723. local xof = 1 - 4 / width
  724. local yof = 1 - 4 / rows
  725. local x_y = width > rows and xof or yof
  726. btn_size = width > rows and
  727. (3.5 + (xof * 2)) / width or (3.5 + (yof * 2)) / rows
  728. _btn_size = btn_size
  729. X = (btn_size * ((i - 1) % width) + XOFFSET -
  730. (sfinv_only and 2.83 or 0)) * (0.83 - (x_y / 5))
  731. Y = (btn_size * floor((i - 1) / width) +
  732. (sfinv_only and 5.81 or 3.92) + x_y) * (0.86 - (x_y / 5))
  733. end
  734. if X > rightest then
  735. rightest = X
  736. end
  737. local groups
  738. if is_group(name) then
  739. groups = extract_groups(name)
  740. item = groups_to_items(groups)
  741. end
  742. local label = groups and "\nG" or ""
  743. local replace
  744. if replacements then
  745. for j = 1, #replacements do
  746. local replacement = replacements[j]
  747. if replacement[1] == name then
  748. label = (label ~= "" and "\n" or "") .. label .. "\nR"
  749. replace = replacement[2]
  750. end
  751. end
  752. end
  753. Y = Y + (sfinv_only and 0.7 or 0)
  754. if not large_recipe then
  755. fs[#fs + 1] = fmt(FMT.image, X, Y, btn_size, btn_size, PNG.selected)
  756. end
  757. fs[#fs + 1] = fmt(FMT.item_image_button,
  758. X, Y, btn_size, btn_size, item, item, label)
  759. local def = reg_items[name]
  760. local infos = {
  761. unknown = not def or nil,
  762. groups = groups,
  763. burntime = fuel_cache[name],
  764. cooktime = cooktime,
  765. replace = replace,
  766. newline = check_newline(def),
  767. }
  768. if next(infos) then
  769. fs[#fs + 1] = get_tooltip(item, infos)
  770. end
  771. end
  772. if large_recipe then
  773. fs[#fs + 1] = "style_type[item_image_button;border=false]"
  774. end
  775. get_output_fs(data, fs, {
  776. recipe = rcp,
  777. shapeless = shapeless,
  778. rightest = rightest,
  779. btn_size = btn_size,
  780. _btn_size = _btn_size,
  781. spacing = spacing,
  782. rarity = rarity,
  783. })
  784. end
  785. local function get_panels(data, fs)
  786. local start_y = sfinv_only and 0.33 or 0
  787. local panels = {
  788. {dat = data.usages or {}, height = 3.5},
  789. {dat = data.recipes or {}, height = 3.5},
  790. }
  791. if not sfinv_only then
  792. panels.favs = {height = 2.19}
  793. else
  794. panels = data.show_usages and {{dat = data.usages}} or {{dat = data.recipes}}
  795. end
  796. for k, v in pairs(panels) do
  797. start_y = start_y + 1
  798. local spacing = (start_y - 1) * 3.6
  799. if not sfinv_only then
  800. fs[#fs + 1] = fmt("background9[8.1,%f;6.6,%f;%s;false;%d]",
  801. -0.2 + spacing, v.height, PNG.bg_full, 10)
  802. if k == 2 then
  803. local fav = is_fav(data)
  804. local nfavs = #data.favs
  805. fs[#fs + 1] = fmt(
  806. "style[fav;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]",
  807. fmt("craftguide_fav%s.png", fav and "" or "_off"),
  808. fmt("craftguide_fav%s.png", fav and "_off" or ""),
  809. fmt("craftguide_fav%s.png", fav and "_off" or ""))
  810. if nfavs < 6 or (nfavs >= 6 and fav) then
  811. fs[#fs + 1] = fmt(FMT.image_button,
  812. 14, spacing, 0.5, 0.45, "", "fav", "")
  813. end
  814. fs[#fs + 1] = fmt("tooltip[fav;%s]",
  815. fav and ES"Unmark this item" or ES"Mark this item")
  816. end
  817. end
  818. local rn = v.dat and #v.dat or -1
  819. local _rn = tostring(rn)
  820. local xu = tostring(data.unum) .. _rn
  821. local xr = tostring(data.rnum) .. _rn
  822. xu = max(-0.3, -((#xu - 3) * 0.05))
  823. xr = max(-0.3, -((#xr - 3) * 0.05))
  824. local is_recipe = sfinv_only and not data.show_usages or k == 2
  825. local lbl = ""
  826. if not sfinv_only and rn == 0 then
  827. local X = XOFFSET - 0.7
  828. local Y = YOFFSET - 0.4 + spacing
  829. fs[#fs + 1] = fmt(FMT.image, X, Y, 2, 2, PNG.nothing)
  830. fs[#fs + 1] = fmt(FMT.tooltip,
  831. X, Y, 2, 2, is_recipe and ES"No recipes" or ES"No usages")
  832. elseif (not sfinv_only and is_recipe) or
  833. (sfinv_only and not data.show_usages) then
  834. lbl = ES("Recipe @1 of @2", data.rnum, rn)
  835. elseif not sfinv_only or (sfinv_only and data.show_usages) then
  836. lbl = ES("Usage @1 of @2", data.unum, rn)
  837. elseif sfinv_only then
  838. lbl = data.show_usages and
  839. ES("Usage @1 of @2", data.unum, rn) or
  840. ES("Recipe @1 of @2", data.rnum, rn)
  841. end
  842. fs[#fs + 1] = fmt(FMT.label,
  843. XOFFSET + (sfinv_only and 2.3 or 1.6) + (is_recipe and xr or xu),
  844. YOFFSET + (sfinv_only and 3.4 or 1.5 + spacing), lbl)
  845. if rn > 1 then
  846. local btn_suffix = is_recipe and "recipe" or "usage"
  847. local prev_name = fmt("prev_%s", btn_suffix)
  848. local next_name = fmt("next_%s", btn_suffix)
  849. local x_arrow = XOFFSET + (sfinv_only and 1.7 or 1)
  850. local y_arrow = YOFFSET + (sfinv_only and 3.3 or 1.4 + spacing)
  851. fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2),
  852. x_arrow + (is_recipe and xr or xu), y_arrow,
  853. PNG.prev, prev_name, "",
  854. x_arrow + 1.8, y_arrow, PNG.next, next_name, "")
  855. end
  856. local rcp = v.dat and (is_recipe and v.dat[data.rnum] or v.dat[data.unum])
  857. if rcp then
  858. get_grid_fs(data, fs, rcp, spacing)
  859. end
  860. if k == "favs" and not sfinv_only then
  861. fs[#fs + 1] = fmt(FMT.label, 8.3, spacing - 0.1, ES"Bookmarks")
  862. for i = 1, #data.favs do
  863. local item = data.favs[i]
  864. local X = 7.85 + (i - 0.5)
  865. local Y = spacing + 0.45
  866. if data.query_item == item then
  867. fs[#fs + 1] = fmt(FMT.image, X, Y, 1.1, 1.1, PNG.selected)
  868. end
  869. fs[#fs + 1] = fmt(FMT.item_image_button,
  870. X, Y, 1.1, 1.1, item, item, "")
  871. end
  872. end
  873. end
  874. end
  875. local function make_fs(data)
  876. local fs = {}
  877. fs[#fs + 1] = fmt([[
  878. size[%f,%f]
  879. no_prepend[]
  880. bgcolor[#0000]
  881. ]],
  882. 9 + (data.query_item and 6.7 or 0) - 1.2, LINES - 0.3)
  883. if not sfinv_only then
  884. fs[#fs + 1] = fmt("background9[-0.15,-0.2;%f,%f;%s;false;%d]",
  885. 9 - 0.9, LINES + 0.4, PNG.bg_full, 10)
  886. end
  887. fs[#fs + 1] = fmt([[
  888. style[filter;border=false]
  889. field[0.4,0.2;2.5,1;filter;;%s]
  890. field_close_on_enter[filter;false]
  891. box[0,0;2.4,0.6;#bababa25]
  892. ]],
  893. ESC(data.filter))
  894. fs[#fs + 1] = fmt([[
  895. style_type[image_button;border=false]
  896. style_type[item_image_button;border=false;bgimg_hovered=%s;bgimg_pressed=%s]
  897. style[search;fgimg=%s;fgimg_hovered=%s]
  898. style[clear;fgimg=%s;fgimg_hovered=%s]
  899. style[prev_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
  900. style[next_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
  901. ]],
  902. PNG.selected, PNG.selected,
  903. PNG.search, PNG.search_hover,
  904. PNG.clear, PNG.clear_hover,
  905. PNG.prev, PNG.prev_hover, PNG.prev_hover,
  906. PNG.next, PNG.next_hover, PNG.next_hover)
  907. fs[#fs + 1] = fmt(mul_elem(FMT.image_button, 4),
  908. sfinv_only and 2.6 or 2.54, -0.06, 0.85, 0.85, "", "search", "",
  909. sfinv_only and 3.3 or 3.25, -0.06, 0.85, 0.85, "", "clear", "",
  910. sfinv_only and 5.45 or (9 * 6.83) / 11, -0.06, 0.85, 0.85, "", "prev_page", "",
  911. sfinv_only and 7.2 or (9 * 8.75) / 11, -0.06, 0.85, 0.85, "", "next_page", "")
  912. data.pagemax = max(1, ceil(#data.items / IPP))
  913. fs[#fs + 1] = fmt("label[%f,%f;%s / %u]",
  914. sfinv_only and 6.35 or (9 * 7.85) / 11,
  915. 0.06, clr("#ff0", data.pagenum), data.pagemax)
  916. if #data.items == 0 then
  917. local no_item = ES"No item to show"
  918. local pos = 3
  919. if next(recipe_filters) and #init_items > 0 and data.filter == "" then
  920. no_item = ES"Collect items to reveal more recipes"
  921. pos = pos - 1
  922. end
  923. fs[#fs + 1] = fmt(FMT.label, pos, 2, no_item)
  924. end
  925. local first_item = (data.pagenum - 1) * IPP
  926. for i = first_item, first_item + IPP - 1 do
  927. local item = data.items[i + 1]
  928. if not item then break end
  929. local X = i % ROWS
  930. local Y = (i % IPP - X) / ROWS + 1
  931. X = X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05
  932. Y = Y - (Y * 0.1) - 0.1
  933. if data.query_item == item then
  934. fs[#fs + 1] = fmt(FMT.image, X, Y, 1, 1, PNG.selected)
  935. end
  936. fs[#fs + 1] = fmt("item_image_button[%f,%f;%f,%f;%s;%s_inv;]",
  937. X, Y, 1, 1, item, item)
  938. end
  939. if (data.recipes and #data.recipes > 0) or (data.usages and #data.usages > 0) then
  940. get_panels(data, fs)
  941. end
  942. return concat(fs)
  943. end
  944. local show_fs = function(player, name)
  945. local data = pdata[name]
  946. if sfinv_only then
  947. sfinv.set_player_inventory_formspec(player)
  948. else
  949. show_formspec(name, "craftguide", make_fs(data))
  950. end
  951. end
  952. craftguide.register_craft_type("digging", {
  953. description = ES"Digging",
  954. icon = "craftguide_steelpick.png",
  955. })
  956. craftguide.register_craft_type("digging_chance", {
  957. description = ES"Digging Chance",
  958. icon = "craftguide_mesepick.png",
  959. })
  960. local function search(data)
  961. local filter = data.filter
  962. if searches[filter] then
  963. data.items = searches[filter]
  964. return
  965. end
  966. local opt = "^(.-)%+([%w_]+)=([%w_,]+)"
  967. local search_filter = next(search_filters) and match(filter, opt)
  968. local filters = {}
  969. if search_filter then
  970. for filter_name, values in gmatch(filter, sub(opt, 6)) do
  971. if search_filters[filter_name] then
  972. values = split(values, ",")
  973. filters[filter_name] = values
  974. end
  975. end
  976. end
  977. local filtered_list, c = {}, 0
  978. for i = 1, #data.items_raw do
  979. local item = data.items_raw[i]
  980. local def = reg_items[item]
  981. local desc = (def and def.description) and lower(def.description) or ""
  982. local search_in = fmt("%s %s", item, desc)
  983. local to_add
  984. if search_filter then
  985. for filter_name, values in pairs(filters) do
  986. if values then
  987. local func = search_filters[filter_name]
  988. to_add = func(item, values) and (search_filter == "" or
  989. find(search_in, search_filter, 1, true))
  990. end
  991. end
  992. else
  993. to_add = find(search_in, filter, 1, true)
  994. end
  995. if to_add then
  996. c = c + 1
  997. filtered_list[c] = item
  998. end
  999. end
  1000. if not next(recipe_filters) then
  1001. -- Cache the results only if searched 2 times
  1002. if searches[filter] == nil then
  1003. searches[filter] = false
  1004. else
  1005. searches[filter] = filtered_list
  1006. end
  1007. end
  1008. data.items = filtered_list
  1009. end
  1010. craftguide.add_search_filter("groups", function(item, groups)
  1011. local def = reg_items[item]
  1012. local has_groups = true
  1013. for i = 1, #groups do
  1014. local group = groups[i]
  1015. if not def.groups[group] then
  1016. has_groups = nil
  1017. break
  1018. end
  1019. end
  1020. return has_groups
  1021. end)
  1022. --[[ As `core.get_craft_recipe` and `core.get_all_craft_recipes` do not
  1023. return the replacements and toolrepair, we have to override
  1024. `core.register_craft` and do some reverse engineering.
  1025. See engine's issues #4901 and #8920. ]]
  1026. fuel_cache.replacements = {}
  1027. local old_register_craft = core.register_craft
  1028. core.register_craft = function(def)
  1029. old_register_craft(def)
  1030. if def.type == "toolrepair" then
  1031. toolrepair = def.additional_wear * -100
  1032. end
  1033. local output = def.output or (true_str(def.recipe) and def.recipe) or nil
  1034. if not output then return end
  1035. output = {match(output, "%S+")}
  1036. local groups
  1037. if is_group(output[1]) then
  1038. groups = extract_groups(output[1])
  1039. output = groups_to_items(groups, true)
  1040. end
  1041. for i = 1, #output do
  1042. local name = output[i]
  1043. if def.type ~= "fuel" then
  1044. def.items = {}
  1045. end
  1046. if def.type == "fuel" then
  1047. fuel_cache[name] = def.burntime
  1048. fuel_cache.replacements[name] = def.replacements
  1049. elseif def.type == "cooking" then
  1050. def.width = def.cooktime
  1051. def.cooktime = nil
  1052. def.items[1] = def.recipe
  1053. elseif def.type == "shapeless" then
  1054. def.width = 0
  1055. for j = 1, #def.recipe do
  1056. def.items[#def.items + 1] = def.recipe[j]
  1057. end
  1058. else
  1059. def.width = #def.recipe[1]
  1060. local c = 0
  1061. for j = 1, #def.recipe do
  1062. if def.recipe[j] then
  1063. for h = 1, def.width do
  1064. c = c + 1
  1065. local it = def.recipe[j][h]
  1066. if it and it ~= "" then
  1067. def.items[c] = it
  1068. end
  1069. end
  1070. end
  1071. end
  1072. end
  1073. if def.type ~= "fuel" then
  1074. def.recipe = nil
  1075. recipes_cache[name] = recipes_cache[name] or {}
  1076. insert(recipes_cache[name], 1, def)
  1077. end
  1078. end
  1079. end
  1080. local old_clear_craft = core.clear_craft
  1081. core.clear_craft = function(def)
  1082. old_clear_craft(def)
  1083. if true_str(def) then
  1084. def = match(def, "%S*")
  1085. recipes_cache[def] = nil
  1086. fuel_cache[def] = nil
  1087. elseif is_table(def) then
  1088. return -- TODO
  1089. end
  1090. end
  1091. local function handle_drops_table(name, drop)
  1092. -- Code borrowed and modified from unified_inventory
  1093. -- https://github.com/minetest-mods/unified_inventory/blob/master/api.lua
  1094. local drop_sure, drop_maybe = {}, {}
  1095. local drop_items = drop.items or {}
  1096. local max_items_left = drop.max_items
  1097. local max_start = true
  1098. for i = 1, #drop_items do
  1099. if max_items_left and max_items_left <= 0 then break end
  1100. local di = drop_items[i]
  1101. for j = 1, #di.items do
  1102. local dstack = ItemStack(di.items[j])
  1103. local dname = dstack:get_name()
  1104. if not dstack:is_empty() and dname ~= name then
  1105. local dcount = dstack:get_count()
  1106. if #di.items == 1 and di.rarity == 1 and max_start then
  1107. if not drop_sure[dname] then
  1108. drop_sure[dname] = 0
  1109. end
  1110. drop_sure[dname] = drop_sure[dname] + dcount
  1111. if max_items_left then
  1112. max_items_left = max_items_left - 1
  1113. if max_items_left <= 0 then break end
  1114. end
  1115. else
  1116. if max_items_left then
  1117. max_start = false
  1118. end
  1119. if not drop_maybe[dname] then
  1120. drop_maybe[dname] = {}
  1121. end
  1122. if not drop_maybe[dname].output then
  1123. drop_maybe[dname].output = 0
  1124. end
  1125. drop_maybe[dname] = {
  1126. output = drop_maybe[dname].output + dcount,
  1127. rarity = di.rarity,
  1128. }
  1129. end
  1130. end
  1131. end
  1132. end
  1133. for item, count in pairs(drop_sure) do
  1134. craftguide.register_craft{
  1135. type = "digging",
  1136. items = {name},
  1137. output = fmt("%s %u", item, count),
  1138. }
  1139. end
  1140. for item, data in pairs(drop_maybe) do
  1141. craftguide.register_craft{
  1142. type = "digging_chance",
  1143. items = {name},
  1144. output = fmt("%s %u", item, data.output),
  1145. rarity = data.rarity,
  1146. }
  1147. end
  1148. end
  1149. local function register_drops(name, drop)
  1150. local dstack = ItemStack(drop)
  1151. if not dstack:is_empty() and dstack:get_name() ~= name then
  1152. craftguide.register_craft{
  1153. type = "digging",
  1154. items = {name},
  1155. output = drop,
  1156. }
  1157. elseif is_table(drop) then
  1158. handle_drops_table(name, drop)
  1159. end
  1160. end
  1161. local function handle_aliases(hash)
  1162. for oldname, newname in pairs(reg_aliases) do
  1163. cache_recipes(oldname)
  1164. local recipes = recipes_cache[oldname]
  1165. if recipes then
  1166. if not recipes_cache[newname] then
  1167. recipes_cache[newname] = {}
  1168. end
  1169. local similar
  1170. for i = 1, #recipes_cache[oldname] do
  1171. local rcp_old = recipes_cache[oldname][i]
  1172. for j = 1, #recipes_cache[newname] do
  1173. local rcp_new = recipes_cache[newname][j]
  1174. rcp_new.type = nil
  1175. rcp_new.method = nil
  1176. if table_eq(rcp_old, rcp_new) then
  1177. similar = true
  1178. break
  1179. end
  1180. end
  1181. if not similar then
  1182. insert(recipes_cache[newname], rcp_old)
  1183. end
  1184. end
  1185. end
  1186. if newname ~= "" and recipes_cache[oldname] and not hash[newname] then
  1187. init_items[#init_items + 1] = newname
  1188. end
  1189. end
  1190. end
  1191. local function show_item(def)
  1192. return not (def.groups.not_in_craft_guide == 1 or
  1193. def.groups.not_in_creative_inventory == 1) and
  1194. def.description and def.description ~= ""
  1195. end
  1196. local function get_init_items()
  1197. local init_items_bak = storage:get "init_items"
  1198. if autocache == false and init_items_bak then
  1199. init_items = dslz(init_items_bak)
  1200. fuel_cache = dslz(storage:get "fuel_cache")
  1201. usages_cache = dslz(storage:get "usages_cache")
  1202. recipes_cache = dslz(storage:get "recipes_cache")
  1203. else
  1204. print "[craftguide] Caching data (this may take a while)"
  1205. local hash = {}
  1206. for name, def in pairs(reg_items) do
  1207. if show_item(def) then
  1208. if not fuel_cache[name] then
  1209. cache_fuel(name)
  1210. end
  1211. if not recipes_cache[name] then
  1212. cache_recipes(name)
  1213. end
  1214. cache_usages(name)
  1215. register_drops(name, def.drop)
  1216. if name ~= "" and recipes_cache[name] or usages_cache[name] then
  1217. init_items[#init_items + 1] = name
  1218. hash[name] = true
  1219. end
  1220. end
  1221. end
  1222. handle_aliases(hash)
  1223. sort(init_items)
  1224. storage:set_string("init_items", slz(init_items))
  1225. storage:set_string("fuel_cache", slz(fuel_cache))
  1226. storage:set_string("usages_cache", slz(usages_cache))
  1227. storage:set_string("recipes_cache", slz(recipes_cache))
  1228. end
  1229. if http and true_str(craftguide.export_url) then
  1230. local post_data = {
  1231. recipes = recipes_cache,
  1232. usages = usages_cache,
  1233. fuel = fuel_cache,
  1234. }
  1235. http.fetch_async{
  1236. url = craftguide.export_url,
  1237. post_data = write_json(post_data),
  1238. }
  1239. end
  1240. end
  1241. local function init_data(name)
  1242. pdata[name] = {
  1243. filter = "",
  1244. pagenum = 1,
  1245. items = init_items,
  1246. items_raw = init_items,
  1247. favs = {},
  1248. fs_version = get_fs_version(name),
  1249. }
  1250. end
  1251. local function reset_data(data)
  1252. data.filter = ""
  1253. data.pagenum = 1
  1254. data.rnum = 1
  1255. data.unum = 1
  1256. data.query_item = nil
  1257. data.recipes = nil
  1258. data.usages = nil
  1259. data.show_usages = nil
  1260. data.items = data.items_raw
  1261. end
  1262. on_mods_loaded(get_init_items)
  1263. on_joinplayer(function(player)
  1264. local name = player:get_player_name()
  1265. init_data(name)
  1266. if pdata[name].fs_version < FORMSPEC_MINIMAL_VERSION then
  1267. outdated(name)
  1268. end
  1269. end)
  1270. local function fields(player, _f)
  1271. local name = player:get_player_name()
  1272. local data = pdata[name]
  1273. if _f.clear then
  1274. reset_data(data)
  1275. elseif _f.prev_recipe or _f.next_recipe then
  1276. local num = data.rnum + (_f.prev_recipe and -1 or 1)
  1277. data.rnum = data.recipes[num] and num or (_f.prev_recipe and #data.recipes or 1)
  1278. elseif _f.prev_usage or _f.next_usage then
  1279. local num = data.unum + (_f.prev_usage and -1 or 1)
  1280. data.unum = data.usages[num] and num or (_f.prev_usage and #data.usages or 1)
  1281. elseif _f.key_enter_field == "filter" or _f.search then
  1282. if _f.filter == "" then
  1283. reset_data(data)
  1284. return true, show_fs(player, name)
  1285. end
  1286. local str = lower(_f.filter)
  1287. if data.filter == str then return end
  1288. data.filter = str
  1289. data.pagenum = 1
  1290. search(data)
  1291. elseif _f.prev_page or _f.next_page then
  1292. if data.pagemax == 1 then return end
  1293. data.pagenum = data.pagenum - (_f.prev_page and 1 or -1)
  1294. if data.pagenum > data.pagemax then
  1295. data.pagenum = 1
  1296. elseif data.pagenum == 0 then
  1297. data.pagenum = data.pagemax
  1298. end
  1299. elseif _f.fav then
  1300. local fav, i = is_fav(data)
  1301. local total = #data.favs
  1302. if total < 6 and not fav then
  1303. data.favs[total + 1] = data.query_item
  1304. elseif fav then
  1305. remove(data.favs, i)
  1306. end
  1307. else
  1308. local item
  1309. for field in pairs(_f) do
  1310. if find(field, ":") then
  1311. item = field
  1312. break
  1313. end
  1314. end
  1315. if not item then
  1316. return
  1317. elseif sub(item, -4) == "_inv" then
  1318. item = sub(item, 1, -5)
  1319. elseif sub(item, 1, 1) == "_" then
  1320. item = sub(item, 2)
  1321. end
  1322. item = reg_aliases[item] or item
  1323. if sfinv_only then
  1324. if item ~= data.query_item then
  1325. data.show_usages = nil
  1326. else
  1327. data.show_usages = not data.show_usages
  1328. end
  1329. elseif item == data.query_item then
  1330. return
  1331. end
  1332. local recipes, usages = get_recipes(item, data, player)
  1333. if not recipes and not usages then return end
  1334. if data.show_usages and not usages then return end
  1335. data.query_item = item
  1336. data.recipes = recipes
  1337. data.usages = usages
  1338. data.rnum = 1
  1339. data.unum = 1
  1340. end
  1341. return true, show_fs(player, name)
  1342. end
  1343. if sfinv_only then
  1344. sfinv.register_page("craftguide:craftguide", {
  1345. title = S"Craft Guide",
  1346. is_in_nav = function(self, player, context)
  1347. local name = player:get_player_name()
  1348. return get_fs_version(name) >= FORMSPEC_MINIMAL_VERSION
  1349. end,
  1350. get = function(self, player, context)
  1351. local name = player:get_player_name()
  1352. local data = pdata[name]
  1353. return sfinv.make_formspec(player, context, make_fs(data))
  1354. end,
  1355. on_enter = function(self, player, context)
  1356. if next(recipe_filters) then
  1357. local name = player:get_player_name()
  1358. local data = pdata[name]
  1359. data.items_raw = get_filtered_items(player)
  1360. search(data)
  1361. end
  1362. end,
  1363. on_player_receive_fields = function(self, player, context, _f)
  1364. fields(player, _f)
  1365. end,
  1366. })
  1367. else
  1368. on_receive_fields(function(player, formname, _f)
  1369. if formname == "craftguide" then
  1370. fields(player, _f)
  1371. end
  1372. end)
  1373. local function on_use(user)
  1374. local name = user:get_player_name()
  1375. local data = pdata[name]
  1376. if data.fs_version < FORMSPEC_MINIMAL_VERSION then
  1377. return outdated(name)
  1378. end
  1379. if next(recipe_filters) then
  1380. data.items_raw = get_filtered_items(user)
  1381. search(data)
  1382. end
  1383. show_formspec(name, "craftguide", make_fs(data))
  1384. end
  1385. core.register_craftitem("craftguide:book", {
  1386. description = S"Crafting Guide",
  1387. inventory_image = PNG.book,
  1388. wield_image = PNG.book,
  1389. stack_max = 1,
  1390. groups = {book = 1},
  1391. on_use = function(itemstack, user)
  1392. on_use(user)
  1393. end
  1394. })
  1395. core.register_node("craftguide:sign", {
  1396. description = S"Crafting Guide Sign",
  1397. drawtype = "nodebox",
  1398. tiles = {PNG.sign},
  1399. inventory_image = PNG.sign,
  1400. wield_image = PNG.sign,
  1401. paramtype = "light",
  1402. paramtype2 = "wallmounted",
  1403. sunlight_propagates = true,
  1404. groups = {
  1405. choppy = 1,
  1406. attached_node = 1,
  1407. oddly_breakable_by_hand = 1,
  1408. flammable = 3,
  1409. },
  1410. node_box = {
  1411. type = "wallmounted",
  1412. wall_top = {-0.5, 0.4375, -0.5, 0.5, 0.5, 0.5},
  1413. wall_bottom = {-0.5, -0.5, -0.5, 0.5, -0.4375, 0.5},
  1414. wall_side = {-0.5, -0.5, -0.5, -0.4375, 0.5, 0.5}
  1415. },
  1416. on_construct = function(pos)
  1417. local meta = core.get_meta(pos)
  1418. meta:set_string("infotext", "Crafting Guide Sign")
  1419. end,
  1420. on_rightclick = function(pos, node, user, itemstack)
  1421. on_use(user)
  1422. end
  1423. })
  1424. core.register_craft{
  1425. output = "craftguide:book",
  1426. type = "shapeless",
  1427. recipe = {"default:book"}
  1428. }
  1429. core.register_craft{
  1430. type = "fuel",
  1431. recipe = "craftguide:book",
  1432. burntime = 3
  1433. }
  1434. core.register_craft{
  1435. output = "craftguide:sign",
  1436. type = "shapeless",
  1437. recipe = {"default:sign_wall_wood"}
  1438. }
  1439. core.register_craft{
  1440. type = "fuel",
  1441. recipe = "craftguide:sign",
  1442. burntime = 10
  1443. }
  1444. if rawget(_G, "sfinv_buttons") then
  1445. sfinv_buttons.register_button("craftguide", {
  1446. title = S"Crafting Guide",
  1447. tooltip = S"Shows a list of available crafting recipes",
  1448. image = PNG.book,
  1449. action = function(player)
  1450. on_use(player)
  1451. end,
  1452. })
  1453. end
  1454. end
  1455. if progressive_mode then
  1456. local POLL_FREQ = 0.25
  1457. local HUD_TIMER_MAX = 1.5
  1458. local function item_in_inv(item, inv_items)
  1459. local inv_items_size = #inv_items
  1460. if is_group(item) then
  1461. local groups = extract_groups(item)
  1462. for i = 1, inv_items_size do
  1463. local def = reg_items[inv_items[i]]
  1464. if def then
  1465. local item_groups = def.groups
  1466. if item_has_groups(item_groups, groups) then
  1467. return true
  1468. end
  1469. end
  1470. end
  1471. else
  1472. for i = 1, inv_items_size do
  1473. if inv_items[i] == item then
  1474. return true
  1475. end
  1476. end
  1477. end
  1478. end
  1479. local function recipe_in_inv(recipe, inv_items)
  1480. for _, item in pairs(recipe.items) do
  1481. if not item_in_inv(item, inv_items) then return end
  1482. end
  1483. return true
  1484. end
  1485. local function progressive_filter(recipes, player)
  1486. if not recipes then
  1487. return {}
  1488. end
  1489. local name = player:get_player_name()
  1490. local data = pdata[name]
  1491. if #data.inv_items == 0 then
  1492. return {}
  1493. end
  1494. local filtered, c = {}, 0
  1495. for i = 1, #recipes do
  1496. local recipe = recipes[i]
  1497. if recipe_in_inv(recipe, data.inv_items) then
  1498. c = c + 1
  1499. filtered[c] = recipe
  1500. end
  1501. end
  1502. return filtered
  1503. end
  1504. local item_lists = {"main", "craft", "craftpreview"}
  1505. local function get_inv_items(player)
  1506. local inv = player:get_inventory()
  1507. local stacks = {}
  1508. for i = 1, #item_lists do
  1509. local list = inv:get_list(item_lists[i])
  1510. table_merge(stacks, list)
  1511. end
  1512. local inv_items, c = {}, 0
  1513. for i = 1, #stacks do
  1514. local stack = stacks[i]
  1515. if not stack:is_empty() then
  1516. local name = stack:get_name()
  1517. if reg_items[name] then
  1518. c = c + 1
  1519. inv_items[c] = name
  1520. end
  1521. end
  1522. end
  1523. return inv_items
  1524. end
  1525. local function init_hud(player, data)
  1526. data.hud = {
  1527. bg = player:hud_add{
  1528. hud_elem_type = "image",
  1529. position = {x = 0.78, y = 1},
  1530. alignment = {x = 1, y = 1},
  1531. scale = {x = 370, y = 112},
  1532. text = PNG.bg,
  1533. },
  1534. book = player:hud_add{
  1535. hud_elem_type = "image",
  1536. position = {x = 0.79, y = 1.02},
  1537. alignment = {x = 1, y = 1},
  1538. scale = {x = 4, y = 4},
  1539. text = PNG.book,
  1540. },
  1541. text = player:hud_add{
  1542. hud_elem_type = "text",
  1543. position = {x = 0.84, y = 1.04},
  1544. alignment = {x = 1, y = 1},
  1545. number = 0xffffff,
  1546. text = "",
  1547. },
  1548. }
  1549. end
  1550. local function show_hud_success(player, data)
  1551. -- It'd better to have an engine function `hud_move` to only need
  1552. -- 2 calls for the notification's back and forth.
  1553. local hud_info_bg = player:hud_get(data.hud.bg)
  1554. local dt = 0.016
  1555. if hud_info_bg.position.y <= 0.9 then
  1556. data.show_hud = false
  1557. data.hud_timer = (data.hud_timer or 0) + dt
  1558. end
  1559. if data.show_hud then
  1560. for _, def in pairs(data.hud) do
  1561. local hud_info = player:hud_get(def)
  1562. player:hud_change(def, "position", {
  1563. x = hud_info.position.x,
  1564. y = hud_info.position.y - (dt / 5)
  1565. })
  1566. end
  1567. player:hud_change(data.hud.text, "text",
  1568. S("@1 new recipe(s) discovered!", data.discovered))
  1569. elseif data.show_hud == false then
  1570. if data.hud_timer >= HUD_TIMER_MAX then
  1571. for _, def in pairs(data.hud) do
  1572. local hud_info = player:hud_get(def)
  1573. player:hud_change(def, "position", {
  1574. x = hud_info.position.x,
  1575. y = hud_info.position.y + (dt / 5)
  1576. })
  1577. end
  1578. if hud_info_bg.position.y >= 1 then
  1579. data.show_hud = nil
  1580. data.hud_timer = nil
  1581. end
  1582. end
  1583. end
  1584. end
  1585. -- Workaround. Need an engine call to detect when the contents of
  1586. -- the player inventory changed, instead.
  1587. local function poll_new_items()
  1588. local players = get_players()
  1589. for i = 1, #players do
  1590. local player = players[i]
  1591. local name = player:get_player_name()
  1592. local data = pdata[name]
  1593. local inv_items = get_inv_items(player)
  1594. local diff = array_diff(inv_items, data.inv_items)
  1595. if #diff > 0 then
  1596. data.inv_items = table_merge(diff, data.inv_items)
  1597. local oldknown = data.known_recipes or 0
  1598. local items = get_filtered_items(player, data)
  1599. data.discovered = data.known_recipes - oldknown
  1600. if data.show_hud == nil and data.discovered > 0 then
  1601. data.show_hud = true
  1602. end
  1603. if sfinv_only then
  1604. data.items_raw = items
  1605. search(data)
  1606. sfinv.set_player_inventory_formspec(player)
  1607. end
  1608. end
  1609. end
  1610. after(POLL_FREQ, poll_new_items)
  1611. end
  1612. poll_new_items()
  1613. globalstep(function()
  1614. local players = get_players()
  1615. for i = 1, #players do
  1616. local player = players[i]
  1617. local name = player:get_player_name()
  1618. local data = pdata[name]
  1619. if data.show_hud ~= nil and singleplayer then
  1620. show_hud_success(player, data)
  1621. end
  1622. end
  1623. end)
  1624. craftguide.add_recipe_filter("Default progressive filter", progressive_filter)
  1625. on_joinplayer(function(player)
  1626. local name = player:get_player_name()
  1627. local data = pdata[name]
  1628. local meta = player:get_meta()
  1629. data.inv_items = dslz(meta:get_string "inv_items") or {}
  1630. data.known_recipes = dslz(meta:get_string "known_recipes") or 0
  1631. if singleplayer then
  1632. init_hud(player, data)
  1633. end
  1634. end)
  1635. local to_save = {"inv_items", "known_recipes"}
  1636. local function save_meta(player)
  1637. local meta = player:get_meta()
  1638. local name = player:get_player_name()
  1639. local data = pdata[name]
  1640. for i = 1, #to_save do
  1641. local meta_name = to_save[i]
  1642. meta:set_string(meta_name, slz(data[meta_name]))
  1643. end
  1644. end
  1645. on_leaveplayer(save_meta)
  1646. on_shutdown(function()
  1647. local players = get_players()
  1648. for i = 1, #players do
  1649. local player = players[i]
  1650. save_meta(player)
  1651. end
  1652. end)
  1653. end
  1654. on_leaveplayer(function(player)
  1655. local name = player:get_player_name()
  1656. pdata[name] = nil
  1657. end)
  1658. function craftguide.show(name, item, show_usages)
  1659. if not true_str(name)then
  1660. return err "craftguide.show(): player name missing"
  1661. end
  1662. local data = pdata[name]
  1663. local player = get_player_by_name(name)
  1664. local query_item = data.query_item
  1665. reset_data(data)
  1666. item = reg_items[item] and item or query_item
  1667. local recipes, usages = get_recipes(item, data, player)
  1668. if not recipes and not usages then
  1669. if not recipes_cache[item] and not usages_cache[item] then
  1670. return false, msg(name, fmt("%s: %s",
  1671. S"No recipe or usage for this item", get_desc(item)))
  1672. end
  1673. return false, msg(name, fmt("%s: %s",
  1674. S"You don't know a recipe or usage for this item", get_desc(item)))
  1675. end
  1676. data.query_item = item
  1677. data.recipes = recipes
  1678. data.usages = usages
  1679. if sfinv_only then
  1680. data.show_usages = show_usages
  1681. end
  1682. show_fs(player, name)
  1683. end
  1684. register_command("craft", {
  1685. description = S"Show recipe(s) of the pointed node",
  1686. func = function(name)
  1687. local player = get_player_by_name(name)
  1688. local dir = player:get_look_dir()
  1689. local ppos = player:get_pos()
  1690. ppos.y = ppos.y + 1.625
  1691. local node_name
  1692. for i = 1, 10 do
  1693. local look_at = vec_add(ppos, vec_mul(dir, i))
  1694. local node = core.get_node(look_at)
  1695. if node.name ~= "air" then
  1696. node_name = node.name
  1697. break
  1698. end
  1699. end
  1700. if not node_name then
  1701. return false, msg(name, S"No node pointed")
  1702. end
  1703. return true, craftguide.show(name, node_name)
  1704. end,
  1705. })