Allow cross-category aliases, change API
[minetest_doc.git] / init.lua
blobb6f2e529092d98afed8b90719708eaba919d986e
1 -- Boilerplate to support localized strings if intllib mod is installed.
2 local S, F
3 if minetest.get_modpath("intllib") then
4 S = intllib.Getter()
5 else
6 S = function(s,a,...)a={a,...}return s:gsub("@(%d+)",function(n)return a[tonumber(n)]end)end
7 end
8 F = function(f) return minetest.formspec_escape(S(f)) end
10 -- Compability for 0.4.14 or earlier
11 local colorize
12 if core.colorize then
13 colorize = core.colorize
14 else
15 colorize = function(color, text) return text end
16 end
18 doc = {}
20 -- Some informational variables
21 -- DO NOT CHANGE THEM AFTERWARDS AT RUNTIME!
23 -- Version number (follows the SemVer specification 2.0.0)
24 doc.VERSION = {}
25 doc.VERSION.MAJOR = 0
26 doc.VERSION.MINOR = 11
27 doc.VERSION.PATCH = 0
28 doc.VERSION.STRING = doc.VERSION.MAJOR.."."..doc.VERSION.MINOR.."."..doc.VERSION.PATCH
30 -- Formspec information
31 doc.FORMSPEC = {}
32 -- Width of formspec
33 doc.FORMSPEC.WIDTH = 15
34 doc.FORMSPEC.HEIGHT = 10.5
36 --[[ Recommended bounding box coordinates for widgets to be placed in entry pages. Make sure
37 all entry widgets are completely inside these coordinates to avoid overlapping. ]]
38 doc.FORMSPEC.ENTRY_START_X = 0
39 doc.FORMSPEC.ENTRY_START_Y = 0.5
40 doc.FORMSPEC.ENTRY_END_X = doc.FORMSPEC.WIDTH
41 doc.FORMSPEC.ENTRY_END_Y = doc.FORMSPEC.HEIGHT - 0.5
42 doc.FORMSPEC.ENTRY_WIDTH = doc.FORMSPEC.ENTRY_END_X - doc.FORMSPEC.ENTRY_START_X
43 doc.FORMSPEC.ENTRY_HEIGHT = doc.FORMSPEC.ENTRY_END_Y - doc.FORMSPEC.ENTRY_START_Y
45 --TODO: Use container formspec element later
47 -- Internal helper variables
48 local DOC_INTRO = S("This is the help.")
50 local COLOR_NOT_VIEWED = "#00FFFF" -- cyan
51 local COLOR_VIEWED = "#FFFFFF" -- white
52 local COLOR_HIDDEN = "#999999" -- gray
53 local COLOR_ERROR = "#FF0000" -- red
55 local CATEGORYFIELDSIZE = {
56 WIDTH = math.ceil(doc.FORMSPEC.WIDTH / 4),
57 HEIGHT = math.floor(doc.FORMSPEC.HEIGHT-1),
60 -- Maximum characters per line in the text widget
61 local TEXT_LINELENGTH = 80
63 doc.data = {}
64 doc.data.categories = {}
65 doc.data.aliases = {}
66 -- Default order (includes categories of other mods from the Docuentation System modpack)
67 doc.data.category_order = {"basics", "nodes", "tools", "craftitems", "advanced"}
68 doc.data.category_count = 0
69 doc.data.players = {}
71 -- Space for additional APIs
72 doc.sub = {}
74 -- Status variables
75 local set_category_order_was_called = false
77 --[[ Core API functions ]]
79 -- Add a new category
80 function doc.add_category(id, def)
81 if doc.data.categories[id] == nil and id ~= nil then
82 doc.data.categories[id] = {}
83 doc.data.categories[id].entries = {}
84 doc.data.categories[id].entry_count = 0
85 doc.data.categories[id].hidden_count = 0
86 doc.data.categories[id].def = def
87 -- Determine order position
88 local order_id = nil
89 for i=1,#doc.data.category_order do
90 if doc.data.category_order[i] == id then
91 order_id = i
92 break
93 end
94 end
95 if order_id == nil then
96 table.insert(doc.data.category_order, id)
97 doc.data.categories[id].order_position = #doc.data.category_order
98 else
99 doc.data.categories[id].order_position = order_id
101 doc.data.category_count = doc.data.category_count + 1
102 return true
103 else
104 return false
108 -- Add a new entry
109 function doc.add_entry(category_id, entry_id, def)
110 local cat = doc.data.categories[category_id]
111 if cat ~= nil then
112 local hidden = def.hidden or (def.hidden == nil and cat.def.hide_entries_by_default)
113 if hidden then
114 cat.hidden_count = cat.hidden_count + 1
115 def.hidden = hidden
117 cat.entry_count = doc.data.categories[category_id].entry_count + 1
118 if def.name == nil or def.name == "" then
119 minetest.log("warning", "[doc] Nameless entry added. Entry ID: "..entry_id)
121 cat.entries[entry_id] = def
122 return true
123 else
124 return false
128 -- Marks a particular entry as viewed by a certain player, which also
129 -- automatically reveals it
130 function doc.mark_entry_as_viewed(playername, category_id, entry_id)
131 local entry, category_id, entry_id = doc.get_entry(category_id, entry_id)
132 if not entry then
133 return
135 if doc.data.players[playername].stored_data.viewed[category_id] == nil then
136 doc.data.players[playername].stored_data.viewed[category_id] = {}
137 doc.data.players[playername].stored_data.viewed_count[category_id] = 0
139 if doc.entry_exists(category_id, entry_id) and doc.data.players[playername].stored_data.viewed[category_id][entry_id] ~= true then
140 doc.data.players[playername].stored_data.viewed[category_id][entry_id] = true
141 doc.data.players[playername].stored_data.viewed_count[category_id] = doc.data.players[playername].stored_data.viewed_count[category_id] + 1
142 -- Needed because viewed entries get a different color
143 doc.data.players[playername].entry_textlist_needs_updating = true
145 doc.mark_entry_as_revealed(playername, category_id, entry_id)
148 -- Marks a particular entry as revealed/unhidden by a certain player
149 function doc.mark_entry_as_revealed(playername, category_id, entry_id)
150 local entry, category_id, entry_id = doc.get_entry(category_id, entry_id)
151 if not entry then
152 return
154 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
155 doc.data.players[playername].stored_data.revealed[category_id] = {}
156 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
158 if doc.entry_exists(category_id, entry_id) and entry.hidden and doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then
159 doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true
160 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1
161 -- Needed because a new entry is added to the list of visible entries
162 doc.data.players[playername].entry_textlist_needs_updating = true
163 if minetest.get_modpath("central_message") ~= nil then
164 local cat = doc.data.categories[category_id]
165 cmsg.push_message_player(minetest.get_player_by_name(playername), S("New help entry unlocked: @1 > @2", cat.def.name, entry.name))
167 -- To avoid sound spamming, don't play sound more than once per second
168 local last_sound = doc.data.players[playername].last_reveal_sound
169 if last_sound == nil or os.difftime(os.time(), last_sound) >= 1 then
170 -- Play notification sound
171 minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername })
172 doc.data.players[playername].last_reveal_sound = os.time()
177 -- Reveal
178 function doc.mark_all_entries_as_revealed(playername)
179 -- Has at least 1 new entry been revealed?
180 local reveal1 = false
181 for category_id, category in pairs(doc.data.categories) do
182 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
183 doc.data.players[playername].stored_data.revealed[category_id] = {}
184 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
186 for entry_id, _ in pairs(category.entries) do
187 if doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then
188 doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true
189 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1
190 reveal1 = true
195 local msg
196 if reveal1 then
197 -- Needed because new entries are added to player's view on entry list
198 doc.data.players[playername].entry_textlist_needs_updating = true
200 msg = S("All help entries revealed!")
202 -- Play notification sound (ignore sound limit intentionally)
203 minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername })
204 doc.data.players[playername].last_reveal_sound = os.time()
205 else
206 msg = S("All help entries are already revealed.")
208 -- Notify
209 if minetest.get_modpath("central_message") ~= nil then
210 cmsg.push_message_player(minetest.get_player_by_name(playername), msg)
211 else
212 minetest.chat_send_player(playername, msg)
216 -- Returns true if the specified entry has been viewed by the player
217 function doc.entry_viewed(playername, category_id, entry_id)
218 local entry, category_id, entry_id = doc.get_entry(category_id, entry_id)
219 if doc.data.players[playername].stored_data.viewed[category_id] == nil then
220 return false
221 else
222 return doc.data.players[playername].stored_data.viewed[category_id][entry_id] == true
226 -- Returns true if the specified entry is hidden from the player
227 function doc.entry_revealed(playername, category_id, entry_id)
228 local entry, category_id, entry_id = doc.get_entry(category_id, entry_id)
229 local hidden = doc.data.categories[category_id].entries[entry_id].hidden
230 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
231 return not hidden
232 else
233 if hidden then
234 return doc.data.players[playername].stored_data.revealed[category_id][entry_id] == true
235 else
236 return true
241 -- Returns category definition
242 function doc.get_category_definition(category_id)
243 if doc.data.categories[category_id] == nil then
244 return nil
246 return doc.data.categories[category_id].def
249 -- Returns entry definition
250 function doc.get_entry_definition(category_id, entry_id)
251 if not doc.entry_exists(category_id, entry_id) then
252 return nil
254 local entry, _, _ = doc.get_entry(category_id, entry_id)
255 return entry
258 -- Opens the main documentation formspec for the player
259 function doc.show_doc(playername)
260 if doc.get_category_count() <= 0 then
261 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
262 return
264 local formspec = doc.formspec_core()..doc.formspec_main(playername)
265 minetest.show_formspec(playername, "doc:main", formspec)
268 -- Opens the documentation formspec for the player at the specified category
269 function doc.show_category(playername, category_id)
270 if doc.get_category_count() <= 0 then
271 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
272 return
274 doc.data.players[playername].catsel = nil
275 doc.data.players[playername].category = category_id
276 doc.data.players[playername].entry = nil
277 local formspec = doc.formspec_core(2)..doc.formspec_category(category_id, playername)
278 minetest.show_formspec(playername, "doc:category", formspec)
281 -- Opens the documentation formspec for the player showing the specified entry in a category
282 function doc.show_entry(playername, category_id, entry_id, ignore_hidden)
283 if doc.get_category_count() <= 0 then
284 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
285 return
287 local entry, category_id, entry_id = doc.get_entry(category_id, entry_id)
288 if ignore_hidden or doc.entry_revealed(playername, category_id, entry_id) then
289 local playerdata = doc.data.players[playername]
290 playerdata.category = category_id
291 playerdata.entry = entry_id
293 doc.mark_entry_as_viewed(playername, category_id, entry_id)
294 playerdata.entry_textlist_needs_updating = true
295 doc.generate_entry_list(category_id, playername)
297 playerdata.catsel = playerdata.catsel_list[entry_id]
298 playerdata.galidx = 1
300 local formspec = doc.formspec_core(3)..doc.formspec_entry(category_id, entry_id, playername)
301 minetest.show_formspec(playername, "doc:entry", formspec)
302 else
303 minetest.show_formspec(playername, "doc:error_hidden", doc.formspec_error_hidden(category_id, entry_id))
307 -- Returns true if and only if:
308 -- * The specified category exists
309 -- * This category contains the specified entry
310 -- Aliases are taken into account
311 function doc.entry_exists(category_id, entry_id)
312 return doc.get_entry(category_id, entry_id) ~= nil
315 -- Sets the order of categories in the category list
316 function doc.set_category_order(categories)
317 local reverse_categories = {}
318 for cid=1,#categories do
319 reverse_categories[categories[cid]] = cid
321 doc.data.category_order = categories
322 for cid, cat in pairs(doc.data.categories) do
323 if reverse_categories[cid] == nil then
324 table.insert(doc.data.category_order, cid)
327 reverse_categories = {}
328 for cid=1, #doc.data.category_order do
329 reverse_categories[categories[cid]] = cid
332 for cid, cat in pairs(doc.data.categories) do
333 cat.order_position = reverse_categories[cid]
335 if set_category_order_was_called then
336 minetest.log("warning", "[doc] doc.set_category_order was called again!")
338 set_category_order_was_called = true
341 -- Adds an alias for an entry. Attempting to open an entry by an alias name
342 -- results in opening the entry of the original name.
343 function doc.add_entry_alias(category_id_orig, entry_id_orig, category_id_alias, entry_id_alias)
344 if not doc.data.aliases[category_id_alias] then
345 doc.data.aliases[category_id_alias] = {}
347 doc.data.aliases[category_id_alias][entry_id_alias] = { category_id = category_id_orig, entry_id = entry_id_orig }
350 -- Returns number of categories
351 function doc.get_category_count()
352 return doc.data.category_count
355 -- Returns number of entries in category
356 function doc.get_entry_count(category_id)
357 return doc.data.categories[category_id].entry_count
360 -- Returns how many entries have been viewed by the player
361 function doc.get_viewed_count(playername, category_id)
362 local playerdata = doc.data.players[playername]
363 if playerdata == nil then
364 return nil
366 local count = playerdata.stored_data.viewed_count[category_id]
367 if count == nil then
368 playerdata.stored_data.viewed[category_id] = {}
369 count = 0
370 playerdata.stored_data.viewed_count[category_id] = count
371 return count
372 else
373 return count
377 -- Returns how many entries have been revealed by the player
378 function doc.get_revealed_count(playername, category_id)
379 local playerdata = doc.data.players[playername]
380 if playerdata == nil then
381 return nil
383 local count = playerdata.stored_data.revealed_count[category_id]
384 if count == nil then
385 playerdata.stored_data.revealed[category_id] = {}
386 count = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
387 playerdata.stored_data.revealed_count[category_id] = count
388 return count
389 else
390 return count
394 -- Returns how many entries are hidden from the player
395 function doc.get_hidden_count(playername, category_id)
396 local playerdata = doc.data.players[playername]
397 if playerdata == nil then
398 return nil
400 local total = doc.get_entry_count(category_id)
401 local rcount = playerdata.stored_data.revealed_count[category_id]
402 if rcount == nil then
403 return total
404 else
405 return total - rcount
409 -- Returns the currently viewed entry and/or category of the player
410 function doc.get_selection(playername)
411 local playerdata = doc.data.players[playername]
412 if playerdata ~= nil then
413 local cat = playerdata.category
414 if cat then
415 local entry = playerdata.entry
416 if entry then
417 return cat, entry
418 else
419 return cat
421 else
422 return nil
424 else
425 return nil
429 -- Template function templates, to be used for build_formspec in doc.add_category
430 doc.entry_builders = {}
432 -- Inserts line breaks into a single paragraph and collapses all whitespace (including newlines)
433 -- into spaces
434 local linebreaker_single = function(text, linelength)
435 if linelength == nil then
436 linelength = TEXT_LINELENGTH
438 local remain = linelength
439 local res = {}
440 local line = {}
441 local split = function(s)
442 local res = {}
443 for w in string.gmatch(s, "%S+") do
444 res[#res+1] = w
446 return res
449 for _, word in ipairs(split(text)) do
450 if string.len(word) + 1 > remain then
451 table.insert(res, table.concat(line, " "))
452 line = { word }
453 remain = linelength - string.len(word)
454 else
455 table.insert(line, word)
456 remain = remain - (string.len(word) + 1)
460 table.insert(res, table.concat(line, " "))
461 return table.concat(res, "\n")
464 -- Inserts automatic line breaks into an entire text and preserves existing newlines
465 local linebreaker = function(text, linelength)
466 local out = ""
467 for s in string.gmatch(text, "([^\n]*)") do
468 local l = linebreaker_single(s, linelength)
469 out = out .. l
470 if(string.len(l) == 0) then
471 out = out .. "\n"
474 -- Remove last newline
475 if string.len(out) >= 1 then
476 out = string.sub(out, 1, string.len(out) - 1)
478 return out
481 -- Inserts text suitable for a textlist (including automatic word-wrap)
482 local text_for_textlist = function(text, linelength)
483 text = linebreaker(text, linelength)
484 text = minetest.formspec_escape(text)
485 text = string.gsub(text, "\n", ",")
486 return text
489 -- Scrollable freeform text
490 doc.entry_builders.text = function(data)
491 local formstring = doc.widgets.text(data, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y, doc.FORMSPEC.ENTRY_WIDTH - 0.2, doc.FORMSPEC.ENTRY_HEIGHT)
492 return formstring
495 -- Scrollable freeform text with an optional standard gallery (3 rows, 3:2 aspect ratio)
496 doc.entry_builders.text_and_gallery = function(data, playername)
497 -- How much height the image gallery “steals” from the text widget
498 local stolen_height = 0
499 local formstring = ""
500 -- Only add the gallery if images are in the data, otherwise, the text widget gets all of the space
501 if data.images ~= nil then
502 local gallery
503 gallery, stolen_height = doc.widgets.gallery(data.images, playername, nil, doc.FORMSPEC.ENTRY_END_Y + 0.2, nil, nil, nil, nil, false)
504 formstring = formstring .. gallery
506 formstring = formstring .. doc.widgets.text(data.text,
507 doc.FORMSPEC.ENTRY_START_X,
508 doc.FORMSPEC.ENTRY_START_Y,
509 doc.FORMSPEC.ENTRY_WIDTH - 0.2,
510 doc.FORMSPEC.ENTRY_HEIGHT - stolen_height)
512 return formstring
515 doc.widgets = {}
517 local text_id = 1
518 -- Scrollable freeform text
519 doc.widgets.text = function(data, x, y, width, height)
520 if x == nil then
521 x = doc.FORMSPEC.ENTRY_START_X
523 if y == nil then
524 y = doc.FORMSPEC.ENTRY_START_Y
526 if width == nil then
527 width = doc.FORMSPEC.ENTRY_WIDTH
529 if height == nil then
530 height = doc.FORMSPEC.ENTRY_HEIGHT
532 local baselength = TEXT_LINELENGTH
533 local widget_basewidth = doc.FORMSPEC.WIDTH
534 local linelength = math.max(20, math.floor(baselength * (width / widget_basewidth)))
536 local widget_id = "doc_widget_text"..text_id
537 text_id = text_id + 1
538 -- TODO: Wait for Minetest to provide a native widget for scrollable read-only text with automatic line breaks.
539 -- Currently, all of this had to be hacked into this script manually by using/abusing the table widget
540 local formstring = "tablecolumns[text]"..
541 "tableoptions[background=#000000FF;highlight=#000000FF;border=false]"..
542 "table["..tostring(x)..","..tostring(y)..";"..tostring(width)..","..tostring(height)..";"..widget_id..";"..text_for_textlist(data, linelength).."]"
543 return formstring, widget_id
546 -- Image gallery
547 -- Currently, only one gallery per entry is supported. TODO: Add support for multiple galleries in an entry (low priority)
548 doc.widgets.gallery = function(imagedata, playername, x, y, aspect_ratio, width, rows, align_left, align_top)
549 if playername == nil then return nil end -- emergency exit
551 local formstring = ""
553 -- Defaults
554 if x == nil then
555 if align_left == false then
556 x = doc.FORMSPEC.ENTRY_END_X
557 else
558 x = doc.FORMSPEC.ENTRY_START_X
561 if y == nil then
562 if align_top == false then
563 y = doc.FORMSPEC.ENTRY_END_Y
564 else
565 y = doc.FORMSPEC.ENTRY_START_Y
568 if width == nil then width = doc.FORMSPEC.ENTRY_WIDTH end
569 if rows == nil then rows = 3 end
571 if align_left == false then
572 x = x - width
575 local imageindex = doc.data.players[playername].galidx
576 doc.data.players[playername].maxgalidx = #imagedata
577 doc.data.players[playername].galrows = rows
579 if aspect_ratio == nil then aspect_ratio = (2/3) end
580 local pos = 0
581 local totalimagewidth, iw, ih
582 local bw = 0.5
583 local buttonoffset = 0
584 if #imagedata > rows then
585 totalimagewidth = width - bw*2
586 iw = totalimagewidth / rows
587 ih = iw * aspect_ratio
588 if align_top == false then
589 y = y - ih
592 local tt
593 if imageindex > 1 then
594 formstring = formstring .. "button["..x..","..y..";"..bw..","..ih..";doc_button_gallery_prev;"..F("<").."]"
595 if rows == 1 then
596 tt = F("Show previous image")
597 else
598 tt = F("Show previous gallery page")
600 formstring = formstring .. "tooltip[doc_button_gallery_prev;"..tt.."]"
602 if (imageindex + rows) <= #imagedata then
603 local rightx = buttonoffset + (x + rows * iw)
604 formstring = formstring .. "button["..rightx..","..y..";"..bw..","..ih..";doc_button_gallery_next;"..F(">").."]"
605 if rows == 1 then
606 tt = F("Show next image")
607 else
608 tt = F("Show next gallery page")
610 formstring = formstring .. "tooltip[doc_button_gallery_next;"..tt.."]"
612 buttonoffset = bw
613 else
614 totalimagewidth = width
615 iw = totalimagewidth / rows
616 ih = iw * aspect_ratio
617 if align_top == false then
618 y = y - ih
621 for i=imageindex, math.min(#imagedata, (imageindex-1)+rows) do
622 local xoffset = buttonoffset + (x + pos * iw)
623 local nx = xoffset - 0.2
624 local ny = y - 0.05
625 if imagedata[i].imagetype == "item" then
626 formstring = formstring .. "item_image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]"
627 else
628 formstring = formstring .. "image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]"
630 formstring = formstring .. "label["..nx..","..ny..";"..i.."]"
631 pos = pos + 1
633 local bw, bh
635 return formstring, ih
638 -- Direct formspec
639 doc.entry_builders.formspec = function(data)
640 return data
643 --[[ Internal stuff ]]
645 -- Loading and saving player data
647 local filepath = minetest.get_worldpath().."/doc.mt"
648 local file = io.open(filepath, "r")
649 if file then
650 minetest.log("action", "[doc] doc.mt opened.")
651 local string = file:read()
652 io.close(file)
653 if(string ~= nil) then
654 local savetable = minetest.deserialize(string)
655 for name, players_stored_data in pairs(savetable.players_stored_data) do
656 doc.data.players[name] = {}
657 doc.data.players[name].stored_data = players_stored_data
659 minetest.debug("[doc] doc.mt successfully read.")
664 function doc.save_to_file()
665 local savetable = {}
666 savetable.players_stored_data = {}
667 for name, playerdata in pairs(doc.data.players) do
668 savetable.players_stored_data[name] = playerdata.stored_data
671 local savestring = minetest.serialize(savetable)
673 local filepath = minetest.get_worldpath().."/doc.mt"
674 local file = io.open(filepath, "w")
675 if file then
676 file:write(savestring)
677 io.close(file)
678 minetest.log("action", "[doc] Wrote player data into "..filepath..".")
679 else
680 minetest.log("error", "[doc] Failed to write player data into "..filepath..".")
684 minetest.register_on_leaveplayer(function(player)
685 doc.save_to_file()
686 end)
688 minetest.register_on_shutdown(function()
689 minetest.log("action", "[doc] Server shuts down. Player data is about to be saved.")
690 doc.save_to_file()
691 end)
693 --[[ Functions for internal use ]]
695 function doc.formspec_core(tab)
696 if tab == nil then tab = 1 else tab = tostring(tab) end
697 return "size["..doc.FORMSPEC.WIDTH..","..doc.FORMSPEC.HEIGHT.."]tabheader[0,0;doc_header;"..
698 minetest.formspec_escape(S("Category list")) .. "," ..
699 minetest.formspec_escape(S("Entry list")) .. "," ..
700 minetest.formspec_escape(S("Entry")) .. ";"
701 ..tab..";true;true]" ..
702 "bgcolor[#343434FF]"
705 function doc.formspec_main(playername)
706 local formstring = "label[0,0;"..minetest.formspec_escape(DOC_INTRO) .. "\n"
707 if doc.get_category_count() >= 1 then
708 formstring = formstring .. F("Please select a category you wish to learn more about:").."]"
709 if doc.get_category_count() <= (CATEGORYFIELDSIZE.WIDTH * CATEGORYFIELDSIZE.HEIGHT) then
710 local y = 1
711 local x = 1
712 -- Show all categories in order
713 for c=1,#doc.data.category_order do
714 local id = doc.data.category_order[c]
715 local data = doc.data.categories[id]
716 local bw = doc.FORMSPEC.WIDTH / math.floor(((doc.data.category_count-1) / CATEGORYFIELDSIZE.HEIGHT)+1)
717 -- Skip categories which do not exist
718 if data ~= nil then
719 -- Category buton
720 local button = "button["..((x-1)*bw)..","..y..";"..bw..",1;doc_button_category_"..id..";"..minetest.formspec_escape(data.def.name).."]"
721 local tooltip = ""
722 -- Optional description
723 if data.def.description ~= nil then
724 tooltip = "tooltip[doc_button_category_"..id..";"..minetest.formspec_escape(data.def.description).."]"
726 formstring = formstring .. button .. tooltip
727 y = y + 1
728 if y > CATEGORYFIELDSIZE.HEIGHT then
729 x = x + 1
730 y = 1
734 else
735 formstring = formstring .. "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_mainlist;"
736 for c=1,#doc.data.category_order do
737 local id = doc.data.category_order[c]
738 local data = doc.data.categories[id]
739 formstring = formstring .. minetest.formspec_escape(data.def.name)
740 if c < #doc.data.category_order then
741 formstring = formstring .. ","
744 local sel = doc.data.categories[doc.data.players[playername].category]
745 if sel ~= nil then
746 formstring = formstring .. ";"
747 formstring = formstring .. doc.data.categories[doc.data.players[playername].category].order_position
749 formstring = formstring .. "]"
750 formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_category;"..F("Show category").."]"
752 else
753 formstring = formstring .. "]"
755 return formstring
758 function doc.formspec_error_no_categories()
759 local formstring = "size[8,6]textarea[0.25,0;8,6;;"
760 formstring = formstring ..
761 minetest.formspec_escape(
762 colorize(COLOR_ERROR, S("Error: No help available.")) .. "\n\n" ..
763 S("No categories have been registered, but they are required to provide help.\nThe Documentation System [doc] does not come with help contents on its own, it needs additional mods to add help content. Please make sure such mods are enabled on for this world, and try again.")) .. "\n\n" ..
764 S("Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia.")
765 formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]"
766 return formstring
769 function doc.formspec_error_hidden(category_id, entry_id)
770 local formstring = "size[8,6]textarea[0.25,0;8,6;;"
771 formstring = formstring .. minetest.formspec_escape(
772 colorize(COLOR_ERROR, S("Error: Access denied.")) .. "\n\n" ..
773 S("Access to the requested entry has been denied; this entry is secret. You may unlock access by progressing in the game. Figure out on your own how to unlock this entry."))
774 formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]"
775 return formstring
778 -- Returns the entry definition and true entry ID of an entry, taking aliases into account
779 function doc.get_entry(category_id, entry_id)
780 local category = doc.data.categories[category_id]
781 local entry
782 if category ~= nil then
783 entry = category.entries[entry_id]
785 if category == nil or entry == nil then
786 local c_alias = doc.data.aliases[category_id]
787 if c_alias then
788 local alias = c_alias[entry_id]
789 if alias then
790 category_id = alias.category_id
791 entry_id = alias.entry_id
792 category = doc.data.categories[category_id]
793 if category then
794 entry = category.entries[entry_id]
795 else
796 return nil
798 else
799 return nil
801 else
802 return nil
805 return entry, category_id, entry_id
808 function doc.generate_entry_list(cid, playername)
809 local formstring
810 if doc.data.players[playername].entry_textlist == nil
811 or doc.data.players[playername].catsel_list == nil
812 or doc.data.players[playername].category ~= cid
813 or doc.data.players[playername].entry_textlist_needs_updating == true then
814 local entry_textlist = "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_catlist;"
815 local counter = 0
816 doc.data.players[playername].entry_ids = {}
817 local entries = doc.get_sorted_entry_names(cid)
818 doc.data.players[playername].catsel_list = {}
819 for i=1, #entries do
820 local eid = entries[i]
821 local edata = doc.data.categories[cid].entries[eid]
822 if doc.entry_revealed(playername, cid, eid) then
823 table.insert(doc.data.players[playername].entry_ids, eid)
824 doc.data.players[playername].catsel_list[eid] = counter + 1
825 -- Colorize entries based on viewed status
826 local viewedprefix = COLOR_NOT_VIEWED
827 local name = edata.name
828 if name == nil or name == "" then
829 name = S("Nameless entry (@1)", eid)
830 if doc.entry_viewed(playername, cid, eid) then
831 viewedprefix = "#FF4444"
832 else
833 viewedprefix = COLOR_ERROR
835 elseif doc.entry_viewed(playername, cid, eid) then
836 viewedprefix = COLOR_VIEWED
838 entry_textlist = entry_textlist .. viewedprefix .. minetest.formspec_escape(name) .. ","
839 counter = counter + 1
842 if counter >= 1 then
843 entry_textlist = string.sub(entry_textlist, 1, #entry_textlist-1)
845 local catsel = doc.data.players[playername].catsel
846 if catsel then
847 entry_textlist = entry_textlist .. ";"..catsel
849 entry_textlist = entry_textlist .. "]"
850 doc.data.players[playername].entry_textlist = entry_textlist
851 formstring = entry_textlist
852 doc.data.players[playername].entry_textlist_needs_updating = false
853 else
854 formstring = doc.data.players[playername].entry_textlist
856 return formstring
859 function doc.get_sorted_entry_names(cid)
860 local sort_table = {}
861 local entry_table = {}
862 local cat = doc.data.categories[cid]
863 local used_eids = {}
864 -- Helper function to extract the entry ID out of the output table
865 local extract = function(entry_table)
866 local eids = {}
867 for k,v in pairs(entry_table) do
868 local eid = v.eid
869 table.insert(eids, eid)
871 return eids
873 -- Predefined sorting
874 if cat.def.sorting == "custom" then
875 for i=1,#cat.def.sorting_data do
876 local new_entry = table.copy(cat.entries[cat.def.sorting_data[i]])
877 new_entry.eid = cat.def.sorting_data[i]
878 table.insert(entry_table, new_entry)
879 used_eids[cat.def.sorting_data[i]] = true
882 for eid,entry in pairs(cat.entries) do
883 local new_entry = table.copy(entry)
884 new_entry.eid = eid
885 if not used_eids[eid] then
886 table.insert(entry_table, new_entry)
888 table.insert(sort_table, entry.name)
890 if cat.def.sorting == "custom" then
891 return extract(entry_table)
892 else
893 table.sort(sort_table)
895 local reverse_sort_table = table.copy(sort_table)
896 for i=1, #sort_table do
897 reverse_sort_table[sort_table[i]] = i
899 local comp
900 if cat.def.sorting ~= "nosort" then
901 -- Sorting by user function
902 if cat.def.sorting == "function" then
903 comp = cat.def.sorting_data
904 -- Alphabetic sorting
905 elseif cat.def.sorting == "abc" or cat.def.sorting == nil then
906 comp = function(e1, e2)
907 if reverse_sort_table[e1.name] < reverse_sort_table[e2.name] then return true else return false end
910 table.sort(entry_table, comp)
913 return extract(entry_table)
916 function doc.formspec_category(id, playername)
917 local formstring
918 if id == nil then
919 formstring = "label[0,0;"..F("Help > (No Category)") .. "]"
920 formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]"
921 formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]"
922 else
923 formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1", doc.data.categories[id].def.name)).."]"
924 local total = doc.get_entry_count(id)
925 if total >= 1 then
926 local revealed = doc.get_revealed_count(playername, id)
927 if revealed == 0 then
928 formstring = formstring .. "label[0,0.5;"..F("Currently all entries in this category are hidden from you.\nUnlock new entries by progressing in the game.").."]"
929 formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]"
930 else
931 formstring = formstring .. "label[0,0.5;"..F("This category has the following entries:").."]"
932 formstring = formstring .. doc.generate_entry_list(id, playername)
933 formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_entry;"..F("Show entry").."]"
934 formstring = formstring .. "label["..(doc.FORMSPEC.WIDTH-4)..","..(doc.FORMSPEC.HEIGHT-1)..";"..minetest.formspec_escape(S("Number of entries: @1", total)).."\n"
935 local viewed = doc.get_viewed_count(playername, id)
936 local hidden = total - revealed
937 local new = total - viewed - hidden
938 -- TODO/FIXME: Check if number of hidden/viewed entries is always correct
939 if viewed < total then
940 formstring = formstring .. colorize(COLOR_NOT_VIEWED, minetest.formspec_escape(S("New entries: @1", new)))
941 if hidden > 0 then
942 formstring = formstring .. "\n"
943 formstring = formstring .. colorize(COLOR_HIDDEN, minetest.formspec_escape(S("Hidden entries: @1", hidden))).."]"
944 else
945 formstring = formstring .. "]"
947 else
948 formstring = formstring .. F("All entries read.").."]"
951 else
952 formstring = formstring .. "label[0,0.5;"..F("This category is empty.").."]"
953 formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]"
956 return formstring
959 function doc.formspec_entry_navigation(category_id, entry_id)
960 if doc.get_entry_count(category_id) < 1 then
961 return ""
963 local formstring = ""
964 formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-2)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_prev;"..F("<").."]"
965 formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-1)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_next;"..F(">").."]"
966 formstring = formstring .. "tooltip[doc_button_goto_prev;"..F("Show previous entry").."]"
967 formstring = formstring .. "tooltip[doc_button_goto_next;"..F("Show next entry").."]"
968 return formstring
971 function doc.formspec_entry(category_id, entry_id, playername)
972 local formstring
973 if category_id == nil then
974 formstring = "label[0,0;"..F("Help > (No Category)") .. "]"
975 formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]"
976 formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]"
977 elseif entry_id == nil then
978 formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1 > (No Entry)", doc.data.categories[category_id].def.name)) .. "]"
979 if doc.get_entry_count(category_id) >= 1 then
980 formstring = formstring .. "label[0,0.5;"..F("You haven't chosen an entry yet. Please choose one in the entry list first.").."]"
981 formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_category;"..F("Go to entry list").."]"
982 else
983 formstring = formstring .. "label[0,0.5;"..F("This category does not have any entries.").."]"
984 formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]"
986 else
988 local category = doc.data.categories[category_id]
989 local entry = doc.get_entry(category_id, entry_id)
990 local ename = entry.name
991 if ename == nil or ename == "" then
992 ename = S("Nameless entry (@1)", entry_id)
994 formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1 > @2", category.def.name, ename)).."]"
995 formstring = formstring .. category.def.build_formspec(entry.data, playername)
996 formstring = formstring .. doc.formspec_entry_navigation(category_id, entry_id)
998 return formstring
1001 function doc.process_form(player,formname,fields)
1002 local playername = player:get_player_name()
1003 --[[ process clicks on the tab header ]]
1004 if(formname == "doc:main" or formname == "doc:category" or formname == "doc:entry") then
1005 if fields.doc_header ~= nil then
1006 local tab = tonumber(fields.doc_header)
1007 local formspec, subformname, contents
1008 local cid, eid
1009 cid = doc.data.players[playername].category
1010 eid = doc.data.players[playername].entry
1011 if(tab==1) then
1012 contents = doc.formspec_main(playername)
1013 subformname = "main"
1014 elseif(tab==2) then
1015 contents = doc.formspec_category(cid, playername)
1016 subformname = "category"
1017 elseif(tab==3) then
1018 doc.data.players[playername].galidx = 1
1019 contents = doc.formspec_entry(cid, eid, playername)
1020 if cid ~= nil and eid ~= nil then
1021 doc.mark_entry_as_viewed(playername, cid, eid)
1023 subformname = "entry"
1025 formspec = doc.formspec_core(tab)..contents
1026 minetest.show_formspec(playername, "doc:" .. subformname, formspec)
1027 return
1030 if(formname == "doc:main") then
1031 for cid,_ in pairs(doc.data.categories) do
1032 if fields["doc_button_category_"..cid] then
1033 doc.data.players[playername].catsel = nil
1034 doc.data.players[playername].category = cid
1035 doc.data.players[playername].entry = nil
1036 doc.data.players[playername].entry_textlist_needs_updating = true
1037 local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername)
1038 minetest.show_formspec(playername, "doc:category", formspec)
1039 break
1042 if fields["doc_mainlist"] then
1043 local event = minetest.explode_textlist_event(fields["doc_mainlist"])
1044 local cid = doc.data.category_order[event.index]
1045 if cid ~= nil then
1046 if event.type == "CHG" then
1047 doc.data.players[playername].catsel = nil
1048 doc.data.players[playername].category = cid
1049 doc.data.players[playername].entry = nil
1050 doc.data.players[playername].entry_textlist_needs_updating = true
1051 elseif event.type == "DCL" then
1052 doc.data.players[playername].catsel = nil
1053 doc.data.players[playername].category = cid
1054 doc.data.players[playername].entry = nil
1055 doc.data.players[playername].entry_textlist_needs_updating = true
1056 local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername)
1057 minetest.show_formspec(playername, "doc:category", formspec)
1061 if fields["doc_button_goto_category"] then
1062 local cid = doc.data.players[playername].category
1063 doc.data.players[playername].catsel = nil
1064 doc.data.players[playername].entry = nil
1065 doc.data.players[playername].entry_textlist_needs_updating = true
1066 local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername)
1067 minetest.show_formspec(playername, "doc:category", formspec)
1069 elseif(formname == "doc:category") then
1070 if fields["doc_button_goto_entry"] then
1071 local cid = doc.data.players[playername].category
1072 if cid ~= nil then
1073 local eid = nil
1074 local eids, catsel = doc.data.players[playername].entry_ids, doc.data.players[playername].catsel
1075 if eids ~= nil and catsel ~= nil then
1076 eid = eids[catsel]
1078 doc.data.players[playername].galidx = 1
1079 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername)
1080 minetest.show_formspec(playername, "doc:entry", formspec)
1081 doc.mark_entry_as_viewed(playername, cid, eid)
1084 if fields["doc_button_goto_main"] then
1085 local formspec = doc.formspec_core(1)..doc.formspec_main(playername)
1086 minetest.show_formspec(playername, "doc:main", formspec)
1088 if fields["doc_catlist"] then
1089 local event = minetest.explode_textlist_event(fields["doc_catlist"])
1090 if event.type == "CHG" then
1091 doc.data.players[playername].catsel = event.index
1092 doc.data.players[playername].entry = doc.data.players[playername].entry_ids[event.index]
1093 doc.data.players[playername].entry_textlist_needs_updating = true
1094 elseif event.type == "DCL" then
1095 local cid = doc.data.players[playername].category
1096 local eid = nil
1097 local eids, catsel = doc.data.players[playername].entry_ids, event.index
1098 if eids ~= nil and catsel ~= nil then
1099 eid = eids[catsel]
1101 doc.mark_entry_as_viewed(playername, cid, eid)
1102 doc.data.players[playername].entry_textlist_needs_updating = true
1103 doc.data.players[playername].galidx = 1
1104 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername)
1105 minetest.show_formspec(playername, "doc:entry", formspec)
1108 elseif(formname == "doc:entry") then
1109 if fields["doc_button_goto_main"] then
1110 local formspec = doc.formspec_core(1)..doc.formspec_main(playername)
1111 minetest.show_formspec(playername, "doc:main", formspec)
1112 elseif fields["doc_button_goto_category"] then
1113 local formspec = doc.formspec_core(2)..doc.formspec_category(doc.data.players[playername].category, playername)
1114 minetest.show_formspec(playername, "doc:category", formspec)
1115 elseif fields["doc_button_goto_next"] then
1116 if doc.data.players[playername].catsel == nil then return end -- emergency exit
1117 local eids = doc.data.players[playername].entry_ids
1118 local cid = doc.data.players[playername].category
1119 local new_catsel= doc.data.players[playername].catsel + 1
1120 local new_eid = eids[new_catsel]
1121 if #eids > 1 and new_catsel <= #eids then
1122 doc.mark_entry_as_viewed(playername, cid, new_eid)
1123 doc.data.players[playername].catsel = new_catsel
1124 doc.data.players[playername].entry = new_eid
1125 doc.data.players[playername].galidx = 1
1126 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername)
1127 minetest.show_formspec(playername, "doc:entry", formspec)
1129 elseif fields["doc_button_goto_prev"] then
1130 if doc.data.players[playername].catsel == nil then return end -- emergency exit
1131 local eids = doc.data.players[playername].entry_ids
1132 local cid = doc.data.players[playername].category
1133 local new_catsel= doc.data.players[playername].catsel - 1
1134 local new_eid = eids[new_catsel]
1135 if #eids > 1 and new_catsel >= 1 then
1136 doc.mark_entry_as_viewed(playername, cid, new_eid)
1137 doc.data.players[playername].catsel = new_catsel
1138 doc.data.players[playername].entry = new_eid
1139 doc.data.players[playername].galidx = 1
1140 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername)
1141 minetest.show_formspec(playername, "doc:entry", formspec)
1143 elseif fields["doc_button_gallery_prev"] then
1144 local cid, eid = doc.get_selection(playername)
1145 if doc.data.players[playername].galidx - doc.data.players[playername].galrows > 0 then
1146 doc.data.players[playername].galidx = doc.data.players[playername].galidx - doc.data.players[playername].galrows
1148 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername)
1149 minetest.show_formspec(playername, "doc:entry", formspec)
1150 elseif fields["doc_button_gallery_next"] then
1151 local cid, eid = doc.get_selection(playername)
1152 if doc.data.players[playername].galidx + doc.data.players[playername].galrows <= doc.data.players[playername].maxgalidx then
1153 doc.data.players[playername].galidx = doc.data.players[playername].galidx + doc.data.players[playername].galrows
1155 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername)
1156 minetest.show_formspec(playername, "doc:entry", formspec)
1158 else
1159 if fields["doc_inventory_plus"] and minetest.get_modpath("inventory_plus") then
1160 doc.show_doc(playername)
1161 return
1166 minetest.register_on_player_receive_fields(doc.process_form)
1168 minetest.register_chatcommand("helpform", {
1169 params = "",
1170 description = S("Open a window providing help entries about Minetest and more"),
1171 privs = {},
1172 func = function(playername, param)
1173 doc.show_doc(playername)
1174 end,
1178 minetest.register_on_joinplayer(function(player)
1179 local playername = player:get_player_name()
1180 local playerdata = doc.data.players[playername]
1181 if playerdata == nil then
1182 -- Initialize player data
1183 doc.data.players[playername] = {}
1184 playerdata = doc.data.players[playername]
1185 -- Gallery index, stores current index of first displayed image in a gallery
1186 playerdata.galidx = 1
1187 -- Maximum gallery index (index of last image in gallery)
1188 playerdata.maxgalidx = 1
1189 -- Number of rows in an gallery of the current entry
1190 playerdata.galrows = 1
1191 -- Table for persistant data
1192 playerdata.stored_data = {}
1193 -- Contains viewed entries
1194 playerdata.stored_data.viewed = {}
1195 -- Count viewed entries
1196 playerdata.stored_data.viewed_count = {}
1197 -- Contains revealed/unhidden entries
1198 playerdata.stored_data.revealed = {}
1199 -- Count revealed entries
1200 playerdata.stored_data.revealed_count = {}
1201 else
1202 -- Completely rebuild viewed and revealed counts from scratch
1203 for cid, cat in pairs(doc.data.categories) do
1204 if playerdata.stored_data.viewed[cid] == nil then
1205 playerdata.stored_data.viewed[cid] = {}
1207 if playerdata.stored_data.revealed[cid] == nil then
1208 playerdata.stored_data.revealed[cid] = {}
1210 local vc = 0
1211 local rc = doc.get_entry_count(cid) - doc.data.categories[cid].hidden_count
1212 for eid, entry in pairs(cat.entries) do
1213 if playerdata.stored_data.viewed[cid][eid] then
1214 vc = vc + 1
1215 playerdata.stored_data.revealed[cid][eid] = true
1217 if playerdata.stored_data.revealed[cid][eid] and entry.hidden then
1218 rc = rc + 1
1221 playerdata.stored_data.viewed_count[cid] = vc
1222 playerdata.stored_data.revealed_count[cid] = rc
1226 -- Add button for Inventory++
1227 if minetest.get_modpath("inventory_plus") ~= nil then
1228 inventory_plus.register_button(player, "doc_inventory_plus", S("Help"))
1230 end)
1232 ---[[ Add buttons for inventory mods ]]
1233 local button_action = function(player)
1234 doc.show_doc(player:get_player_name())
1237 -- Unified Inventory
1238 if minetest.get_modpath("unified_inventory") ~= nil then
1239 unified_inventory.register_button("doc", {
1240 type = "image",
1241 image = "doc_button_icon_hires.png",
1242 tooltip = S("Help"),
1243 action = button_action,
1247 -- sfinv_buttons
1248 if minetest.get_modpath("sfinv_buttons") ~= nil then
1249 sfinv_buttons.register_button("doc", {
1250 image = "doc_button_icon_lores.png",
1251 tooltip = S("Collection of help texts"),
1252 title = S("Help"),
1253 action = button_action,
1258 minetest.register_privilege("help_reveal", {
1259 description = S("Allows you to reveal all hidden help entries with /help_reveal"),
1260 give_to_singleplayer = false
1263 minetest.register_chatcommand("help_reveal", {
1264 params = "",
1265 description = S("Reveal all hidden help entries to you"),
1266 privs = { help_reveal = true },
1267 func = function(name, param)
1268 doc.mark_all_entries_as_revealed(name)
1269 end,