init.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. -- ZCG mod for minetest
  2. -- See README for more information
  3. -- Released by Zeg9 under WTFPL
  4. zcg = zcg or {}
  5. zcg.modpath = minetest.get_modpath("zcg")
  6. zcg.users = zcg.users or {}
  7. zcg.crafts = zcg.crafts or {}
  8. zcg.itemlist = zcg.itemlist or {}
  9. zcg.items_in_group = function(group)
  10. local items = {}
  11. local ok = true
  12. for name, item in pairs(minetest.registered_items) do
  13. -- the node should be in all groups
  14. ok = true
  15. for _, g in ipairs(group:split(',')) do
  16. if not item.groups[g] then
  17. ok = false
  18. end
  19. end
  20. if ok then table.insert(items,name) end
  21. end
  22. return items
  23. end
  24. local table_copy = function(table)
  25. local out = {}
  26. for k,v in pairs(table) do
  27. out[k] = v
  28. end
  29. return out
  30. end
  31. zcg.add_craft = function(input, realout, output, groups)
  32. if minetest.get_item_group(output, "not_in_craft_guide") > 0 then
  33. return
  34. end
  35. if not groups then groups = {} end
  36. local c = {}
  37. c.width = input.width
  38. c.type = input.type
  39. c.items = input.items
  40. if type(realout) == "string" then
  41. c.result = realout
  42. elseif type(realout) == "table" then
  43. --minetest.log(dump(realout))
  44. if type(realout.output) == "string" then
  45. c.result = realout.output
  46. elseif type(realout.output) == "table" then
  47. -- Recipe output should be two items (separating recipe).
  48. assert(type(realout.output[1]) == "string")
  49. assert(type(realout.output[2]) == "string")
  50. c.result = table.copy(realout.output)
  51. end
  52. end
  53. assert(type(c.result) == "string" or type(c.result) == "table")
  54. if c.items == nil then return end
  55. for i, item in pairs(c.items) do
  56. if item:sub(0,6) == "group:" then
  57. -- The recipe item name may contain a count value.
  58. -- We must extract just the name after the "group:".
  59. local strpart = item:sub(7)
  60. local parts = string.split(strpart, " ")
  61. assert(type(parts[1]) == "string")
  62. local groupname = parts[1]
  63. local groupcount = parts[2] or 1
  64. if groups[groupname] ~= nil then
  65. c.items[i] = groups[groupname] .. " " .. groupcount
  66. else
  67. for _, gi in ipairs(zcg.items_in_group(groupname)) do
  68. local g2 = groups
  69. g2[groupname] = gi
  70. zcg.add_craft({
  71. width = c.width,
  72. type = c.type,
  73. items = table_copy(c.items)
  74. }, realout, output, g2) -- it is needed to copy the table, else groups won't work right
  75. end
  76. return
  77. end
  78. end
  79. end
  80. if c.width == 0 then c.width = 3 end
  81. table.insert(zcg.crafts[output],c)
  82. end
  83. zcg.load_crafts = function(name)
  84. zcg.crafts[name] = {}
  85. local _recipes = minetest.get_all_craft_recipes(name)
  86. if _recipes then
  87. for i, recipe in ipairs(_recipes) do
  88. if (recipe and recipe.items and recipe.type) then
  89. assert(type(recipe.output) ~= "nil")
  90. zcg.add_craft(recipe, recipe.output, name)
  91. end
  92. end
  93. end
  94. if zcg.crafts[name] == nil or #zcg.crafts[name] == 0 then
  95. zcg.crafts[name] = nil
  96. else
  97. table.insert(zcg.itemlist,name)
  98. end
  99. end
  100. zcg.formspec = function(pn)
  101. local page = zcg.users[pn].page
  102. local alt = zcg.users[pn].alt
  103. local current_item = zcg.users[pn].current_item
  104. local formspec = "size[8,8.5]" ..
  105. default.gui_bg ..
  106. default.gui_bg_img ..
  107. default.gui_slots ..
  108. "button[0,0.5;2,.5;main;Back]"
  109. if zcg.users[pn].history.index > 1 then
  110. formspec = formspec .. "image_button[0,1.5;1,1;zcg_previous.png;zcg_previous;;false;false;zcg_previous_press.png]"
  111. else
  112. formspec = formspec .. "image[0,1.5;1,1;zcg_previous_inactive.png]"
  113. end
  114. if zcg.users[pn].history.index < #zcg.users[pn].history.list then
  115. formspec = formspec .. "image_button[1,1.5;1,1;zcg_next.png;zcg_next;;false;false;zcg_next_press.png]"
  116. else
  117. formspec = formspec .. "image[1,1.5;1,1;zcg_next_inactive.png]"
  118. end
  119. -- Show craft recipe
  120. if current_item ~= "" then
  121. if zcg.crafts[current_item] then
  122. if alt > #zcg.crafts[current_item] then
  123. alt = #zcg.crafts[current_item]
  124. end
  125. if alt > 1 then
  126. formspec = formspec .. "button[7,0.5;1,1;zcg_alt:"..(alt-1)..";^]"
  127. end
  128. if alt < #zcg.crafts[current_item] then
  129. formspec = formspec .. "button[7,2.5;1,1;zcg_alt:"..(alt+1)..";v]"
  130. end
  131. local c = zcg.crafts[current_item][alt]
  132. if c then
  133. local x = 3
  134. local y = 0
  135. -- Crafting recipe generated here.
  136. for i, item in pairs(c.items) do
  137. local stack = ItemStack(item)
  138. local itemname = stack:get_name()
  139. formspec = formspec .. "item_image_button["..((i-1)%c.width+x)..","..(math.floor((i-1)/c.width+y)+0.5)..";1,1;"..item..";zcg:"..itemname..";]"
  140. end
  141. if c.type == "normal" or
  142. c.type == "cooking" or
  143. c.type == "grinding" or
  144. c.type == "cutting" or
  145. c.type == "extracting" or
  146. c.type == "alloying" or
  147. c.type == "separating" or
  148. c.type == "compressing" or
  149. c.type == "crushing" then
  150. formspec = formspec .. "image[6,2.5;1,1;zcg_method_"..c.type..".png]"
  151. else -- we don't have an image for other types of crafting
  152. formspec = formspec .. "label[0,2.5;Method: "..c.type.."]"
  153. end
  154. if c.type == "normal" then
  155. formspec = formspec .. "label[0,2.5;Method: Crafting]"
  156. elseif c.type == "cooking" then
  157. formspec = formspec .. "label[0,2.5;Method: Cooking/Smelting]"
  158. elseif c.type == "grinding" then
  159. formspec = formspec .. "label[0,2.5;Method: Grinding]"
  160. elseif c.type == "crushing" then
  161. formspec = formspec .. "label[0,2.5;Method: Crushing]"
  162. elseif c.type == "cutting" then
  163. formspec = formspec .. "label[0,2.5;Method: Cutting]"
  164. elseif c.type == "extracting" then
  165. formspec = formspec .. "label[0,2.5;Method: Extracting]"
  166. elseif c.type == "compressing" then
  167. formspec = formspec .. "label[0,2.5;Method: Compressing]"
  168. elseif c.type == "alloying" then
  169. formspec = formspec .. "label[0,2.5;Method: Alloying]"
  170. elseif c.type == "separating" then
  171. formspec = formspec .. "label[0,2.5;Method: Separating]"
  172. end
  173. if type(c.result) == "string" then
  174. formspec = formspec .. "image[6,1.5;1,1;zcg_craft_arrow.png]"
  175. formspec = formspec .. "item_image_button[7,1.5;1,1;".. c.result ..";;]"
  176. elseif type(c.result) == "table" then
  177. -- Separating recipes have two outputs.
  178. formspec = formspec .. "item_image_button[6,1.5;1,1;".. c.result[1] ..";;]"
  179. formspec = formspec .. "item_image_button[7,1.5;1,1;".. c.result[2] ..";;]"
  180. end
  181. --minetest.chat_send_all(dump(c))
  182. end
  183. end
  184. end
  185. -- Node list
  186. local npp = 8*3 -- nodes per page
  187. local i = 0 -- for positionning buttons
  188. local s = 0 -- for skipping pages
  189. local whichlist = zcg.itemlist
  190. local listname = " total items."
  191. if zcg.users[pn].searchtext ~= "" then
  192. whichlist = zcg.users[pn].searchlist
  193. page = zcg.users[pn].spage
  194. listname = " result(s)."
  195. end
  196. if #whichlist > 0 then
  197. formspec = formspec ..
  198. "label[0,4.0;" .. #whichlist .. " " .. listname .. "]"
  199. for _, name in ipairs(whichlist) do
  200. if s < page*npp then s = s+1 else
  201. if i >= npp then break end
  202. formspec = formspec .. "item_image_button["..(i%8)..","..(math.floor(i/8)+4.5)..";1,1;"..name..";zcg:"..name..";]"
  203. i = i+1
  204. end
  205. end
  206. else
  207. formspec = formspec ..
  208. "label[0,4.0;No results.]"
  209. end
  210. -- Page buttons.
  211. local maxpage = (math.ceil(#whichlist/npp))
  212. if maxpage < 1 then maxpage = 1 end -- In case no results, must have 1 page.
  213. local curpage = page+1
  214. if page > 0 then
  215. formspec = formspec .. "button[0,8;1,.5;zcg_page:"..(page-1)..";<<]"
  216. else
  217. formspec = formspec .. "button[0,8;1,.5;zcg_page:"..(maxpage-1)..";<<]"
  218. end
  219. if curpage < maxpage then
  220. formspec = formspec .. "button[1,8;1,.5;zcg_page:"..(page+1)..";>>]"
  221. elseif curpage >= maxpage then
  222. formspec = formspec .. "button[1,8;1,.5;zcg_page:".. 0 ..";>>]"
  223. end
  224. -- The Y is approximatively the good one to have it centered vertically...
  225. formspec = formspec .. "label[2,7.85;Page " .. curpage .."/".. maxpage .."]"
  226. -- Search field.
  227. formspec = formspec ..
  228. "button[6,8;1,0.5;zcg_search;?]" ..
  229. "button[7,8;1,0.5;zcg_clear;X]"
  230. local text = zcg.users[pn].searchtext or ""
  231. formspec = formspec ..
  232. "field[4,8.1;2.3,1;zcg_sbox;;" .. minetest.formspec_escape(text) .. "]" ..
  233. "field_close_on_enter[zcg_sbox;false]"
  234. return formspec
  235. end
  236. function zcg.update_search(pn, tsearch)
  237. minetest.log("action", "<" .. rename.gpn(pn) .. "> executes craftguide search for \"" .. tsearch .. "\".")
  238. zcg.users[pn].searchlist = {}
  239. if tsearch == "" or tsearch == "<INVAL>" then
  240. return
  241. end
  242. -- Let user search multiple tokens at once.
  243. local texts = string.split(tsearch)
  244. if not texts or #texts == 0 then
  245. return
  246. end
  247. local list = {}
  248. local find = string.find
  249. local ipairs = ipairs
  250. local pairs = pairs
  251. local type = type
  252. local items = minetest.registered_items
  253. -- Returns true only if all tokens in list are found in the search string.
  254. local function find_all(search, combined)
  255. local count = 0
  256. for i=1, #combined do
  257. if find(search, combined[i], 1, true) then
  258. count = count + 1
  259. else
  260. -- Early finish.
  261. return false
  262. end
  263. end
  264. return (count == #combined)
  265. end
  266. for i=1, #texts, 1 do
  267. local text = string.trim(texts[i]):lower()
  268. local combined = string.split(text, " ")
  269. for k=1, #combined do
  270. combined[k] = string.trim(combined[k]):lower()
  271. end
  272. for _, name in ipairs(zcg.itemlist) do
  273. if find_all(name:lower(), combined) then
  274. list[name] = true
  275. else
  276. local ndef = items[name]
  277. if ndef then
  278. -- Search description for a match.
  279. if ndef.description then
  280. if find_all(ndef.description:lower(), combined) then
  281. list[name] = true
  282. end
  283. end
  284. end -- if ndef.
  285. end
  286. end
  287. end -- for all texts.
  288. -- Duplicate results are removed.
  289. local flat = {}
  290. for k, v in pairs(list) do
  291. flat[#flat + 1] = k
  292. end
  293. zcg.users[pn].searchlist = flat
  294. end
  295. -- Sound volumes.
  296. local click_gain = 0.4
  297. local page_gain = 0.7
  298. local sound_range = 16
  299. -- It seems the player's main inventory formspec does not have a formname.
  300. local singleplayer = minetest.is_singleplayer()
  301. zcg.on_receive_fields = function(player, formname, fields)
  302. local played_sound = false
  303. local pn = player:get_player_name()
  304. afk_removal.reset_timeout(pn)
  305. if zcg.users[pn] == nil then
  306. zcg.users[pn] = {
  307. current_item = "",
  308. alt = 1,
  309. page = 0,
  310. history = {index=0, list={}},
  311. searchlist = {},
  312. searchtext = "",
  313. spage = 0, -- Keep search page # seperate from main page #.
  314. }
  315. end
  316. if fields.zcg then
  317. inventory_plus.set_inventory_formspec(player, zcg.formspec(pn))
  318. if not played_sound and not fields.quit then
  319. ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
  320. end
  321. return
  322. elseif fields.zcg_previous then
  323. if zcg.users[pn].history.index > 1 then
  324. zcg.users[pn].history.index = zcg.users[pn].history.index - 1
  325. zcg.users[pn].current_item = zcg.users[pn].history.list[zcg.users[pn].history.index]
  326. zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
  327. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  328. end
  329. elseif fields.zcg_next then
  330. if zcg.users[pn].history.index < #zcg.users[pn].history.list then
  331. zcg.users[pn].history.index = zcg.users[pn].history.index + 1
  332. zcg.users[pn].current_item = zcg.users[pn].history.list[zcg.users[pn].history.index]
  333. zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
  334. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  335. end
  336. end
  337. for k, v in pairs(fields) do
  338. if (k:sub(0,4)=="zcg:") then
  339. local ni = k:sub(5)
  340. zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
  341. if zcg.crafts[ni] then
  342. local previtem = zcg.users[pn].current_item
  343. zcg.users[pn].current_item = ni
  344. table.insert(zcg.users[pn].history.list, ni)
  345. zcg.users[pn].history.index = #zcg.users[pn].history.list
  346. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  347. -- Add item to inventory if creative access is enabled.
  348. if gdac.player_is_admin(pn) or singleplayer then
  349. -- If player clicked twice.
  350. if previtem == ni then
  351. local inv = player:get_inventory()
  352. local stack = ItemStack(ni)
  353. stack:set_count(stack:get_stack_max())
  354. local leftover = inv:add_item("main", stack)
  355. -- Notify if a mapping kit was added.
  356. if map.is_mapping_kit(stack:get_name()) then
  357. map.update_inventory_info(pn)
  358. end
  359. if not leftover or leftover:get_count() == 0 then
  360. local desc = utility.get_short_desc(stack:get_definition().description or "Undescribed Item")
  361. minetest.chat_send_player(pn, "# Server: Added '" .. desc .. "' to inventory!")
  362. else
  363. minetest.chat_send_player(pn, "# Server: Not enough room in inventory!")
  364. end
  365. end
  366. end
  367. end
  368. elseif (k:sub(0,9)=="zcg_page:") then
  369. if zcg.users[pn].searchtext == "" then
  370. zcg.users[pn].page = tonumber(k:sub(10))
  371. else
  372. zcg.users[pn].spage = tonumber(k:sub(10))
  373. end
  374. ambiance.sound_play("pageflip", player:get_pos(), page_gain, sound_range)
  375. played_sound = true
  376. zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
  377. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  378. elseif (k:sub(0,8)=="zcg_alt:") then
  379. zcg.users[pn].alt = tonumber(k:sub(9))
  380. zcg.users[pn].searchtext = fields.zcg_sbox or "<INVAL>"
  381. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  382. elseif (k == "zcg_clear") then
  383. zcg.users[pn].searchlist = {}
  384. zcg.users[pn].searchtext = ""
  385. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  386. elseif (k == "zcg_search") then
  387. local newtext = fields.zcg_sbox or "<INVAL>"
  388. if newtext ~= zcg.users[pn].searchtext then -- Don't update if same.
  389. zcg.users[pn].searchlist = {}
  390. zcg.users[pn].searchtext = newtext
  391. if string.len(zcg.users[pn].searchtext) > 128 then
  392. zcg.users[pn].searchtext = "<INVAL>"
  393. else
  394. local res, err = pcall(function() zcg.update_search(pn, zcg.users[pn].searchtext) end)
  395. if not res and type(err) == "string" then
  396. minetest.log("error", err)
  397. end
  398. zcg.users[pn].spage = 0 -- Reset user's search page.
  399. end
  400. inventory_plus.set_inventory_formspec(player,zcg.formspec(pn))
  401. ambiance.sound_play("pageflip", player:get_pos(), page_gain, sound_range)
  402. played_sound = true
  403. end
  404. end
  405. end
  406. if not played_sound and not fields.quit then
  407. ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
  408. end
  409. if not played_sound and (fields.close or fields.exit or fields.done) then
  410. ambiance.sound_play("button_click", player:get_pos(), click_gain, sound_range)
  411. end
  412. -- This works because the 'quit' field is sent whenever the player tabs or ESC's out of a formspec,
  413. -- but not when changing to show a different formspec.
  414. if fields.quit then
  415. if passport.open_keys[pn] then
  416. passport.open_keys[pn] = nil
  417. ambiance.sound_play("fancy_chime2", player:get_pos(), 1.0, 20, "", false)
  418. end
  419. end
  420. end
  421. if not zcg.registered then
  422. -- Load all crafts directly after server-init time.
  423. -- We can't do this at craft-register time because the logic needs access to
  424. -- the groups of the recipe output items, which may not be known by the engine
  425. -- until after recipes for the items are registered.
  426. minetest.register_on_mods_loaded(function()
  427. local t1 = os.clock()
  428. -- Must search through ALL registered items! Cannot use shortcut tables.
  429. for name, item in pairs(minetest.registered_items) do
  430. if name and name ~= "" then
  431. -- Ignore stairs nodes. They do have generic/standard recipes, but we
  432. -- wouldn't show them anway -- WAY too much CG spam.
  433. if not name:find("^%:?stairs:") then
  434. zcg.load_crafts(name)
  435. end
  436. end
  437. end
  438. table.sort(zcg.itemlist)
  439. local t2 = os.clock()
  440. minetest.log("action", "Loading craft recipes took " .. (t2 - t1) .. " seconds.")
  441. end)
  442. -- Register button once.
  443. inventory_plus.register_button("zcg", "Craft Journal")
  444. -- Per Lua docs, newest functions are called first.
  445. -- Therefore resister inside minetest.after() to ensure this function is
  446. -- registered AFTER all other mods have registered theirs.
  447. minetest.after(0, function()
  448. minetest.register_on_player_receive_fields(function(...)
  449. return zcg.on_receive_fields(...)
  450. end)
  451. end)
  452. local c = "zcg:core"
  453. local f = zcg.modpath .. "/init.lua"
  454. reload.register_file(c, f, false)
  455. zcg.registered = true
  456. end