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.

2097 lines
47KB

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