local S = minetest.get_translator(minetest.get_current_modname()) local F = function(f) return minetest.formspec_escape(S(f)) end local mod_central_messages = minetest.get_modpath("central_message") local mod_inventory_plus = minetest.get_modpath("inventory_plus") local math = math local colorize = minetest.colorize doc = {} -- Some informational variables -- DO NOT CHANGE THEM AFTERWARDS AT RUNTIME! -- Version number (follows the SemVer specification 2.0.0) doc.VERSION = {} doc.VERSION.MAJOR = 1 doc.VERSION.MINOR = 2 doc.VERSION.PATCH = 1 doc.VERSION.STRING = doc.VERSION.MAJOR.."."..doc.VERSION.MINOR.."."..doc.VERSION.PATCH -- Formspec information doc.FORMSPEC = {} -- Width of formspec doc.FORMSPEC.WIDTH = 15 doc.FORMSPEC.HEIGHT = 10.5 --[[ Recommended bounding box coordinates for widgets to be placed in entry pages. Make sure all entry widgets are completely inside these coordinates to avoid overlapping. ]] doc.FORMSPEC.ENTRY_START_X = 0 doc.FORMSPEC.ENTRY_START_Y = 0.5 doc.FORMSPEC.ENTRY_END_X = doc.FORMSPEC.WIDTH doc.FORMSPEC.ENTRY_END_Y = doc.FORMSPEC.HEIGHT - 0.5 doc.FORMSPEC.ENTRY_WIDTH = doc.FORMSPEC.ENTRY_END_X - doc.FORMSPEC.ENTRY_START_X doc.FORMSPEC.ENTRY_HEIGHT = doc.FORMSPEC.ENTRY_END_Y - doc.FORMSPEC.ENTRY_START_Y --TODO: Use container formspec element later -- Internal helper variables local DOC_INTRO = S("This is the help.") local COLOR_NOT_VIEWED = "#00FFFF" -- cyan local COLOR_VIEWED = "#FFFFFF" -- white local COLOR_HIDDEN = "#999999" -- gray local COLOR_ERROR = "#FF0000" -- red local CATEGORYFIELDSIZE = { WIDTH = math.ceil(doc.FORMSPEC.WIDTH / 4), HEIGHT = math.floor(doc.FORMSPEC.HEIGHT-1), } doc.data = {} doc.data.categories = {} doc.data.aliases = {} -- Default order (includes categories of other mods from the Docuentation System modpack) doc.data.category_order = {"basics", "nodes", "tools", "craftitems", "advanced", "mobs"} doc.data.category_count = 0 doc.data.players = {} -- Space for additional APIs doc.sub = {} -- Status variables local set_category_order_was_called = false -- Returns the entry definition and true entry ID of an entry, taking aliases into account local function get_entry(category_id, entry_id) local category = doc.data.categories[category_id] local entry if category then entry = category.entries[entry_id] end if category == nil or entry == nil then local c_alias = doc.data.aliases[category_id] if c_alias then local alias = c_alias[entry_id] if alias then category_id = alias.category_id entry_id = alias.entry_id category = doc.data.categories[category_id] if category then entry = category.entries[entry_id] else return nil end else return nil end else return nil end end return entry, category_id, entry_id end --[[ Core API functions ]] -- Add a new category function doc.add_category(id, def) if doc.data.categories[id] == nil and id then doc.data.categories[id] = {} doc.data.categories[id].entries = {} doc.data.categories[id].entry_count = 0 doc.data.categories[id].hidden_count = 0 doc.data.categories[id].def = def -- Determine order position local order_id = nil for i=1,#doc.data.category_order do if doc.data.category_order[i] == id then order_id = i break end end if order_id == nil then table.insert(doc.data.category_order, id) doc.data.categories[id].order_position = #doc.data.category_order else doc.data.categories[id].order_position = order_id end doc.data.category_count = doc.data.category_count + 1 return true else return false end end -- Add a new entry function doc.add_entry(category_id, entry_id, def) local cat = doc.data.categories[category_id] if cat then local hidden = def.hidden or (def.hidden == nil and cat.def.hide_entries_by_default) if hidden then cat.hidden_count = cat.hidden_count + 1 def.hidden = hidden end cat.entry_count = doc.data.categories[category_id].entry_count + 1 if def.name == nil or def.name == "" then minetest.log("warning", "[doc] Nameless entry added. Entry ID: "..entry_id) end cat.entries[entry_id] = def return true else return false end end -- Marks a particular entry as viewed by a certain player, which also -- automatically reveals it function doc.mark_entry_as_viewed(playername, category_id, entry_id) local entry, category_id, entry_id = get_entry(category_id, entry_id) if not entry then return end if doc.data.players[playername].stored_data.viewed[category_id] == nil then doc.data.players[playername].stored_data.viewed[category_id] = {} doc.data.players[playername].stored_data.viewed_count[category_id] = 0 end if doc.entry_exists(category_id, entry_id) and doc.data.players[playername].stored_data.viewed[category_id][entry_id] ~= true then doc.data.players[playername].stored_data.viewed[category_id][entry_id] = true doc.data.players[playername].stored_data.viewed_count[category_id] = doc.data.players[playername].stored_data.viewed_count[category_id] + 1 -- Needed because viewed entries get a different color doc.data.players[playername].entry_textlist_needs_updating = true end doc.mark_entry_as_revealed(playername, category_id, entry_id) end -- Marks a particular entry as revealed/unhidden by a certain player function doc.mark_entry_as_revealed(playername, category_id, entry_id) local entry, category_id, entry_id = get_entry(category_id, entry_id) if not entry then return end if doc.data.players[playername].stored_data.revealed[category_id] == nil then doc.data.players[playername].stored_data.revealed[category_id] = {} doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count end 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 doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1 -- Needed because a new entry is added to the list of visible entries doc.data.players[playername].entry_textlist_needs_updating = true -- Notify player of entry revelation if doc.data.players[playername].stored_data.notify_on_reveal == true then if mod_central_messages then local cat = doc.data.categories[category_id] cmsg.push_message_player(minetest.get_player_by_name(playername), S("New help entry unlocked: @1 > @2", cat.def.name, entry.name)) end -- To avoid sound spamming, don't play sound more than once per second local last_sound = doc.data.players[playername].last_reveal_sound if last_sound == nil or os.difftime(os.time(), last_sound) >= 1 then -- Play notification sound minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername }, true) doc.data.players[playername].last_reveal_sound = os.time() end end end end -- Reveal function doc.mark_all_entries_as_revealed(playername) -- Has at least 1 new entry been revealed? local reveal1 = false for category_id, category in pairs(doc.data.categories) do if doc.data.players[playername].stored_data.revealed[category_id] == nil then doc.data.players[playername].stored_data.revealed[category_id] = {} doc.data.players[playername].stored_data.revealed_count[category_id] = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count end for entry_id, _ in pairs(category.entries) do if doc.data.players[playername].stored_data.revealed[category_id][entry_id] ~= true then doc.data.players[playername].stored_data.revealed[category_id][entry_id] = true doc.data.players[playername].stored_data.revealed_count[category_id] = doc.data.players[playername].stored_data.revealed_count[category_id] + 1 reveal1 = true end end end local msg if reveal1 then -- Needed because new entries are added to player's view on entry list doc.data.players[playername].entry_textlist_needs_updating = true msg = S("All help entries revealed!") -- Play notification sound (ignore sound limit intentionally) minetest.sound_play({ name = "doc_reveal", gain = 0.2 }, { to_player = playername }, true) doc.data.players[playername].last_reveal_sound = os.time() else msg = S("All help entries are already revealed.") end -- Notify if mod_central_messages then cmsg.push_message_player(minetest.get_player_by_name(playername), msg) else minetest.chat_send_player(playername, msg) end end -- Returns true if the specified entry has been viewed by the player function doc.entry_viewed(playername, category_id, entry_id) local _, category_id, entry_id = get_entry(category_id, entry_id) if doc.data.players[playername].stored_data.viewed[category_id] == nil then return false else return doc.data.players[playername].stored_data.viewed[category_id][entry_id] == true end end -- Returns true if the specified entry is hidden from the player function doc.entry_revealed(playername, category_id, entry_id) local _, category_id, entry_id = get_entry(category_id, entry_id) local hidden = doc.data.categories[category_id].entries[entry_id].hidden if doc.data.players[playername].stored_data.revealed[category_id] == nil then return not hidden else if hidden then return doc.data.players[playername].stored_data.revealed[category_id][entry_id] == true else return true end end end -- Returns category definition function doc.get_category_definition(category_id) if doc.data.categories[category_id] == nil then return nil end return doc.data.categories[category_id].def end -- Returns entry definition function doc.get_entry_definition(category_id, entry_id) if not doc.entry_exists(category_id, entry_id) then return nil end local entry, _, _ = get_entry(category_id, entry_id) return entry end -- Opens the main documentation formspec for the player function doc.show_doc(playername) if doc.get_category_count() <= 0 then minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) return end local formspec = doc.formspec_core()..doc.formspec_main(playername) minetest.show_formspec(playername, "doc:main", formspec) end -- Opens the documentation formspec for the player at the specified category function doc.show_category(playername, category_id) if doc.get_category_count() <= 0 then minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) return end doc.data.players[playername].catsel = nil doc.data.players[playername].category = category_id doc.data.players[playername].entry = nil local formspec = doc.formspec_core(2)..doc.formspec_category(category_id, playername) minetest.show_formspec(playername, "doc:category", formspec) end -- Opens the documentation formspec for the player showing the specified entry in a category function doc.show_entry(playername, category_id, entry_id, ignore_hidden) if doc.get_category_count() <= 0 then minetest.show_formspec(playername, "doc:error_no_categories", doc.formspec_error_no_categories()) return end local _, category_id, entry_id = get_entry(category_id, entry_id) if ignore_hidden or doc.entry_revealed(playername, category_id, entry_id) then local playerdata = doc.data.players[playername] playerdata.category = category_id playerdata.entry = entry_id doc.mark_entry_as_viewed(playername, category_id, entry_id) playerdata.entry_textlist_needs_updating = true doc.generate_entry_list(category_id, playername) playerdata.catsel = playerdata.catsel_list[entry_id] playerdata.galidx = 1 local formspec = doc.formspec_core(3)..doc.formspec_entry(category_id, entry_id, playername) minetest.show_formspec(playername, "doc:entry", formspec) else minetest.show_formspec(playername, "doc:error_hidden", doc.formspec_error_hidden(category_id, entry_id)) end end -- Returns true if and only if: -- * The specified category exists -- * This category contains the specified entry -- Aliases are taken into account function doc.entry_exists(category_id, entry_id) return get_entry(category_id, entry_id) ~= nil end -- Sets the order of categories in the category list function doc.set_category_order(categories) local reverse_categories = {} for cid=1,#categories do reverse_categories[categories[cid]] = cid end doc.data.category_order = categories for cid, cat in pairs(doc.data.categories) do if reverse_categories[cid] == nil then table.insert(doc.data.category_order, cid) end end reverse_categories = {} for cid=1, #doc.data.category_order do reverse_categories[categories[cid]] = cid end for cid, cat in pairs(doc.data.categories) do cat.order_position = reverse_categories[cid] end if set_category_order_was_called then minetest.log("warning", "[doc] doc.set_category_order was called again!") end set_category_order_was_called = true end -- Adds an alias for an entry. Attempting to open an entry by an alias name -- results in opening the entry of the original name. function doc.add_entry_alias(category_id_orig, entry_id_orig, category_id_alias, entry_id_alias) if not doc.data.aliases[category_id_alias] then doc.data.aliases[category_id_alias] = {} end doc.data.aliases[category_id_alias][entry_id_alias] = { category_id = category_id_orig, entry_id = entry_id_orig } end -- Returns number of categories function doc.get_category_count() return doc.data.category_count end -- Returns number of entries in category function doc.get_entry_count(category_id) return doc.data.categories[category_id].entry_count end -- Returns how many entries have been viewed by the player function doc.get_viewed_count(playername, category_id) local playerdata = doc.data.players[playername] if playerdata == nil then return nil end local count = playerdata.stored_data.viewed_count[category_id] if count == nil then playerdata.stored_data.viewed[category_id] = {} count = 0 playerdata.stored_data.viewed_count[category_id] = count return count else return count end end -- Returns how many entries have been revealed by the player function doc.get_revealed_count(playername, category_id) local playerdata = doc.data.players[playername] if playerdata == nil then return nil end local count = playerdata.stored_data.revealed_count[category_id] if count == nil then playerdata.stored_data.revealed[category_id] = {} count = doc.get_entry_count(category_id) - doc.data.categories[category_id].hidden_count playerdata.stored_data.revealed_count[category_id] = count return count else return count end end -- Returns how many entries are hidden from the player function doc.get_hidden_count(playername, category_id) local playerdata = doc.data.players[playername] if playerdata == nil then return nil end local total = doc.get_entry_count(category_id) local rcount = playerdata.stored_data.revealed_count[category_id] if rcount == nil then return total else return total - rcount end end -- Returns the currently viewed entry and/or category of the player function doc.get_selection(playername) local playerdata = doc.data.players[playername] if playerdata then local cat = playerdata.category if cat then local entry = playerdata.entry if entry then return cat, entry else return cat end else return nil end else return nil end end -- Template function templates, to be used for build_formspec in doc.add_category doc.entry_builders = {} -- Scrollable freeform text function doc.entry_builders.text(data) local formstring = doc.widgets.text(data, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y, doc.FORMSPEC.ENTRY_WIDTH - 0.4, doc.FORMSPEC.ENTRY_HEIGHT) return formstring end -- Scrollable freeform text with an optional standard gallery (3 rows, 3:2 aspect ratio) function doc.entry_builders.text_and_gallery(data, playername) -- How much height the image gallery “steals” from the text widget local stolen_height = 0 local formstring = "" -- Only add the gallery if images are in the data, otherwise, the text widget gets all of the space if data.images then local gallery gallery, stolen_height = doc.widgets.gallery(data.images, playername, nil, doc.FORMSPEC.ENTRY_END_Y + 0.2, nil, nil, nil, nil, false) formstring = formstring .. gallery end formstring = formstring .. doc.widgets.text(data.text, doc.FORMSPEC.ENTRY_START_X, doc.FORMSPEC.ENTRY_START_Y, doc.FORMSPEC.ENTRY_WIDTH - 0.4, doc.FORMSPEC.ENTRY_HEIGHT - stolen_height) return formstring end doc.widgets = {} -- Scrollable freeform text function doc.widgets.text(data, x, y, width, height) if x == nil then x = doc.FORMSPEC.ENTRY_START_X end -- Offset to table[], which was used for this in a previous version local xfix = x + 0.35 if y == nil then y = doc.FORMSPEC.ENTRY_START_Y end if width == nil then width = doc.FORMSPEC.ENTRY_WIDTH end if height == nil then height = doc.FORMSPEC.ENTRY_HEIGHT end -- Weird offset for textarea[] local heightfix = height + 1 -- Also add background box local formstring = "box["..tostring(x-0.175)..","..tostring(y)..";"..tostring(width)..","..tostring(height)..";#000000]" .. "textarea["..tostring(xfix)..","..tostring(y)..";"..tostring(width)..","..tostring(heightfix)..";;;"..minetest.formspec_escape(data).."]" return formstring end -- Image gallery -- Currently, only one gallery per entry is supported. TODO: Add support for multiple galleries in an entry (low priority) function doc.widgets.gallery(imagedata, playername, x, y, aspect_ratio, width, rows, align_left, align_top) if playername == nil then return nil end -- emergency exit local formstring = "" -- Defaults if x == nil then if align_left == false then x = doc.FORMSPEC.ENTRY_END_X else x = doc.FORMSPEC.ENTRY_START_X end end if y == nil then if align_top == false then y = doc.FORMSPEC.ENTRY_END_Y else y = doc.FORMSPEC.ENTRY_START_Y end end if width == nil then width = doc.FORMSPEC.ENTRY_WIDTH end if rows == nil then rows = 3 end if align_left == false then x = x - width end local imageindex = doc.data.players[playername].galidx doc.data.players[playername].maxgalidx = #imagedata doc.data.players[playername].galrows = rows if aspect_ratio == nil then aspect_ratio = (2/3) end local pos = 0 local totalimagewidth, iw, ih local bw = 0.5 local buttonoffset = 0 if #imagedata > rows then totalimagewidth = width - bw*2 iw = totalimagewidth / rows ih = iw * aspect_ratio if align_top == false then y = y - ih end local tt if imageindex > 1 then formstring = formstring .. "button["..x..","..y..";"..bw..","..ih..";doc_button_gallery_prev;"..F("<").."]" if rows == 1 then tt = F("Show previous image") else tt = F("Show previous gallery page") end formstring = formstring .. "tooltip[doc_button_gallery_prev;"..tt.."]" end if (imageindex + rows) <= #imagedata then local rightx = buttonoffset + (x + rows * iw) formstring = formstring .. "button["..rightx..","..y..";"..bw..","..ih..";doc_button_gallery_next;"..F(">").."]" if rows == 1 then tt = F("Show next image") else tt = F("Show next gallery page") end formstring = formstring .. "tooltip[doc_button_gallery_next;"..tt.."]" end buttonoffset = bw else totalimagewidth = width iw = totalimagewidth / rows ih = iw * aspect_ratio if align_top == false then y = y - ih end end for i=imageindex, math.min(#imagedata, (imageindex-1)+rows) do local xoffset = buttonoffset + (x + pos * iw) local nx = xoffset - 0.2 local ny = y - 0.05 if imagedata[i].imagetype == "item" then formstring = formstring .. "item_image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]" else formstring = formstring .. "image["..xoffset..","..y..";"..iw..","..ih..";"..imagedata[i].image.."]" end formstring = formstring .. "label["..nx..","..ny..";"..i.."]" pos = pos + 1 end return formstring, ih end -- Direct formspec function doc.entry_builders.formspec(data) return data end --[[ Internal stuff ]] -- Loading and saving player data do local filepath = minetest.get_worldpath().."/doc.mt" local file = io.open(filepath, "r") if file then minetest.log("action", "[doc] doc.mt opened.") local string = file:read() io.close(file) if string then local savetable = minetest.deserialize(string) for name, players_stored_data in pairs(savetable.players_stored_data) do doc.data.players[name] = {} doc.data.players[name].stored_data = players_stored_data end minetest.log("action", "[doc] doc.mt successfully read.") end end end function doc.save_to_file() local savetable = {} savetable.players_stored_data = {} for name, playerdata in pairs(doc.data.players) do savetable.players_stored_data[name] = playerdata.stored_data end local savestring = minetest.serialize(savetable) local filepath = minetest.get_worldpath().."/doc.mt" local file = io.open(filepath, "w") if file then file:write(savestring) io.close(file) minetest.log("action", "[doc] Wrote player data into "..filepath..".") else minetest.log("error", "[doc] Failed to write player data into "..filepath..".") end end minetest.register_on_leaveplayer(function(player) doc.save_to_file() end) minetest.register_on_shutdown(function() minetest.log("action", "[doc] Server shuts down. Player data is about to be saved.") doc.save_to_file() end) --[[ Functions for internal use ]] function doc.formspec_core(tab) if tab == nil then tab = 1 else tab = tostring(tab) end return "size["..doc.FORMSPEC.WIDTH..","..doc.FORMSPEC.HEIGHT.."]".. "tabheader[0,0;doc_header;".. minetest.formspec_escape(S("Category list")) .. "," .. minetest.formspec_escape(S("Entry list")) .. "," .. minetest.formspec_escape(S("Entry")) .. ";" ..tab..";false;false]" -- Let the Game decide on the style, such as background, etc. end function doc.formspec_main(playername) local formstring = "textarea[0.35,0;"..doc.FORMSPEC.WIDTH..",1;;;"..minetest.formspec_escape(DOC_INTRO) .. "\n" local notify_checkbox_x, notify_checkbox_y if doc.get_category_count() >= 1 then formstring = formstring .. F("Please select a category you wish to learn more about:").."]" if doc.get_category_count() <= (CATEGORYFIELDSIZE.WIDTH * CATEGORYFIELDSIZE.HEIGHT) then local y = 1 local x = 1 -- Show all categories in order for c=1,#doc.data.category_order do local id = doc.data.category_order[c] local data = doc.data.categories[id] local bw = doc.FORMSPEC.WIDTH / math.floor(((doc.data.category_count-1) / CATEGORYFIELDSIZE.HEIGHT)+1) -- Skip categories which do not exist if data then -- Category buton local button = "button["..((x-1)*bw)..","..y..";"..bw..",1;doc_button_category_"..id..";"..minetest.formspec_escape(data.def.name).."]" local tooltip = "" -- Optional description if data.def.description then tooltip = "tooltip[doc_button_category_"..id..";"..minetest.formspec_escape(data.def.description).."]" end formstring = formstring .. button .. tooltip y = y + 1 if y > CATEGORYFIELDSIZE.HEIGHT then x = x + 1 y = 1 end end end notify_checkbox_x = 0 notify_checkbox_y = doc.FORMSPEC.HEIGHT-0.5 else formstring = formstring .. "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_mainlist;" for c=1,#doc.data.category_order do local id = doc.data.category_order[c] local data = doc.data.categories[id] formstring = formstring .. minetest.formspec_escape(data.def.name) if c < #doc.data.category_order then formstring = formstring .. "," end end local sel = doc.data.categories[doc.data.players[playername].category] if sel then formstring = formstring .. ";" formstring = formstring .. doc.data.categories[doc.data.players[playername].category].order_position end formstring = formstring .. "]" formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_category;"..F("Show category").."]" notify_checkbox_x = 3.5 notify_checkbox_y = doc.FORMSPEC.HEIGHT-1 end local text if mod_central_messages then text = F("Notify me when new help is available") else text = F("Play notification sound when new help is available") end formstring = formstring .. "checkbox["..notify_checkbox_x..","..notify_checkbox_y..";doc_setting_notify_on_reveal;"..text..";".. tostring(doc.data.players[playername].stored_data.notify_on_reveal == true) .. "]" else formstring = formstring .. "]" end return formstring end function doc.formspec_error_no_categories() local formstring = "size[8,6]textarea[0.25,0;8,6;;" formstring = formstring .. minetest.formspec_escape( colorize(COLOR_ERROR, S("Error: No help available.")) .. "\n\n" .. S("No categories have been registered, but they are required to provide help.").."\n".. S("The 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" .. S("Recommended mods: doc_basics, doc_items, doc_identifier, doc_encyclopedia.") formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]" return formstring end function doc.formspec_error_hidden(category_id, entry_id) local formstring = "size[8,6]textarea[0.25,0;8,6;;" formstring = formstring .. minetest.formspec_escape( colorize(COLOR_ERROR, S("Error: Access denied.")) .. "\n\n" .. 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.")) formstring = formstring .. ";]button_exit[3,5;2,1;okay;"..F("OK").."]" return formstring end function doc.generate_entry_list(cid, playername) local formstring if doc.data.players[playername].entry_textlist == nil or doc.data.players[playername].catsel_list == nil or doc.data.players[playername].category ~= cid or doc.data.players[playername].entry_textlist_needs_updating == true then local entry_textlist = "textlist[0,1;"..(doc.FORMSPEC.WIDTH-0.2)..","..(doc.FORMSPEC.HEIGHT-2)..";doc_catlist;" local counter = 0 doc.data.players[playername].entry_ids = {} local entries = doc.get_sorted_entry_names(cid) doc.data.players[playername].catsel_list = {} for i=1, #entries do local eid = entries[i] local edata = doc.data.categories[cid].entries[eid] if doc.entry_revealed(playername, cid, eid) then table.insert(doc.data.players[playername].entry_ids, eid) doc.data.players[playername].catsel_list[eid] = counter + 1 -- Colorize entries based on viewed status local viewedprefix = COLOR_NOT_VIEWED local name = edata.name if name == nil or name == "" then name = S("Nameless entry (@1)", eid) if doc.entry_viewed(playername, cid, eid) then viewedprefix = "#FF4444" else viewedprefix = COLOR_ERROR end elseif doc.entry_viewed(playername, cid, eid) then viewedprefix = COLOR_VIEWED end entry_textlist = entry_textlist .. viewedprefix .. minetest.formspec_escape(name) .. "," counter = counter + 1 end end if counter >= 1 then entry_textlist = string.sub(entry_textlist, 1, #entry_textlist-1) end local catsel = doc.data.players[playername].catsel if catsel then entry_textlist = entry_textlist .. ";"..catsel end entry_textlist = entry_textlist .. "]" doc.data.players[playername].entry_textlist = entry_textlist formstring = entry_textlist doc.data.players[playername].entry_textlist_needs_updating = false else formstring = doc.data.players[playername].entry_textlist end return formstring end function doc.get_sorted_entry_names(cid) local sort_table = {} local entry_table = {} local cat = doc.data.categories[cid] local used_eids = {} -- Helper function to extract the entry ID out of the output table local function extract(entry_table) local eids = {} for k,v in pairs(entry_table) do local eid = v.eid table.insert(eids, eid) end return eids end -- Predefined sorting if cat.def.sorting == "custom" then for i=1,#cat.def.sorting_data do local new_entry = table.copy(cat.entries[cat.def.sorting_data[i]]) new_entry.eid = cat.def.sorting_data[i] table.insert(entry_table, new_entry) used_eids[cat.def.sorting_data[i]] = true end end for eid,entry in pairs(cat.entries) do local new_entry = table.copy(entry) new_entry.eid = eid if not used_eids[eid] then table.insert(entry_table, new_entry) end table.insert(sort_table, entry.name) end if cat.def.sorting == "custom" then return extract(entry_table) else table.sort(sort_table) end local reverse_sort_table = table.copy(sort_table) for i=1, #sort_table do reverse_sort_table[sort_table[i]] = i end local comp if cat.def.sorting ~= "nosort" then -- Sorting by user function if cat.def.sorting == "function" then comp = cat.def.sorting_data -- Alphabetic sorting elseif cat.def.sorting == "abc" or cat.def.sorting == nil then comp = function(e1, e2) if reverse_sort_table[e1.name] < reverse_sort_table[e2.name] then return true else return false end end end table.sort(entry_table, comp) end return extract(entry_table) end function doc.formspec_category(id, playername) local formstring if id == nil then formstring = "label[0,0;"..F("Help > (No Category)") .. "]" formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]" formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]" else formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1", doc.data.categories[id].def.name)).."]" local total = doc.get_entry_count(id) if total >= 1 then local revealed = doc.get_revealed_count(playername, id) if revealed == 0 then formstring = formstring .. "label[0,0.5;"..minetest.formspec_escape(S("Currently all entries in this category are hidden from you.").."\n"..S("Unlock new entries by progressing in the game.")).."]" formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" else formstring = formstring .. "label[0,0.5;"..F("This category has the following entries:").."]" formstring = formstring .. doc.generate_entry_list(id, playername) formstring = formstring .. "button[0,"..(doc.FORMSPEC.HEIGHT-1)..";3,1;doc_button_goto_entry;"..F("Show entry").."]" formstring = formstring .. "label["..(doc.FORMSPEC.WIDTH-4)..","..(doc.FORMSPEC.HEIGHT-1)..";"..minetest.formspec_escape(S("Number of entries: @1", total)).."\n" local viewed = doc.get_viewed_count(playername, id) local hidden = total - revealed local new = total - viewed - hidden -- TODO/FIXME: Check if number of hidden/viewed entries is always correct if viewed < total then formstring = formstring .. colorize(COLOR_NOT_VIEWED, minetest.formspec_escape(S("New entries: @1", new))) if hidden > 0 then formstring = formstring .. "\n" formstring = formstring .. colorize(COLOR_HIDDEN, minetest.formspec_escape(S("Hidden entries: @1", hidden))).."]" else formstring = formstring .. "]" end else formstring = formstring .. F("All entries read.").."]" end end else formstring = formstring .. "label[0,0.5;"..F("This category is empty.").."]" formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" end end return formstring end function doc.formspec_entry_navigation(category_id, entry_id) if doc.get_entry_count(category_id) < 1 then return "" end local formstring = "" formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-2)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_prev;"..F("<").."]" formstring = formstring .. "button["..(doc.FORMSPEC.WIDTH-1)..","..(doc.FORMSPEC.HEIGHT-0.5)..";1,1;doc_button_goto_next;"..F(">").."]" formstring = formstring .. "tooltip[doc_button_goto_prev;"..F("Show previous entry").."]" formstring = formstring .. "tooltip[doc_button_goto_next;"..F("Show next entry").."]" return formstring end function doc.formspec_entry(category_id, entry_id, playername) local formstring if category_id == nil then formstring = "label[0,0;"..F("Help > (No Category)") .. "]" formstring = formstring .. "label[0,0.5;"..F("You haven't chosen a category yet. Please choose one in the category list first.").."]" formstring = formstring .. "button[0,1;3,1;doc_button_goto_main;"..F("Go to category list").."]" elseif entry_id == nil then formstring = "label[0,0;"..minetest.formspec_escape(S("Help > @1 > (No Entry)", doc.data.categories[category_id].def.name)) .. "]" if doc.get_entry_count(category_id) >= 1 then formstring = formstring .. "label[0,0.5;"..F("You haven't chosen an entry yet. Please choose one in the entry list first.").."]" formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_category;"..F("Go to entry list").."]" else formstring = formstring .. "label[0,0.5;"..F("This category does not have any entries.").."]" formstring = formstring .. "button[0,1.5;3,1;doc_button_goto_main;"..F("Go to category list").."]" end else local category = doc.data.categories[category_id] local entry = get_entry(category_id, entry_id) local ename = entry.name if ename == nil or ename == "" then ename = S("Nameless entry (@1)", entry_id) end formstring = "style_type[textarea;textcolor=#FFFFFF]" formstring = formstring .. "label[0,0;"..minetest.formspec_escape(S("Help > @1 > @2", category.def.name, ename)).."]" formstring = formstring .. category.def.build_formspec(entry.data, playername) formstring = formstring .. doc.formspec_entry_navigation(category_id, entry_id) end return formstring end function doc.process_form(player,formname,fields) local playername = player:get_player_name() --[[ process clicks on the tab header ]] if(formname == "doc:main" or formname == "doc:category" or formname == "doc:entry") then if fields.doc_header then local tab = tonumber(fields.doc_header) local formspec, subformname, contents local cid, eid cid = doc.data.players[playername].category eid = doc.data.players[playername].entry if(tab==1) then contents = doc.formspec_main(playername) subformname = "main" elseif(tab==2) then contents = doc.formspec_category(cid, playername) subformname = "category" elseif(tab==3) then doc.data.players[playername].galidx = 1 contents = doc.formspec_entry(cid, eid, playername) if cid and eid then doc.mark_entry_as_viewed(playername, cid, eid) end subformname = "entry" end formspec = doc.formspec_core(tab)..contents minetest.show_formspec(playername, "doc:" .. subformname, formspec) return end end if(formname == "doc:main") then for cid,_ in pairs(doc.data.categories) do if fields["doc_button_category_"..cid] then doc.data.players[playername].catsel = nil doc.data.players[playername].category = cid doc.data.players[playername].entry = nil doc.data.players[playername].entry_textlist_needs_updating = true local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) minetest.show_formspec(playername, "doc:category", formspec) break end end if fields["doc_mainlist"] then local event = minetest.explode_textlist_event(fields["doc_mainlist"]) local cid = doc.data.category_order[event.index] if cid then if event.type == "CHG" then doc.data.players[playername].catsel = nil doc.data.players[playername].category = cid doc.data.players[playername].entry = nil doc.data.players[playername].entry_textlist_needs_updating = true elseif event.type == "DCL" then doc.data.players[playername].catsel = nil doc.data.players[playername].category = cid doc.data.players[playername].entry = nil doc.data.players[playername].entry_textlist_needs_updating = true local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) minetest.show_formspec(playername, "doc:category", formspec) end end end if fields["doc_button_goto_category"] then local cid = doc.data.players[playername].category doc.data.players[playername].catsel = nil doc.data.players[playername].entry = nil doc.data.players[playername].entry_textlist_needs_updating = true local formspec = doc.formspec_core(2)..doc.formspec_category(cid, playername) minetest.show_formspec(playername, "doc:category", formspec) end if fields["doc_setting_notify_on_reveal"] then doc.data.players[playername].stored_data.notify_on_reveal = fields["doc_setting_notify_on_reveal"] == "true" end elseif(formname == "doc:category") then if fields["doc_button_goto_entry"] then local cid = doc.data.players[playername].category if cid then local eid = nil local eids, catsel = doc.data.players[playername].entry_ids, doc.data.players[playername].catsel if eids and catsel then eid = eids[catsel] end doc.data.players[playername].galidx = 1 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) doc.mark_entry_as_viewed(playername, cid, eid) end end if fields["doc_button_goto_main"] then local formspec = doc.formspec_core(1)..doc.formspec_main(playername) minetest.show_formspec(playername, "doc:main", formspec) end if fields["doc_catlist"] then local event = minetest.explode_textlist_event(fields["doc_catlist"]) if event.type == "CHG" then doc.data.players[playername].catsel = event.index doc.data.players[playername].entry = doc.data.players[playername].entry_ids[event.index] doc.data.players[playername].entry_textlist_needs_updating = true elseif event.type == "DCL" then local cid = doc.data.players[playername].category local eid = nil local eids, catsel = doc.data.players[playername].entry_ids, event.index if eids and catsel then eid = eids[catsel] end doc.mark_entry_as_viewed(playername, cid, eid) doc.data.players[playername].entry_textlist_needs_updating = true doc.data.players[playername].galidx = 1 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) end end elseif(formname == "doc:entry") then if fields["doc_button_goto_main"] then local formspec = doc.formspec_core(1)..doc.formspec_main(playername) minetest.show_formspec(playername, "doc:main", formspec) elseif fields["doc_button_goto_category"] then local formspec = doc.formspec_core(2)..doc.formspec_category(doc.data.players[playername].category, playername) minetest.show_formspec(playername, "doc:category", formspec) elseif fields["doc_button_goto_next"] then if doc.data.players[playername].catsel == nil then return end -- emergency exit local eids = doc.data.players[playername].entry_ids local cid = doc.data.players[playername].category local new_catsel= doc.data.players[playername].catsel + 1 local new_eid = eids[new_catsel] if #eids > 1 and new_catsel <= #eids then doc.mark_entry_as_viewed(playername, cid, new_eid) doc.data.players[playername].catsel = new_catsel doc.data.players[playername].entry = new_eid doc.data.players[playername].galidx = 1 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) end elseif fields["doc_button_goto_prev"] then if doc.data.players[playername].catsel == nil then return end -- emergency exit local eids = doc.data.players[playername].entry_ids local cid = doc.data.players[playername].category local new_catsel= doc.data.players[playername].catsel - 1 local new_eid = eids[new_catsel] if #eids > 1 and new_catsel >= 1 then doc.mark_entry_as_viewed(playername, cid, new_eid) doc.data.players[playername].catsel = new_catsel doc.data.players[playername].entry = new_eid doc.data.players[playername].galidx = 1 local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, new_eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) end elseif fields["doc_button_gallery_prev"] then local cid, eid = doc.get_selection(playername) if doc.data.players[playername].galidx - doc.data.players[playername].galrows > 0 then doc.data.players[playername].galidx = doc.data.players[playername].galidx - doc.data.players[playername].galrows end local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) elseif fields["doc_button_gallery_next"] then local cid, eid = doc.get_selection(playername) if doc.data.players[playername].galidx + doc.data.players[playername].galrows <= doc.data.players[playername].maxgalidx then doc.data.players[playername].galidx = doc.data.players[playername].galidx + doc.data.players[playername].galrows end local formspec = doc.formspec_core(3)..doc.formspec_entry(cid, eid, playername) minetest.show_formspec(playername, "doc:entry", formspec) end else if fields["doc_inventory_plus"] and mod_inventory_plus then doc.show_doc(playername) return end end end minetest.register_on_player_receive_fields(doc.process_form) minetest.register_chatcommand("helpform", { params = "", description = S("Open a window providing help entries about Minetest and more"), privs = {}, func = function(playername, param) doc.show_doc(playername) end, } ) minetest.register_on_joinplayer(function(player) local playername = player:get_player_name() local playerdata = doc.data.players[playername] if playerdata == nil then -- Initialize player data doc.data.players[playername] = {} playerdata = doc.data.players[playername] -- Gallery index, stores current index of first displayed image in a gallery playerdata.galidx = 1 -- Maximum gallery index (index of last image in gallery) playerdata.maxgalidx = 1 -- Number of rows in an gallery of the current entry playerdata.galrows = 1 -- Table for persistant data playerdata.stored_data = {} -- Contains viewed entries playerdata.stored_data.viewed = {} -- Count viewed entries playerdata.stored_data.viewed_count = {} -- Contains revealed/unhidden entries playerdata.stored_data.revealed = {} -- Count revealed entries playerdata.stored_data.revealed_count = {} else -- Completely rebuild viewed and revealed counts from scratch for cid, cat in pairs(doc.data.categories) do if playerdata.stored_data.viewed[cid] == nil then playerdata.stored_data.viewed[cid] = {} end if playerdata.stored_data.revealed[cid] == nil then playerdata.stored_data.revealed[cid] = {} end local vc = 0 local rc = doc.get_entry_count(cid) - doc.data.categories[cid].hidden_count for eid, entry in pairs(cat.entries) do if playerdata.stored_data.viewed[cid][eid] then vc = vc + 1 playerdata.stored_data.revealed[cid][eid] = true end if playerdata.stored_data.revealed[cid][eid] and entry.hidden then rc = rc + 1 end end playerdata.stored_data.viewed_count[cid] = vc playerdata.stored_data.revealed_count[cid] = rc end end -- Add button for Inventory++ if mod_inventory_plus then inventory_plus.register_button(player, "doc_inventory_plus", S("Help")) end end) ---[[ Add buttons for inventory mods ]] local function button_action(player) doc.show_doc(player:get_player_name()) end -- Unified Inventory if minetest.get_modpath("unified_inventory") then unified_inventory.register_button("doc", { type = "image", image = "doc_button_icon_hires.png", tooltip = S("Help"), action = button_action, }) end -- sfinv_buttons if minetest.get_modpath("sfinv_buttons") then sfinv_buttons.register_button("doc", { image = "doc_button_icon_lores.png", tooltip = S("Collection of help texts"), title = S("Help"), action = button_action, }) end minetest.register_privilege("help_reveal", { description = S("Allows you to reveal all hidden help entries with /help_reveal"), give_to_singleplayer = false }) minetest.register_chatcommand("help_reveal", { params = "", description = S("Reveal all hidden help entries to you"), privs = { help_reveal = true }, func = function(name, param) doc.mark_all_entries_as_revealed(name) end, })