Localize doc.get_entry
[minetest_doc.git] / init.lua
blobf42ea559e641c52c7f4268370aaca3b4bb64358a
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 -- Returns the entry definition and true entry ID of an entry, taking aliases into account
78 local function get_entry(category_id, entry_id)
79 local category = doc.data.categories[category_id]
80 local entry
81 if category ~= nil then
82 entry = category.entries[entry_id]
83 end
84 if category == nil or entry == nil then
85 local c_alias = doc.data.aliases[category_id]
86 if c_alias then
87 local alias = c_alias[entry_id]
88 if alias then
89 category_id = alias.category_id
90 entry_id = alias.entry_id
91 category = doc.data.categories[category_id]
92 if category then
93 entry = category.entries[entry_id]
94 else
95 return nil
96 end
97 else
98 return nil
99 end
100 else
101 return nil
104 return entry, category_id, entry_id
107 --[[ Core API functions ]]
109 -- Add a new category
110 function doc.add_category(id, def)
111 if doc.data.categories[id] == nil and id ~= nil then
112 doc.data.categories[id] = {}
113 doc.data.categories[id].entries = {}
114 doc.data.categories[id].entry_count = 0
115 doc.data.categories[id].hidden_count = 0
116 doc.data.categories[id].def = def
117 -- Determine order position
118 local order_id = nil
119 for i=1,#doc.data.category_order do
120 if doc.data.category_order[i] == id then
121 order_id = i
122 break
125 if order_id == nil then
126 table.insert(doc.data.category_order, id)
127 doc.data.categories[id].order_position = #doc.data.category_order
128 else
129 doc.data.categories[id].order_position = order_id
131 doc.data.category_count = doc.data.category_count + 1
132 return true
133 else
134 return false
138 -- Add a new entry
139 function doc.add_entry(category_id, entry_id, def)
140 local cat = doc.data.categories[category_id]
141 if cat ~= nil then
142 local hidden = def.hidden or (def.hidden == nil and cat.def.hide_entries_by_default)
143 if hidden then
144 cat.hidden_count = cat.hidden_count + 1
145 def.hidden = hidden
147 cat.entry_count = doc.data.categories[category_id].entry_count + 1
148 if def.name == nil or def.name == "" then
149 minetest.log("warning", "[doc] Nameless entry added. Entry ID: "..entry_id)
151 cat.entries[entry_id] = def
152 return true
153 else
154 return false
158 -- Marks a particular entry as viewed by a certain player, which also
159 -- automatically reveals it
160 function doc.mark_entry_as_viewed(playername, category_id, entry_id)
161 local entry, category_id, entry_id = get_entry(category_id, entry_id)
162 if not entry then
163 return
165 if doc.data.players[playername].stored_data.viewed[category_id] == nil then
166 doc.data.players[playername].stored_data.viewed[category_id] = {}
167 doc.data.players[playername].stored_data.viewed_count[category_id] = 0
169 if doc.entry_exists(category_id, entry_id) and doc.data.players[playername].stored_data.viewed[category_id][entry_id] ~= true then
170 doc.data.players[playername].stored_data.viewed[category_id][entry_id] = true
171 doc.data.players[playername].stored_data.viewed_count[category_id] = doc.data.players[playername].stored_data.viewed_count[category_id] + 1
172 -- Needed because viewed entries get a different color
173 doc.data.players[playername].entry_textlist_needs_updating = true
175 doc.mark_entry_as_revealed(playername, category_id, entry_id)
178 -- Marks a particular entry as revealed/unhidden by a certain player
179 function doc.mark_entry_as_revealed(playername, category_id, entry_id)
180 local entry, category_id, entry_id = get_entry(category_id, entry_id)
181 if not entry then
182 return
184 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
185 doc.data.players[playername].stored_data.revealed[category_id] = {}
186 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
188 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
189 doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true
190 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1
191 -- Needed because a new entry is added to the list of visible entries
192 doc.data.players[playername].entry_textlist_needs_updating = true
193 if minetest.get_modpath("central_message") ~= nil then
194 local cat = doc.data.categories[category_id]
195 cmsg.push_message_player(minetest.get_player_by_name(playername), S("New help entry unlocked: @1 > @2", cat.def.name, entry.name))
197 -- To avoid sound spamming, don't play sound more than once per second
198 local last_sound = doc.data.players[playername].last_reveal_sound
199 if last_sound == nil or os.difftime(os.time(), last_sound) >= 1 then
200 -- Play notification sound
201 minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername })
202 doc.data.players[playername].last_reveal_sound = os.time()
207 -- Reveal
208 function doc.mark_all_entries_as_revealed(playername)
209 -- Has at least 1 new entry been revealed?
210 local reveal1 = false
211 for category_id, category in pairs(doc.data.categories) do
212 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
213 doc.data.players[playername].stored_data.revealed[category_id] = {}
214 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
216 for entry_id, _ in pairs(category.entries) do
217 if doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then
218 doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true
219 doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1
220 reveal1 = true
225 local msg
226 if reveal1 then
227 -- Needed because new entries are added to player's view on entry list
228 doc.data.players[playername].entry_textlist_needs_updating = true
230 msg = S("All help entries revealed!")
232 -- Play notification sound (ignore sound limit intentionally)
233 minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername })
234 doc.data.players[playername].last_reveal_sound = os.time()
235 else
236 msg = S("All help entries are already revealed.")
238 -- Notify
239 if minetest.get_modpath("central_message") ~= nil then
240 cmsg.push_message_player(minetest.get_player_by_name(playername), msg)
241 else
242 minetest.chat_send_player(playername, msg)
246 -- Returns true if the specified entry has been viewed by the player
247 function doc.entry_viewed(playername, category_id, entry_id)
248 local entry, category_id, entry_id = get_entry(category_id, entry_id)
249 if doc.data.players[playername].stored_data.viewed[category_id] == nil then
250 return false
251 else
252 return doc.data.players[playername].stored_data.viewed[category_id][entry_id] == true
256 -- Returns true if the specified entry is hidden from the player
257 function doc.entry_revealed(playername, category_id, entry_id)
258 local entry, category_id, entry_id = get_entry(category_id, entry_id)
259 local hidden = doc.data.categories[category_id].entries[entry_id].hidden
260 if doc.data.players[playername].stored_data.revealed[category_id] == nil then
261 return not hidden
262 else
263 if hidden then
264 return doc.data.players[playername].stored_data.revealed[category_id][entry_id] == true
265 else
266 return true
271 -- Returns category definition
272 function doc.get_category_definition(category_id)
273 if doc.data.categories[category_id] == nil then
274 return nil
276 return doc.data.categories[category_id].def
279 -- Returns entry definition
280 function doc.get_entry_definition(category_id, entry_id)
281 if not doc.entry_exists(category_id, entry_id) then
282 return nil
284 local entry, _, _ = get_entry(category_id, entry_id)
285 return entry
288 -- Opens the main documentation formspec for the player
289 function doc.show_doc(playername)
290 if doc.get_category_count() <= 0 then
291 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
292 return
294 local formspec = doc.formspec_core()..doc.formspec_main(playername)
295 minetest.show_formspec(playername, "doc:main", formspec)
298 -- Opens the documentation formspec for the player at the specified category
299 function doc.show_category(playername, category_id)
300 if doc.get_category_count() <= 0 then
301 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
302 return
304 doc.data.players[playername].catsel = nil
305 doc.data.players[playername].category = category_id
306 doc.data.players[playername].entry = nil
307 local formspec = doc.formspec_core(2)..doc.formspec_category(category_id, playername)
308 minetest.show_formspec(playername, "doc:category", formspec)
311 -- Opens the documentation formspec for the player showing the specified entry in a category
312 function doc.show_entry(playername, category_id, entry_id, ignore_hidden)
313 if doc.get_category_count() <= 0 then
314 minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories())
315 return
317 local entry, category_id, entry_id = get_entry(category_id, entry_id)
318 if ignore_hidden or doc.entry_revealed(playername, category_id, entry_id) then
319 local playerdata = doc.data.players[playername]
320 playerdata.category = category_id
321 playerdata.entry = entry_id
323 doc.mark_entry_as_viewed(playername, category_id, entry_id)
324 playerdata.entry_textlist_needs_updating = true
325 doc.generate_entry_list(category_id, playername)
327 playerdata.catsel = playerdata.catsel_list[entry_id]
328 playerdata.galidx = 1
330 local formspec = doc.formspec_core(3)..doc.formspec_entry(category_id, entry_id, playername)
331 minetest.show_formspec(playername, "doc:entry", formspec)
332 else
333 minetest.show_formspec(playername, "doc:error_hidden", doc.formspec_error_hidden(category_id, entry_id))
337 -- Returns true if and only if:
338 -- * The specified category exists
339 -- * This category contains the specified entry
340 -- Aliases are taken into account
341 function doc.entry_exists(category_id, entry_id)
342 return get_entry(category_id, entry_id) ~= nil
345 -- Sets the order of categories in the category list
346 function doc.set_category_order(categories)
347 local reverse_categories = {}
348 for cid=1,#categories do
349 reverse_categories[categories[cid]] = cid
351 doc.data.category_order = categories
352 for cid, cat in pairs(doc.data.categories) do
353 if reverse_categories[cid] == nil then
354 table.insert(doc.data.category_order, cid)
357 reverse_categories = {}
358 for cid=1, #doc.data.category_order do
359 reverse_categories[categories[cid]] = cid
362 for cid, cat in pairs(doc.data.categories) do
363 cat.order_position = reverse_categories[cid]
365 if set_category_order_was_called then
366 minetest.log("warning", "[doc] doc.set_category_order was called again!")
368 set_category_order_was_called = true
371 -- Adds an alias for an entry. Attempting to open an entry by an alias name
372 -- results in opening the entry of the original name.
373 function doc.add_entry_alias(category_id_orig, entry_id_orig, category_id_alias, entry_id_alias)
374 if not doc.data.aliases[category_id_alias] then
375 doc.data.aliases[category_id_alias] = {}
377 doc.data.aliases[category_id_alias][entry_id_alias] = { category_id = category_id_orig, entry_id = entry_id_orig }
380 -- Returns number of categories
381 function doc.get_category_count()
382 return doc.data.category_count
385 -- Returns number of entries in category
386 function doc.get_entry_count(category_id)
387 return doc.data.categories[category_id].entry_count
390 -- Returns how many entries have been viewed by the player
391 function doc.get_viewed_count(playername, category_id)
392 local playerdata = doc.data.players[playername]
393 if playerdata == nil then
394 return nil
396 local count = playerdata.stored_data.viewed_count[category_id]
397 if count == nil then
398 playerdata.stored_data.viewed[category_id] = {}
399 count = 0
400 playerdata.stored_data.viewed_count[category_id] = count
401 return count
402 else
403 return count
407 -- Returns how many entries have been revealed by the player
408 function doc.get_revealed_count(playername, category_id)
409 local playerdata = doc.data.players[playername]
410 if playerdata == nil then
411 return nil
413 local count = playerdata.stored_data.revealed_count[category_id]
414 if count == nil then
415 playerdata.stored_data.revealed[category_id] = {}
416 count = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count
417 playerdata.stored_data.revealed_count[category_id] = count
418 return count
419 else
420 return count
424 -- Returns how many entries are hidden from the player
425 function doc.get_hidden_count(playername, category_id)
426 local playerdata = doc.data.players[playername]
427 if playerdata == nil then
428 return nil
430 local total = doc.get_entry_count(category_id)
431 local rcount = playerdata.stored_data.revealed_count[category_id]
432 if rcount == nil then
433 return total
434 else
435 return total - rcount
439 -- Returns the currently viewed entry and/or category of the player
440 function doc.get_selection(playername)
441 local playerdata = doc.data.players[playername]
442 if playerdata ~= nil then
443 local cat = playerdata.category
444 if cat then
445 local entry = playerdata.entry
446 if entry then
447 return cat, entry
448 else
449 return cat
451 else
452 return nil
454 else
455 return nil
459 -- Template function templates, to be used for build_formspec in doc.add_category
460 doc.entry_builders = {}
462 -- Inserts line breaks into a single paragraph and collapses all whitespace (including newlines)
463 -- into spaces
464 local linebreaker_single = function(text, linelength)
465 if linelength == nil then
466 linelength = TEXT_LINELENGTH
468 local remain = linelength
469 local res = {}
470 local line = {}
471 local split = function(s)
472 local res = {}
473 for w in string.gmatch(s, "%S+") do
474 res[#res+1] = w
476 return res
479 for _, word in ipairs(split(text)) do
480 if string.len(word) + 1 > remain then
481 table.insert(res, table.concat(line, " "))
482 line = { word }
483 remain = linelength - string.len(word)
484 else
485 table.insert(line, word)
486 remain = remain - (string.len(word) + 1)
490 table.insert(res, table.concat(line, " "))
491 return table.concat(res, "\n")
494 -- Inserts automatic line breaks into an entire text and preserves existing newlines
495 local linebreaker = function(text, linelength)
496 local out = ""
497 for s in string.gmatch(text, "([^\n]*)") do
498 local l = linebreaker_single(s, linelength)
499 out = out .. l
500 if(string.len(l) == 0) then
501 out = out .. "\n"
504 -- Remove last newline
505 if string.len(out) >= 1 then
506 out = string.sub(out, 1, string.len(out) - 1)
508 return out
511 -- Inserts text suitable for a textlist (including automatic word-wrap)
512 local text_for_textlist = function(text, linelength)
513 text = linebreaker(text, linelength)
514 text = minetest.formspec_escape(text)
515 text = string.gsub(text, "\n", ",")
516 return text
519 -- Scrollable freeform text
520 doc.entry_builders.text = function(data)
521 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)
522 return formstring
525 -- Scrollable freeform text with an optional standard gallery (3 rows, 3:2 aspect ratio)
526 doc.entry_builders.text_and_gallery = function(data, playername)
527 -- How much height the image gallery “steals” from the text widget
528 local stolen_height = 0
529 local formstring = ""
530 -- Only add the gallery if images are in the data, otherwise, the text widget gets all of the space
531 if data.images ~= nil then
532 local gallery
533 gallery, stolen_height = doc.widgets.gallery(data.images, playername, nil, doc.FORMSPEC.ENTRY_END_Y + 0.2, nil, nil, nil, nil, false)
534 formstring = formstring .. gallery
536 formstring = formstring .. doc.widgets.text(data.text,
537 doc.FORMSPEC.ENTRY_START_X,
538 doc.FORMSPEC.ENTRY_START_Y,
539 doc.FORMSPEC.ENTRY_WIDTH - 0.2,
540 doc.FORMSPEC.ENTRY_HEIGHT - stolen_height)
542 return formstring
545 doc.widgets = {}
547 local text_id = 1
548 -- Scrollable freeform text
549 doc.widgets.text = function(data, x, y, width, height)
550 if x == nil then
551 x = doc.FORMSPEC.ENTRY_START_X
553 if y == nil then
554 y = doc.FORMSPEC.ENTRY_START_Y
556 if width == nil then
557 width = doc.FORMSPEC.ENTRY_WIDTH
559 if height == nil then
560 height = doc.FORMSPEC.ENTRY_HEIGHT
562 local baselength = TEXT_LINELENGTH
563 local widget_basewidth = doc.FORMSPEC.WIDTH
564 local linelength = math.max(20, math.floor(baselength * (width / widget_basewidth)))
566 local widget_id = "doc_widget_text"..text_id
567 text_id = text_id + 1
568 -- TODO: Wait for Minetest to provide a native widget for scrollable read-only text with automatic line breaks.
569 -- Currently, all of this had to be hacked into this script manually by using/abusing the table widget
570 local formstring = "tablecolumns[text]"..
571 "tableoptions[background=#000000FF;highlight=#000000FF;border=false]"..
572 "table["..tostring(x)..","..tostring(y)..";"..tostring(width)..","..tostring(height)..";"..widget_id..";"..text_for_textlist(data, linelength).."]"
573 return formstring, widget_id
576 -- Image gallery
577 -- Currently, only one gallery per entry is supported. TODO: Add support for multiple galleries in an entry (low priority)
578 doc.widgets.gallery = function(imagedata, playername, x, y, aspect_ratio, width, rows, align_left, align_top)
579 if playername == nil then return nil end -- emergency exit
581 local formstring = ""
583 -- Defaults
584 if x == nil then
585 if align_left == false then
586 x = doc.FORMSPEC.ENTRY_END_X
587 else
588 x = doc.FORMSPEC.ENTRY_START_X
591 if y == nil then
592 if align_top == false then
593 y = doc.FORMSPEC.ENTRY_END_Y
594 else
595 y = doc.FORMSPEC.ENTRY_START_Y
598 if width == nil then width = doc.FORMSPEC.ENTRY_WIDTH end
599 if rows == nil then rows = 3 end
601 if align_left == false then
602 x = x - width
605 local imageindex = doc.data.players[playername].galidx
606 doc.data.players[playername].maxgalidx = #imagedata
607 doc.data.players[playername].galrows = rows
609 if aspect_ratio == nil then aspect_ratio = (2/3) end
610 local pos = 0
611 local totalimagewidth, iw, ih
612 local bw = 0.5
613 local buttonoffset = 0
614 if #imagedata > rows then
615 totalimagewidth = width - bw*2
616 iw = totalimagewidth / rows
617 ih = iw * aspect_ratio
618 if align_top == false then
619 y = y - ih
622 local tt
623 if imageindex > 1 then
624 formstring = formstring .. "button["..x..","..y..";"..bw..","..ih..";doc_button_gallery_prev;"..F("<").."]"
625 if rows == 1 then
626 tt = F("Show previous image")
627 else
628 tt = F("Show previous gallery page")
630 formstring = formstring .. "tooltip[doc_button_gallery_prev;"..tt.."]"
632 if (imageindex + rows) <= #imagedata then
633 local rightx = buttonoffset + (x + rows * iw)
634 formstring = formstring .. "button["..rightx..","..y..";"..bw..","..ih..";doc_button_gallery_next;"..F(">").."]"
635 if rows == 1 then
636 tt = F("Show next image")
637 else
638 tt = F("Show next gallery page")
640 formstring = formstring .. "tooltip[doc_button_gallery_next;"..tt.."]"
642 buttonoffset = bw
643 else
644 totalimagewidth = width
645 iw = totalimagewidth / rows
646 ih = iw * aspect_ratio
647 if align_top == false then
648 y = y - ih
651 for i=imageindex, math.min(#imagedata, (imageindex-1)+rows) do
652 local xoffset = buttonoffset + (x + pos * iw)
653 local nx = xoffset - 0.2
654 local ny = y - 0.05
655 if imagedata[i].imagetype == "item" then
656 formstring = formstring .. "item_image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]"
657 else
658 formstring = formstring .. "image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]"
660 formstring = formstring .. "label["..nx..","..ny..";"..i.."]"
661 pos = pos + 1
663 local bw, bh
665 return formstring, ih
668 -- Direct formspec
669 doc.entry_builders.formspec = function(data)
670 return data
673 --[[ Internal stuff ]]
675 -- Loading and saving player data
677 local filepath = minetest.get_worldpath().."/doc.mt"
678 local file = io.open(filepath, "r")
679 if file then
680 minetest.log("action", "[doc] doc.mt opened.")
681 local string = file:read()
682 io.close(file)
683 if(string ~= nil) then
684 local savetable = minetest.deserialize(string)
685 for name, players_stored_data in pairs(savetable.players_stored_data) do
686 doc.data.players[name] = {}
687 doc.data.players[name].stored_data = players_stored_data
689 minetest.debug("[doc] doc.mt successfully read.")
694 function doc.save_to_file()
695 local savetable = {}
696 savetable.players_stored_data = {}
697 for name, playerdata in pairs(doc.data.players) do
698 savetable.players_stored_data[name] = playerdata.stored_data
701 local savestring = minetest.serialize(savetable)
703 local filepath = minetest.get_worldpath().."/doc.mt"
704 local file = io.open(filepath, "w")
705 if file then
706 file:write(savestring)
707 io.close(file)
708 minetest.log("action", "[doc] Wrote player data into "..filepath..".")
709 else
710 minetest.log("error", "[doc] Failed to write player data into "..filepath..".")
714 minetest.register_on_leaveplayer(function(player)
715 doc.save_to_file()
716 end)
718 minetest.register_on_shutdown(function()
719 minetest.log("action", "[doc] Server shuts down. Player data is about to be saved.")
720 doc.save_to_file()
721 end)
723 --[[ Functions for internal use ]]
725 function doc.formspec_core(tab)
726 if tab == nil then tab = 1 else tab = tostring(tab) end
727 return "size["..doc.FORMSPEC.WIDTH..","..doc.FORMSPEC.HEIGHT.."]tabheader[0,0;doc_header;"..
728 minetest.formspec_escape(S("Category list")) .. "," ..
729 minetest.formspec_escape(S("Entry list")) .. "," ..
730 minetest.formspec_escape(S("Entry")) .. ";"
731 ..tab..";true;true]" ..
732 "bgcolor[#343434FF]"
735 function doc.formspec_main(playername)
736 local formstring = "label[0,0;"..minetest.formspec_escape(DOC_INTRO) .. "\n"
737 if doc.get_category_count() >= 1 then
738 formstring = formstring .. F("Please select a category you wish to learn more about:").."]"
739 if doc.get_category_count() <= (CATEGORYFIELDSIZE.WIDTH * CATEGORYFIELDSIZE.HEIGHT) then
740 local y = 1
741 local x = 1
742 -- Show all categories in order
743 for c=1,#doc.data.category_order do
744 local id = doc.data.category_order[c]
745 local data = doc.data.categories[id]
746 local bw = doc.FORMSPEC.WIDTH / math.floor(((doc.data.category_count-1) / CATEGORYFIELDSIZE.HEIGHT)+1)
747 -- Skip categories which do not exist
748 if data ~= nil then
749 -- Category buton
750 local button = "button["..((x-1)*bw)..","..y..";"..bw..",1;doc_button_category_"..id..";"..minetest.formspec_escape(data.def.name).."]"
751 local tooltip = ""
752 -- Optional description
753 if data.def.description ~= nil then
754 tooltip = "tooltip[doc_button_category_"..id..";"..minetest.formspec_escape(data.def.description).."]"
756 formstring = formstring .. button .. tooltip
757 y = y + 1
758 if y > CATEGORYFIELDSIZE.HEIGHT then
759 x = x + 1
760 y = 1
764 else
765 formstring = formstring .. "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_mainlist;"
766 for c=1,#doc.data.category_order do
767 local id = doc.data.category_order[c]
768 local data = doc.data.categories[id]
769 formstring = formstring .. minetest.formspec_escape(data.def.name)
770 if c < #doc.data.category_order then
771 formstring = formstring .. ","
774 local sel = doc.data.categories[doc.data.players[playername].category]
775 if sel ~= nil then
776 formstring = formstring .. ";"
777 formstring = formstring .. doc.data.categories[doc.data.players[playername].category].order_position
779 formstring = formstring .. "]"
780 formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_category;"..F("Show category").."]"
782 else
783 formstring = formstring .. "]"
785 return formstring
788 function doc.formspec_error_no_categories()
789 local formstring = "size[8,6]textarea[0.25,0;8,6;;"
790 formstring = formstring ..
791 minetest.formspec_escape(
792 colorize(COLOR_ERROR, S("Error: No help available.")) .. "\n\n" ..
793 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" ..
794 S("Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia.")
795 formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]"
796 return formstring
799 function doc.formspec_error_hidden(category_id, entry_id)
800 local formstring = "size[8,6]textarea[0.25,0;8,6;;"
801 formstring = formstring .. minetest.formspec_escape(
802 colorize(COLOR_ERROR, S("Error: Access denied.")) .. "\n\n" ..
803 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."))
804 formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]"
805 return formstring
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 = 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,