init.lua 49 KB

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