diff options
Diffstat (limited to 'builtin')
40 files changed, 2604 insertions, 1098 deletions
diff --git a/builtin/client/chatcommands.lua b/builtin/client/chatcommands.lua index 5cb1b40bb..a563a6627 100644 --- a/builtin/client/chatcommands.lua +++ b/builtin/client/chatcommands.lua @@ -1,6 +1,5 @@ -- Minetest: builtin/client/chatcommands.lua - core.register_on_sending_chat_message(function(message) if message:sub(1,2) == ".." then return false @@ -8,7 +7,7 @@ core.register_on_sending_chat_message(function(message) local first_char = message:sub(1,1) if first_char == "/" or first_char == "." then - core.display_chat_message(core.gettext("issued command: ") .. message) + core.display_chat_message(core.gettext("Issued command: ") .. message) end if first_char ~= "." then @@ -19,7 +18,12 @@ core.register_on_sending_chat_message(function(message) param = param or "" if not cmd then - core.display_chat_message(core.gettext("-!- Empty command")) + core.display_chat_message("-!- " .. core.gettext("Empty command.")) + return true + end + + -- Run core.registered_on_chatcommand callbacks. + if core.run_callbacks(core.registered_on_chatcommand, 5, cmd, param) then return true end @@ -31,7 +35,7 @@ core.register_on_sending_chat_message(function(message) core.display_chat_message(result) end else - core.display_chat_message(core.gettext("-!- Invalid command: ") .. cmd) + core.display_chat_message("-!- " .. core.gettext("Invalid command: ") .. cmd) end return true @@ -61,7 +65,7 @@ core.register_chatcommand("clear_chat_queue", { description = core.gettext("Clear the out chat queue"), func = function(param) core.clear_out_chat_queue() - return true, core.gettext("The out chat queue is now empty") + return true, core.gettext("The out chat queue is now empty.") end, }) diff --git a/builtin/client/death_formspec.lua b/builtin/client/death_formspec.lua index e755ac5c1..7df0cbd75 100644 --- a/builtin/client/death_formspec.lua +++ b/builtin/client/death_formspec.lua @@ -2,7 +2,7 @@ -- handled by the engine. core.register_on_death(function() - core.display_chat_message("You died.") + core.display_chat_message(core.gettext("You died.")) local formspec = "size[11,5.5]bgcolor[#320000b4;true]" .. "label[4.85,1.35;" .. fgettext("You died") .. "]button_exit[4,3;3,0.5;btn_respawn;".. fgettext("Respawn") .."]" diff --git a/builtin/client/register.lua b/builtin/client/register.lua index c1b4965c1..27a6b02d9 100644 --- a/builtin/client/register.lua +++ b/builtin/client/register.lua @@ -4,6 +4,13 @@ core.callback_origins = {} local getinfo = debug.getinfo debug.getinfo = nil +--- Runs given callbacks. +-- +-- Note: this function is also called from C++ +-- @tparam table callbacks a table with registered callbacks, like `core.registered_on_*` +-- @tparam number mode a RunCallbacksMode, as defined in src/script/common/c_internal.h +-- @param ... arguments for the callback +-- @return depends on mode function core.run_callbacks(callbacks, mode, ...) assert(type(callbacks) == "table") local cb_len = #callbacks @@ -63,6 +70,7 @@ core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration core.registered_on_shutdown, core.register_on_shutdown = make_registration() core.registered_on_receiving_chat_message, core.register_on_receiving_chat_message = make_registration() core.registered_on_sending_chat_message, core.register_on_sending_chat_message = make_registration() +core.registered_on_chatcommand, core.register_on_chatcommand = make_registration() core.registered_on_death, core.register_on_death = make_registration() core.registered_on_hp_modification, core.register_on_hp_modification = make_registration() core.registered_on_damage_taken, core.register_on_damage_taken = make_registration() diff --git a/builtin/common/chatcommands.lua b/builtin/common/chatcommands.lua index 52edda659..c945e7bdb 100644 --- a/builtin/common/chatcommands.lua +++ b/builtin/common/chatcommands.lua @@ -1,5 +1,9 @@ -- Minetest: builtin/common/chatcommands.lua +-- For server-side translations (if INIT == "game") +-- Otherwise, use core.gettext +local S = core.get_translator("__builtin") + core.registered_chatcommands = {} function core.register_chatcommand(cmd, def) @@ -29,25 +33,12 @@ function core.override_chatcommand(name, redefinition) core.registered_chatcommands[name] = chatcommand end -local cmd_marker = "/" - -local function gettext(...) - return ... -end - -local function gettext_replace(text, replace) - return text:gsub("$1", replace) -end - - -if INIT == "client" then - cmd_marker = "." - gettext = core.gettext - gettext_replace = fgettext_ne -end - local function do_help_cmd(name, param) local function format_help_line(cmd, def) + local cmd_marker = "/" + if INIT == "client" then + cmd_marker = "." + end local msg = core.colorize("#00ffff", cmd_marker .. cmd) if def.params and def.params ~= "" then msg = msg .. " " .. def.params @@ -65,9 +56,21 @@ local function do_help_cmd(name, param) end end table.sort(cmds) - return true, gettext("Available commands: ") .. table.concat(cmds, " ") .. "\n" - .. gettext_replace("Use '$1help <cmd>' to get more information," - .. " or '$1help all' to list everything.", cmd_marker) + local msg + if INIT == "game" then + msg = S("Available commands: @1", + table.concat(cmds, " ")) .. "\n" + .. S("Use '/help <cmd>' to get more " + .. "information, or '/help all' to list " + .. "everything.") + else + msg = core.gettext("Available commands: ") + .. table.concat(cmds, " ") .. "\n" + .. core.gettext("Use '.help <cmd>' to get more " + .. "information, or '.help all' to list " + .. "everything.") + end + return true, msg elseif param == "all" then local cmds = {} for cmd, def in pairs(core.registered_chatcommands) do @@ -76,19 +79,31 @@ local function do_help_cmd(name, param) end end table.sort(cmds) - return true, gettext("Available commands:").."\n"..table.concat(cmds, "\n") + local msg + if INIT == "game" then + msg = S("Available commands:") + else + msg = core.gettext("Available commands:") + end + return true, msg.."\n"..table.concat(cmds, "\n") elseif INIT == "game" and param == "privs" then local privs = {} for priv, def in pairs(core.registered_privileges) do privs[#privs + 1] = priv .. ": " .. def.description end table.sort(privs) - return true, "Available privileges:\n"..table.concat(privs, "\n") + return true, S("Available privileges:").."\n"..table.concat(privs, "\n") else local cmd = param local def = core.registered_chatcommands[cmd] if not def then - return false, gettext("Command not available: ")..cmd + local msg + if INIT == "game" then + msg = S("Command not available: @1", cmd) + else + msg = core.gettext("Command not available: ") .. cmd + end + return false, msg else return true, format_help_line(cmd, def) end @@ -97,16 +112,16 @@ end if INIT == "client" then core.register_chatcommand("help", { - params = gettext("[all | <cmd>]"), - description = gettext("Get help for commands"), + params = core.gettext("[all | <cmd>]"), + description = core.gettext("Get help for commands"), func = function(param) return do_help_cmd(nil, param) end, }) else core.register_chatcommand("help", { - params = "[all | privs | <cmd>]", - description = "Get help for commands or list privileges", + params = S("[all | privs | <cmd>]"), + description = S("Get help for commands or list privileges"), func = do_help_cmd, }) end diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua index 8afa5bc87..e814b4c43 100644 --- a/builtin/common/information_formspecs.lua +++ b/builtin/common/information_formspecs.lua @@ -20,7 +20,8 @@ local LIST_FORMSPEC_DESCRIPTION = [[ button_exit[5,7;3,1;quit;%s] ]] -local formspec_escape = core.formspec_escape +local F = core.formspec_escape +local S = core.get_translator("__builtin") local check_player_privs = core.check_player_privs @@ -51,22 +52,23 @@ core.after(0, load_mod_command_tree) local function build_chatcommands_formspec(name, sel, copy) local rows = {} - rows[1] = "#FFF,0,Command,Parameters" + rows[1] = "#FFF,0,"..F(S("Command"))..","..F(S("Parameters")) - local description = "For more information, click on any entry in the list.\n" .. - "Double-click to copy the entry to the chat history." + local description = S("For more information, click on " + .. "any entry in the list.").. "\n" .. + S("Double-click to copy the entry to the chat history.") for i, data in ipairs(mod_cmds) do - rows[#rows + 1] = COLOR_BLUE .. ",0," .. formspec_escape(data[1]) .. "," + rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. "," for j, cmds in ipairs(data[2]) do local has_priv = check_player_privs(name, cmds[2].privs) rows[#rows + 1] = ("%s,1,%s,%s"):format( has_priv and COLOR_GREEN or COLOR_GRAY, - cmds[1], formspec_escape(cmds[2].params)) + cmds[1], F(cmds[2].params)) if sel == #rows then description = cmds[2].description if copy then - core.chat_send_player(name, ("Command: %s %s"):format( + core.chat_send_player(name, S("Command: @1 @2", core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params)) end end @@ -74,9 +76,9 @@ local function build_chatcommands_formspec(name, sel, copy) end return LIST_FORMSPEC_DESCRIPTION:format( - "Available commands: (see also: /help <cmd>)", + F(S("Available commands: (see also: /help <cmd>)")), table.concat(rows, ","), sel or 0, - description, "Close" + F(description), F(S("Close")) ) end @@ -91,19 +93,19 @@ local function build_privs_formspec(name) table.sort(privs, function(a, b) return a[1] < b[1] end) local rows = {} - rows[1] = "#FFF,0,Privilege,Description" + rows[1] = "#FFF,0,"..F(S("Privilege"))..","..F(S("Description")) local player_privs = core.get_player_privs(name) for i, data in ipairs(privs) do rows[#rows + 1] = ("%s,0,%s,%s"):format( player_privs[data[1]] and COLOR_GREEN or COLOR_GRAY, - data[1], formspec_escape(data[2].description)) + data[1], F(data[2].description)) end return LIST_FORMSPEC:format( - "Available privileges:", + F(S("Available privileges:")), table.concat(rows, ","), - "Close" + F(S("Close")) ) end @@ -115,7 +117,7 @@ core.register_on_player_receive_fields(function(player, formname, fields) return end - local event = minetest.explode_table_event(fields.list) + local event = core.explode_table_event(fields.list) if event.type ~= "INV" then local name = player:get_player_name() core.show_formspec(name, "__builtin:help_cmds", diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index e29a9f422..0f3897f47 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -697,3 +697,7 @@ function core.privs_to_string(privs, delim) end return table.concat(list, delim) end + +function core.is_nan(number) + return number ~= number +end diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua index 3715e231b..424d329fb 100644 --- a/builtin/fstk/tabview.lua +++ b/builtin/fstk/tabview.lua @@ -58,26 +58,20 @@ end -------------------------------------------------------------------------------- local function get_formspec(self) - local formspec = "" + if self.hidden or (self.parent ~= nil and self.parent.hidden) then + return "" + end + local tab = self.tablist[self.last_tab_index] - if not self.hidden and (self.parent == nil or not self.parent.hidden) then + local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) - if self.parent == nil then - local tsize = self.tablist[self.last_tab_index].tabsize or - {width=self.width, height=self.height} - formspec = formspec .. - string.format("size[%f,%f,%s]",tsize.width,tsize.height, - dump(self.fixed_size)) - end - formspec = formspec .. self:tab_header() - formspec = formspec .. - self.tablist[self.last_tab_index].get_formspec( - self, - self.tablist[self.last_tab_index].name, - self.tablist[self.last_tab_index].tabdata, - self.tablist[self.last_tab_index].tabsize - ) + if self.parent == nil and not prepend then + local tsize = tab.tabsize or {width=self.width, height=self.height} + prepend = string.format("size[%f,%f,%s]", tsize.width, tsize.height, + dump(self.fixed_size)) end + + local formspec = (prepend or "") .. self:tab_header() .. content return formspec end @@ -97,14 +91,9 @@ local function handle_buttons(self,fields) return true end - if self.tablist[self.last_tab_index].button_handler ~= nil then - return - self.tablist[self.last_tab_index].button_handler( - self, - fields, - self.tablist[self.last_tab_index].name, - self.tablist[self.last_tab_index].tabdata - ) + local tab = self.tablist[self.last_tab_index] + if tab.button_handler ~= nil then + return tab.button_handler(self, fields, tab.name, tab.tabdata) end return false @@ -122,14 +111,9 @@ local function handle_events(self,event) return true end - if self.tablist[self.last_tab_index].evt_handler ~= nil then - return - self.tablist[self.last_tab_index].evt_handler( - self, - event, - self.tablist[self.last_tab_index].name, - self.tablist[self.last_tab_index].tabdata - ) + local tab = self.tablist[self.last_tab_index] + if tab.evt_handler ~= nil then + return tab.evt_handler(self, event, tab.name, tab.tabdata) end return false diff --git a/builtin/fstk/ui.lua b/builtin/fstk/ui.lua index 7eeebdd47..976659ed3 100644 --- a/builtin/fstk/ui.lua +++ b/builtin/fstk/ui.lua @@ -18,6 +18,8 @@ ui = {} ui.childlist = {} ui.default = nil +-- Whether fstk is currently showing its own formspec instead of active ui elements. +ui.overridden = false -------------------------------------------------------------------------------- function ui.add(child) @@ -55,6 +57,7 @@ end -------------------------------------------------------------------------------- function ui.update() + ui.overridden = false local formspec = {} -- handle errors @@ -71,6 +74,7 @@ function ui.update() "button[2,6.6;4,1;btn_reconnect_yes;" .. fgettext("Reconnect") .. "]", "button[8,6.6;4,1;btn_reconnect_no;" .. fgettext("Main menu") .. "]" } + ui.overridden = true elseif gamedata ~= nil and gamedata.errormessage ~= nil then local error_message = core.formspec_escape(gamedata.errormessage) @@ -89,6 +93,7 @@ function ui.update() error_title, error_message), "button[5,6.6;4,1;btn_error_confirm;" .. fgettext("OK") .. "]" } + ui.overridden = true else local active_toplevel_ui_elements = 0 for key,value in pairs(ui.childlist) do @@ -185,6 +190,16 @@ end -------------------------------------------------------------------------------- core.event_handler = function(event) + -- Handle error messages + if ui.overridden then + if event == "MenuQuit" then + gamedata.errormessage = nil + gamedata.reconnect_requested = false + ui.update() + end + return + end + if ui.handle_events(event) then ui.update() return diff --git a/builtin/game/chat.lua b/builtin/game/chat.lua index 1d277730a..bf2d7851e 100644 --- a/builtin/game/chat.lua +++ b/builtin/game/chat.lua @@ -1,5 +1,7 @@ -- Minetest: builtin/game/chat.lua +local S = core.get_translator("__builtin") + -- Helper function that implements search and replace without pattern matching -- Returns the string and a boolean indicating whether or not the string was modified local function safe_gsub(s, replace, with) @@ -45,6 +47,8 @@ end core.chatcommands = core.registered_chatcommands -- BACKWARDS COMPATIBILITY +local msg_time_threshold = + tonumber(core.settings:get("chatcommand_msg_time_threshold")) or 0.1 core.register_on_chat_message(function(name, message) if message:sub(1,1) ~= "/" then return @@ -52,23 +56,30 @@ core.register_on_chat_message(function(name, message) local cmd, param = string.match(message, "^/([^ ]+) *(.*)") if not cmd then - core.chat_send_player(name, "-!- Empty command") + core.chat_send_player(name, "-!- "..S("Empty command.")) return true end param = param or "" + -- Run core.registered_on_chatcommands callbacks. + if core.run_callbacks(core.registered_on_chatcommands, 5, name, cmd, param) then + return true + end + local cmd_def = core.registered_chatcommands[cmd] if not cmd_def then - core.chat_send_player(name, "-!- Invalid command: " .. cmd) + core.chat_send_player(name, "-!- "..S("Invalid command: @1", cmd)) return true end local has_privs, missing_privs = core.check_player_privs(name, cmd_def.privs) if has_privs then core.set_last_run_mod(cmd_def.mod_origin) + local t_before = minetest.get_us_time() local success, result = cmd_def.func(name, param) + local delay = (minetest.get_us_time() - t_before) / 1000000 if success == false and result == nil then - core.chat_send_player(name, "-!- Invalid command usage") + core.chat_send_player(name, "-!- "..S("Invalid command usage.")) local help_def = core.registered_chatcommands["help"] if help_def then local _, helpmsg = help_def.func(name, cmd) @@ -76,13 +87,26 @@ core.register_on_chat_message(function(name, message) core.chat_send_player(name, helpmsg) end end - elseif result then - core.chat_send_player(name, result) + else + if delay > msg_time_threshold then + -- Show how much time it took to execute the command + if result then + result = result .. + minetest.colorize("#f3d2ff", " (%.5g s)"):format(delay) + else + result = minetest.colorize("#f3d2ff", + "Command execution took %.5f s"):format(delay) + end + end + if result then + core.chat_send_player(name, result) + end end else - core.chat_send_player(name, "You don't have permission" - .. " to run this command (missing privileges: " - .. table.concat(missing_privs, ", ") .. ")") + core.chat_send_player(name, + S("You don't have permission to run this command " + .. "(missing privileges: @1).", + table.concat(missing_privs, ", "))) end return true -- Handled chat message end) @@ -102,12 +126,13 @@ local function parse_range_str(player_name, str) if args[1] == "here" then p1, p2 = core.get_player_radius_area(player_name, tonumber(args[2])) if p1 == nil then - return false, "Unable to get player " .. player_name .. " position" + return false, S("Unable to get position of player @1.", player_name) end else p1, p2 = core.string_to_area(str) if p1 == nil then - return false, "Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)" + return false, S("Incorrect area format. " + .. "Expected: (x1,y1,z1) (x2,y2,z2)") end end @@ -118,9 +143,9 @@ end -- Chat commands -- core.register_chatcommand("me", { - params = "<action>", - description = "Show chat action (e.g., '/me orders a pizza' displays" - .. " '<player name> orders a pizza')", + params = S("<action>"), + description = S("Show chat action (e.g., '/me orders a pizza' " + .. "displays '<player name> orders a pizza')"), privs = {shout=true}, func = function(name, param) core.chat_send_all("* " .. name .. " " .. param) @@ -129,43 +154,44 @@ core.register_chatcommand("me", { }) core.register_chatcommand("admin", { - description = "Show the name of the server owner", + description = S("Show the name of the server owner"), func = function(name) local admin = core.settings:get("name") if admin then - return true, "The administrator of this server is " .. admin .. "." + return true, S("The administrator of this server is @1.", admin) else - return false, "There's no administrator named in the config file." + return false, S("There's no administrator named " + .. "in the config file.") end end, }) core.register_chatcommand("privs", { - params = "[<name>]", - description = "Show privileges of yourself or another player", + params = S("[<name>]"), + description = S("Show privileges of yourself or another player"), func = function(caller, param) param = param:trim() local name = (param ~= "" and param or caller) if not core.player_exists(name) then - return false, "Player " .. name .. " does not exist." + return false, S("Player @1 does not exist.", name) end - return true, "Privileges of " .. name .. ": " - .. core.privs_to_string( - core.get_player_privs(name), ", ") + return true, S("Privileges of @1: @2", name, + core.privs_to_string( + core.get_player_privs(name), ", ")) end, }) core.register_chatcommand("haspriv", { - params = "<privilege>", - description = "Return list of all online players with privilege.", + params = S("<privilege>"), + description = S("Return list of all online players with privilege"), privs = {basic_privs = true}, func = function(caller, param) param = param:trim() if param == "" then - return false, "Invalid parameters (see /help haspriv)" + return false, S("Invalid parameters (see /help haspriv).") end if not core.registered_privileges[param] then - return false, "Unknown privilege!" + return false, S("Unknown privilege!") end local privs = core.string_to_privs(param) local players_with_priv = {} @@ -175,19 +201,20 @@ core.register_chatcommand("haspriv", { table.insert(players_with_priv, player_name) end end - return true, "Players online with the \"" .. param .. "\" privilege: " .. - table.concat(players_with_priv, ", ") + return true, S("Players online with the \"@1\" privilege: @2", + param, + table.concat(players_with_priv, ", ")) end }) local function handle_grant_command(caller, grantname, grantprivstr) local caller_privs = core.get_player_privs(caller) if not (caller_privs.privs or caller_privs.basic_privs) then - return false, "Your privileges are insufficient." + return false, S("Your privileges are insufficient.") end if not core.get_auth_handler().get_auth(grantname) then - return false, "Player " .. grantname .. " does not exist." + return false, S("Player @1 does not exist.", grantname) end local grantprivs = core.string_to_privs(grantprivstr) if grantprivstr == "all" then @@ -199,10 +226,10 @@ local function handle_grant_command(caller, grantname, grantprivstr) core.string_to_privs(core.settings:get("basic_privs") or "interact,shout") for priv, _ in pairs(grantprivs) do if not basic_privs[priv] and not caller_privs.privs then - return false, "Your privileges are insufficient." + return false, S("Your privileges are insufficient.") end if not core.registered_privileges[priv] then - privs_unknown = privs_unknown .. "Unknown privilege: " .. priv .. "\n" + privs_unknown = privs_unknown .. S("Unknown privilege: @1", priv) .. "\n" end privs[priv] = true end @@ -216,33 +243,33 @@ local function handle_grant_command(caller, grantname, grantprivstr) core.set_player_privs(grantname, privs) core.log("action", caller..' granted ('..core.privs_to_string(grantprivs, ', ')..') privileges to '..grantname) if grantname ~= caller then - core.chat_send_player(grantname, caller - .. " granted you privileges: " - .. core.privs_to_string(grantprivs, ' ')) + core.chat_send_player(grantname, + S("@1 granted you privileges: @2", caller, + core.privs_to_string(grantprivs, ' '))) end - return true, "Privileges of " .. grantname .. ": " - .. core.privs_to_string( - core.get_player_privs(grantname), ' ') + return true, S("Privileges of @1: @2", grantname, + core.privs_to_string( + core.get_player_privs(grantname), ' ')) end core.register_chatcommand("grant", { - params = "<name> (<privilege> | all)", - description = "Give privileges to player", + params = S("<name> (<privilege> | all)"), + description = S("Give privileges to player"), func = function(name, param) local grantname, grantprivstr = string.match(param, "([^ ]+) (.+)") if not grantname or not grantprivstr then - return false, "Invalid parameters (see /help grant)" + return false, S("Invalid parameters (see /help grant).") end return handle_grant_command(name, grantname, grantprivstr) end, }) core.register_chatcommand("grantme", { - params = "<privilege> | all", - description = "Grant privileges to yourself", + params = S("<privilege> | all"), + description = S("Grant privileges to yourself"), func = function(name, param) if param == "" then - return false, "Invalid parameters (see /help grantme)" + return false, S("Invalid parameters (see /help grantme).") end return handle_grant_command(name, name, param) end, @@ -251,11 +278,11 @@ core.register_chatcommand("grantme", { local function handle_revoke_command(caller, revokename, revokeprivstr) local caller_privs = core.get_player_privs(caller) if not (caller_privs.privs or caller_privs.basic_privs) then - return false, "Your privileges are insufficient." + return false, S("Your privileges are insufficient.") end if not core.get_auth_handler().get_auth(revokename) then - return false, "Player " .. revokename .. " does not exist." + return false, S("Player @1 does not exist.", revokename) end local revokeprivs = core.string_to_privs(revokeprivstr) @@ -264,7 +291,7 @@ local function handle_revoke_command(caller, revokename, revokeprivstr) core.string_to_privs(core.settings:get("basic_privs") or "interact,shout") for priv, _ in pairs(revokeprivs) do if not basic_privs[priv] and not caller_privs.privs then - return false, "Your privileges are insufficient." + return false, S("Your privileges are insufficient.") end end @@ -287,43 +314,43 @@ local function handle_revoke_command(caller, revokename, revokeprivstr) ..core.privs_to_string(revokeprivs, ', ') ..') privileges from '..revokename) if revokename ~= caller then - core.chat_send_player(revokename, caller - .. " revoked privileges from you: " - .. core.privs_to_string(revokeprivs, ' ')) + core.chat_send_player(revokename, + S("@1 revoked privileges from you: @2", caller, + core.privs_to_string(revokeprivs, ' '))) end - return true, "Privileges of " .. revokename .. ": " - .. core.privs_to_string( - core.get_player_privs(revokename), ' ') + return true, S("Privileges of @1: @2", revokename, + core.privs_to_string( + core.get_player_privs(revokename), ' ')) end core.register_chatcommand("revoke", { - params = "<name> (<privilege> | all)", - description = "Remove privileges from player", + params = S("<name> (<privilege> | all)"), + description = S("Remove privileges from player"), privs = {}, func = function(name, param) local revokename, revokeprivstr = string.match(param, "([^ ]+) (.+)") if not revokename or not revokeprivstr then - return false, "Invalid parameters (see /help revoke)" + return false, S("Invalid parameters (see /help revoke).") end return handle_revoke_command(name, revokename, revokeprivstr) end, }) core.register_chatcommand("revokeme", { - params = "<privilege> | all", - description = "Revoke privileges from yourself", + params = S("<privilege> | all"), + description = S("Revoke privileges from yourself"), privs = {}, func = function(name, param) if param == "" then - return false, "Invalid parameters (see /help revokeme)" + return false, S("Invalid parameters (see /help revokeme).") end return handle_revoke_command(name, name, param) end, }) core.register_chatcommand("setpassword", { - params = "<name> <password>", - description = "Set player's password", + params = S("<name> <password>"), + description = S("Set player's password"), privs = {password=true}, func = function(name, param) local toname, raw_password = string.match(param, "^([^ ]+) +(.+)$") @@ -333,207 +360,197 @@ core.register_chatcommand("setpassword", { end if not toname then - return false, "Name field required" + return false, S("Name field required.") end - local act_str_past, act_str_pres + local msg_chat, msg_log, msg_ret if not raw_password then core.set_player_password(toname, "") - act_str_past = "cleared" - act_str_pres = "clears" + msg_chat = S("Your password was cleared by @1.", name) + msg_log = name .. " clears password of " .. toname .. "." + msg_ret = S("Password of player \"@1\" cleared.", toname) else core.set_player_password(toname, core.get_password_hash(toname, raw_password)) - act_str_past = "set" - act_str_pres = "sets" + msg_chat = S("Your password was set by @1.", name) + msg_log = name .. " sets password of " .. toname .. "." + msg_ret = S("Password of player \"@1\" set.", toname) end if toname ~= name then - core.chat_send_player(toname, "Your password was " - .. act_str_past .. " by " .. name) + core.chat_send_player(toname, msg_chat) end - core.log("action", name .. " " .. act_str_pres .. - " password of " .. toname .. ".") + core.log("action", msg_log) - return true, "Password of player \"" .. toname .. "\" " .. act_str_past + return true, msg_ret end, }) core.register_chatcommand("clearpassword", { - params = "<name>", - description = "Set empty password for a player", + params = S("<name>"), + description = S("Set empty password for a player"), privs = {password=true}, func = function(name, param) local toname = param if toname == "" then - return false, "Name field required" + return false, S("Name field required.") end core.set_player_password(toname, '') core.log("action", name .. " clears password of " .. toname .. ".") - return true, "Password of player \"" .. toname .. "\" cleared" + return true, S("Password of player \"@1\" cleared.", toname) end, }) core.register_chatcommand("auth_reload", { params = "", - description = "Reload authentication data", + description = S("Reload authentication data"), privs = {server=true}, func = function(name, param) local done = core.auth_reload() - return done, (done and "Done." or "Failed.") + return done, (done and S("Done.") or S("Failed.")) end, }) core.register_chatcommand("remove_player", { - params = "<name>", - description = "Remove a player's data", + params = S("<name>"), + description = S("Remove a player's data"), privs = {server=true}, func = function(name, param) local toname = param if toname == "" then - return false, "Name field required" + return false, S("Name field required.") end local rc = core.remove_player(toname) if rc == 0 then core.log("action", name .. " removed player data of " .. toname .. ".") - return true, "Player \"" .. toname .. "\" removed." + return true, S("Player \"@1\" removed.", toname) elseif rc == 1 then - return true, "No such player \"" .. toname .. "\" to remove." + return true, S("No such player \"@1\" to remove.", toname) elseif rc == 2 then - return true, "Player \"" .. toname .. "\" is connected, cannot remove." + return true, S("Player \"@1\" is connected, cannot remove.", toname) end - return false, "Unhandled remove_player return code " .. rc .. "" + return false, S("Unhandled remove_player return code @1.", tostring(rc)) end, }) -core.register_chatcommand("teleport", { - params = "<X>,<Y>,<Z> | <to_name> | (<name> <X>,<Y>,<Z>) | (<name> <to_name>)", - description = "Teleport to position or player", - privs = {teleport=true}, - func = function(name, param) - -- Returns (pos, true) if found, otherwise (pos, false) - local function find_free_position_near(pos) - local tries = { - {x=1,y=0,z=0}, - {x=-1,y=0,z=0}, - {x=0,y=0,z=1}, - {x=0,y=0,z=-1}, - } - for _, d in ipairs(tries) do - local p = {x = pos.x+d.x, y = pos.y+d.y, z = pos.z+d.z} - local n = core.get_node_or_nil(p) - if n and n.name then - local def = core.registered_nodes[n.name] - if def and not def.walkable then - return p, true - end - end + +-- pos may be a non-integer position +local function find_free_position_near(pos) + local tries = { + {x=1, y=0, z=0}, + {x=-1, y=0, z=0}, + {x=0, y=0, z=1}, + {x=0, y=0, z=-1}, + } + for _, d in ipairs(tries) do + local p = vector.add(pos, d) + local n = core.get_node_or_nil(p) + if n then + local def = core.registered_nodes[n.name] + if def and not def.walkable then + return p end - return pos, false end + end + return pos +end +-- Teleports player <name> to <p> if possible +local function teleport_to_pos(name, p) + local lm = 31000 + if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm + or p.z < -lm or p.z > lm then + return false, S("Cannot teleport out of map bounds!") + end + local teleportee = core.get_player_by_name(name) + if not teleportee then + return false, S("Cannot get player with name @1.", name) + end + if teleportee:get_attach() then + return false, S("Cannot teleport, @1 " .. + "is attached to an object!", name) + end + teleportee:set_pos(p) + return true, S("Teleporting @1 to @2.", name, core.pos_to_string(p, 1)) +end + +-- Teleports player <name> next to player <target_name> if possible +local function teleport_to_player(name, target_name) + if name == target_name then + return false, S("One does not teleport to oneself.") + end + local teleportee = core.get_player_by_name(name) + if not teleportee then + return false, S("Cannot get teleportee with name @1.", name) + end + if teleportee:get_attach() then + return false, S("Cannot teleport, @1 " .. + "is attached to an object!", name) + end + local target = core.get_player_by_name(target_name) + if not target then + return false, S("Cannot get target player with name @1.", target_name) + end + local p = find_free_position_near(target:get_pos()) + teleportee:set_pos(p) + return true, S("Teleporting @1 to @2 at @3.", name, target_name, + core.pos_to_string(p, 1)) +end + +core.register_chatcommand("teleport", { + params = S("<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>"), + description = S("Teleport to position or player"), + privs = {teleport=true}, + func = function(name, param) local p = {} - p.x, p.y, p.z = string.match(param, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$") - p.x = tonumber(p.x) - p.y = tonumber(p.y) - p.z = tonumber(p.z) + p.x, p.y, p.z = param:match("^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$") + p = vector.apply(p, tonumber) if p.x and p.y and p.z then - - local lm = 31000 - if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm or p.z < -lm or p.z > lm then - return false, "Cannot teleport out of map bounds!" - end - local teleportee = core.get_player_by_name(name) - if teleportee then - if teleportee:get_attach() then - return false, "Can't teleport, you're attached to an object!" - end - teleportee:set_pos(p) - return true, "Teleporting to "..core.pos_to_string(p) - end + return teleport_to_pos(name, p) end local target_name = param:match("^([^ ]+)$") - local teleportee = core.get_player_by_name(name) - - p = nil if target_name then - local target = core.get_player_by_name(target_name) - if target then - p = target:get_pos() - end + return teleport_to_player(name, target_name) end - if teleportee and p then - if teleportee:get_attach() then - return false, "Can't teleport, you're attached to an object!" - end - p = find_free_position_near(p) - teleportee:set_pos(p) - return true, "Teleporting to " .. target_name - .. " at "..core.pos_to_string(p) - end + local has_bring_priv = core.check_player_privs(name, {bring=true}) + local missing_bring_msg = S("You don't have permission to teleport " .. + "other players (missing privilege: @1).", "bring") - if not core.check_player_privs(name, {bring=true}) then - return false, "You don't have permission to teleport other players (missing bring privilege)" - end - - teleportee = nil - p = {} local teleportee_name teleportee_name, p.x, p.y, p.z = param:match( "^([^ ]+) +([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$") - p.x, p.y, p.z = tonumber(p.x), tonumber(p.y), tonumber(p.z) - if teleportee_name then - teleportee = core.get_player_by_name(teleportee_name) - end - if teleportee and p.x and p.y and p.z then - if teleportee:get_attach() then - return false, "Can't teleport, player is attached to an object!" + p = vector.apply(p, tonumber) + if teleportee_name and p.x and p.y and p.z then + if not has_bring_priv then + return false, missing_bring_msg end - teleportee:set_pos(p) - return true, "Teleporting " .. teleportee_name - .. " to " .. core.pos_to_string(p) + return teleport_to_pos(teleportee_name, p) end - teleportee = nil - p = nil teleportee_name, target_name = string.match(param, "^([^ ]+) +([^ ]+)$") - if teleportee_name then - teleportee = core.get_player_by_name(teleportee_name) - end - if target_name then - local target = core.get_player_by_name(target_name) - if target then - p = target:get_pos() - end - end - if teleportee and p then - if teleportee:get_attach() then - return false, "Can't teleport, player is attached to an object!" + if teleportee_name and target_name then + if not has_bring_priv then + return false, missing_bring_msg end - p = find_free_position_near(p) - teleportee:set_pos(p) - return true, "Teleporting " .. teleportee_name - .. " to " .. target_name - .. " at " .. core.pos_to_string(p) + return teleport_to_player(teleportee_name, target_name) end - return false, 'Invalid parameters ("' .. param - .. '") or player not found (see /help teleport)' + return false end, }) core.register_chatcommand("set", { - params = "([-n] <name> <value>) | <name>", - description = "Set or read server configuration setting", + params = S("([-n] <name> <value>) | <name>"), + description = S("Set or read server configuration setting"), privs = {server=true}, func = function(name, param) local arg, setname, setvalue = string.match(param, "(-[n]) ([^ ]+) (.+)") @@ -545,22 +562,23 @@ core.register_chatcommand("set", { setname, setvalue = string.match(param, "([^ ]+) (.+)") if setname and setvalue then if not core.settings:get(setname) then - return false, "Failed. Use '/set -n <name> <value>' to create a new setting." + return false, S("Failed. Use '/set -n <name> <value>' " + .. "to create a new setting.") end core.settings:set(setname, setvalue) - return true, setname .. " = " .. setvalue + return true, S("@1 = @2", setname, setvalue) end setname = string.match(param, "([^ ]+)") if setname then setvalue = core.settings:get(setname) if not setvalue then - setvalue = "<not set>" + setvalue = S("<not set>") end - return true, setname .. " = " .. setvalue + return true, S("@1 = @2", setname, setvalue) end - return false, "Invalid parameters (see /help set)." + return false, S("Invalid parameters (see /help set).") end, }) @@ -573,26 +591,27 @@ local function emergeblocks_callback(pos, action, num_calls_remaining, ctx) if ctx.current_blocks == ctx.total_blocks then core.chat_send_player(ctx.requestor_name, - string.format("Finished emerging %d blocks in %.2fms.", - ctx.total_blocks, (os.clock() - ctx.start_time) * 1000)) + S("Finished emerging @1 blocks in @2ms.", + ctx.total_blocks, + string.format("%.2f", (os.clock() - ctx.start_time) * 1000))) end end local function emergeblocks_progress_update(ctx) if ctx.current_blocks ~= ctx.total_blocks then core.chat_send_player(ctx.requestor_name, - string.format("emergeblocks update: %d/%d blocks emerged (%.1f%%)", + S("emergeblocks update: @1/@2 blocks emerged (@3%)", ctx.current_blocks, ctx.total_blocks, - (ctx.current_blocks / ctx.total_blocks) * 100)) + string.format("%.1f", (ctx.current_blocks / ctx.total_blocks) * 100))) core.after(2, emergeblocks_progress_update, ctx) end end core.register_chatcommand("emergeblocks", { - params = "(here [<radius>]) | (<pos1> <pos2>)", - description = "Load (or, if nonexistent, generate) map blocks " - .. "contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)", + params = S("(here [<radius>]) | (<pos1> <pos2>)"), + description = S("Load (or, if nonexistent, generate) map blocks contained in " + .. "area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)"), privs = {server=true}, func = function(name, param) local p1, p2 = parse_range_str(name, param) @@ -610,15 +629,15 @@ core.register_chatcommand("emergeblocks", { core.emerge_area(p1, p2, emergeblocks_callback, context) core.after(2, emergeblocks_progress_update, context) - return true, "Started emerge of area ranging from " .. - core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1) + return true, S("Started emerge of area ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) end, }) core.register_chatcommand("deleteblocks", { - params = "(here [<radius>]) | (<pos1> <pos2>)", - description = "Delete map blocks contained in area pos1 to pos2 " - .. "(<pos1> and <pos2> must be in parentheses)", + params = S("(here [<radius>]) | (<pos1> <pos2>)"), + description = S("Delete map blocks contained in area pos1 to pos2 " + .. "(<pos1> and <pos2> must be in parentheses)"), privs = {server=true}, func = function(name, param) local p1, p2 = parse_range_str(name, param) @@ -627,18 +646,20 @@ core.register_chatcommand("deleteblocks", { end if core.delete_area(p1, p2) then - return true, "Successfully cleared area ranging from " .. - core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1) + return true, S("Successfully cleared area " + .. "ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) else - return false, "Failed to clear one or more blocks in area" + return false, S("Failed to clear one or more " + .. "blocks in area.") end end, }) core.register_chatcommand("fixlight", { - params = "(here [<radius>]) | (<pos1> <pos2>)", - description = "Resets lighting in the area between pos1 and pos2 " - .. "(<pos1> and <pos2> must be in parentheses)", + params = S("(here [<radius>]) | (<pos1> <pos2>)"), + description = S("Resets lighting in the area between pos1 and pos2 " + .. "(<pos1> and <pos2> must be in parentheses)"), privs = {server = true}, func = function(name, param) local p1, p2 = parse_range_str(name, param) @@ -647,17 +668,18 @@ core.register_chatcommand("fixlight", { end if core.fix_light(p1, p2) then - return true, "Successfully reset light in the area ranging from " .. - core.pos_to_string(p1, 1) .. " to " .. core.pos_to_string(p2, 1) + return true, S("Successfully reset light in the area " + .. "ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) else - return false, "Failed to load one or more blocks in area" + return false, S("Failed to load one or more blocks in area.") end end, }) core.register_chatcommand("mods", { params = "", - description = "List mods installed on the server", + description = S("List mods installed on the server"), privs = {}, func = function(name, param) return true, table.concat(core.get_modnames(), ", ") @@ -669,117 +691,136 @@ local function handle_give_command(cmd, giver, receiver, stackstring) .. ', stackstring="' .. stackstring .. '"') local itemstack = ItemStack(stackstring) if itemstack:is_empty() then - return false, "Cannot give an empty item" + return false, S("Cannot give an empty item.") elseif (not itemstack:is_known()) or (itemstack:get_name() == "unknown") then - return false, "Cannot give an unknown item" + return false, S("Cannot give an unknown item.") -- Forbid giving 'ignore' due to unwanted side effects elseif itemstack:get_name() == "ignore" then - return false, "Giving 'ignore' is not allowed" + return false, S("Giving 'ignore' is not allowed.") end local receiverref = core.get_player_by_name(receiver) if receiverref == nil then - return false, receiver .. " is not a known player" + return false, S("@1 is not a known player.", receiver) end local leftover = receiverref:get_inventory():add_item("main", itemstack) local partiality if leftover:is_empty() then - partiality = "" + partiality = nil elseif leftover:get_count() == itemstack:get_count() then - partiality = "could not be " + partiality = false else - partiality = "partially " + partiality = true end -- The actual item stack string may be different from what the "giver" -- entered (e.g. big numbers are always interpreted as 2^16-1). stackstring = itemstack:to_string() + local msg + if partiality == true then + msg = S("@1 partially added to inventory.", stackstring) + elseif partiality == false then + msg = S("@1 could not be added to inventory.", stackstring) + else + msg = S("@1 added to inventory.", stackstring) + end if giver == receiver then - local msg = "%q %sadded to inventory." - return true, msg:format(stackstring, partiality) + return true, msg else - core.chat_send_player(receiver, ("%q %sadded to inventory.") - :format(stackstring, partiality)) - local msg = "%q %sadded to %s's inventory." - return true, msg:format(stackstring, partiality, receiver) + core.chat_send_player(receiver, msg) + local msg_other + if partiality == true then + msg_other = S("@1 partially added to inventory of @2.", + stackstring, receiver) + elseif partiality == false then + msg_other = S("@1 could not be added to inventory of @2.", + stackstring, receiver) + else + msg_other = S("@1 added to inventory of @2.", + stackstring, receiver) + end + return true, msg_other end end core.register_chatcommand("give", { - params = "<name> <ItemString> [<count> [<wear>]]", - description = "Give item to player", + params = S("<name> <ItemString> [<count> [<wear>]]"), + description = S("Give item to player"), privs = {give=true}, func = function(name, param) local toname, itemstring = string.match(param, "^([^ ]+) +(.+)$") if not toname or not itemstring then - return false, "Name and ItemString required" + return false, S("Name and ItemString required.") end return handle_give_command("/give", name, toname, itemstring) end, }) core.register_chatcommand("giveme", { - params = "<ItemString> [<count> [<wear>]]", - description = "Give item to yourself", + params = S("<ItemString> [<count> [<wear>]]"), + description = S("Give item to yourself"), privs = {give=true}, func = function(name, param) local itemstring = string.match(param, "(.+)$") if not itemstring then - return false, "ItemString required" + return false, S("ItemString required.") end return handle_give_command("/giveme", name, name, itemstring) end, }) core.register_chatcommand("spawnentity", { - params = "<EntityName> [<X>,<Y>,<Z>]", - description = "Spawn entity at given (or your) position", + params = S("<EntityName> [<X>,<Y>,<Z>]"), + description = S("Spawn entity at given (or your) position"), privs = {give=true, interact=true}, func = function(name, param) local entityname, p = string.match(param, "^([^ ]+) *(.*)$") if not entityname then - return false, "EntityName required" + return false, S("EntityName required.") end core.log("action", ("%s invokes /spawnentity, entityname=%q") :format(name, entityname)) local player = core.get_player_by_name(name) if player == nil then core.log("error", "Unable to spawn entity, player is nil") - return false, "Unable to spawn entity, player is nil" + return false, S("Unable to spawn entity, player is nil.") end if not core.registered_entities[entityname] then - return false, "Cannot spawn an unknown entity" + return false, S("Cannot spawn an unknown entity.") end if p == "" then p = player:get_pos() else p = core.string_to_pos(p) if p == nil then - return false, "Invalid parameters ('" .. param .. "')" + return false, S("Invalid parameters (@1).", param) end end p.y = p.y + 1 local obj = core.add_entity(p, entityname) - local msg = obj and "%q spawned." or "%q failed to spawn." - return true, msg:format(entityname) + if obj then + return true, S("@1 spawned.", entityname) + else + return true, S("@1 failed to spawn.", entityname) + end end, }) core.register_chatcommand("pulverize", { params = "", - description = "Destroy item in hand", + description = S("Destroy item in hand"), func = function(name, param) local player = core.get_player_by_name(name) if not player then core.log("error", "Unable to pulverize, no player.") - return false, "Unable to pulverize, no player." + return false, S("Unable to pulverize, no player.") end local wielded_item = player:get_wielded_item() if wielded_item:is_empty() then - return false, "Unable to pulverize, no item in hand." + return false, S("Unable to pulverize, no item in hand.") end core.log("action", name .. " pulverized \"" .. wielded_item:get_name() .. " " .. wielded_item:get_count() .. "\"") player:set_wielded_item(nil) - return true, "An item was pulverized." + return true, S("An item was pulverized.") end, }) @@ -795,14 +836,15 @@ core.register_on_punchnode(function(pos, node, puncher) end) core.register_chatcommand("rollback_check", { - params = "[<range>] [<seconds>] [<limit>]", - description = "Check who last touched a node or a node near it" - .. " within the time specified by <seconds>. Default: range = 0," - .. " seconds = 86400 = 24h, limit = 5. Set <seconds> to inf for no time limit", + params = S("[<range>] [<seconds>] [<limit>]"), + description = S("Check who last touched a node or a node near it " + .. "within the time specified by <seconds>. " + .. "Default: range = 0, seconds = 86400 = 24h, limit = 5. " + .. "Set <seconds> to inf for no time limit"), privs = {rollback=true}, func = function(name, param) if not core.settings:get_bool("enable_rollback_recording") then - return false, "Rollback functions are disabled." + return false, S("Rollback functions are disabled.") end local range, seconds, limit = param:match("(%d+) *(%d*) *(%d*)") @@ -810,30 +852,30 @@ core.register_chatcommand("rollback_check", { seconds = tonumber(seconds) or 86400 limit = tonumber(limit) or 5 if limit > 100 then - return false, "That limit is too high!" + return false, S("That limit is too high!") end core.rollback_punch_callbacks[name] = function(pos, node, puncher) local name = puncher:get_player_name() - core.chat_send_player(name, "Checking " .. core.pos_to_string(pos) .. "...") + core.chat_send_player(name, S("Checking @1 ...", core.pos_to_string(pos))) local actions = core.rollback_get_node_actions(pos, range, seconds, limit) if not actions then - core.chat_send_player(name, "Rollback functions are disabled") + core.chat_send_player(name, S("Rollback functions are disabled.")) return end local num_actions = #actions if num_actions == 0 then - core.chat_send_player(name, "Nobody has touched" - .. " the specified location in " - .. seconds .. " seconds") + core.chat_send_player(name, + S("Nobody has touched the specified " + .. "location in @1 seconds.", + seconds)) return end local time = os.time() for i = num_actions, 1, -1 do local action = actions[i] core.chat_send_player(name, - ("%s %s %s -> %s %d seconds ago.") - :format( + S("@1 @2 @3 -> @4 @5 seconds ago.", core.pos_to_string(action.pos), action.actor, action.oldnode.name, @@ -842,110 +884,123 @@ core.register_chatcommand("rollback_check", { end end - return true, "Punch a node (range=" .. range .. ", seconds=" - .. seconds .. "s, limit=" .. limit .. ")" + return true, S("Punch a node (range=@1, seconds=@2, limit=@3).", + range, seconds, limit) end, }) core.register_chatcommand("rollback", { - params = "(<name> [<seconds>]) | (:<actor> [<seconds>])", - description = "Revert actions of a player. Default for <seconds> is 60. Set <seconds> to inf for no time limit", + params = S("(<name> [<seconds>]) | (:<actor> [<seconds>])"), + description = S("Revert actions of a player. " + .. "Default for <seconds> is 60. " + .. "Set <seconds> to inf for no time limit"), privs = {rollback=true}, func = function(name, param) if not core.settings:get_bool("enable_rollback_recording") then - return false, "Rollback functions are disabled." + return false, S("Rollback functions are disabled.") end local target_name, seconds = string.match(param, ":([^ ]+) *(%d*)") + local rev_msg if not target_name then local player_name player_name, seconds = string.match(param, "([^ ]+) *(%d*)") if not player_name then - return false, "Invalid parameters. See /help rollback" - .. " and /help rollback_check." + return false, S("Invalid parameters. " + .. "See /help rollback and " + .. "/help rollback_check.") end + seconds = tonumber(seconds) or 60 target_name = "player:"..player_name + rev_msg = S("Reverting actions of player '@1' since @2 seconds.", + player_name, seconds) + else + seconds = tonumber(seconds) or 60 + rev_msg = S("Reverting actions of @1 since @2 seconds.", + target_name, seconds) end - seconds = tonumber(seconds) or 60 - core.chat_send_player(name, "Reverting actions of " - .. target_name .. " since " - .. seconds .. " seconds.") + core.chat_send_player(name, rev_msg) local success, log = core.rollback_revert_actions_by( target_name, seconds) local response = "" if #log > 100 then - response = "(log is too long to show)\n" + response = S("(log is too long to show)").."\n" else for _, line in pairs(log) do response = response .. line .. "\n" end end - response = response .. "Reverting actions " - .. (success and "succeeded." or "FAILED.") + if success then + response = response .. S("Reverting actions succeeded.") + else + response = response .. S("Reverting actions FAILED.") + end return success, response end, }) core.register_chatcommand("status", { - description = "Show server status", + description = S("Show server status"), func = function(name, param) local status = core.get_server_status(name, false) if status and status ~= "" then return true, status end - return false, "This command was disabled by a mod or game" + return false, S("This command was disabled by a mod or game.") end, }) core.register_chatcommand("time", { - params = "[<0..23>:<0..59> | <0..24000>]", - description = "Show or set time of day", + params = S("[<0..23>:<0..59> | <0..24000>]"), + description = S("Show or set time of day"), privs = {}, func = function(name, param) if param == "" then local current_time = math.floor(core.get_timeofday() * 1440) local minutes = current_time % 60 local hour = (current_time - minutes) / 60 - return true, ("Current time is %d:%02d"):format(hour, minutes) + return true, S("Current time is @1:@2.", + string.format("%d", hour), + string.format("%02d", minutes)) end local player_privs = core.get_player_privs(name) if not player_privs.settime then - return false, "You don't have permission to run this command " .. - "(missing privilege: settime)." + return false, S("You don't have permission to run " + .. "this command (missing privilege: @1).", "settime") end local hour, minute = param:match("^(%d+):(%d+)$") if not hour then local new_time = tonumber(param) if not new_time then - return false, "Invalid time." + return false, S("Invalid time.") end -- Backward compatibility. core.set_timeofday((new_time % 24000) / 24000) core.log("action", name .. " sets time to " .. new_time) - return true, "Time of day changed." + return true, S("Time of day changed.") end hour = tonumber(hour) minute = tonumber(minute) if hour < 0 or hour > 23 then - return false, "Invalid hour (must be between 0 and 23 inclusive)." + return false, S("Invalid hour (must be between 0 and 23 inclusive).") elseif minute < 0 or minute > 59 then - return false, "Invalid minute (must be between 0 and 59 inclusive)." + return false, S("Invalid minute (must be between 0 and 59 inclusive).") end core.set_timeofday((hour * 60 + minute) / 1440) core.log("action", ("%s sets time to %d:%02d"):format(name, hour, minute)) - return true, "Time of day changed." + return true, S("Time of day changed.") end, }) core.register_chatcommand("days", { - description = "Show day count since world creation", + description = S("Show day count since world creation"), func = function(name, param) - return true, "Current day is " .. core.get_day_count() + return true, S("Current day is @1.", core.get_day_count()) end }) core.register_chatcommand("shutdown", { - params = "[<delay_in_seconds> | -1] [reconnect] [<message>]", - description = "Shutdown server (-1 cancels a delayed shutdown)", + params = S("[<delay_in_seconds> | -1] [reconnect] [<message>]"), + description = S("Shutdown server (-1 cancels a delayed shutdown)"), privs = {server=true}, func = function(name, param) local delay, reconnect, message @@ -958,7 +1013,7 @@ core.register_chatcommand("shutdown", { if delay == 0 then core.log("action", name .. " shuts down server") - core.chat_send_all("*** Server shutting down (operator request).") + core.chat_send_all("*** "..S("Server shutting down (operator request).")) end core.request_shutdown(message:trim(), core.is_yes(reconnect), delay) return true @@ -966,65 +1021,65 @@ core.register_chatcommand("shutdown", { }) core.register_chatcommand("ban", { - params = "[<name>]", - description = "Ban the IP of a player or show the ban list", + params = S("[<name>]"), + description = S("Ban the IP of a player or show the ban list"), privs = {ban=true}, func = function(name, param) if param == "" then local ban_list = core.get_ban_list() if ban_list == "" then - return true, "The ban list is empty." + return true, S("The ban list is empty.") else - return true, "Ban list: " .. ban_list + return true, S("Ban list: @1", ban_list) end end if not core.get_player_by_name(param) then - return false, "Player is not online." + return false, S("Player is not online.") end if not core.ban_player(param) then - return false, "Failed to ban player." + return false, S("Failed to ban player.") end local desc = core.get_ban_description(param) core.log("action", name .. " bans " .. desc .. ".") - return true, "Banned " .. desc .. "." + return true, S("Banned @1.", desc) end, }) core.register_chatcommand("unban", { - params = "<name> | <IP_address>", - description = "Remove IP ban belonging to a player/IP", + params = S("<name> | <IP_address>"), + description = S("Remove IP ban belonging to a player/IP"), privs = {ban=true}, func = function(name, param) if not core.unban_player_or_ip(param) then - return false, "Failed to unban player/IP." + return false, S("Failed to unban player/IP.") end core.log("action", name .. " unbans " .. param) - return true, "Unbanned " .. param + return true, S("Unbanned @1.", param) end, }) core.register_chatcommand("kick", { - params = "<name> [<reason>]", - description = "Kick a player", + params = S("<name> [<reason>]"), + description = S("Kick a player"), privs = {kick=true}, func = function(name, param) local tokick, reason = param:match("([^ ]+) (.+)") tokick = tokick or param if not core.kick_player(tokick, reason) then - return false, "Failed to kick player " .. tokick + return false, S("Failed to kick player @1.", tokick) end local log_reason = "" if reason then log_reason = " with reason \"" .. reason .. "\"" end core.log("action", name .. " kicks " .. tokick .. log_reason) - return true, "Kicked " .. tokick + return true, S("Kicked @1.", tokick) end, }) core.register_chatcommand("clearobjects", { - params = "[full | quick]", - description = "Clear all objects in world", + params = S("[full | quick]"), + description = S("Clear all objects in world"), privs = {server=true}, func = function(name, param) local options = {} @@ -1033,45 +1088,44 @@ core.register_chatcommand("clearobjects", { elseif param == "full" then options.mode = "full" else - return false, "Invalid usage, see /help clearobjects." + return false, S("Invalid usage, see /help clearobjects.") end - core.log("action", name .. " clears all objects (" + core.log("action", name .. " clears objects (" .. options.mode .. " mode).") - core.chat_send_all("Clearing all objects. This may take a long time." - .. " You may experience a timeout. (by " - .. name .. ")") + if options.mode == "full" then + core.chat_send_all(S("Clearing all objects. This may take a long time. " + .. "You may experience a timeout. (by @1)", name)) + end core.clear_objects(options) core.log("action", "Object clearing done.") - core.chat_send_all("*** Cleared all objects.") + core.chat_send_all("*** "..S("Cleared all objects.")) return true end, }) core.register_chatcommand("msg", { - params = "<name> <message>", - description = "Send a direct message to a player", + params = S("<name> <message>"), + description = S("Send a direct message to a player"), privs = {shout=true}, func = function(name, param) local sendto, message = param:match("^(%S+)%s(.+)$") if not sendto then - return false, "Invalid usage, see /help msg." + return false, S("Invalid usage, see /help msg.") end if not core.get_player_by_name(sendto) then - return false, "The player " .. sendto - .. " is not online." + return false, S("The player @1 is not online.", sendto) end core.log("action", "DM from " .. name .. " to " .. sendto .. ": " .. message) - core.chat_send_player(sendto, "DM from " .. name .. ": " - .. message) - return true, "Message sent." + core.chat_send_player(sendto, S("DM from @1: @2", name, message)) + return true, S("Message sent.") end, }) core.register_chatcommand("last-login", { - params = "[<name>]", - description = "Get the last login time of a player or yourself", + params = S("[<name>]"), + description = S("Get the last login time of a player or yourself"), func = function(name, param) if param == "" then param = name @@ -1079,25 +1133,27 @@ core.register_chatcommand("last-login", { local pauth = core.get_auth_handler().get_auth(param) if pauth and pauth.last_login and pauth.last_login ~= -1 then -- Time in UTC, ISO 8601 format - return true, param.."'s last login time was " .. - os.date("!%Y-%m-%dT%H:%M:%SZ", pauth.last_login) + return true, S("@1's last login time was @2.", + param, + os.date("!%Y-%m-%dT%H:%M:%SZ", pauth.last_login)) end - return false, param.."'s last login time is unknown" + return false, S("@1's last login time is unknown.", param) end, }) core.register_chatcommand("clearinv", { - params = "[<name>]", - description = "Clear the inventory of yourself or another player", + params = S("[<name>]"), + description = S("Clear the inventory of yourself or another player"), func = function(name, param) local player if param and param ~= "" and param ~= name then if not core.check_player_privs(name, {server=true}) then - return false, "You don't have permission" - .. " to clear another player's inventory (missing privilege: server)" + return false, S("You don't have permission to " + .. "clear another player's inventory " + .. "(missing privilege: @1).", "server") end player = core.get_player_by_name(param) - core.chat_send_player(param, name.." cleared your inventory.") + core.chat_send_player(param, S("@1 cleared your inventory.", name)) else player = core.get_player_by_name(name) end @@ -1107,25 +1163,25 @@ core.register_chatcommand("clearinv", { player:get_inventory():set_list("craft", {}) player:get_inventory():set_list("craftpreview", {}) core.log("action", name.." clears "..player:get_player_name().."'s inventory") - return true, "Cleared "..player:get_player_name().."'s inventory." + return true, S("Cleared @1's inventory.", player:get_player_name()) else - return false, "Player must be online to clear inventory!" + return false, S("Player must be online to clear inventory!") end end, }) local function handle_kill_command(killer, victim) if core.settings:get_bool("enable_damage") == false then - return false, "Players can't be killed, damage has been disabled." + return false, S("Players can't be killed, damage has been disabled.") end local victimref = core.get_player_by_name(victim) if victimref == nil then - return false, string.format("Player %s is not online.", victim) + return false, S("Player @1 is not online.", victim) elseif victimref:get_hp() <= 0 then if killer == victim then - return false, "You are already dead." + return false, S("You are already dead.") else - return false, string.format("%s is already dead.", victim) + return false, S("@1 is already dead.", victim) end end if not killer == victim then @@ -1133,12 +1189,12 @@ local function handle_kill_command(killer, victim) end -- Kill victim victimref:set_hp(0) - return true, string.format("%s has been killed.", victim) + return true, S("@1 has been killed.", victim) end core.register_chatcommand("kill", { - params = "[<name>]", - description = "Kill player or yourself", + params = S("[<name>]"), + description = S("Kill player or yourself"), privs = {server=true}, func = function(name, param) return handle_kill_command(name, param == "" and name or param) diff --git a/builtin/game/deprecated.lua b/builtin/game/deprecated.lua index 20f0482eb..c5c7848f5 100644 --- a/builtin/game/deprecated.lua +++ b/builtin/game/deprecated.lua @@ -1,29 +1,6 @@ -- Minetest: builtin/deprecated.lua -- --- Default material types --- -local function digprop_err() - core.log("deprecated", "The core.digprop_* functions are obsolete and need to be replaced by item groups.") -end - -core.digprop_constanttime = digprop_err -core.digprop_stonelike = digprop_err -core.digprop_dirtlike = digprop_err -core.digprop_gravellike = digprop_err -core.digprop_woodlike = digprop_err -core.digprop_leaveslike = digprop_err -core.digprop_glasslike = digprop_err - -function core.node_metadata_inventory_move_allow_all() - core.log("deprecated", "core.node_metadata_inventory_move_allow_all is obsolete and does nothing.") -end - -function core.add_to_creative_inventory(itemstring) - core.log("deprecated", "core.add_to_creative_inventory is obsolete and does nothing.") -end - --- -- EnvRef -- core.env = {} @@ -77,7 +54,7 @@ core.setting_save = setting_proxy("write") function core.register_on_auth_fail(func) core.log("deprecated", "core.register_on_auth_fail " .. - "is obsolete and should be replaced by " .. + "is deprecated and should be replaced by " .. "core.register_on_authplayer instead.") core.register_on_authplayer(function (player_name, ip, is_success) diff --git a/builtin/game/falling.lua b/builtin/game/falling.lua index 4bfcca9e7..057d0d0ed 100644 --- a/builtin/game/falling.lua +++ b/builtin/game/falling.lua @@ -84,6 +84,9 @@ core.register_entity(":__builtin:falling_node", { local textures if def.tiles and def.tiles[1] then local tile = def.tiles[1] + if def.drawtype == "torchlike" and def.paramtype2 ~= "wallmounted" then + tile = def.tiles[2] or def.tiles[1] + end if type(tile) == "table" then tile = tile.name end @@ -127,7 +130,7 @@ core.register_entity(":__builtin:falling_node", { -- Set collision box (certain nodeboxes only for now) local nb_types = {fixed=true, leveled=true, connected=true} if def.drawtype == "nodebox" and def.node_box and - nb_types[def.node_box.type] then + nb_types[def.node_box.type] and def.node_box.fixed then local box = table.copy(def.node_box.fixed) if type(box[1]) == "table" then box = #box == 1 and box[1] or nil -- We can only use a single box @@ -144,9 +147,13 @@ core.register_entity(":__builtin:falling_node", { -- Rotate entity if def.drawtype == "torchlike" then - self.object:set_yaw(math.pi*0.25) - elseif (node.param2 ~= 0 and (def.wield_image == "" - or def.wield_image == nil)) + if def.paramtype2 == "wallmounted" then + self.object:set_yaw(math.pi*0.25) + else + self.object:set_yaw(-math.pi*0.25) + end + elseif ((node.param2 ~= 0 or def.drawtype == "nodebox" or def.drawtype == "mesh") + and (def.wield_image == "" or def.wield_image == nil)) or def.drawtype == "signlike" or def.drawtype == "mesh" or def.drawtype == "normal" @@ -161,16 +168,30 @@ core.register_entity(":__builtin:falling_node", { elseif (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted") then local rot = node.param2 % 8 local pitch, yaw, roll = 0, 0, 0 - if rot == 1 then - pitch, yaw = math.pi, math.pi - elseif rot == 2 then - pitch, yaw = math.pi/2, math.pi/2 - elseif rot == 3 then - pitch, yaw = math.pi/2, -math.pi/2 - elseif rot == 4 then - pitch, yaw = math.pi/2, math.pi - elseif rot == 5 then - pitch, yaw = math.pi/2, 0 + if def.drawtype == "nodebox" or def.drawtype == "mesh" then + if rot == 0 then + pitch, yaw = math.pi/2, 0 + elseif rot == 1 then + pitch, yaw = -math.pi/2, math.pi + elseif rot == 2 then + pitch, yaw = 0, math.pi/2 + elseif rot == 3 then + pitch, yaw = 0, -math.pi/2 + elseif rot == 4 then + pitch, yaw = 0, math.pi + end + else + if rot == 1 then + pitch, yaw = math.pi, math.pi + elseif rot == 2 then + pitch, yaw = math.pi/2, math.pi/2 + elseif rot == 3 then + pitch, yaw = math.pi/2, -math.pi/2 + elseif rot == 4 then + pitch, yaw = math.pi/2, math.pi + elseif rot == 5 then + pitch, yaw = math.pi/2, 0 + end end if def.drawtype == "signlike" then pitch = pitch - math.pi/2 @@ -179,7 +200,7 @@ core.register_entity(":__builtin:falling_node", { elseif rot == 1 then yaw = yaw - math.pi/2 end - elseif def.drawtype == "mesh" or def.drawtype == "normal" then + elseif def.drawtype == "mesh" or def.drawtype == "normal" or def.drawtype == "nodebox" then if rot >= 0 and rot <= 1 then roll = roll + math.pi else diff --git a/builtin/game/features.lua b/builtin/game/features.lua index a15475333..36ff1f0b0 100644 --- a/builtin/game/features.lua +++ b/builtin/game/features.lua @@ -17,6 +17,8 @@ core.features = { area_store_persistent_ids = true, pathfinder_works = true, object_step_has_moveresult = true, + direct_velocity_on_players = true, + use_texture_alpha_string_modes = true, } function core.has_feature(arg) diff --git a/builtin/game/item.lua b/builtin/game/item.lua index f680ce0d4..b68177c22 100644 --- a/builtin/game/item.lua +++ b/builtin/game/item.lua @@ -551,12 +551,13 @@ function core.node_dig(pos, node, digger) local diggername = user_name(digger) local log = make_log(diggername) local def = core.registered_nodes[node.name] + -- Copy pos because the callback could modify it if def and (not def.diggable or - (def.can_dig and not def.can_dig(pos, digger))) then + (def.can_dig and not def.can_dig(vector.new(pos), digger))) then log("info", diggername .. " tried to dig " .. node.name .. " which is not diggable " .. core.pos_to_string(pos)) - return + return false end if core.is_protected(pos, diggername) then @@ -565,7 +566,7 @@ function core.node_dig(pos, node, digger) .. " at protected position " .. core.pos_to_string(pos)) core.record_protection_violation(pos, diggername) - return + return false end log('action', diggername .. " digs " @@ -648,6 +649,8 @@ function core.node_dig(pos, node, digger) local node_copy = {name=node.name, param1=node.param1, param2=node.param2} callback(pos_copy, node_copy, digger) end + + return true end function core.itemstring_with_palette(item, palette_index) @@ -675,7 +678,7 @@ end -- Item definition defaults -- -local default_stack_max = tonumber(minetest.settings:get("default_stack_max")) or 99 +local default_stack_max = tonumber(core.settings:get("default_stack_max")) or 99 core.nodedef_default = { -- Item properties @@ -704,10 +707,6 @@ core.nodedef_default = { on_receive_fields = nil, - on_metadata_inventory_move = core.node_metadata_inventory_move_allow_all, - on_metadata_inventory_offer = core.node_metadata_inventory_offer_allow_all, - on_metadata_inventory_take = core.node_metadata_inventory_take_allow_all, - -- Node properties drawtype = "normal", visual_scale = 1.0, @@ -718,7 +717,6 @@ core.nodedef_default = { -- {name="", backface_culling=true}, -- {name="", backface_culling=true}, --}, - alpha = 255, post_effect_color = {a=0, r=0, g=0, b=0}, paramtype = "none", paramtype2 = "none", diff --git a/builtin/game/knockback.lua b/builtin/game/knockback.lua index b5c4cbc5a..a937aa186 100644 --- a/builtin/game/knockback.lua +++ b/builtin/game/knockback.lua @@ -42,5 +42,5 @@ core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool return -- barely noticeable, so don't even send end - player:add_player_velocity(kdir) + player:add_velocity(kdir) end) diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index 341e613c2..fcb86146d 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -1,5 +1,7 @@ -- Minetest: builtin/misc.lua +local S = core.get_translator("__builtin") + -- -- Misc. API functions -- @@ -42,15 +44,15 @@ end function core.send_join_message(player_name) if not core.is_singleplayer() then - core.chat_send_all("*** " .. player_name .. " joined the game.") + core.chat_send_all("*** " .. S("@1 joined the game.", player_name)) end end function core.send_leave_message(player_name, timed_out) - local announcement = "*** " .. player_name .. " left the game." + local announcement = "*** " .. S("@1 left the game.", player_name) if timed_out then - announcement = announcement .. " (timed out)" + announcement = "*** " .. S("@1 left the game (timed out).", player_name) end core.chat_send_all(announcement) end @@ -151,6 +153,12 @@ function core.setting_get_pos(name) end +-- See l_env.cpp for the other functions +function core.get_artificial_light(param1) + return math.floor(param1 / 16) +end + + -- To be overriden by protection mods function core.is_protected(pos, name) @@ -260,3 +268,26 @@ end function core.cancel_shutdown_requests() core.request_shutdown("", false, -1) end + + +-- Callback handling for dynamic_add_media + +local dynamic_add_media_raw = core.dynamic_add_media_raw +core.dynamic_add_media_raw = nil +function core.dynamic_add_media(filepath, callback) + local ret = dynamic_add_media_raw(filepath) + if ret == false then + return ret + end + if callback == nil then + core.log("deprecated", "Calling minetest.dynamic_add_media without ".. + "a callback is deprecated and will stop working in future versions.") + else + -- At the moment async loading is not actually implemented, so we + -- immediately call the callback ourselves + for _, name in ipairs(ret) do + callback(name) + end + end + return true +end diff --git a/builtin/game/privileges.lua b/builtin/game/privileges.lua index c7417d2f4..aee32a34e 100644 --- a/builtin/game/privileges.lua +++ b/builtin/game/privileges.lua @@ -1,5 +1,7 @@ -- Minetest: builtin/privileges.lua +local S = core.get_translator("__builtin") + -- -- Privileges -- @@ -15,7 +17,7 @@ function core.register_privilege(name, param) def.give_to_admin = def.give_to_singleplayer end if def.description == nil then - def.description = "(no description)" + def.description = S("(no description)") end end local def @@ -28,69 +30,69 @@ function core.register_privilege(name, param) core.registered_privileges[name] = def end -core.register_privilege("interact", "Can interact with things and modify the world") -core.register_privilege("shout", "Can speak in chat") -core.register_privilege("basic_privs", "Can modify 'shout' and 'interact' privileges") -core.register_privilege("privs", "Can modify privileges") +core.register_privilege("interact", S("Can interact with things and modify the world")) +core.register_privilege("shout", S("Can speak in chat")) +core.register_privilege("basic_privs", S("Can modify 'shout' and 'interact' privileges")) +core.register_privilege("privs", S("Can modify privileges")) core.register_privilege("teleport", { - description = "Can teleport self", + description = S("Can teleport self"), give_to_singleplayer = false, }) core.register_privilege("bring", { - description = "Can teleport other players", + description = S("Can teleport other players"), give_to_singleplayer = false, }) core.register_privilege("settime", { - description = "Can set the time of day using /time", + description = S("Can set the time of day using /time"), give_to_singleplayer = false, }) core.register_privilege("server", { - description = "Can do server maintenance stuff", + description = S("Can do server maintenance stuff"), give_to_singleplayer = false, give_to_admin = true, }) core.register_privilege("protection_bypass", { - description = "Can bypass node protection in the world", + description = S("Can bypass node protection in the world"), give_to_singleplayer = false, }) core.register_privilege("ban", { - description = "Can ban and unban players", + description = S("Can ban and unban players"), give_to_singleplayer = false, give_to_admin = true, }) core.register_privilege("kick", { - description = "Can kick players", + description = S("Can kick players"), give_to_singleplayer = false, give_to_admin = true, }) core.register_privilege("give", { - description = "Can use /give and /giveme", + description = S("Can use /give and /giveme"), give_to_singleplayer = false, }) core.register_privilege("password", { - description = "Can use /setpassword and /clearpassword", + description = S("Can use /setpassword and /clearpassword"), give_to_singleplayer = false, give_to_admin = true, }) core.register_privilege("fly", { - description = "Can use fly mode", + description = S("Can use fly mode"), give_to_singleplayer = false, }) core.register_privilege("fast", { - description = "Can use fast mode", + description = S("Can use fast mode"), give_to_singleplayer = false, }) core.register_privilege("noclip", { - description = "Can fly through solid nodes using noclip mode", + description = S("Can fly through solid nodes using noclip mode"), give_to_singleplayer = false, }) core.register_privilege("rollback", { - description = "Can use the rollback functionality", + description = S("Can use the rollback functionality"), give_to_singleplayer = false, }) core.register_privilege("debug", { - description = "Allows enabling various debug options that may affect gameplay", + description = S("Allows enabling various debug options that may affect gameplay"), give_to_singleplayer = false, give_to_admin = true, }) diff --git a/builtin/game/register.lua b/builtin/game/register.lua index 1034d4f2b..c07535855 100644 --- a/builtin/game/register.lua +++ b/builtin/game/register.lua @@ -1,5 +1,7 @@ -- Minetest: builtin/misc_register.lua +local S = core.get_translator("__builtin") + -- -- Make raw registration functions inaccessible to anyone except this file -- @@ -320,20 +322,13 @@ for name in pairs(forbidden_item_names) do register_alias_raw(name, "") end - --- Obsolete: --- Aliases for core.register_alias (how ironic...) --- core.alias_node = core.register_alias --- core.alias_tool = core.register_alias --- core.alias_craftitem = core.register_alias - -- -- Built-in node definitions. Also defined in C. -- core.register_item(":unknown", { type = "none", - description = "Unknown Item", + description = S("Unknown Item"), inventory_image = "unknown_item.png", on_place = core.item_place, on_secondary_use = core.item_secondary_use, @@ -343,7 +338,7 @@ core.register_item(":unknown", { }) core.register_node(":air", { - description = "Air", + description = S("Air"), inventory_image = "air.png", wield_image = "air.png", drawtype = "airlike", @@ -360,7 +355,7 @@ core.register_node(":air", { }) core.register_node(":ignore", { - description = "Ignore", + description = S("Ignore"), inventory_image = "ignore.png", wield_image = "ignore.png", drawtype = "airlike", @@ -373,11 +368,12 @@ core.register_node(":ignore", { air_equivalent = true, drop = "", groups = {not_in_creative_inventory=1}, + node_placement_prediction = "", on_place = function(itemstack, placer, pointed_thing) core.chat_send_player( placer:get_player_name(), core.colorize("#FF0000", - "You can't place 'ignore' nodes!")) + S("You can't place 'ignore' nodes!"))) return "" end, }) @@ -584,6 +580,7 @@ core.unregister_biome = make_wrap_deregistration(core.register_biome, core.clear_registered_biomes, core.registered_biomes) core.registered_on_chat_messages, core.register_on_chat_message = make_registration() +core.registered_on_chatcommands, core.register_on_chatcommand = make_registration() core.registered_globalsteps, core.register_globalstep = make_registration() core.registered_playerevents, core.register_playerevent = make_registration() core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration() @@ -612,6 +609,7 @@ core.registered_can_bypass_userlimit, core.register_can_bypass_userlimit = make_ core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration() core.registered_on_player_inventory_actions, core.register_on_player_inventory_action = make_registration() core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration() +core.registered_on_rightclickplayers, core.register_on_rightclickplayer = make_registration() -- -- Compatibility for on_mapgen_init() diff --git a/builtin/game/statbars.lua b/builtin/game/statbars.lua index d192029c5..db5087a16 100644 --- a/builtin/game/statbars.lua +++ b/builtin/game/statbars.lua @@ -84,8 +84,8 @@ local function update_builtin_statbars(player) end if hud.id_breathbar and (not show_breathbar or breath == breath_max) then - minetest.after(1, function(player_name, breath_bar) - local player = minetest.get_player_by_name(player_name) + core.after(1, function(player_name, breath_bar) + local player = core.get_player_by_name(player_name) if player then player:hud_remove(breath_bar) end diff --git a/builtin/init.lua b/builtin/init.lua index 75bb3db85..89b1fdc64 100644 --- a/builtin/init.lua +++ b/builtin/init.lua @@ -39,9 +39,20 @@ if INIT == "game" then assert(not core.get_http_api) elseif INIT == "mainmenu" then local mm_script = core.settings:get("main_menu_script") + local custom_loaded = false if mm_script and mm_script ~= "" then - dofile(mm_script) - else + local testfile = io.open(mm_script, "r") + if testfile then + testfile:close() + dofile(mm_script) + custom_loaded = true + core.log("info", "Loaded custom main menu script: "..mm_script) + else + core.log("error", "Failed to load custom main menu script: "..mm_script) + core.log("info", "Falling back to default main menu script") + end + end + if not custom_loaded then dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua") end elseif INIT == "async" then diff --git a/builtin/locale/__builtin.de.tr b/builtin/locale/__builtin.de.tr new file mode 100644 index 000000000..eaadf611b --- /dev/null +++ b/builtin/locale/__builtin.de.tr @@ -0,0 +1,225 @@ +# textdomain: __builtin +Empty command.=Leerer Befehl. +Invalid command: @1=Ungültiger Befehl: @1 +Invalid command usage.=Ungültige Befehlsverwendung. +You don't have permission to run this command (missing privileges: @1).=Sie haben keine Erlaubnis, diesen Befehl auszuführen (fehlende Privilegien: @1). +Unable to get position of player @1.=Konnte Position vom Spieler @1 nicht ermitteln. +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)=Ungültiges Gebietsformat. Erwartet: (x1,y1,z1) (x2,y2,z2) +<action>=<Aktion> +Show chat action (e.g., '/me orders a pizza' displays '<player name> orders a pizza')=Chataktion zeigen (z.B. wird „/me isst Pizza“ zu „<Spielername> isst Pizza“) +Show the name of the server owner=Den Namen des Servereigentümers zeigen +The administrator of this server is @1.=Der Administrator dieses Servers ist @1. +There's no administrator named in the config file.=In der Konfigurationsdatei wurde kein Administrator angegeben. +[<name>]=[<Name>] +Show privileges of yourself or another player=Ihre eigenen Privilegien oder die eines anderen Spielers anzeigen +Player @1 does not exist.=Spieler @1 existiert nicht. +Privileges of @1: @2=Privilegien von @1: @2 +<privilege>=<Privileg> +Return list of all online players with privilege=Liste aller Spieler mit einem Privileg ausgeben +Invalid parameters (see /help haspriv).=Ungültige Parameter (siehe „/help haspriv“). +Unknown privilege!=Unbekanntes Privileg! +Players online with the "@1" privilege: @2=Derzeit online spielende Spieler mit dem „@1“-Privileg: @2 +Your privileges are insufficient.=Ihre Privilegien sind unzureichend. +Unknown privilege: @1=Unbekanntes Privileg: @1 +@1 granted you privileges: @2=@1 gewährte Ihnen Privilegien: @2 +<name> (<privilege> | all)=<Name> (<Privileg> | all) +Give privileges to player=Privileg an Spieler vergeben +Invalid parameters (see /help grant).=Ungültige Parameter (siehe „/help grant“). +<privilege> | all=<Privileg> | all +Grant privileges to yourself=Privilegien an Ihnen selbst vergeben +Invalid parameters (see /help grantme).=Ungültige Parameter (siehe „/help grantme“). +@1 revoked privileges from you: @2=@1 entfernte Privilegien von Ihnen: @2 +Remove privileges from player=Privilegien von Spieler entfernen +Invalid parameters (see /help revoke).=Ungültige Parameter (siehe „/help revoke“). +Revoke privileges from yourself=Privilegien von Ihnen selbst entfernen +Invalid parameters (see /help revokeme).=Ungültige Parameter (siehe „/help revokeme“). +<name> <password>=<Name> <Passwort> +Set player's password=Passwort von Spieler setzen +Name field required.=Namensfeld benötigt. +Your password was cleared by @1.=Ihr Passwort wurde von @1 geleert. +Password of player "@1" cleared.=Passwort von Spieler „@1“ geleert. +Your password was set by @1.=Ihr Passwort wurde von @1 gesetzt. +Password of player "@1" set.=Passwort von Spieler „@1“ gesetzt. +<name>=<Name> +Set empty password for a player=Leeres Passwort für einen Spieler setzen +Reload authentication data=Authentifizierungsdaten erneut laden +Done.=Fertig. +Failed.=Fehlgeschlagen. +Remove a player's data=Daten eines Spielers löschen +Player "@1" removed.=Spieler „@1“ gelöscht. +No such player "@1" to remove.=Es gibt keinen Spieler „@1“, der gelöscht werden könnte. +Player "@1" is connected, cannot remove.=Spieler „@1“ ist verbunden, er kann nicht gelöscht werden. +Unhandled remove_player return code @1.=Nicht berücksichtigter remove_player-Rückgabewert @1. +Cannot teleport out of map bounds!=Eine Teleportation außerhalb der Kartengrenzen ist nicht möglich! +Cannot get player with name @1.=Spieler mit Namen @1 kann nicht gefunden werden. +Cannot teleport, @1 is attached to an object!=Teleportation nicht möglich, @1 ist an einem Objekt befestigt! +Teleporting @1 to @2.=Teleportation von @1 nach @2 +One does not teleport to oneself.=Man teleportiert sich doch nicht zu sich selbst. +Cannot get teleportee with name @1.=Der zu teleportierende Spieler mit Namen @1 kann nicht gefunden werden. +Cannot get target player with name @1.=Zielspieler mit Namen @1 kann nicht gefunden werden. +Teleporting @1 to @2 at @3.=Teleportation von @1 zu @2 bei @3 +<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>=<X>,<Y>,<Z> | <zu_Name> | <Name> <X>,<Y>,<Z> | <Name> <zu_Name> +Teleport to position or player=Zu Position oder Spieler teleportieren +You don't have permission to teleport other players (missing privilege: @1).=Sie haben nicht die Erlaubnis, andere Spieler zu teleportieren (fehlendes Privileg: @1). +([-n] <name> <value>) | <name>=([-n] <Name> <Wert>) | <Name> +Set or read server configuration setting=Serverkonfigurationseinstellung setzen oder lesen +Failed. Use '/set -n <name> <value>' to create a new setting.=Fehlgeschlagen. Benutzen Sie „/set -n <Name> <Wert>“, um eine neue Einstellung zu erstellen. +@1 @= @2=@1 @= @2 +<not set>=<nicht gesetzt> +Invalid parameters (see /help set).=Ungültige Parameter (siehe „/help set“). +Finished emerging @1 blocks in @2ms.=Fertig mit Erzeugung von @1 Blöcken in @2 ms. +emergeblocks update: @1/@2 blocks emerged (@3%)=emergeblocks-Update: @1/@2 Kartenblöcke geladen (@3%) +(here [<radius>]) | (<pos1> <pos2>)=(here [<Radius>]) | (<Pos1> <Pos2>) +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)=Lade (oder, wenn nicht existent, generiere) Kartenblöcke im Gebiet zwischen Pos1 und Pos2 (<Pos1> und <Pos2> müssen in Klammern stehen) +Started emerge of area ranging from @1 to @2.=Start des Ladevorgangs des Gebiets zwischen @1 und @2. +Delete map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)=Kartenblöcke innerhalb des Gebiets zwischen Pos1 und Pos2 löschen (<Pos1> und <Pos2> müssen in Klammern stehen) +Successfully cleared area ranging from @1 to @2.=Gebiet zwischen @1 und @2 erfolgreich geleert. +Failed to clear one or more blocks in area.=Fehlgeschlagen: Ein oder mehrere Kartenblöcke im Gebiet konnten nicht geleert werden. +Resets lighting in the area between pos1 and pos2 (<pos1> and <pos2> must be in parentheses)=Setzt das Licht im Gebiet zwischen Pos1 und Pos2 zurück (<Pos1> und <Pos2> müssen in Klammern stehen) +Successfully reset light in the area ranging from @1 to @2.=Das Licht im Gebiet zwischen @1 und @2 wurde erfolgreich zurückgesetzt. +Failed to load one or more blocks in area.=Fehlgeschlagen: Ein oder mehrere Kartenblöcke im Gebiet konnten nicht geladen werden. +List mods installed on the server=Installierte Mods auf dem Server auflisten +Cannot give an empty item.=Ein leerer Gegenstand kann nicht gegeben werden. +Cannot give an unknown item.=Ein unbekannter Gegenstand kann nicht gegeben werden. +Giving 'ignore' is not allowed.=„ignore“ darf nicht gegeben werden. +@1 is not a known player.=@1 ist kein bekannter Spieler. +@1 partially added to inventory.=@1 teilweise ins Inventar eingefügt. +@1 could not be added to inventory.=@1 konnte nicht ins Inventar eingefügt werden. +@1 added to inventory.=@1 zum Inventar hinzugefügt. +@1 partially added to inventory of @2.=@1 teilweise ins Inventar von @2 eingefügt. +@1 could not be added to inventory of @2.=@1 konnte nicht ins Inventar von @2 eingefügt werden. +@1 added to inventory of @2.=@1 ins Inventar von @2 eingefügt. +<name> <ItemString> [<count> [<wear>]]=<Name> <ItemString> [<Anzahl> [<Abnutzung>]] +Give item to player=Gegenstand an Spieler geben +Name and ItemString required.=Name und ItemString benötigt. +<ItemString> [<count> [<wear>]]=<ItemString> [<Anzahl> [<Abnutzung>]] +Give item to yourself=Gegenstand Ihnen selbst geben +ItemString required.=ItemString benötigt. +<EntityName> [<X>,<Y>,<Z>]=<EntityName> [<X>,<Y>,<Z>] +Spawn entity at given (or your) position=Entity an angegebener (oder Ihrer eigenen) Position spawnen +EntityName required.=EntityName benötigt. +Unable to spawn entity, player is nil.=Entity konnte nicht gespawnt werden, Spieler ist nil. +Cannot spawn an unknown entity.=Ein unbekanntes Entity kann nicht gespawnt werden. +Invalid parameters (@1).=Ungültige Parameter (@1). +@1 spawned.=@1 gespawnt. +@1 failed to spawn.=@1 konnte nicht gespawnt werden. +Destroy item in hand=Gegenstand in der Hand zerstören +Unable to pulverize, no player.=Konnte nicht pulverisieren, kein Spieler. +Unable to pulverize, no item in hand.=Konnte nicht pulverisieren, kein Gegenstand in der Hand. +An item was pulverized.=Ein Gegenstand wurde pulverisiert. +[<range>] [<seconds>] [<limit>]=[<Reichweite>] [<Sekunden>] [<Limit>] +Check who last touched a node or a node near it within the time specified by <seconds>. Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set <seconds> to inf for no time limit=Überprüfen, wer als letztes einen Node oder einen Node in der Nähe innerhalb der in <Sekunden> angegebenen Zeitspanne angefasst hat. Standard: Reichweite @= 0, Sekunden @= 86400 @= 24h, Limit @= 5. <Sekunden> auf „inf“ setzen, um Zeitlimit zu deaktivieren. +Rollback functions are disabled.=Rollback-Funktionen sind deaktiviert. +That limit is too high!=Dieses Limit ist zu hoch! +Checking @1 ...=Überprüfe @1 ... +Nobody has touched the specified location in @1 seconds.=Niemand hat die angegebene Position seit @1 Sekunden angefasst. +@1 @2 @3 -> @4 @5 seconds ago.=@1 @2 @3 -> @4 vor @5 Sekunden. +Punch a node (range@=@1, seconds@=@2, limit@=@3).=Hauen Sie einen Node (Reichweite@=@1, Sekunden@=@2, Limit@=@3). +(<name> [<seconds>]) | (:<actor> [<seconds>])=(<Name> [<Sekunden>]) | (:<Akteur> [<Sekunden>]) +Revert actions of a player. Default for <seconds> is 60. Set <seconds> to inf for no time limit=Aktionen eines Spielers zurückrollen. Standard für <Sekunden> ist 60. <Sekunden> auf „inf“ setzen, um Zeitlimit zu deaktivieren +Invalid parameters. See /help rollback and /help rollback_check.=Ungültige Parameter. Siehe /help rollback und /help rollback_check. +Reverting actions of player '@1' since @2 seconds.=Die Aktionen des Spielers „@1“ seit @2 Sekunden werden rückgängig gemacht. +Reverting actions of @1 since @2 seconds.=Die Aktionen von @1 seit @2 Sekunden werden rückgängig gemacht. +(log is too long to show)=(Protokoll ist zu lang für die Anzeige) +Reverting actions succeeded.=Die Aktionen wurden erfolgreich rückgängig gemacht. +Reverting actions FAILED.=FEHLGESCHLAGEN: Die Aktionen konnten nicht rückgängig gemacht werden. +Show server status=Serverstatus anzeigen +This command was disabled by a mod or game.=Dieser Befehl wurde von einer Mod oder einem Spiel deaktiviert. +[<0..23>:<0..59> | <0..24000>]=[<0..23>:<0..59> | <0..24000>] +Show or set time of day=Tageszeit anzeigen oder setzen +Current time is @1:@2.=Es ist jetzt @1:@2 Uhr. +You don't have permission to run this command (missing privilege: @1).=Sie haben nicht die Erlaubnis, diesen Befehl auszuführen (fehlendes Privileg: @1). +Invalid time.=Ungültige Zeit. +Time of day changed.=Tageszeit geändert. +Invalid hour (must be between 0 and 23 inclusive).=Ungültige Stunde (muss zwischen 0 und 23 inklusive liegen). +Invalid minute (must be between 0 and 59 inclusive).=Ungültige Minute (muss zwischen 0 und 59 inklusive liegen). +Show day count since world creation=Anzahl Tage seit der Erschaffung der Welt anzeigen +Current day is @1.=Aktueller Tag ist @1. +[<delay_in_seconds> | -1] [reconnect] [<message>]=[<Verzögerung_in_Sekunden> | -1] [reconnect] [<Nachricht>] +Shutdown server (-1 cancels a delayed shutdown)=Server herunterfahren (-1 bricht einen verzögerten Abschaltvorgang ab) +Server shutting down (operator request).=Server wird heruntergefahren (Betreiberanfrage). +Ban the IP of a player or show the ban list=Die IP eines Spielers verbannen oder die Bannliste anzeigen +The ban list is empty.=Die Bannliste ist leer. +Ban list: @1=Bannliste: @1 +Player is not online.=Spieler ist nicht online. +Failed to ban player.=Konnte Spieler nicht verbannen. +Banned @1.=@1 verbannt. +<name> | <IP_address>=<Name> | <IP_Adresse> +Remove IP ban belonging to a player/IP=Einen IP-Bann auf einen Spieler zurücknehmen +Failed to unban player/IP.=Konnte Bann auf Spieler/IP nicht zurücknehmen. +Unbanned @1.=Bann auf @1 zurückgenommen. +<name> [<reason>]=<Name> [<Grund>] +Kick a player=Spieler hinauswerfen +Failed to kick player @1.=Spieler @1 konnte nicht hinausgeworfen werden. +Kicked @1.=@1 hinausgeworfen. +[full | quick]=[full | quick] +Clear all objects in world=Alle Objekte in der Welt löschen +Invalid usage, see /help clearobjects.=Ungültige Verwendung, siehe /help clearobjects. +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)=Lösche alle Objekte. Dies kann eine lange Zeit dauern. Eine Netzwerkzeitüberschreitung könnte für Sie auftreten. (von @1) +Objects cleared.=Objekte gelöscht. +Cleared all objects.=Alle Objekte gelöscht. +<name> <message>=<Name> <Nachricht> +Send a direct message to a player=Eine Direktnachricht an einen Spieler senden +Invalid usage, see /help msg.=Ungültige Verwendung, siehe /help msg. +The player @1 is not online.=Der Spieler @1 ist nicht online. +DM from @1: @2=DN von @1: @2 +Message sent.=Nachricht gesendet. +Get the last login time of a player or yourself=Den letzten Loginzeitpunkt eines Spielers oder Ihren eigenen anfragen +@1's last login time was @2.=Letzter Loginzeitpunkt von @1 war @2. +@1's last login time is unknown.=Letzter Loginzeitpunkt von @1 ist unbekannt. +Clear the inventory of yourself or another player=Das Inventar von Ihnen oder einem anderen Spieler leeren +You don't have permission to clear another player's inventory (missing privilege: @1).=Sie haben nicht die Erlaubnis, das Inventar eines anderen Spielers zu leeren (fehlendes Privileg: @1). +@1 cleared your inventory.=@1 hat Ihr Inventar geleert. +Cleared @1's inventory.=Inventar von @1 geleert. +Player must be online to clear inventory!=Spieler muss online sein, um das Inventar leeren zu können! +Players can't be killed, damage has been disabled.=Spieler können nicht getötet werden, Schaden ist deaktiviert. +Player @1 is not online.=Spieler @1 ist nicht online. +You are already dead.=Sie sind schon tot. +@1 is already dead.=@1 ist bereits tot. +@1 has been killed.=@1 wurde getötet. +Kill player or yourself=Einen Spieler oder Sie selbst töten +Available commands: @1=Verfügbare Befehle: @1 +Use '/help <cmd>' to get more information, or '/help all' to list everything.=„/help <Befehl>“ benutzen, um mehr Informationen zu erhalten, oder „/help all“, um alles aufzulisten. +Available commands:=Verfügbare Befehle: +Command not available: @1=Befehl nicht verfügbar: @1 +[all | privs | <cmd>]=[all | privs | <Befehl>] +Get help for commands or list privileges=Hilfe für Befehle erhalten oder Privilegien auflisten +Available privileges:=Verfügbare Privilegien: +Command=Befehl +Parameters=Parameter +For more information, click on any entry in the list.=Für mehr Informationen klicken Sie auf einen beliebigen Eintrag in der Liste. +Double-click to copy the entry to the chat history.=Doppelklicken, um den Eintrag in die Chathistorie einzufügen. +Command: @1 @2=Befehl: @1 @2 +Available commands: (see also: /help <cmd>)=Verfügbare Befehle: (siehe auch: /help <Befehl>) +Close=Schließen +Privilege=Privileg +Description=Beschreibung +print [<filter>] | dump [<filter>] | save [<format> [<filter>]] | reset=print [<Filter>] | dump [<Filter>] | save [<Format> [<Filter>]] +Handle the profiler and profiling data=Den Profiler und Profilingdaten verwalten +Statistics written to action log.=Statistiken zum Aktionsprotokoll geschrieben. +Statistics were reset.=Statistiken wurden zurückgesetzt. +Usage: @1=Verwendung: @1 +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).=Format kann entweder „txt“, „csv“, „lua“, „json“ oder „json_pretty“ sein (die Struktur kann sich in Zukunft ändern). +(no description)=(keine Beschreibung) +Can interact with things and modify the world=Kann mit Dingen interagieren und die Welt verändern +Can speak in chat=Kann im Chat sprechen +Can modify 'shout' and 'interact' privileges=Kann die „shout“- und „interact“-Privilegien anpassen +Can modify privileges=Kann Privilegien anpassen +Can teleport self=Kann sich selbst teleportieren +Can teleport other players=Kann andere Spieler teleportieren +Can set the time of day using /time=Kann die Tageszeit mit /time setzen +Can do server maintenance stuff=Kann Serverwartungsdinge machen +Can bypass node protection in the world=Kann den Schutz auf Blöcken in der Welt umgehen +Can ban and unban players=Kann Spieler verbannen und entbannen +Can kick players=Kann Spieler hinauswerfen +Can use /give and /giveme=Kann /give und /giveme benutzen +Can use /setpassword and /clearpassword=Kann /setpassword und /clearpassword benutzen +Can use fly mode=Kann den Flugmodus benutzen +Can use fast mode=Kann den Schnellmodus benutzen +Can fly through solid nodes using noclip mode=Kann durch feste Blöcke mit dem Geistmodus fliegen +Can use the rollback functionality=Kann die Rollback-Funktionalität benutzen +Allows enabling various debug options that may affect gameplay=Erlaubt die Aktivierung diverser Debugoptionen, die das Spielgeschehen beeinflussen könnten +Unknown Item=Unbekannter Gegenstand +Air=Luft +Ignore=Ignorieren +You can't place 'ignore' nodes!=Sie können keine „ignore“-Blöcke platzieren! diff --git a/builtin/locale/__builtin.it.tr b/builtin/locale/__builtin.it.tr new file mode 100644 index 000000000..94bc870c8 --- /dev/null +++ b/builtin/locale/__builtin.it.tr @@ -0,0 +1,224 @@ +# textdomain: __builtin +Empty command.=Comando vuoto. +Invalid command: @1=Comando non valido: @1 +Invalid command usage.=Utilizzo del comando non valido. +You don't have permission to run this command (missing privileges: @1).=Non hai il permesso di eseguire questo comando (privilegi mancanti: @1). +Unable to get position of player @1.=Impossibile ottenere la posizione del giocatore @1. +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)=Formato dell'area non corretto. Richiesto: (x1,y1,z1) (x2,y2,z2) +<action>=<azione> +Show chat action (e.g., '/me orders a pizza' displays '<player name> orders a pizza')=Mostra un'azione in chat (es. `/me ordina una pizza` mostra `<nome giocatore> ordina una pizza`) +Show the name of the server owner=Mostra il nome del proprietario del server +The administrator of this server is @1.=L'amministratore di questo server è @1. +There's no administrator named in the config file.=Non c'è nessun amministratore nel file di configurazione. +[<name>]=[<nome>] +Show privileges of yourself or another player=Mostra i privilegi propri o di un altro giocatore +Player @1 does not exist.=Il giocatore @1 non esiste. +Privileges of @1: @2=Privilegi di @1: @2 +<privilege>=<privilegio> +Return list of all online players with privilege=Ritorna una lista di tutti i giocatori connessi col tale privilegio +Invalid parameters (see /help haspriv).=Parametri non validi (vedi /help haspriv). +Unknown privilege!=Privilegio sconosciuto! +Players online with the "@1" privilege: @2=Giocatori connessi con il privilegio "@1": @2 +Your privileges are insufficient.=I tuoi privilegi sono insufficienti. +Unknown privilege: @1=Privilegio sconosciuto: @1 +@1 granted you privileges: @2=@1 ti ha assegnato i seguenti privilegi: @2 +<name> (<privilege> | all)=<nome> (<privilegio> | all) +Give privileges to player=Dà privilegi al giocatore +Invalid parameters (see /help grant).=Parametri non validi (vedi /help grant). +<privilege> | all=<privilegio> | all +Grant privileges to yourself=Assegna dei privilegi a te stessǝ +Invalid parameters (see /help grantme).=Parametri non validi (vedi /help grantme). +@1 revoked privileges from you: @2=@1 ti ha revocato i seguenti privilegi: @2 +Remove privileges from player=Rimuove privilegi dal giocatore +Invalid parameters (see /help revoke).=Parametri non validi (vedi /help revoke). +Revoke privileges from yourself=Revoca privilegi a te stessǝ +Invalid parameters (see /help revokeme).=Parametri non validi (vedi /help revokeme). +<name> <password>=<nome> <password> +Set player's password=Imposta la password del giocatore +Name field required.=Campo "nome" richiesto. +Your password was cleared by @1.=La tua password è stata resettata da @1. +Password of player "@1" cleared.=Password del giocatore "@1" resettata. +Your password was set by @1.=La tua password è stata impostata da @1. +Password of player "@1" set.=Password del giocatore "@1" impostata. +<name>=<nome> +Set empty password for a player=Imposta una password vuota a un giocatore +Reload authentication data=Ricarica i dati d'autenticazione +Done.=Fatto. +Failed.=Errore. +Remove a player's data=Rimuove i dati di un giocatore +Player "@1" removed.=Giocatore "@1" rimosso. +No such player "@1" to remove.=Non è presente nessun giocatore "@1" da rimuovere. +Player "@1" is connected, cannot remove.=Il giocatore "@1" è connesso, non può essere rimosso. +Unhandled remove_player return code @1.=Codice ritornato da remove_player non gestito (@1). +Cannot teleport out of map bounds!=Non ci si può teletrasportare fuori dai limiti della mappa! +Cannot get player with name @1.=Impossibile trovare il giocatore chiamato @1. +Cannot teleport, @1 is attached to an object!=Impossibile teletrasportare, @1 è attaccato a un oggetto! +Teleporting @1 to @2.=Teletrasportando @1 da @2. +One does not teleport to oneself.=Non ci si può teletrasportare su se stessi. +Cannot get teleportee with name @1.=Impossibile trovare il giocatore chiamato @1 per il teletrasporto +Cannot get target player with name @1.=Impossibile trovare il giocatore chiamato @1 per il teletrasporto +Teleporting @1 to @2 at @3.=Teletrasportando @1 da @2 a @3 +<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>=<X>,<Y>,<Z> | <da_nome> | <nome> <X>,<Y>,<Z> | <nome> <da_nome> +Teleport to position or player=Teletrasporta a una posizione o da un giocatore +You don't have permission to teleport other players (missing privilege: @1).=Non hai il permesso di teletrasportare altri giocatori (privilegio mancante: @1). +([-n] <name> <value>) | <name>=([-n] <nome> <valore>) | <nome> +Set or read server configuration setting=Imposta o ottieni le configurazioni del server +Failed. Use '/set -n <name> <value>' to create a new setting.=Errore. Usa 'set -n <nome> <valore>' per creare una nuova impostazione +@1 @= @2=@1 @= @2 +<not set>=<non impostato> +Invalid parameters (see /help set).=Parametri non validi (vedi /help set). +Finished emerging @1 blocks in @2ms.=Finito di emergere @1 blocchi in @2ms +emergeblocks update: @1/@2 blocks emerged (@3%)=aggiornamento emergeblocks: @1/@2 blocchi emersi (@3%) +(here [<radius>]) | (<pos1> <pos2>)=(here [<raggio>]) | (<pos1> <pos2>) +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)=Carica (o, se non esiste, genera) blocchi mappa contenuti nell'area tra pos1 e pos2 (<pos1> e <pos2> vanno tra parentesi) +Started emerge of area ranging from @1 to @2.=Iniziata emersione dell'area tra @1 e @2. +Delete map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)=Cancella i blocchi mappa contenuti nell'area tra pos1 e pos2 (<pos1> e <pos2> vanno tra parentesi) +Successfully cleared area ranging from @1 to @2.=Area tra @1 e @2 ripulita con successo. +Failed to clear one or more blocks in area.=Errore nel ripulire uno o più blocchi mappa nell'area +Resets lighting in the area between pos1 and pos2 (<pos1> and <pos2> must be in parentheses)=Reimposta l'illuminazione nell'area tra pos1 e po2 (<pos1> e <pos2> vanno tra parentesi) +Successfully reset light in the area ranging from @1 to @2.=Luce nell'area tra @1 e @2 reimpostata con successo. +Failed to load one or more blocks in area.=Errore nel caricare uno o più blocchi mappa nell'area. +List mods installed on the server=Elenca le mod installate nel server +Cannot give an empty item.=Impossibile dare un oggetto vuoto. +Cannot give an unknown item.=Impossibile dare un oggetto sconosciuto. +Giving 'ignore' is not allowed.=Non è permesso dare 'ignore'. +@1 is not a known player.=@1 non è un giocatore conosciuto. +@1 partially added to inventory.=@1 parzialmente aggiunto all'inventario. +@1 could not be added to inventory.=@1 non può essere aggiunto all'inventario. +@1 added to inventory.=@1 aggiunto all'inventario. +@1 partially added to inventory of @2.=@1 parzialmente aggiunto all'inventario di @2. +@1 could not be added to inventory of @2.=Non è stato possibile aggiungere @1 all'inventario di @2. +@1 added to inventory of @2.=@1 aggiunto all'inventario di @2. +<name> <ItemString> [<count> [<wear>]]=<nome> <NomeOggetto> [<quantità> [<usura>]] +Give item to player=Dà oggetti ai giocatori +Name and ItemString required.=Richiesti nome e NomeOggetto. +<ItemString> [<count> [<wear>]]=<NomeOggetto> [<quantità> [<usura>]] +Give item to yourself=Dà oggetti a te stessǝ +ItemString required.=Richiesto NomeOggetto. +<EntityName> [<X>,<Y>,<Z>]=<NomeEntità> [<X>,<Y>,<Z>] +Spawn entity at given (or your) position=Genera un'entità alla data coordinata (o la tua) +EntityName required.=Richiesto NomeEntità +Unable to spawn entity, player is nil.=Impossibile generare l'entità, il giocatore è nil. +Cannot spawn an unknown entity.=Impossibile generare un'entità sconosciuta. +Invalid parameters (@1).=Parametri non validi (@1). +@1 spawned.=Generata entità @1. +@1 failed to spawn.=Errore nel generare @1 +Destroy item in hand=Distrugge l'oggetto in mano +Unable to pulverize, no player.=Impossibile polverizzare, nessun giocatore. +Unable to pulverize, no item in hand.=Impossibile polverizzare, nessun oggetto in mano. +An item was pulverized.=Un oggetto è stato polverizzato. +[<range>] [<seconds>] [<limit>]=[<raggio>] [<secondi>] [<limite>] +Check who last touched a node or a node near it within the time specified by <seconds>. Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set <seconds> to inf for no time limit=Controlla chi è l'ultimo giocatore che ha toccato un nodo o un nodo nelle sue vicinanze, negli ultimi secondi indicati. Di base: raggio @= 0, secondi @= 86400 @= 24h, limite @= 5. +Rollback functions are disabled.=Le funzioni di rollback sono disabilitate. +That limit is too high!=Il limite è troppo alto! +Checking @1 ...=Controllando @1 ... +Nobody has touched the specified location in @1 seconds.=Nessuno ha toccato il punto specificato negli ultimi @1 secondi. +@1 @2 @3 -> @4 @5 seconds ago.=@1 @2 @3 -> @4 @5 secondi fa. +Punch a node (range@=@1, seconds@=@2, limit@=@3).=Colpisce un nodo (raggio@=@1, secondi@=@2, limite@=@3) +(<name> [<seconds>]) | (:<actor> [<seconds>])=(<nome> [<secondi>]) | (:<attore> [<secondi>]) +Revert actions of a player. Default for <seconds> is 60. Set <seconds> to inf for no time limit=Riavvolge le azioni di un giocatore. Di base, <secondi> è 60. Imposta <secondi> a inf per nessun limite di tempo +Invalid parameters. See /help rollback and /help rollback_check.=Parametri non validi. Vedi /help rollback e /help rollback_check. +Reverting actions of player '@1' since @2 seconds.=Riavvolge le azioni del giocatore '@1' avvenute negli ultimi @2 secondi. +Reverting actions of @1 since @2 seconds.=Riavvolge le azioni di @1 avvenute negli ultimi @2 secondi. +(log is too long to show)=(il log è troppo lungo per essere mostrato) +Reverting actions succeeded.=Riavvolgimento azioni avvenuto con successo. +Reverting actions FAILED.=Errore nel riavvolgere le azioni. +Show server status=Mostra lo stato del server +This command was disabled by a mod or game.=Questo comando è stato disabilitato da una mod o dal gioco. +[<0..23>:<0..59> | <0..24000>]=[<0..23>:<0..59> | <0..24000>] +Show or set time of day=Mostra o imposta l'orario della giornata +Current time is @1:@2.=Orario corrente: @1:@2. +You don't have permission to run this command (missing privilege: @1).=Non hai il permesso di eseguire questo comando (privilegio mancante: @1) +Invalid time.=Orario non valido. +Time of day changed.=Orario della giornata cambiato. +Invalid hour (must be between 0 and 23 inclusive).=Ora non valida (deve essere tra 0 e 23 inclusi) +Invalid minute (must be between 0 and 59 inclusive).=Minuto non valido (deve essere tra 0 e 59 inclusi) +Show day count since world creation=Mostra il conteggio dei giorni da quando il mondo è stato creato +Current day is @1.=Giorno attuale: @1. +[<delay_in_seconds> | -1] [reconnect] [<message>]=[<ritardo_in_secondi> | -1] [reconnect] [<messaggio>] +Shutdown server (-1 cancels a delayed shutdown)=Arresta il server (-1 annulla un arresto programmato) +Server shutting down (operator request).=Arresto del server in corso (per richiesta dell'operatore) +Ban the IP of a player or show the ban list=Bandisce l'IP del giocatore o mostra la lista di quelli banditi +The ban list is empty.=La lista banditi è vuota. +Ban list: @1=Lista banditi: @1 +Player is not online.=Il giocatore non è connesso. +Failed to ban player.=Errore nel bandire il giocatore. +Banned @1.=@1 banditǝ. +<name> | <IP_address>=<nome> | <indirizzo_IP> +Remove IP ban belonging to a player/IP=Perdona l'IP appartenente a un giocatore/IP +Failed to unban player/IP.=Errore nel perdonare il giocatore/IP +Unbanned @1.=@1 perdonatǝ +<name> [<reason>]=<nome> [<ragione>] +Kick a player=Caccia un giocatore +Failed to kick player @1.=Errore nel cacciare il giocatore @1. +Kicked @1.=@1 cacciatǝ. +[full | quick]=[full | quick] +Clear all objects in world=Elimina tutti gli oggetti/entità nel mondo +Invalid usage, see /help clearobjects.=Uso incorretto, vedi /help clearobjects. +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)=Eliminando tutti gli oggetti/entità. Questo potrebbe richiedere molto tempo e farti eventualmente crashare. (di @1) +Cleared all objects.=Tutti gli oggetti sono stati eliminati. +<name> <message>=<nome> <messaggio> +Send a direct message to a player=Invia un messaggio privato al giocatore +Invalid usage, see /help msg.=Uso incorretto, vedi /help msg +The player @1 is not online.=Il giocatore @1 non è connesso. +DM from @1: @2=Messaggio privato da @1: @2 +Message sent.=Messaggio inviato. +Get the last login time of a player or yourself=Ritorna l'ultimo accesso di un giocatore o di te stessǝ +@1's last login time was @2.=L'ultimo accesso di @1 è avvenuto il @2 +@1's last login time is unknown.=L'ultimo accesso di @1 non è conosciuto +Clear the inventory of yourself or another player=Svuota l'inventario tuo o di un altro giocatore +You don't have permission to clear another player's inventory (missing privilege: @1).=Non hai il permesso di svuotare l'inventario di un altro giocatore (privilegio mancante: @1). +@1 cleared your inventory.=@1 ha svuotato il tuo inventario. +Cleared @1's inventory.=L'inventario di @1 è stato svuotato. +Player must be online to clear inventory!=Il giocatore deve essere connesso per svuotarne l'inventario! +Players can't be killed, damage has been disabled.=I giocatori non possono essere uccisi, il danno è disabilitato. +Player @1 is not online.=Il giocatore @1 non è connesso. +You are already dead.=Sei già mortǝ. +@1 is already dead.=@1 è già mortǝ. +@1 has been killed.=@1 è stato uccisǝ. +Kill player or yourself=Uccide un giocatore o te stessǝ +Available commands: @1=Comandi disponibili: @1 +Use '/help <cmd>' to get more information, or '/help all' to list everything.=Usa '/help <comando>' per ottenere più informazioni, o '/help all' per elencare tutti i comandi. +Available commands:=Comandi disponibili: +Command not available: @1=Comando non disponibile: @1 +[all | privs | <cmd>]=[all | privs | <comando>] +Get help for commands or list privileges=Richiama la finestra d'aiuto dei comandi o dei privilegi +Available privileges:=Privilegi disponibili: +Command=Comando +Parameters=Parametri +For more information, click on any entry in the list.=Per più informazioni, clicca su una qualsiasi voce dell'elenco. +Double-click to copy the entry to the chat history.=Doppio click per copiare la voce nella cronologia della chat. +Command: @1 @2=Comando: @1 @2 +Available commands: (see also: /help <cmd>)=Comandi disponibili: (vedi anche /help <comando>) +Close=Chiudi +Privilege=Privilegio +Description=Descrizione +print [<filter>] | dump [<filter>] | save [<format> [<filter>]] | reset=print [<filtro>] | dump [<filtro>] | save [<formato> [<filtro>]] | reset +Handle the profiler and profiling data=Gestisce il profiler e i dati da esso elaborati +Statistics written to action log.=Statistiche scritte nel log delle azioni. +Statistics were reset.=Le statistiche sono state resettate. +Usage: @1=Utilizzo: @1 +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).=I formati supportati sono txt, csv, lua, json e json_pretty (le strutture potrebbero essere soggetti a cambiamenti). +(no description)=(nessuna descrizione) +Can interact with things and modify the world=Si può interagire con le cose e modificare il mondo +Can speak in chat=Si può parlare in chat +Can modify 'shout' and 'interact' privileges=Si possono modificare i privilegi 'shout' e 'interact' +Can modify privileges=Si possono modificare i privilegi +Can teleport self=Si può teletrasportare se stessз +Can teleport other players=Si possono teletrasportare gli altri giocatori +Can set the time of day using /time=Si può impostate l'orario della giornata tramite /time +Can do server maintenance stuff=Si possono eseguire operazioni di manutenzione del server +Can bypass node protection in the world=Si può aggirare la protezione dei nodi nel mondo +Can ban and unban players=Si possono bandire e perdonare i giocatori +Can kick players=Si possono cacciare i giocatori +Can use /give and /giveme=Si possono usare /give e /give me +Can use /setpassword and /clearpassword=Si possono usare /setpassword e /clearpassword +Can use fly mode=Si può usare la modalità volo +Can use fast mode=Si può usare la modalità rapida +Can fly through solid nodes using noclip mode=Si può volare attraverso i nodi solidi con la modalità incorporea +Can use the rollback functionality=Si può usare la funzione di rollback +Allows enabling various debug options that may affect gameplay=Permette di abilitare varie opzioni di debug che potrebbero influenzare l'esperienza di gioco +Unknown Item=Oggetto sconosciuto +Air=Aria +Ignore=Ignora +You can't place 'ignore' nodes!=Non puoi piazzare nodi 'ignore'! diff --git a/builtin/locale/template.txt b/builtin/locale/template.txt new file mode 100644 index 000000000..c5ace1a2f --- /dev/null +++ b/builtin/locale/template.txt @@ -0,0 +1,224 @@ +# textdomain: __builtin +Empty command.= +Invalid command: @1= +Invalid command usage.= +You don't have permission to run this command (missing privileges: @1).= +Unable to get position of player @1.= +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)= +<action>= +Show chat action (e.g., '/me orders a pizza' displays '<player name> orders a pizza')= +Show the name of the server owner= +The administrator of this server is @1.= +There's no administrator named in the config file.= +[<name>]= +Show privileges of yourself or another player= +Player @1 does not exist.= +Privileges of @1: @2= +<privilege>= +Return list of all online players with privilege= +Invalid parameters (see /help haspriv).= +Unknown privilege!= +Players online with the "@1" privilege: @2= +Your privileges are insufficient.= +Unknown privilege: @1= +@1 granted you privileges: @2= +<name> (<privilege> | all)= +Give privileges to player= +Invalid parameters (see /help grant).= +<privilege> | all= +Grant privileges to yourself= +Invalid parameters (see /help grantme).= +@1 revoked privileges from you: @2= +Remove privileges from player= +Invalid parameters (see /help revoke).= +Revoke privileges from yourself= +Invalid parameters (see /help revokeme).= +<name> <password>= +Set player's password= +Name field required.= +Your password was cleared by @1.= +Password of player "@1" cleared.= +Your password was set by @1.= +Password of player "@1" set.= +<name>= +Set empty password for a player= +Reload authentication data= +Done.= +Failed.= +Remove a player's data= +Player "@1" removed.= +No such player "@1" to remove.= +Player "@1" is connected, cannot remove.= +Unhandled remove_player return code @1.= +Cannot teleport out of map bounds!= +Cannot get player with name @1.= +Cannot teleport, @1 is attached to an object!= +Teleporting @1 to @2.= +One does not teleport to oneself.= +Cannot get teleportee with name @1.= +Cannot get target player with name @1.= +Teleporting @1 to @2 at @3.= +<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>= +Teleport to position or player= +You don't have permission to teleport other players (missing privilege: @1).= +([-n] <name> <value>) | <name>= +Set or read server configuration setting= +Failed. Use '/set -n <name> <value>' to create a new setting.= +@1 @= @2= +<not set>= +Invalid parameters (see /help set).= +Finished emerging @1 blocks in @2ms.= +emergeblocks update: @1/@2 blocks emerged (@3%)= +(here [<radius>]) | (<pos1> <pos2>)= +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)= +Started emerge of area ranging from @1 to @2.= +Delete map blocks contained in area pos1 to pos2 (<pos1> and <pos2> must be in parentheses)= +Successfully cleared area ranging from @1 to @2.= +Failed to clear one or more blocks in area.= +Resets lighting in the area between pos1 and pos2 (<pos1> and <pos2> must be in parentheses)= +Successfully reset light in the area ranging from @1 to @2.= +Failed to load one or more blocks in area.= +List mods installed on the server= +Cannot give an empty item.= +Cannot give an unknown item.= +Giving 'ignore' is not allowed.= +@1 is not a known player.= +@1 partially added to inventory.= +@1 could not be added to inventory.= +@1 added to inventory.= +@1 partially added to inventory of @2.= +@1 could not be added to inventory of @2.= +@1 added to inventory of @2.= +<name> <ItemString> [<count> [<wear>]]= +Give item to player= +Name and ItemString required.= +<ItemString> [<count> [<wear>]]= +Give item to yourself= +ItemString required.= +<EntityName> [<X>,<Y>,<Z>]= +Spawn entity at given (or your) position= +EntityName required.= +Unable to spawn entity, player is nil.= +Cannot spawn an unknown entity.= +Invalid parameters (@1).= +@1 spawned.= +@1 failed to spawn.= +Destroy item in hand= +Unable to pulverize, no player.= +Unable to pulverize, no item in hand.= +An item was pulverized.= +[<range>] [<seconds>] [<limit>]= +Check who last touched a node or a node near it within the time specified by <seconds>. Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set <seconds> to inf for no time limit= +Rollback functions are disabled.= +That limit is too high!= +Checking @1 ...= +Nobody has touched the specified location in @1 seconds.= +@1 @2 @3 -> @4 @5 seconds ago.= +Punch a node (range@=@1, seconds@=@2, limit@=@3).= +(<name> [<seconds>]) | (:<actor> [<seconds>])= +Revert actions of a player. Default for <seconds> is 60. Set <seconds> to inf for no time limit= +Invalid parameters. See /help rollback and /help rollback_check.= +Reverting actions of player '@1' since @2 seconds.= +Reverting actions of @1 since @2 seconds.= +(log is too long to show)= +Reverting actions succeeded.= +Reverting actions FAILED.= +Show server status= +This command was disabled by a mod or game.= +[<0..23>:<0..59> | <0..24000>]= +Show or set time of day= +Current time is @1:@2.= +You don't have permission to run this command (missing privilege: @1).= +Invalid time.= +Time of day changed.= +Invalid hour (must be between 0 and 23 inclusive).= +Invalid minute (must be between 0 and 59 inclusive).= +Show day count since world creation= +Current day is @1.= +[<delay_in_seconds> | -1] [reconnect] [<message>]= +Shutdown server (-1 cancels a delayed shutdown)= +Server shutting down (operator request).= +Ban the IP of a player or show the ban list= +The ban list is empty.= +Ban list: @1= +Player is not online.= +Failed to ban player.= +Banned @1.= +<name> | <IP_address>= +Remove IP ban belonging to a player/IP= +Failed to unban player/IP.= +Unbanned @1.= +<name> [<reason>]= +Kick a player= +Failed to kick player @1.= +Kicked @1.= +[full | quick]= +Clear all objects in world= +Invalid usage, see /help clearobjects.= +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)= +Cleared all objects.= +<name> <message>= +Send a direct message to a player= +Invalid usage, see /help msg.= +The player @1 is not online.= +DM from @1: @2= +Message sent.= +Get the last login time of a player or yourself= +@1's last login time was @2.= +@1's last login time is unknown.= +Clear the inventory of yourself or another player= +You don't have permission to clear another player's inventory (missing privilege: @1).= +@1 cleared your inventory.= +Cleared @1's inventory.= +Player must be online to clear inventory!= +Players can't be killed, damage has been disabled.= +Player @1 is not online.= +You are already dead.= +@1 is already dead.= +@1 has been killed.= +Kill player or yourself= +Available commands: @1= +Use '/help <cmd>' to get more information, or '/help all' to list everything.= +Available commands:= +Command not available: @1= +[all | privs | <cmd>]= +Get help for commands or list privileges= +Available privileges:= +Command= +Parameters= +For more information, click on any entry in the list.= +Double-click to copy the entry to the chat history.= +Command: @1 @2= +Available commands: (see also: /help <cmd>)= +Close= +Privilege= +Description= +print [<filter>] | dump [<filter>] | save [<format> [<filter>]] | reset= +Handle the profiler and profiling data= +Statistics written to action log.= +Statistics were reset.= +Usage: @1= +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).= +(no description)= +Can interact with things and modify the world= +Can speak in chat= +Can modify 'shout' and 'interact' privileges= +Can modify privileges= +Can teleport self= +Can teleport other players= +Can set the time of day using /time= +Can do server maintenance stuff= +Can bypass node protection in the world= +Can ban and unban players= +Can kick players= +Can use /give and /giveme= +Can use /setpassword and /clearpassword= +Can use fly mode= +Can use fast mode= +Can fly through solid nodes using noclip mode= +Can use the rollback functionality= +Allows enabling various debug options that may affect gameplay= +Unknown Item= +Air= +Ignore= +You can't place 'ignore' nodes!= diff --git a/builtin/mainmenu/common.lua b/builtin/mainmenu/common.lua index 782d6973f..6db351048 100644 --- a/builtin/mainmenu/common.lua +++ b/builtin/mainmenu/common.lua @@ -14,14 +14,11 @@ --You should have received a copy of the GNU Lesser General Public License along --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --------------------------------------------------------------------------------- + -- Global menu data --------------------------------------------------------------------------------- menudata = {} --------------------------------------------------------------------------------- -- Local cached values --------------------------------------------------------------------------------- local min_supp_proto, max_supp_proto function common_update_cached_supp_proto() @@ -29,14 +26,12 @@ function common_update_cached_supp_proto() max_supp_proto = core.get_max_supp_proto() end common_update_cached_supp_proto() --------------------------------------------------------------------------------- + -- Menu helper functions --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- local function render_client_count(n) - if n > 99 then return '99+' - elseif n >= 0 then return tostring(n) + if n > 999 then return '99+' + elseif n >= 0 then return tostring(n) else return '?' end end @@ -50,74 +45,40 @@ local function configure_selected_world_params(idx) end end --------------------------------------------------------------------------------- -function image_column(tooltip, flagname) - return "image,tooltip=" .. core.formspec_escape(tooltip) .. "," .. - "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. - "1=" .. core.formspec_escape(defaulttexturedir .. - (flagname and "server_flags_" .. flagname .. ".png" or "blank.png")) .. "," .. - "2=" .. core.formspec_escape(defaulttexturedir .. "server_ping_4.png") .. "," .. - "3=" .. core.formspec_escape(defaulttexturedir .. "server_ping_3.png") .. "," .. - "4=" .. core.formspec_escape(defaulttexturedir .. "server_ping_2.png") .. "," .. - "5=" .. core.formspec_escape(defaulttexturedir .. "server_ping_1.png") -end - --------------------------------------------------------------------------------- -function order_favorite_list(list) - local res = {} - --orders the favorite list after support - for i = 1, #list do - local fav = list[i] - if is_server_protocol_compat(fav.proto_min, fav.proto_max) then - res[#res + 1] = fav - end - end - for i = 1, #list do - local fav = list[i] - if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then - res[#res + 1] = fav - end - end - return res -end - --------------------------------------------------------------------------------- -function render_serverlist_row(spec, is_favorite) +function render_serverlist_row(spec) local text = "" if spec.name then text = text .. core.formspec_escape(spec.name:trim()) elseif spec.address then - text = text .. spec.address:trim() + text = text .. core.formspec_escape(spec.address:trim()) if spec.port then text = text .. ":" .. spec.port end end - local grey_out = not is_server_protocol_compat(spec.proto_min, spec.proto_max) + local grey_out = not spec.is_compatible - local details - if is_favorite then - details = "1," - else - details = "0," - end + local details = {} - if spec.ping then - local ping = spec.ping * 1000 - if ping <= 50 then - details = details .. "2," - elseif ping <= 100 then - details = details .. "3," - elseif ping <= 250 then - details = details .. "4," + if spec.lag or spec.ping then + local lag = (spec.lag or 0) * 1000 + (spec.ping or 0) * 250 + if lag <= 125 then + table.insert(details, "1") + elseif lag <= 175 then + table.insert(details, "2") + elseif lag <= 250 then + table.insert(details, "3") else - details = details .. "5," + table.insert(details, "4") end else - details = details .. "0," + table.insert(details, "0") end - if spec.clients and spec.clients_max then + table.insert(details, ",") + + local color = (grey_out and "#aaaaaa") or ((spec.is_favorite and "#ddddaa") or "#ffffff") + if spec.clients and (spec.clients_max or 0) > 0 then local clients_percent = 100 * spec.clients / spec.clients_max -- Choose a color depending on how many clients are connected @@ -128,74 +89,50 @@ function render_serverlist_row(spec, is_favorite) elseif clients_percent <= 60 then clients_color = '#a1e587' -- 0-60%: green elseif clients_percent <= 90 then clients_color = '#ffdc97' -- 60-90%: yellow elseif clients_percent == 100 then clients_color = '#dd5b5b' -- full server: red (darker) - else clients_color = '#ffba97' -- 90-100%: orange + else clients_color = '#ffba97' -- 90-100%: orange end - details = details .. clients_color .. ',' .. - render_client_count(spec.clients) .. ',/,' .. - render_client_count(spec.clients_max) .. ',' - - elseif grey_out then - details = details .. '#aaaaaa,?,/,?,' + table.insert(details, clients_color) + table.insert(details, render_client_count(spec.clients) .. " / " .. + render_client_count(spec.clients_max)) else - details = details .. ',?,/,?,' + table.insert(details, color) + table.insert(details, "?") end if spec.creative then - details = details .. "1," + table.insert(details, "1") -- creative icon else - details = details .. "0," - end - - if spec.damage then - details = details .. "1," - else - details = details .. "0," + table.insert(details, "0") end if spec.pvp then - details = details .. "1," + table.insert(details, "2") -- pvp icon + elseif spec.damage then + table.insert(details, "1") -- heart icon else - details = details .. "0," + table.insert(details, "0") end - return details .. (grey_out and '#aaaaaa,' or ',') .. text + table.insert(details, color) + table.insert(details, text) + + return table.concat(details, ",") end -------------------------------------------------------------------------------- os.tempfolder = function() - if core.settings:get("TMPFolder") then - return core.settings:get("TMPFolder") .. DIR_DELIM .. "MT_" .. math.random(0,10000) - end - - local filetocheck = os.tmpname() - os.remove(filetocheck) - - -- luacheck: ignore - -- https://blogs.msdn.microsoft.com/vcblog/2014/06/18/c-runtime-crt-features-fixes-and-breaking-changes-in-visual-studio-14-ctp1/ - -- The C runtime (CRT) function called by os.tmpname is tmpnam. - -- Microsofts tmpnam implementation in older CRT / MSVC releases is defective. - -- tmpnam return values starting with a backslash characterize this behavior. - -- https://sourceforge.net/p/mingw-w64/bugs/555/ - -- MinGW tmpnam implementation is forwarded to the CRT directly. - -- https://sourceforge.net/p/mingw-w64/discussion/723797/thread/55520785/ - -- MinGW links to an older CRT release (msvcrt.dll). - -- Due to legal concerns MinGW will never use a newer CRT. - -- - -- Make use of TEMP to compose the temporary filename if an old - -- style tmpnam return value is detected. - if filetocheck:sub(1, 1) == "\\" then - local tempfolder = os.getenv("TEMP") - return tempfolder .. filetocheck - end - - local randname = "MTTempModFolder_" .. math.random(0,10000) - local backstring = filetocheck:reverse() - return filetocheck:sub(0, filetocheck:len() - backstring:find(DIR_DELIM) + 1) .. - randname + local temp = core.get_temp_path() + return temp .. DIR_DELIM .. "MT_" .. math.random(0, 10000) end +os.tmpname = function() + local path = os.tempfolder() + io.open(path, "w"):close() + return path +end -------------------------------------------------------------------------------- + function menu_render_worldlist() local retval = "" local current_worldlist = menudata.worldlist:get_list() @@ -209,7 +146,6 @@ function menu_render_worldlist() return retval end --------------------------------------------------------------------------------- function menu_handle_key_up_down(fields, textlist, settingname) local oldidx, newidx = core.get_textlist_index(textlist), 1 if fields.key_up or fields.key_down then @@ -226,42 +162,6 @@ function menu_handle_key_up_down(fields, textlist, settingname) return false end --------------------------------------------------------------------------------- -function asyncOnlineFavourites() - if not menudata.public_known then - menudata.public_known = {{ - name = fgettext("Loading..."), - description = fgettext_ne("Try reenabling public serverlist and check your internet connection.") - }} - end - menudata.favorites = menudata.public_known - menudata.favorites_is_public = true - - if not menudata.public_downloading then - menudata.public_downloading = true - else - return - end - - core.handle_async( - function(param) - return core.get_favorites("online") - end, - nil, - function(result) - menudata.public_downloading = nil - local favs = order_favorite_list(result) - if favs[1] then - menudata.public_known = favs - menudata.favorites = menudata.public_known - menudata.favorites_is_public = true - end - core.event_handler("Refresh") - end - ) -end - --------------------------------------------------------------------------------- function text2textlist(xpos, ypos, width, height, tl_name, textlen, text, transparency) local textlines = core.wrap_text(text, textlen, true) local retval = "textlist[" .. xpos .. "," .. ypos .. ";" .. width .. @@ -279,7 +179,6 @@ function text2textlist(xpos, ypos, width, height, tl_name, textlen, text, transp return retval end --------------------------------------------------------------------------------- function is_server_protocol_compat(server_proto_min, server_proto_max) if (not server_proto_min) or (not server_proto_max) then -- There is no info. Assume the best and act as if we would be compatible. @@ -287,7 +186,7 @@ function is_server_protocol_compat(server_proto_min, server_proto_max) end return min_supp_proto <= server_proto_max and max_supp_proto >= server_proto_min end --------------------------------------------------------------------------------- + function is_server_protocol_compat_or_error(server_proto_min, server_proto_max) if not is_server_protocol_compat(server_proto_min, server_proto_max) then local server_prot_ver_info, client_prot_ver_info @@ -315,7 +214,7 @@ function is_server_protocol_compat_or_error(server_proto_min, server_proto_max) return true end --------------------------------------------------------------------------------- + function menu_worldmt(selected, setting, value) local world = menudata.worldlist:get_list()[selected] if world then diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua index 2cf70c9c9..9bdf92a74 100644 --- a/builtin/mainmenu/dlg_config_world.lua +++ b/builtin/mainmenu/dlg_config_world.lua @@ -74,7 +74,7 @@ local function get_formspec(data) "label[1.75,0;" .. data.worldspec.name .. "]" if mod.is_modpack or mod.type == "game" then - local info = minetest.formspec_escape( + local info = core.formspec_escape( core.get_content_info(mod.path).description) if info == "" then if mod.is_modpack then diff --git a/builtin/mainmenu/dlg_contentstore.lua b/builtin/mainmenu/dlg_contentstore.lua index 6525f6013..b0736a4fd 100644 --- a/builtin/mainmenu/dlg_contentstore.lua +++ b/builtin/mainmenu/dlg_contentstore.lua @@ -15,7 +15,7 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -if not minetest.get_http_api then +if not core.get_http_api then function create_store_dlg() return messagebox("store", fgettext("ContentDB is not available when Minetest was compiled without cURL")) @@ -23,9 +23,11 @@ if not minetest.get_http_api then return end -local store = { packages = {}, packages_full = {} } +-- Unordered preserves the original order of the ContentDB API, +-- before the package list is ordered based on installed state. +local store = { packages = {}, packages_full = {}, packages_full_unordered = {} } -local http = minetest.get_http_api() +local http = core.get_http_api() -- Screenshot local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb" @@ -150,7 +152,7 @@ local function start_install(package) end local function queue_download(package) - local max_concurrent_downloads = tonumber(minetest.settings:get("contentdb_max_concurrent_downloads")) + local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads")) if number_downloading < max_concurrent_downloads then start_install(package) else @@ -159,6 +161,331 @@ local function queue_download(package) end end +local function get_raw_dependencies(package) + if package.raw_deps then + return package.raw_deps + end + + local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s" + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + local url = base_url .. url_fmt:format(package.id, core.get_max_supp_proto(), version.string) + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + local data = core.parse_json(response.data) or {} + + local content_lookup = {} + for _, pkg in pairs(store.packages_full) do + content_lookup[pkg.id] = pkg + end + + for id, raw_deps in pairs(data) do + local package2 = content_lookup[id:lower()] + if package2 and not package2.raw_deps then + package2.raw_deps = raw_deps + + for _, dep in pairs(raw_deps) do + local packages = {} + for i=1, #dep.packages do + packages[#packages + 1] = content_lookup[dep.packages[i]:lower()] + end + dep.packages = packages + end + end + end + + return package.raw_deps +end + +local function has_hard_deps(raw_deps) + for i=1, #raw_deps do + if not raw_deps[i].is_optional then + return true + end + end + + return false +end + +-- Recursively resolve dependencies, given the installed mods +local function resolve_dependencies_2(raw_deps, installed_mods, out) + local function resolve_dep(dep) + -- Check whether it's already installed + if installed_mods[dep.name] then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = true, + } + end + + -- Find exact name matches + local fallback + for _, package in pairs(dep.packages) do + if package.type ~= "game" then + if package.name == dep.name then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + package = package, + } + elseif not fallback then + fallback = package + end + end + end + + -- Otherwise, find the first mod that fulfils it + if fallback then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + package = fallback, + } + end + + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + } + end + + for _, dep in pairs(raw_deps) do + if not dep.is_optional and not out[dep.name] then + local result = resolve_dep(dep) + out[dep.name] = result + if result and result.package and not result.installed then + local raw_deps2 = get_raw_dependencies(result.package) + if raw_deps2 then + resolve_dependencies_2(raw_deps2, installed_mods, out) + end + end + end + end + + return true +end + +-- Resolve dependencies for a package, calls the recursive version. +local function resolve_dependencies(raw_deps, game) + assert(game) + + local installed_mods = {} + + local mods = {} + pkgmgr.get_game_mods(game, mods) + for _, mod in pairs(mods) do + installed_mods[mod.name] = true + end + + for _, mod in pairs(pkgmgr.global_mods:get_list()) do + installed_mods[mod.name] = true + end + + local out = {} + if not resolve_dependencies_2(raw_deps, installed_mods, out) then + return nil + end + + local retval = {} + for _, dep in pairs(out) do + retval[#retval + 1] = dep + end + + table.sort(retval, function(a, b) + return a.name < b.name + end) + + return retval +end + +local install_dialog = {} +function install_dialog.get_formspec() + local package = install_dialog.package + local raw_deps = install_dialog.raw_deps + local will_install_deps = install_dialog.will_install_deps + + local selected_game_idx = 1 + local selected_gameid = core.settings:get("menu_last_game") + local games = table.copy(pkgmgr.games) + for i=1, #games do + if selected_gameid and games[i].id == selected_gameid then + selected_game_idx = i + end + + games[i] = core.formspec_escape(games[i].name) + end + + local selected_game = pkgmgr.games[selected_game_idx] + local deps_to_install = 0 + local deps_not_found = 0 + + install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game) + local formatted_deps = {} + for _, dep in pairs(install_dialog.dependencies) do + formatted_deps[#formatted_deps + 1] = "#fff" + formatted_deps[#formatted_deps + 1] = core.formspec_escape(dep.name) + if dep.installed then + formatted_deps[#formatted_deps + 1] = "#ccf" + formatted_deps[#formatted_deps + 1] = fgettext("Already installed") + elseif dep.package then + formatted_deps[#formatted_deps + 1] = "#cfc" + formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author) + deps_to_install = deps_to_install + 1 + else + formatted_deps[#formatted_deps + 1] = "#f00" + formatted_deps[#formatted_deps + 1] = fgettext("Not found") + deps_not_found = deps_not_found + 1 + end + end + + local message_bg = "#3333" + local message + if will_install_deps then + message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install) + else + message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install) + end + if deps_not_found > 0 then + message = fgettext("$1 required dependencies could not be found.", deps_not_found) .. + " " .. fgettext("Please check that the base game is correct.", deps_not_found) .. + "\n" .. message + message_bg = mt_color_orange + end + + local formspec = { + "formspec_version[3]", + "size[7,7.85]", + "style[title;border=false]", + "box[0,0;7,0.5;#3333]", + "button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]", + + "container[0.375,0.70]", + + "label[0,0.25;", fgettext("Base Game:"), "]", + "dropdown[2,0;4.25,0.5;gameid;", table.concat(games, ","), ";", selected_game_idx, "]", + + "label[0,0.8;", fgettext("Dependencies:"), "]", + + "tablecolumns[color;text;color;text]", + "table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]", + + "container_end[]", + + "checkbox[0.375,5.1;will_install_deps;", + fgettext("Install missing dependencies"), ";", + will_install_deps and "true" or "false", "]", + + "box[0,5.4;7,1.2;", message_bg, "]", + "textarea[0.375,5.5;6.25,1;;;", message, "]", + + "container[1.375,6.85]", + "button[0,0;2,0.8;install_all;", fgettext("Install"), "]", + "button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]", + "container_end[]", + } + + return table.concat(formspec, "") +end + +function install_dialog.handle_submit(this, fields) + if fields.cancel then + this:delete() + return true + end + + if fields.will_install_deps ~= nil then + install_dialog.will_install_deps = core.is_yes(fields.will_install_deps) + return true + end + + if fields.install_all then + queue_download(install_dialog.package) + + if install_dialog.will_install_deps then + for _, dep in pairs(install_dialog.dependencies) do + if not dep.is_optional and not dep.installed and dep.package then + queue_download(dep.package) + end + end + end + + this:delete() + return true + end + + if fields.gameid then + for _, game in pairs(pkgmgr.games) do + if game.name == fields.gameid then + core.settings:set("menu_last_game", game.id) + break + end + end + return true + end + + return false +end + +function install_dialog.create(package, raw_deps) + install_dialog.dependencies = nil + install_dialog.package = package + install_dialog.raw_deps = raw_deps + install_dialog.will_install_deps = true + return dialog_create("install_dialog", + install_dialog.get_formspec, + install_dialog.handle_submit, + nil) +end + + +local confirm_overwrite = {} +function confirm_overwrite.get_formspec() + local package = confirm_overwrite.package + + return "size[11.5,4.5,true]" .. + "label[2,2;" .. + fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name) .. "]".. + "style[install;bgcolor=red]" .. + "button[3.25,3.5;2.5,0.5;install;" .. fgettext("Overwrite") .. "]" .. + "button[5.75,3.5;2.5,0.5;cancel;" .. fgettext("Cancel") .. "]" +end + +function confirm_overwrite.handle_submit(this, fields) + if fields.cancel then + this:delete() + return true + end + + if fields.install then + this:delete() + confirm_overwrite.callback() + return true + end + + return false +end + +function confirm_overwrite.create(package, callback) + assert(type(package) == "table") + assert(type(callback) == "function") + + confirm_overwrite.package = package + confirm_overwrite.callback = callback + return dialog_create("confirm_overwrite", + confirm_overwrite.get_formspec, + confirm_overwrite.handle_submit, + nil) +end + + local function get_file_extension(path) local parts = path:split(".") return parts[#parts] @@ -226,7 +553,7 @@ function store.load() end end - local timeout = tonumber(minetest.settings:get("curl_file_download_timeout")) + local timeout = tonumber(core.settings:get("curl_file_download_timeout")) local response = http.fetch_sync({ url = url, timeout = timeout }) if not response.succeeded then return @@ -247,6 +574,7 @@ function store.load() end end + store.packages_full_unordered = store.packages_full store.packages = store.packages_full store.loaded = true end @@ -255,7 +583,7 @@ function store.update_paths() local mod_hash = {} pkgmgr.refresh_globals() for _, mod in pairs(pkgmgr.global_mods:get_list()) do - if mod.author then + if mod.author and mod.release > 0 then mod_hash[mod.author:lower() .. "/" .. mod.name] = mod end end @@ -263,14 +591,14 @@ function store.update_paths() local game_hash = {} pkgmgr.update_gamelist() for _, game in pairs(pkgmgr.games) do - if game.author ~= "" then + if game.author ~= "" and game.release > 0 then game_hash[game.author:lower() .. "/" .. game.id] = game end end local txp_hash = {} for _, txp in pairs(pkgmgr.get_texture_packs()) do - if txp.author then + if txp.author and txp.release > 0 then txp_hash[txp.author:lower() .. "/" .. txp.name] = txp end end @@ -294,6 +622,33 @@ function store.update_paths() end end +function store.sort_packages() + local ret = {} + + -- Add installed content + for i=1, #store.packages_full_unordered do + local package = store.packages_full_unordered[i] + if package.path then + ret[#ret + 1] = package + end + end + + -- Sort installed content by title + table.sort(ret, function(a, b) + return a.title < b.title + end) + + -- Add uninstalled content + for i=1, #store.packages_full_unordered do + local package = store.packages_full_unordered[i] + if not package.path then + ret[#ret + 1] = package + end + end + + store.packages_full = ret +end + function store.filter_packages(query) if query == "" and filter_type == 1 then store.packages = store.packages_full @@ -327,7 +682,6 @@ function store.filter_packages(query) store.packages[#store.packages + 1] = package end end - end function store.get_formspec(dlgdata) @@ -340,7 +694,6 @@ function store.get_formspec(dlgdata) local W = 15.75 local H = 9.5 - local formspec if #store.packages_full > 0 then formspec = { @@ -348,12 +701,13 @@ function store.get_formspec(dlgdata) "size[15.75,9.5]", "position[0.5,0.55]", - "style[status;border=false]", + "style[status,downloading,queued;border=false]", "container[0.375,0.375]", "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", "field_close_on_enter[search_string;false]", - "button[7.225,0;2,0.8;search;", fgettext("Search"), "]", + "image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", + "image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", "dropdown[9.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", "container_end[]", @@ -374,7 +728,7 @@ function store.get_formspec(dlgdata) } if number_downloading > 0 then - formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;" + formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;downloading;" if #download_queue > 0 then formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue) else @@ -418,11 +772,17 @@ function store.get_formspec(dlgdata) } end + -- download/queued tooltips always have the same message + local tooltip_colors = ";#dff6f5;#302c2e]" + formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors + formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors + local start_idx = (cur_page - 1) * num_per_page + 1 for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do local package = store.packages[i] + local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8) formspec[#formspec + 1] = "container[0.375," - formspec[#formspec + 1] = (i - start_idx) * 1.375 + (2*0.375 + 0.8) + formspec[#formspec + 1] = container_y formspec[#formspec + 1] = "]" -- image @@ -433,57 +793,55 @@ function store.get_formspec(dlgdata) -- title formspec[#formspec + 1] = "label[1.875,0.1;" formspec[#formspec + 1] = core.formspec_escape( - minetest.colorize(mt_color_green, package.title) .. - minetest.colorize("#BFBFBF", " by " .. package.author)) + core.colorize(mt_color_green, package.title) .. + core.colorize("#BFBFBF", " by " .. package.author)) formspec[#formspec + 1] = "]" -- buttons - local description_width = W - 0.375*5 - 1 - 2*1.5 + local left_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) formspec[#formspec + 1] = "container[" formspec[#formspec + 1] = W - 0.375*2 formspec[#formspec + 1] = ",0.1]" if package.downloading then - formspec[#formspec + 1] = "button[-3.5,0;2,0.8;status;" - formspec[#formspec + 1] = fgettext("Downloading...") - formspec[#formspec + 1] = "]" + formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;" + formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" elseif package.queued then - formspec[#formspec + 1] = "button[-3.5,0;2,0.8;status;" - formspec[#formspec + 1] = fgettext("Queued") - formspec[#formspec + 1] = "]" + formspec[#formspec + 1] = left_base + formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_queued.png;queued]" elseif not package.path then - formspec[#formspec + 1] = "button[-3,0;1.5,0.8;install_" - formspec[#formspec + 1] = tostring(i) - formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = fgettext("Install") - formspec[#formspec + 1] = "]" + local elem_name = "install_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]" + formspec[#formspec + 1] = left_base .. "cdb_add.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors else if package.installed_release < package.release then - description_width = description_width - 1.5 -- The install_ action also handles updating - formspec[#formspec + 1] = "button[-4.5,0;1.5,0.8;install_" - formspec[#formspec + 1] = tostring(i) - formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = fgettext("Update") - formspec[#formspec + 1] = "]" - end + local elem_name = "install_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]" + formspec[#formspec + 1] = left_base .. "cdb_update.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors + else - formspec[#formspec + 1] = "button[-3,0;1.5,0.8;uninstall_" - formspec[#formspec + 1] = tostring(i) - formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = fgettext("Uninstall") - formspec[#formspec + 1] = "]" + local elem_name = "uninstall_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" + formspec[#formspec + 1] = left_base .. "cdb_clear.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors + end end - formspec[#formspec + 1] = "button[-1.5,0;1.5,0.8;view_" - formspec[#formspec + 1] = tostring(i) - formspec[#formspec + 1] = ";" - formspec[#formspec + 1] = fgettext("View") - formspec[#formspec + 1] = "]" + local web_elem_name = "view_" .. i .. ";" + formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" .. + core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. web_elem_name .. + fgettext("View more information in a web browser") .. tooltip_colors formspec[#formspec + 1] = "container_end[]" -- description + local description_width = W - 0.375*5 - 0.85 - 2*0.7 formspec[#formspec + 1] = "textarea[1.855,0.3;" formspec[#formspec + 1] = tostring(description_width) formspec[#formspec + 1] = ",0.8;;;" @@ -504,6 +862,13 @@ function store.handle_submit(this, fields) return true end + if fields.clear then + search_string = "" + cur_page = 1 + store.filter_packages("") + return true + end + if fields.back then this:delete() return true @@ -563,15 +928,47 @@ function store.handle_submit(this, fields) assert(package) if fields["install_" .. i] then - queue_download(package) + local install_parent + if package.type == "mod" then + install_parent = core.get_modpath() + elseif package.type == "game" then + install_parent = core.get_gamepath() + elseif package.type == "txp" then + install_parent = core.get_texturepath() + else + error("Unknown package type: " .. package.type) + end + + + local function on_confirm() + local deps = get_raw_dependencies(package) + if deps and has_hard_deps(deps) then + local dlg = install_dialog.create(package, deps) + dlg:set_parent(this) + this:hide() + dlg:show() + else + queue_download(package) + end + end + + if not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then + local dlg = confirm_overwrite.create(package, on_confirm) + dlg:set_parent(this) + this:hide() + dlg:show() + else + on_confirm() + end + return true end if fields["uninstall_" .. i] then - local dlg_delmod = create_delete_content_dlg(package) - dlg_delmod:set_parent(this) + local dlg = create_delete_content_dlg(package) + dlg:set_parent(this) this:hide() - dlg_delmod:show() + dlg:show() return true end @@ -592,6 +989,9 @@ function create_store_dlg(type) store.load() end + store.update_paths() + store.sort_packages() + search_string = "" cur_page = 1 diff --git a/builtin/mainmenu/dlg_create_world.lua b/builtin/mainmenu/dlg_create_world.lua index b2e706b6b..1938747fe 100644 --- a/builtin/mainmenu/dlg_create_world.lua +++ b/builtin/mainmenu/dlg_create_world.lua @@ -98,7 +98,7 @@ local function create_world_formspec(dialogdata) -- Error out when no games found if #pkgmgr.games == 0 then return "size[12.25,3,true]" .. - "box[0,0;12,2;#ff8800]" .. + "box[0,0;12,2;" .. mt_color_orange .. "]" .. "textarea[0.3,0;11.7,2;;;".. fgettext("You have no games installed.") .. "\n" .. fgettext("Download one from minetest.net") .. "]" .. @@ -363,10 +363,18 @@ local function create_world_buttonhandler(this, fields) local gameindex = core.get_textlist_index("games") if gameindex ~= nil then + -- For unnamed worlds use the generated name 'world<number>', + -- where the number increments: it is set to 1 larger than the largest + -- generated name number found. if worldname == "" then - local random_number = math.random(10000, 99999) - local random_world_name = "Unnamed" .. random_number - worldname = random_world_name + local worldnum_max = 0 + for _, world in ipairs(menudata.worldlist:get_list()) do + if world.name:match("^world%d+$") then + local worldnum = tonumber(world.name:sub(6)) + worldnum_max = math.max(worldnum_max, worldnum) + end + end + worldname = "world" .. worldnum_max + 1 end core.settings:set("fixed_map_seed", fields["te_seed"]) @@ -435,7 +443,7 @@ local function create_world_buttonhandler(this, fields) end if fields["mgv6_biomes"] then - local entry = minetest.formspec_escape(fields["mgv6_biomes"]) + local entry = core.formspec_escape(fields["mgv6_biomes"]) for b=1, #mgv6_biomes do if entry == mgv6_biomes[b][1] then local ftable = core.settings:get_flags("mgv6_spflags") diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua index 96d02d06c..45089c7c9 100644 --- a/builtin/mainmenu/init.lua +++ b/builtin/mainmenu/init.lua @@ -19,6 +19,7 @@ mt_color_grey = "#AAAAAA" mt_color_blue = "#6389FF" mt_color_green = "#72FF63" mt_color_dark_green = "#25C191" +mt_color_orange = "#FF8800" local menupath = core.get_mainmenu_path() local basepath = core.get_builtin_path() @@ -33,6 +34,7 @@ dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua") dofile(menupath .. DIR_DELIM .. "async_event.lua") dofile(menupath .. DIR_DELIM .. "common.lua") dofile(menupath .. DIR_DELIM .. "pkgmgr.lua") +dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua") dofile(menupath .. DIR_DELIM .. "textures.lua") dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua") @@ -105,6 +107,16 @@ local function init_globals() if last_tab and tv_main.current_tab ~= last_tab then tv_main:set_tab(last_tab) end + + -- In case the folder of the last selected game has been deleted, + -- display "Minetest" as a header + if tv_main.current_tab == "local" then + local game = pkgmgr.find_by_gameid(core.settings:get("menu_last_game")) + if game == nil then + mm_texture.reset() + end + end + ui.set_default("maintab") tv_main:show() diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/pkgmgr.lua index 5b8807310..787936e31 100644 --- a/builtin/mainmenu/pkgmgr.lua +++ b/builtin/mainmenu/pkgmgr.lua @@ -72,6 +72,34 @@ local function cleanup_path(temppath) return temppath end +local function load_texture_packs(txtpath, retval) + local list = core.get_dir_list(txtpath, true) + local current_texture_path = core.settings:get("texture_path") + + for _, item in ipairs(list) do + if item ~= "base" then + local name = item + + local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM + if path == current_texture_path then + name = fgettext("$1 (Enabled)", name) + end + + local conf = Settings(path .. "texture_pack.conf") + + retval[#retval + 1] = { + name = item, + author = conf:get("author"), + release = tonumber(conf:get("release")) or 0, + list_name = name, + type = "txp", + path = path, + enabled = path == current_texture_path, + } + end + end +end + function get_mods(path,retval,modpack) local mods = core.get_dir_list(path, true) @@ -107,12 +135,12 @@ function get_mods(path,retval,modpack) -- Read from config toadd.name = name toadd.author = mod_conf.author - toadd.release = tonumber(mod_conf.release or "0") + toadd.release = tonumber(mod_conf.release) or 0 toadd.path = prefix toadd.type = "mod" -- Check modpack.txt - -- Note: modpack.conf is already checked above + -- Note: modpack.conf is already checked above local modpackfile = io.open(prefix .. DIR_DELIM .. "modpack.txt") if modpackfile then modpackfile:close() @@ -136,32 +164,13 @@ pkgmgr = {} function pkgmgr.get_texture_packs() local txtpath = core.get_texturepath() - local list = core.get_dir_list(txtpath, true) + local txtpath_system = core.get_texturepath_share() local retval = {} - local current_texture_path = core.settings:get("texture_path") - - for _, item in ipairs(list) do - if item ~= "base" then - local name = item - - local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM - if path == current_texture_path then - name = fgettext("$1 (Enabled)", name) - end - - local conf = Settings(path .. "texture_pack.conf") - - retval[#retval + 1] = { - name = item, - author = conf:get("author"), - release = tonumber(conf:get("release") or "0"), - list_name = name, - type = "txp", - path = path, - enabled = path == current_texture_path, - } - end + load_texture_packs(txtpath, retval) + -- on portable versions these two paths coincide. It avoids loading the path twice + if txtpath ~= txtpath_system then + load_texture_packs(txtpath_system, retval) end table.sort(retval, function(a, b) @@ -404,18 +413,7 @@ function pkgmgr.is_modpack_entirely_enabled(data, name) end ---------- toggles or en/disables a mod or modpack and its dependencies -------- -function pkgmgr.enable_mod(this, toset) - local list = this.data.list:get_list() - local mod = list[this.data.selected_mod] - - -- Game mods can't be enabled or disabled - if mod.is_game_content then - return - end - - local toggled_mods = {} - - local enabled_mods = {} +local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod) if not mod.is_modpack then -- Toggle or en/disable the mod if toset == nil then @@ -434,23 +432,29 @@ function pkgmgr.enable_mod(this, toset) -- interleaved unsupported for i = 1, #list do if list[i].modpack == mod.name then - if toset == nil then - toset = not list[i].enabled - end - if list[i].enabled ~= toset then - list[i].enabled = toset - toggled_mods[#toggled_mods+1] = list[i].name - end - if toset then - enabled_mods[list[i].name] = true - end + toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, list[i]) end end end +end + +function pkgmgr.enable_mod(this, toset) + local list = this.data.list:get_list() + local mod = list[this.data.selected_mod] + + -- Game mods can't be enabled or disabled + if mod.is_game_content then + return + end + + local toggled_mods = {} + local enabled_mods = {} + toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod) + if not toset then -- Mod(s) were disabled, so no dependencies need to be enabled table.sort(toggled_mods) - minetest.log("info", "Following mods were disabled: " .. + core.log("info", "Following mods were disabled: " .. table.concat(toggled_mods, ", ")) return end @@ -487,7 +491,7 @@ function pkgmgr.enable_mod(this, toset) enabled_mods[name] = true local mod_to_enable = list[mod_ids[name]] if not mod_to_enable then - minetest.log("warning", "Mod dependency \"" .. name .. + core.log("warning", "Mod dependency \"" .. name .. "\" not found!") else if mod_to_enable.enabled == false then @@ -508,7 +512,7 @@ function pkgmgr.enable_mod(this, toset) -- Log the list of enabled mods table.sort(toggled_mods) - minetest.log("info", "Following mods were enabled: " .. + core.log("info", "Following mods were enabled: " .. table.concat(toggled_mods, ", ")) end diff --git a/builtin/mainmenu/serverlistmgr.lua b/builtin/mainmenu/serverlistmgr.lua new file mode 100644 index 000000000..9876d8ac5 --- /dev/null +++ b/builtin/mainmenu/serverlistmgr.lua @@ -0,0 +1,250 @@ +--Minetest +--Copyright (C) 2020 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +serverlistmgr = {} + +-------------------------------------------------------------------------------- +local function order_server_list(list) + local res = {} + --orders the favorite list after support + for i = 1, #list do + local fav = list[i] + if is_server_protocol_compat(fav.proto_min, fav.proto_max) then + res[#res + 1] = fav + end + end + for i = 1, #list do + local fav = list[i] + if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then + res[#res + 1] = fav + end + end + return res +end + +local public_downloading = false + +-------------------------------------------------------------------------------- +function serverlistmgr.sync() + if not serverlistmgr.servers then + serverlistmgr.servers = {{ + name = fgettext("Loading..."), + description = fgettext_ne("Try reenabling public serverlist and check your internet connection.") + }} + end + + local serverlist_url = core.settings:get("serverlist_url") or "" + if not core.get_http_api or serverlist_url == "" then + serverlistmgr.servers = {{ + name = fgettext("Public server list is disabled"), + description = "" + }} + return + end + + if public_downloading then + return + end + public_downloading = true + + core.handle_async( + function(param) + local http = core.get_http_api() + local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format( + core.settings:get("serverlist_url"), + core.get_min_supp_proto(), + core.get_max_supp_proto()) + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return {} + end + + local retval = core.parse_json(response.data) + return retval and retval.list or {} + end, + nil, + function(result) + public_downloading = nil + local favs = order_server_list(result) + if favs[1] then + serverlistmgr.servers = favs + end + core.event_handler("Refresh") + end + ) +end + +-------------------------------------------------------------------------------- +local function get_favorites_path() + local base = core.get_user_path() .. DIR_DELIM .. "client" .. DIR_DELIM .. "serverlist" .. DIR_DELIM + return base .. core.settings:get("serverlist_file") +end + +-------------------------------------------------------------------------------- +local function save_favorites(favorites) + local filename = core.settings:get("serverlist_file") + -- If setting specifies legacy format change the filename to the new one + if filename:sub(#filename - 3):lower() == ".txt" then + core.settings:set("serverlist_file", filename:sub(1, #filename - 4) .. ".json") + end + + local path = get_favorites_path() + core.create_dir(path) + core.safe_file_write(path, core.write_json(favorites)) +end + +-------------------------------------------------------------------------------- +function serverlistmgr.read_legacy_favorites(path) + local file = io.open(path, "r") + if not file then + return nil + end + + local lines = {} + for line in file:lines() do + lines[#lines + 1] = line + end + file:close() + + local favorites = {} + + local i = 1 + while i < #lines do + local function pop() + local line = lines[i] + i = i + 1 + return line and line:trim() + end + + if pop():lower() == "[server]" then + local name = pop() + local address = pop() + local port = tonumber(pop()) + local description = pop() + + if name == "" then + name = nil + end + + if description == "" then + description = nil + end + + if not address or #address < 3 then + core.log("warning", "Malformed favorites file, missing address at line " .. i) + elseif not port or port < 1 or port > 65535 then + core.log("warning", "Malformed favorites file, missing port at line " .. i) + elseif (name and name:upper() == "[SERVER]") or + (address and address:upper() == "[SERVER]") or + (description and description:upper() == "[SERVER]") then + core.log("warning", "Potentially malformed favorites file, overran at line " .. i) + else + favorites[#favorites + 1] = { + name = name, + address = address, + port = port, + description = description + } + end + end + end + + return favorites +end + +-------------------------------------------------------------------------------- +local function read_favorites() + local path = get_favorites_path() + + -- If new format configured fall back to reading the legacy file + if path:sub(#path - 4):lower() == ".json" then + local file = io.open(path, "r") + if file then + local json = file:read("*all") + file:close() + return core.parse_json(json) + end + + path = path:sub(1, #path - 5) .. ".txt" + end + + local favs = serverlistmgr.read_legacy_favorites(path) + if favs then + save_favorites(favs) + os.remove(path) + end + return favs +end + +-------------------------------------------------------------------------------- +local function delete_favorite(favorites, del_favorite) + for i=1, #favorites do + local fav = favorites[i] + + if fav.address == del_favorite.address and fav.port == del_favorite.port then + table.remove(favorites, i) + return + end + end +end + +-------------------------------------------------------------------------------- +function serverlistmgr.get_favorites() + if serverlistmgr.favorites then + return serverlistmgr.favorites + end + + serverlistmgr.favorites = {} + + -- Add favorites, removing duplicates + local seen = {} + for _, fav in ipairs(read_favorites() or {}) do + local key = ("%s:%d"):format(fav.address:lower(), fav.port) + if not seen[key] then + seen[key] = true + serverlistmgr.favorites[#serverlistmgr.favorites + 1] = fav + end + end + + return serverlistmgr.favorites +end + +-------------------------------------------------------------------------------- +function serverlistmgr.add_favorite(new_favorite) + assert(type(new_favorite.port) == "number") + + -- Whitelist favorite keys + new_favorite = { + name = new_favorite.name, + address = new_favorite.address, + port = new_favorite.port, + description = new_favorite.description, + } + + local favorites = serverlistmgr.get_favorites() + delete_favorite(favorites, new_favorite) + table.insert(favorites, 1, new_favorite) + save_favorites(favorites) +end + +-------------------------------------------------------------------------------- +function serverlistmgr.delete_favorite(del_favorite) + local favorites = serverlistmgr.get_favorites() + delete_favorite(favorites, del_favorite) + save_favorites(favorites) +end diff --git a/builtin/mainmenu/tab_content.lua b/builtin/mainmenu/tab_content.lua index 336730bf4..fb7f121f8 100644 --- a/builtin/mainmenu/tab_content.lua +++ b/builtin/mainmenu/tab_content.lua @@ -146,10 +146,25 @@ local function get_formspec(tabview, name, tabdata) end -------------------------------------------------------------------------------- +local function handle_doubleclick(pkg) + if pkg.type == "txp" then + if core.settings:get("texture_path") == pkg.path then + core.settings:set("texture_path", "") + else + core.settings:set("texture_path", pkg.path) + end + packages = nil + end +end + +-------------------------------------------------------------------------------- local function handle_buttons(tabview, fields, tabname, tabdata) if fields["pkglist"] ~= nil then local event = core.explode_table_event(fields["pkglist"]) tabdata.selected_pkg = event.row + if event.type == "DCL" then + handle_doubleclick(packages:get_list()[tabdata.selected_pkg]) + end return true end diff --git a/builtin/mainmenu/tab_credits.lua b/builtin/mainmenu/tab_credits.lua index c2b7e503a..a34dd58bb 100644 --- a/builtin/mainmenu/tab_credits.lua +++ b/builtin/mainmenu/tab_credits.lua @@ -23,28 +23,37 @@ local core_developers = { "Nathanaël Courant (Nore/Ekdohibs) <nore@mesecons.net>", "Loic Blot (nerzhul/nrz) <loic.blot@unix-experience.fr>", "paramat", - "Auke Kok (sofar) <sofar@foo-projects.org>", "Andrew Ward (rubenwardy) <rw@rubenwardy.com>", "Krock/SmallJoker <mk939@ymail.com>", "Lars Hofhansl <larsh@apache.org>", + "Pierre-Yves Rollo <dev@pyrollo.com>", + "v-rob <robinsonvincent89@gmail.com>", } +-- For updating active/previous contributors, see the script in ./util/gather_git_credits.py + local active_contributors = { - "Hugues Ross [Formspecs]", + "Wuzzy [devtest game, visual corrections]", + "Zughy [Visual improvements, various fixes]", "Maksim (MoNTE48) [Android]", - "DS [Formspecs]", - "pyrollo [Formspecs: Hypertext]", - "v-rob [Formspecs]", - "Jordach [set_sky]", - "random-geek [Formspecs]", - "Wuzzy [Pathfinder, builtin, translations]", - "ANAND (ClobberXD) [Fixes, per-player FOV]", - "Warr1024 [Fixes]", - "Paul Ouellette (pauloue) [Fixes, Script API]", - "Jean-Patrick G (kilbith) <jeanpatrick.guerrero@gmail.com> [Audiovisuals]", - "HybridDog [Script API]", + "numzero [Graphics and rendering]", + "appgurueu [Various internal fixes]", + "Desour [Formspec and vector API changes]", + "HybridDog [Rendering fixes and documentation]", + "Hugues Ross [Graphics-related improvements]", + "ANAND (ClobberXD) [Mouse buttons rebinding]", + "luk3yx [Fixes]", + "hecks [Audiovisuals, Lua API]", + "LoneWolfHT [Object crosshair, documentation fixes]", + "Lejo [Server-related improvements]", + "EvidenceB [Compass HUD element]", + "Paul Ouellette (pauloue) [Lua API, documentation]", + "TheTermos [Collision detection, physics]", + "David CARLIER [Unix & Haiku build fixes]", "dcbrwn [Object shading]", - "srifqi [Fixes]", + "Elias Fleckenstein [API features/fixes]", + "Jean-Patrick Guerrero (kilbith) [model element, visual fixes]", + "k.h.lai [Memory leak fixes, documentation]", } local previous_core_developers = { @@ -60,30 +69,23 @@ local previous_core_developers = { "sapier", "Zeno", "ShadowNinja <shadowninja@minetest.net>", + "Auke Kok (sofar) <sofar@foo-projects.org>", } local previous_contributors = { "Nils Dagsson Moskopp (erlehmann) <nils@dieweltistgarnichtso.net> [Minetest Logo]", - "Dániel Juhász (juhdanad) <juhdanad@gmail.com>", "red-001 <red-001@outlook.ie>", - "numberZero [Audiovisuals: meshgen]", "Giuseppe Bilotta", + "Dániel Juhász (juhdanad) <juhdanad@gmail.com>", "MirceaKitsune <mirceakitsune@gmail.com>", "Constantin Wenger (SpeedProg)", "Ciaran Gultnieks (CiaranG)", "stujones11 [Android UX improvements]", - "Jeija <jeija@mesecons.net> [HTTP, particles]", - "Vincent Glize (Dumbeldor) [Cleanups, CSM APIs]", - "Ben Deutsch [Rendering, Fixes, SQLite auth]", - "TeTpaAka [Hand overriding, nametag colors]", - "Rui [Sound Pitch]", - "Duane Robertson <duane@duanerobertson.com> [MGValleys]", - "Raymoo [Tool Capabilities]", "Rogier <rogier777@gmail.com> [Fixes]", "Gregory Currie (gregorycu) [optimisation]", - "TriBlade9 <triblade9@mail.com> [Audiovisuals]", - "T4im [Profiler]", - "Jurgen Doser (doserj) <jurgen.doser@gmail.com>", + "srifqi [Fixes]", + "JacobF", + "Jeija <jeija@mesecons.net> [HTTP, particles]", } local function buildCreditList(source) @@ -100,9 +102,10 @@ return { cbf_formspec = function(tabview, name, tabdata) local logofile = defaulttexturedir .. "logo.png" local version = core.get_version() - return "image[0.5,1;" .. core.formspec_escape(logofile) .. "]" .. - "label[0.5,2.8;" .. version.project .. " " .. version.string .. "]" .. - "button[0.5,3;2,2;homepage;minetest.net]" .. + local fs = "image[0.75,0.5;2.2,2.2;" .. core.formspec_escape(logofile) .. "]" .. + "style[label_button;border=false]" .. + "button[0.5,2;2.5,2;label_button;" .. version.project .. " " .. version.string .. "]" .. + "button[0.75,2.75;2,2;homepage;minetest.net]" .. "tablecolumns[color;text]" .. "tableoptions[background=#00000000;highlight=#00000000;border=false]" .. "table[3.5,-0.25;8.5,6.05;list_credits;" .. @@ -115,10 +118,23 @@ return { "#FFFF00," .. fgettext("Previous Contributors") .. ",," .. buildCreditList(previous_contributors) .. "," .. ";1]" + + if PLATFORM ~= "Android" then + fs = fs .. "tooltip[userdata;" .. + fgettext("Opens the directory that contains user-provided worlds, games, mods,\n" .. + "and texture packs in a file manager / explorer.") .. "]" + fs = fs .. "button[0,4.75;3.5,1;userdata;" .. fgettext("Open User Data Directory") .. "]" + end + + return fs end, cbf_button_handler = function(this, fields, name, tabdata) if fields.homepage then core.open_url("https://www.minetest.net") end + + if fields.userdata then + core.open_dir(core.get_user_path()) + end end, } diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua index 1aee246fc..0e06c3bef 100644 --- a/builtin/mainmenu/tab_local.lua +++ b/builtin/mainmenu/tab_local.lua @@ -18,6 +18,7 @@ local enable_gamebar = PLATFORM ~= "Android" local current_game, singleplayer_refresh_gamebar + if enable_gamebar then function current_game() local last_game_id = core.settings:get("menu_last_game") @@ -115,7 +116,7 @@ local function get_formspec(tabview, name, tabdata) retval = retval .. "button[3.9,3.8;2.8,1;world_delete;".. fgettext("Delete") .. "]" .. - "button[6.55,3.8;2.8,1;world_configure;".. fgettext("Configure") .. "]" .. + "button[6.55,3.8;2.8,1;world_configure;".. fgettext("Select Mods") .. "]" .. "button[9.2,3.8;2.8,1;world_create;".. fgettext("New") .. "]" .. "label[3.9,-0.05;".. fgettext("Select World:") .. "]".. "checkbox[0,-0.20;cb_creative_mode;".. fgettext("Creative Mode") .. ";" .. diff --git a/builtin/mainmenu/tab_online.lua b/builtin/mainmenu/tab_online.lua index 7985fd84a..fb7409864 100644 --- a/builtin/mainmenu/tab_online.lua +++ b/builtin/mainmenu/tab_online.lua @@ -15,17 +15,50 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. --------------------------------------------------------------------------------- +local function get_sorted_servers() + local servers = { + fav = {}, + public = {}, + incompatible = {} + } + + local favs = serverlistmgr.get_favorites() + local taken_favs = {} + local result = menudata.search_result or serverlistmgr.servers + for _, server in ipairs(result) do + server.is_favorite = false + for index, fav in ipairs(favs) do + if server.address == fav.address and server.port == fav.port then + taken_favs[index] = true + server.is_favorite = true + break + end + end + server.is_compatible = is_server_protocol_compat(server.proto_min, server.proto_max) + if server.is_favorite then + table.insert(servers.fav, server) + elseif server.is_compatible then + table.insert(servers.public, server) + else + table.insert(servers.incompatible, server) + end + end + + if not menudata.search_result then + for index, fav in ipairs(favs) do + if not taken_favs[index] then + table.insert(servers.fav, fav) + end + end + end + + return servers +end + local function get_formspec(tabview, name, tabdata) -- Update the cached supported proto info, -- it may have changed after a change by the settings menu. common_update_cached_supp_proto() - local fav_selected - if menudata.search_result then - fav_selected = menudata.search_result[tabdata.fav_selected] - else - fav_selected = menudata.favorites[tabdata.fav_selected] - end if not tabdata.search_for then tabdata.search_for = "" @@ -33,128 +66,221 @@ local function get_formspec(tabview, name, tabdata) local retval = -- Search - "field[0.15,0.075;5.91,1;te_search;;" .. core.formspec_escape(tabdata.search_for) .. "]" .. - "button[5.62,-0.25;1.5,1;btn_mp_search;" .. fgettext("Search") .. "]" .. - "image_button[6.97,-.165;.83,.83;" .. core.formspec_escape(defaulttexturedir .. "refresh.png") - .. ";btn_mp_refresh;]" .. + "field[0.25,0.25;7,0.75;te_search;;" .. core.formspec_escape(tabdata.search_for) .. "]" .. + "container[7.25,0.25]" .. + "image_button[0,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "search.png") .. ";btn_mp_search;]" .. + "image_button[0.75,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "clear.png") .. ";btn_mp_clear;]" .. + "image_button[1.5,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "refresh.png") .. ";btn_mp_refresh;]" .. + "tooltip[btn_mp_clear;" .. fgettext("Clear") .. "]" .. + "tooltip[btn_mp_search;" .. fgettext("Search") .. "]" .. + "tooltip[btn_mp_refresh;" .. fgettext("Refresh") .. "]" .. + "container_end[]" .. + + "container[9.75,0]" .. + "box[0,0;5.75,7;#666666]" .. -- Address / Port - "label[7.75,-0.25;" .. fgettext("Address / Port") .. "]" .. - "field[8,0.65;3.25,0.5;te_address;;" .. + "label[0.25,0.35;" .. fgettext("Address") .. "]" .. + "label[4.25,0.35;" .. fgettext("Port") .. "]" .. + "field[0.25,0.5;4,0.75;te_address;;" .. core.formspec_escape(core.settings:get("address")) .. "]" .. - "field[11.1,0.65;1.4,0.5;te_port;;" .. + "field[4.25,0.5;1.25,0.75;te_port;;" .. core.formspec_escape(core.settings:get("remote_port")) .. "]" .. -- Name / Password - "label[7.75,0.95;" .. fgettext("Name / Password") .. "]" .. - "field[8,1.85;2.9,0.5;te_name;;" .. + "label[0.25,1.55;" .. fgettext("Name") .. "]" .. + "label[3,1.55;" .. fgettext("Password") .. "]" .. + "field[0.25,1.75;2.75,0.75;te_name;;" .. core.formspec_escape(core.settings:get("name")) .. "]" .. - "pwdfield[10.73,1.85;1.77,0.5;te_pwd;]" .. + "pwdfield[3,1.75;2.5,0.75;te_pwd;]" .. -- Description Background - "box[7.73,2.25;4.25,2.6;#999999]".. + "label[0.25,2.75;" .. fgettext("Server Description") .. "]" .. + "box[0.25,3;5.25,2.75;#999999]".. -- Connect - "button[9.88,4.9;2.3,1;btn_mp_connect;" .. fgettext("Connect") .. "]" + "button[3,6;2.5,0.75;btn_mp_connect;" .. fgettext("Connect") .. "]" - if tabdata.fav_selected and fav_selected then + if tabdata.selected then if gamedata.fav then - retval = retval .. "button[7.73,4.9;2.3,1;btn_delete_favorite;" .. + retval = retval .. "button[0.25,6;2.5,0.75;btn_delete_favorite;" .. fgettext("Del. Favorite") .. "]" end - if fav_selected.description then - retval = retval .. "textarea[8.1,2.3;4.23,2.9;;;" .. - core.formspec_escape((gamedata.serverdescription or ""), true) .. "]" + if gamedata.serverdescription then + retval = retval .. "textarea[0.25,3;5.25,2.75;;;" .. + core.formspec_escape(gamedata.serverdescription) .. "]" end end - --favourites + retval = retval .. "container_end[]" + + -- Table retval = retval .. "tablecolumns[" .. - image_column(fgettext("Favorite"), "favorite") .. ";" .. - image_column(fgettext("Ping")) .. ",padding=0.25;" .. - "color,span=3;" .. - "text,align=right;" .. -- clients - "text,align=center,padding=0.25;" .. -- "/" - "text,align=right,padding=0.25;" .. -- clients_max - image_column(fgettext("Creative mode"), "creative") .. ",padding=1;" .. - image_column(fgettext("Damage enabled"), "damage") .. ",padding=0.25;" .. - --~ PvP = Player versus Player - image_column(fgettext("PvP enabled"), "pvp") .. ",padding=0.25;" .. + "image,tooltip=" .. fgettext("Ping") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_ping_4.png") .. "," .. + "2=" .. core.formspec_escape(defaulttexturedir .. "server_ping_3.png") .. "," .. + "3=" .. core.formspec_escape(defaulttexturedir .. "server_ping_2.png") .. "," .. + "4=" .. core.formspec_escape(defaulttexturedir .. "server_ping_1.png") .. "," .. + "5=" .. core.formspec_escape(defaulttexturedir .. "server_favorite.png") .. "," .. + "6=" .. core.formspec_escape(defaulttexturedir .. "server_public.png") .. "," .. + "7=" .. core.formspec_escape(defaulttexturedir .. "server_incompatible.png") .. ";" .. "color,span=1;" .. - "text,padding=1]" .. - "table[-0.15,0.6;7.75,5.15;favourites;" - - if menudata.search_result then - for i = 1, #menudata.search_result do - local favs = core.get_favorites("local") - local server = menudata.search_result[i] - - for fav_id = 1, #favs do - if server.address == favs[fav_id].address and - server.port == favs[fav_id].port then - server.is_favorite = true - end + "text,align=inline;".. + "color,span=1;" .. + "text,align=inline,width=4.25;" .. + "image,tooltip=" .. fgettext("Creative mode") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_creative.png") .. "," .. + "align=inline,padding=0.25,width=1.5;" .. + --~ PvP = Player versus Player + "image,tooltip=" .. fgettext("Damage / PvP") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_damage.png") .. "," .. + "2=" .. core.formspec_escape(defaulttexturedir .. "server_flags_pvp.png") .. "," .. + "align=inline,padding=0.25,width=1.5;" .. + "color,align=inline,span=1;" .. + "text,align=inline,padding=1]" .. + "table[0.25,1;9.25,5.75;servers;" + + local servers = get_sorted_servers() + + local dividers = { + fav = "5,#ffff00," .. fgettext("Favorites") .. ",,,0,0,,", + public = "6,#4bdd42," .. fgettext("Public Servers") .. ",,,0,0,,", + incompatible = "7,"..mt_color_grey.."," .. fgettext("Incompatible Servers") .. ",,,0,0,," + } + local order = {"fav", "public", "incompatible"} + + tabdata.lookup = {} -- maps row number to server + local rows = {} + for _, section in ipairs(order) do + local section_servers = servers[section] + if next(section_servers) ~= nil then + rows[#rows + 1] = dividers[section] + for _, server in ipairs(section_servers) do + tabdata.lookup[#rows + 1] = server + rows[#rows + 1] = render_serverlist_row(server) end + end + end - if i ~= 1 then - retval = retval .. "," - end + retval = retval .. table.concat(rows, ",") - retval = retval .. render_serverlist_row(server, server.is_favorite) - end - elseif #menudata.favorites > 0 then - local favs = core.get_favorites("local") - if #favs > 0 then - for i = 1, #favs do - for j = 1, #menudata.favorites do - if menudata.favorites[j].address == favs[i].address and - menudata.favorites[j].port == favs[i].port then - table.insert(menudata.favorites, i, table.remove(menudata.favorites, j)) - end + if tabdata.selected then + retval = retval .. ";" .. tabdata.selected .. "]" + else + retval = retval .. ";0]" + end + + return retval, "size[15.5,7,false]real_coordinates[true]" +end + +-------------------------------------------------------------------------------- + +local function search_server_list(input) + menudata.search_result = nil + if #serverlistmgr.servers < 2 then + return + end + + -- setup the keyword list + local keywords = {} + for word in input:gmatch("%S+") do + word = word:gsub("(%W)", "%%%1") + table.insert(keywords, word) + end + + if #keywords == 0 then + return + end + + menudata.search_result = {} + + -- Search the serverlist + local search_result = {} + for i = 1, #serverlistmgr.servers do + local server = serverlistmgr.servers[i] + local found = 0 + for k = 1, #keywords do + local keyword = keywords[k] + if server.name then + local sername = server.name:lower() + local _, count = sername:gsub(keyword, keyword) + found = found + count * 4 end - if favs[i].address ~= menudata.favorites[i].address then - table.insert(menudata.favorites, i, favs[i]) - end + + if server.description then + local desc = server.description:lower() + local _, count = desc:gsub(keyword, keyword) + found = found + count * 2 end end - retval = retval .. render_serverlist_row(menudata.favorites[1], (#favs > 0)) - for i = 2, #menudata.favorites do - retval = retval .. "," .. render_serverlist_row(menudata.favorites[i], (i <= #favs)) + if found > 0 then + local points = (#serverlistmgr.servers - i) / 5 + found + server.points = points + table.insert(search_result, server) end end - if tabdata.fav_selected then - retval = retval .. ";" .. tabdata.fav_selected .. "]" - else - retval = retval .. ";0]" + if #search_result == 0 then + return end - return retval + table.sort(search_result, function(a, b) + return a.points > b.points + end) + menudata.search_result = search_result end --------------------------------------------------------------------------------- -local function main_button_handler(tabview, fields, name, tabdata) - local serverlist = menudata.search_result or menudata.favorites +local function set_selected_server(tabdata, idx, server) + -- reset selection + if idx == nil or server == nil then + tabdata.selected = nil + + core.settings:set("address", "") + core.settings:set("remote_port", "30000") + return + end + + local address = server.address + local port = server.port + gamedata.serverdescription = server.description + gamedata.fav = false + for _, fav in ipairs(serverlistmgr.get_favorites()) do + if address == fav.address and port == fav.port then + gamedata.fav = true + break + end + end + + if address and port then + core.settings:set("address", address) + core.settings:set("remote_port", port) + end + tabdata.selected = idx +end + +local function main_button_handler(tabview, fields, name, tabdata) if fields.te_name then gamedata.playername = fields.te_name core.settings:set("name", fields.te_name) end - if fields.favourites then - local event = core.explode_table_event(fields.favourites) - local fav = serverlist[event.row] + if fields.servers then + local event = core.explode_table_event(fields.servers) + local server = tabdata.lookup[event.row] - if event.type == "DCL" then - if event.row <= #serverlist then - if menudata.favorites_is_public and - not is_server_protocol_compat_or_error( - fav.proto_min, fav.proto_max) then + if server then + if event.type == "DCL" then + if not is_server_protocol_compat_or_error( + server.proto_min, server.proto_max) then return true end - gamedata.address = fav.address - gamedata.port = fav.port + gamedata.address = server.address + gamedata.port = server.port gamedata.playername = fields.te_name gamedata.selected_world = 0 @@ -162,150 +288,54 @@ local function main_button_handler(tabview, fields, name, tabdata) gamedata.password = fields.te_pwd end - gamedata.servername = fav.name - gamedata.serverdescription = fav.description + gamedata.servername = server.name + gamedata.serverdescription = server.description if gamedata.address and gamedata.port then core.settings:set("address", gamedata.address) core.settings:set("remote_port", gamedata.port) core.start() end + return true end - return true - end - - if event.type == "CHG" then - if event.row <= #serverlist then - gamedata.fav = false - local favs = core.get_favorites("local") - local address = fav.address - local port = fav.port - gamedata.serverdescription = fav.description - - for i = 1, #favs do - if fav.address == favs[i].address and - fav.port == favs[i].port then - gamedata.fav = true - end - end - - if address and port then - core.settings:set("address", address) - core.settings:set("remote_port", port) - end - tabdata.fav_selected = event.row + if event.type == "CHG" then + set_selected_server(tabdata, event.row, server) + return true end - return true end end - if fields.key_up or fields.key_down then - local fav_idx = core.get_table_index("favourites") - local fav = serverlist[fav_idx] - - if fav_idx then - if fields.key_up and fav_idx > 1 then - fav_idx = fav_idx - 1 - elseif fields.key_down and fav_idx < #menudata.favorites then - fav_idx = fav_idx + 1 - end - else - fav_idx = 1 - end - - if not menudata.favorites or not fav then - tabdata.fav_selected = 0 - return true - end - - local address = fav.address - local port = fav.port - gamedata.serverdescription = fav.description - if address and port then - core.settings:set("address", address) - core.settings:set("remote_port", port) - end - - tabdata.fav_selected = fav_idx + if fields.btn_delete_favorite then + local idx = core.get_table_index("servers") + if not idx then return end + local server = tabdata.lookup[idx] + if not server then return end + + serverlistmgr.delete_favorite(server) + -- the server at [idx+1] will be at idx once list is refreshed + set_selected_server(tabdata, idx, tabdata.lookup[idx+1]) return true end - if fields.btn_delete_favorite then - local current_favourite = core.get_table_index("favourites") - if not current_favourite then return end - - core.delete_favorite(current_favourite) - asyncOnlineFavourites() - tabdata.fav_selected = nil - - core.settings:set("address", "") - core.settings:set("remote_port", "30000") + if fields.btn_mp_clear then + tabdata.search_for = "" + menudata.search_result = nil return true end if fields.btn_mp_search or fields.key_enter_field == "te_search" then - tabdata.fav_selected = 1 - local input = fields.te_search:lower() tabdata.search_for = fields.te_search - - if #menudata.favorites < 2 then - return true + search_server_list(fields.te_search:lower()) + if menudata.search_result then + -- first server in row 2 due to header + set_selected_server(tabdata, 2, menudata.search_result[1]) end - menudata.search_result = {} - - -- setup the keyword list - local keywords = {} - for word in input:gmatch("%S+") do - word = word:gsub("(%W)", "%%%1") - table.insert(keywords, word) - end - - if #keywords == 0 then - menudata.search_result = nil - return true - end - - -- Search the serverlist - local search_result = {} - for i = 1, #menudata.favorites do - local server = menudata.favorites[i] - local found = 0 - for k = 1, #keywords do - local keyword = keywords[k] - if server.name then - local sername = server.name:lower() - local _, count = sername:gsub(keyword, keyword) - found = found + count * 4 - end - - if server.description then - local desc = server.description:lower() - local _, count = desc:gsub(keyword, keyword) - found = found + count * 2 - end - end - if found > 0 then - local points = (#menudata.favorites - i) / 5 + found - server.points = points - table.insert(search_result, server) - end - end - if #search_result > 0 then - table.sort(search_result, function(a, b) - return a.points > b.points - end) - menudata.search_result = search_result - local first_server = search_result[1] - core.settings:set("address", first_server.address) - core.settings:set("remote_port", first_server.port) - gamedata.serverdescription = first_server.description - end return true end if fields.btn_mp_refresh then - asyncOnlineFavourites() + serverlistmgr.sync() return true end @@ -314,43 +344,51 @@ local function main_button_handler(tabview, fields, name, tabdata) gamedata.playername = fields.te_name gamedata.password = fields.te_pwd gamedata.address = fields.te_address - gamedata.port = fields.te_port + gamedata.port = tonumber(fields.te_port) gamedata.selected_world = 0 - local fav_idx = core.get_table_index("favourites") - local fav = serverlist[fav_idx] - if fav_idx and fav_idx <= #serverlist and - fav.address == fields.te_address and - fav.port == fields.te_port then + local idx = core.get_table_index("servers") + local server = idx and tabdata.lookup[idx] - gamedata.servername = fav.name - gamedata.serverdescription = fav.description + set_selected_server(tabdata) - if menudata.favorites_is_public and - not is_server_protocol_compat_or_error( - fav.proto_min, fav.proto_max) then + if server and server.address == gamedata.address and + server.port == gamedata.port then + + serverlistmgr.add_favorite(server) + + gamedata.servername = server.name + gamedata.serverdescription = server.description + + if not is_server_protocol_compat_or_error( + server.proto_min, server.proto_max) then return true end else gamedata.servername = "" gamedata.serverdescription = "" + + serverlistmgr.add_favorite({ + address = gamedata.address, + port = gamedata.port, + }) end - core.settings:set("address", fields.te_address) - core.settings:set("remote_port", fields.te_port) + core.settings:set("address", gamedata.address) + core.settings:set("remote_port", gamedata.port) core.start() return true end + return false end local function on_change(type, old_tab, new_tab) if type == "LEAVE" then return end - asyncOnlineFavourites() + serverlistmgr.sync() end --------------------------------------------------------------------------------- return { name = "online", caption = fgettext("Join Game"), diff --git a/builtin/mainmenu/tab_settings.lua b/builtin/mainmenu/tab_settings.lua index 510346f8d..29744048a 100644 --- a/builtin/mainmenu/tab_settings.lua +++ b/builtin/mainmenu/tab_settings.lua @@ -154,15 +154,18 @@ local function formspec(tabview, name, tabdata) "box[8,0;3.75,4.5;#999999]" local video_driver = core.settings:get("video_driver") - local shaders_supported = video_driver == "opengl" - local shaders_enabled = false - if shaders_supported then - shaders_enabled = core.settings:get_bool("enable_shaders") + local shaders_enabled = core.settings:get_bool("enable_shaders") + if video_driver == "opengl" then tab_string = tab_string .. "checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders") .. ";" .. tostring(shaders_enabled) .. "]" + elseif video_driver == "ogles2" then + tab_string = tab_string .. + "checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders (experimental)") .. ";" + .. tostring(shaders_enabled) .. "]" else core.settings:set_bool("enable_shaders", false) + shaders_enabled = false tab_string = tab_string .. "label[8.38,0.2;" .. core.colorize("#888888", fgettext("Shaders (unavailable)")) .. "]" @@ -187,31 +190,23 @@ local function formspec(tabview, name, tabdata) if shaders_enabled then tab_string = tab_string .. - "checkbox[8.25,0.5;cb_bumpmapping;" .. fgettext("Bump Mapping") .. ";" - .. dump(core.settings:get_bool("enable_bumpmapping")) .. "]" .. - "checkbox[8.25,1;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";" + "checkbox[8.25,0.5;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";" .. dump(core.settings:get_bool("tone_mapping")) .. "]" .. - "checkbox[8.25,1.5;cb_parallax;" .. fgettext("Parallax Occlusion") .. ";" - .. dump(core.settings:get_bool("enable_parallax_occlusion")) .. "]" .. - "checkbox[8.25,2;cb_waving_water;" .. fgettext("Waving Liquids") .. ";" + "checkbox[8.25,1;cb_waving_water;" .. fgettext("Waving Liquids") .. ";" .. dump(core.settings:get_bool("enable_waving_water")) .. "]" .. - "checkbox[8.25,2.5;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";" + "checkbox[8.25,1.5;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";" .. dump(core.settings:get_bool("enable_waving_leaves")) .. "]" .. - "checkbox[8.25,3;cb_waving_plants;" .. fgettext("Waving Plants") .. ";" + "checkbox[8.25,2;cb_waving_plants;" .. fgettext("Waving Plants") .. ";" .. dump(core.settings:get_bool("enable_waving_plants")) .. "]" else tab_string = tab_string .. "label[8.38,0.7;" .. core.colorize("#888888", - fgettext("Bump Mapping")) .. "]" .. - "label[8.38,1.2;" .. core.colorize("#888888", fgettext("Tone Mapping")) .. "]" .. - "label[8.38,1.7;" .. core.colorize("#888888", - fgettext("Parallax Occlusion")) .. "]" .. - "label[8.38,2.2;" .. core.colorize("#888888", + "label[8.38,1.2;" .. core.colorize("#888888", fgettext("Waving Liquids")) .. "]" .. - "label[8.38,2.7;" .. core.colorize("#888888", + "label[8.38,1.7;" .. core.colorize("#888888", fgettext("Waving Leaves")) .. "]" .. - "label[8.38,3.2;" .. core.colorize("#888888", + "label[8.38,2.2;" .. core.colorize("#888888", fgettext("Waving Plants")) .. "]" end @@ -263,18 +258,10 @@ local function handle_settings_buttons(this, fields, tabname, tabdata) end return true end - if fields["cb_bumpmapping"] then - core.settings:set("enable_bumpmapping", fields["cb_bumpmapping"]) - return true - end if fields["cb_tonemapping"] then core.settings:set("tone_mapping", fields["cb_tonemapping"]) return true end - if fields["cb_parallax"] then - core.settings:set("enable_parallax_occlusion", fields["cb_parallax"]) - return true - end if fields["cb_waving_water"] then core.settings:set("enable_waving_water", fields["cb_waving_water"]) return true diff --git a/builtin/mainmenu/tests/favorites_wellformed.txt b/builtin/mainmenu/tests/favorites_wellformed.txt new file mode 100644 index 000000000..8b87b4398 --- /dev/null +++ b/builtin/mainmenu/tests/favorites_wellformed.txt @@ -0,0 +1,29 @@ +[server] + +127.0.0.1 +30000 + + +[server] + +localhost +30000 + + +[server] + +vps.rubenwardy.com +30001 + + +[server] + +gundul.ddnss.de +39155 + + +[server] +VanessaE's Dreambuilder creative Server +daconcepts.com +30000 +VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets. diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua new file mode 100644 index 000000000..148e9b794 --- /dev/null +++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua @@ -0,0 +1,36 @@ +_G.core = {} +_G.unpack = table.unpack +_G.serverlistmgr = {} + +dofile("builtin/common/misc_helpers.lua") +dofile("builtin/mainmenu/serverlistmgr.lua") + +local base = "builtin/mainmenu/tests/" + +describe("legacy favorites", function() + it("loads well-formed correctly", function() + local favs = serverlistmgr.read_legacy_favorites(base .. "favorites_wellformed.txt") + + local expected = { + { + address = "127.0.0.1", + port = 30000, + }, + + { address = "localhost", port = 30000 }, + + { address = "vps.rubenwardy.com", port = 30001 }, + + { address = "gundul.ddnss.de", port = 39155 }, + + { + address = "daconcepts.com", + port = 30000, + name = "VanessaE's Dreambuilder creative Server", + description = "VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets." + }, + } + + assert.same(expected, favs) + end) +end) diff --git a/builtin/profiler/init.lua b/builtin/profiler/init.lua index a0033d752..7f63dfaea 100644 --- a/builtin/profiler/init.lua +++ b/builtin/profiler/init.lua @@ -15,6 +15,8 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +local S = core.get_translator("__builtin") + local function get_bool_default(name, default) local val = core.settings:get_bool(name) if val == nil then @@ -40,9 +42,9 @@ function profiler.init_chatcommand() instrumentation.init_chatcommand() end - local param_usage = "print [filter] | dump [filter] | save [format [filter]] | reset" + local param_usage = S("print [<filter>] | dump [<filter>] | save [<format> [<filter>]] | reset") core.register_chatcommand("profiler", { - description = "handle the profiler and profiling data", + description = S("Handle the profiler and profiling data"), params = param_usage, privs = { server=true }, func = function(name, param) @@ -51,21 +53,19 @@ function profiler.init_chatcommand() if command == "dump" then core.log("action", reporter.print(sampler.profile, arg0)) - return true, "Statistics written to action log" + return true, S("Statistics written to action log.") elseif command == "print" then return true, reporter.print(sampler.profile, arg0) elseif command == "save" then return reporter.save(sampler.profile, args[1] or "txt", args[2]) elseif command == "reset" then sampler.reset() - return true, "Statistics were reset" + return true, S("Statistics were reset.") end - return false, string.format( - "Usage: %s\n" .. - "Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).", - param_usage - ) + return false, + S("Usage: @1", param_usage) .. "\n" .. + S("Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).") end }) diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua index 237f048fb..6b951a2c2 100644 --- a/builtin/profiler/instrumentation.lua +++ b/builtin/profiler/instrumentation.lua @@ -160,6 +160,7 @@ local function init() -- Simple iteration would ignore lookup via __index. local entity_instrumentation = { "on_activate", + "on_deactivate", "on_step", "on_punch", "on_rightclick", diff --git a/builtin/profiler/reporter.lua b/builtin/profiler/reporter.lua index fed47a36b..5928a3718 100644 --- a/builtin/profiler/reporter.lua +++ b/builtin/profiler/reporter.lua @@ -15,6 +15,10 @@ --with this program; if not, write to the Free Software Foundation, Inc., --51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +local S = core.get_translator("__builtin") +-- Note: In this file, only messages are translated +-- but not the table itself, to keep it simple. + local DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n" local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os local rep, sprintf, tonumber = string.rep, string.format, tonumber @@ -104,11 +108,11 @@ local TxtFormatter = Formatter:new { end, format = function(self, filter) local profile = self.profile - self:print("Values below show absolute/relative times spend per server step by the instrumented function.") - self:print("A total of %d samples were taken", profile.stats_total.samples) + self:print(S("Values below show absolute/relative times spend per server step by the instrumented function.")) + self:print(S("A total of @1 sample(s) were taken.", profile.stats_total.samples)) if filter then - self:print("The output is limited to '%s'", filter) + self:print(S("The output is limited to '@1'.", filter)) end self:print() @@ -259,19 +263,18 @@ function reporter.save(profile, format, filter) local output, io_err = io.open(path, "w") if not output then - return false, "Saving of profile failed with: " .. io_err + return false, S("Saving of profile failed: @1", io_err) end local content, err = serialize_profile(profile, format, filter) if not content then output:close() - return false, "Saving of profile failed with: " .. err + return false, S("Saving of profile failed: @1", err) end output:write(content) output:close() - local logmessage = "Profile saved to " .. path - core.log("action", logmessage) - return true, logmessage + core.log("action", "Profile saved to " .. path) + return true, S("Profile saved to @1", path) end return reporter diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 7f2d12be5..75efe64da 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -75,7 +75,7 @@ free_move (Flying) bool false # If enabled, makes move directions relative to the player's pitch when flying or swimming. pitch_move (Pitch move mode) bool false -# Fast movement (via the "special" key). +# Fast movement (via the "Aux1" key). # This requires the "fast" privilege on the server. fast_move (Fast movement) bool false @@ -99,14 +99,14 @@ invert_mouse (Invert mouse) bool false # Mouse sensitivity multiplier. mouse_sensitivity (Mouse sensitivity) float 0.2 -# If enabled, "special" key instead of "sneak" key is used for climbing down and +# If enabled, "Aux1" key instead of "Sneak" key is used for climbing down and # descending. -aux1_descends (Special key for climbing/descending) bool false +aux1_descends (Aux1 key for climbing/descending) bool false # Double-tapping the jump key toggles fly mode. doubletap_jump (Double tap jump for fly) bool false -# If disabled, "special" key is used to fly fast if both fly and fast mode are +# If disabled, "Aux1" key is used to fly fast if both fly and fast mode are # enabled. always_fly_fast (Always fly and fast) bool true @@ -135,9 +135,9 @@ touchscreen_threshold (Touch screen threshold) int 20 0 100 # If disabled, virtual joystick will center to first-touch's position. fixed_virtual_joystick (Fixed virtual joystick) bool false -# (Android) Use virtual joystick to trigger "aux" button. -# If enabled, virtual joystick will also tap "aux" button when out of main circle. -virtual_joystick_triggers_aux (Virtual joystick triggers aux button) bool false +# (Android) Use virtual joystick to trigger "Aux1" button. +# If enabled, virtual joystick will also tap "Aux1" button when out of main circle. +virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false # Enable joysticks enable_joysticks (Enable joysticks) bool false @@ -152,6 +152,9 @@ joystick_type (Joystick type) enum auto auto,generic,xbox # when holding down a joystick button combination. repeat_joystick_button_time (Joystick button repetition interval) float 0.17 0.001 +# The deadzone of the joystick +joystick_deadzone (Joystick deadzone) int 2048 + # The sensitivity of the joystick axes for moving the # ingame view frustum around. joystick_frustum_sensitivity (Joystick frustum sensitivity) float 170 @@ -196,7 +199,7 @@ keymap_inventory (Inventory key) key KEY_KEY_I # Key for moving fast in fast mode. # See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 -keymap_special1 (Special key) key KEY_KEY_E +keymap_aux1 (Aux1 key) key KEY_KEY_E # Key for opening the chat window. # See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 @@ -448,6 +451,10 @@ keymap_decrease_viewing_range_min (View range decrease key) key - [**Basic] +# Whether nametag backgrounds should be shown by default. +# Mods may still set a background. +show_nametag_backgrounds (Show nametag backgrounds by default) bool true + # Enable vertex buffer objects. # This should greatly improve graphics performance. enable_vbo (VBO) bool true @@ -513,8 +520,13 @@ texture_clean_transparent (Clean transparent textures) bool false # texture autoscaling. texture_min_size (Minimum texture size) int 64 -# Experimental option, might cause visible spaces between blocks -# when set to higher number than 0. +# Use multi-sample antialiasing (MSAA) to smooth out block edges. +# This algorithm smooths out the 3D viewport while keeping the image sharp, +# but it doesn't affect the insides of textures +# (which is especially noticeable with transparent textures). +# Visible spaces appear between nodes when shaders are disabled. +# If set to 0, MSAA is disabled. +# A restart is required after changing this option. fsaa (FSAA) enum 0 0,1,2,4,8,16 # Undersampling is similar to using a lower screen resolution, but it applies @@ -541,31 +553,6 @@ shader_path (Shader path) path # enhanced, highlights and shadows are gradually compressed. tone_mapping (Filmic tone mapping) bool false -[***Bumpmapping] - -# Enables bumpmapping for textures. Normalmaps need to be supplied by the texture pack. -# Requires shaders to be enabled. -enable_bumpmapping (Bumpmapping) bool false - -[***Parallax Occlusion] - -# Enables parallax occlusion mapping. -# Requires shaders to be enabled. -enable_parallax_occlusion (Parallax occlusion) bool false - -# 0 = parallax occlusion with slope information (faster). -# 1 = relief mapping (slower, more accurate). -parallax_occlusion_mode (Parallax occlusion mode) int 1 0 1 - -# Number of parallax occlusion iterations. -parallax_occlusion_iterations (Parallax occlusion iterations) int 4 - -# Overall scale of parallax occlusion effect. -parallax_occlusion_scale (Parallax occlusion scale) float 0.08 - -# Overall bias of parallax occlusion effect, usually scale/2. -parallax_occlusion_bias (Parallax occlusion bias) float 0.04 - [***Waving Nodes] # Set to true to enable waving liquids (like water). @@ -614,7 +601,7 @@ fps_max_unfocused (FPS when unfocused or paused) int 20 1 pause_on_lost_focus (Pause on lost window focus) bool false # View distance in nodes. -viewing_range (Viewing range) int 100 20 4000 +viewing_range (Viewing range) int 190 20 4000 # Camera 'near clipping plane' distance in nodes, between 0 and 0.25 # Only works on GLES platforms. Most users will not need to change this. @@ -678,8 +665,8 @@ texture_path (Texture path) path # The rendering back-end for Irrlicht. # A restart is required after changing this. # Note: On Android, stick with OGLES1 if unsure! App may fail to start otherwise. -# On other platforms, OpenGL is recommended, and it’s the only driver with -# shader support currently. +# On other platforms, OpenGL is recommended. +# Shaders are supported by OpenGL (desktop only) and OGLES2 (experimental) video_driver (Video driver) enum opengl null,software,burningsvideo,direct3d8,direct3d9,opengl,ogles1,ogles2 # Radius of cloud area stated in number of 64 node cloud squares. @@ -981,7 +968,7 @@ serverlist_url (Serverlist URL) string servers.minetest.net # File in client/serverlist/ that contains your favorite servers displayed in the # Multiplayer Tab. -serverlist_file (Serverlist file) string favoriteservers.txt +serverlist_file (Serverlist file) string favoriteservers.json # Maximum size of the out chat queue. # 0 to disable queueing and -1 to make the queue size unlimited. @@ -998,7 +985,7 @@ client_unload_unused_data_timeout (Mapblock unload timeout) int 600 # Maximum number of mapblocks for client to be kept in memory. # Set to -1 for unlimited amount. -client_mapblock_limit (Mapblock limit) int 5000 +client_mapblock_limit (Mapblock limit) int 7500 # Whether to show the client debug info (has the same effect as hitting F5). show_debug (Show debug info) bool false @@ -1068,6 +1055,13 @@ full_block_send_enable_min_time_from_building (Delay in sending blocks after bui # client number. max_packets_per_iteration (Max. packets per iteration) int 1024 +# ZLib compression level to use when sending mapblocks to the client. +# -1 - Zlib's default compression level +# 0 - no compresson, fastest +# 9 - best compression, slowest +# (levels 1-3 use Zlib's "fast" method, 4-9 use the normal method) +map_compression_level_net (Map Compression Level for Network Transfer) int -1 -1 9 + [*Game] # Default game when creating a new world. @@ -1095,7 +1089,7 @@ default_stack_max (Default stack size) int 99 # Enable players getting damage and dying. enable_damage (Damage) bool false -# Enable creative mode for new created maps. +# Enable creative mode for all players creative_mode (Creative) bool false # A chosen map seed for a new map, leave empty for random. @@ -1142,6 +1136,10 @@ enable_rollback_recording (Rollback recording) bool false # @name, @message, @timestamp (optional) chat_message_format (Chat message format) string <@name> @message +# If the execution of a chat command takes longer than this specified time in +# seconds, add the time information to the chat command message +chatcommand_msg_time_threshold (Chat command time message threshold) float 0.1 + # A message to be displayed to all clients when the server shuts down. kick_msg_shutdown (Shutdown message) string Server shutting down. @@ -1157,17 +1155,17 @@ ask_reconnect_on_crash (Ask to reconnect after crash) bool false # Setting this larger than active_block_range will also cause the server # to maintain active objects up to this distance in the direction the # player is looking. (This can avoid mobs suddenly disappearing from view) -active_object_send_range_blocks (Active object send range) int 4 +active_object_send_range_blocks (Active object send range) int 8 # The radius of the volume of blocks around every player that is subject to the # active block stuff, stated in mapblocks (16 nodes). # In active blocks objects are loaded and ABMs run. # This is also the minimum range in which active objects (mobs) are maintained. # This should be configured together with active_object_send_range_blocks. -active_block_range (Active block range) int 3 +active_block_range (Active block range) int 4 # From how far blocks are sent to clients, stated in mapblocks (16 nodes). -max_block_send_distance (Max block send distance) int 10 +max_block_send_distance (Max block send distance) int 12 # Maximum number of forceloaded mapblocks. max_forceloaded_blocks (Maximum forceloaded blocks) int 16 @@ -1240,10 +1238,10 @@ movement_gravity (Gravity) float 9.81 [**Advanced] # Handling for deprecated Lua API calls: -# - legacy: (try to) mimic old behaviour (default for release). -# - log: mimic and log backtrace of deprecated call (default for debug). +# - none: Do not log deprecated calls +# - log: mimic and log backtrace of deprecated call (default). # - error: abort on usage of deprecated call (suggested for mod developers). -deprecated_lua_api_handling (Deprecated Lua API handling) enum legacy legacy,log,error +deprecated_lua_api_handling (Deprecated Lua API handling) enum log none,log,error # Number of extra blocks that can be loaded by /clearobjects at once. # This is a trade-off between sqlite transaction overhead and @@ -1260,6 +1258,13 @@ max_objects_per_block (Maximum objects per block) int 64 # See https://www.sqlite.org/pragma.html#pragma_synchronous sqlite_synchronous (Synchronous SQLite) enum 2 0,1,2 +# ZLib compression level to use when saving mapblocks to disk. +# -1 - Zlib's default compression level +# 0 - no compresson, fastest +# 9 - best compression, slowest +# (levels 1-3 use Zlib's "fast" method, 4-9 use the normal method) +map_compression_level_disk (Map Compression Level for Disk Storage) int 3 -1 9 + # Length of a server tick and the interval at which objects are generally updated over # network. dedicated_server_step (Dedicated server step) float 0.09 @@ -1453,7 +1458,7 @@ mg_name (Mapgen name) enum v7 v7,valleys,carpathian,v5,flat,fractal,singlenode,v water_level (Water level) int 1 # From how far blocks are generated for clients, stated in mapblocks (16 nodes). -max_block_generate_distance (Max block generate distance) int 8 +max_block_generate_distance (Max block generate distance) int 10 # Limit of map generation, in nodes, in all 6 directions from (0, 0, 0). # Only mapchunks completely within the mapgen limit are generated. @@ -2172,15 +2177,15 @@ chunksize (Chunk size) int 5 enable_mapgen_debug_info (Mapgen debug) bool false # Maximum number of blocks that can be queued for loading. -emergequeue_limit_total (Absolute limit of queued blocks to emerge) int 512 +emergequeue_limit_total (Absolute limit of queued blocks to emerge) int 1024 # Maximum number of blocks to be queued that are to be loaded from file. # This limit is enforced per player. -emergequeue_limit_diskonly (Per-player limit of queued blocks load from disk) int 64 +emergequeue_limit_diskonly (Per-player limit of queued blocks load from disk) int 128 # Maximum number of blocks to be queued that are to be generated. # This limit is enforced per player. -emergequeue_limit_generate (Per-player limit of queued blocks to generate) int 64 +emergequeue_limit_generate (Per-player limit of queued blocks to generate) int 128 # Number of emerge threads to use. # Value 0: @@ -2208,4 +2213,5 @@ contentdb_url (ContentDB URL) string https://content.minetest.net contentdb_flag_blacklist (ContentDB Flag Blacklist) string nonfree, desktop_default # Maximum number of concurrent downloads. Downloads exceeding this limit will be queued. +# This should be lower than curl_parallel_limit. contentdb_max_concurrent_downloads (ContentDB Max Concurrent Downloads) int 3 |