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.

2193 lines
51KB

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