init.lua 16 KB

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