aboutsummaryrefslogtreecommitdiff
path: root/builtin
diff options
context:
space:
mode:
Diffstat (limited to 'builtin')
-rw-r--r--builtin/async/init.lua10
-rw-r--r--builtin/client/chatcommands.lua9
-rw-r--r--builtin/client/death_formspec.lua1
-rw-r--r--builtin/client/init.lua1
-rw-r--r--builtin/common/after.lua9
-rw-r--r--builtin/common/chatcommands.lua142
-rw-r--r--builtin/common/information_formspecs.lua60
-rw-r--r--builtin/common/misc_helpers.lua46
-rw-r--r--builtin/common/tests/misc_helpers_spec.lua5
-rw-r--r--builtin/common/tests/serialize_spec.lua13
-rw-r--r--builtin/common/tests/vector_spec.lua303
-rw-r--r--builtin/common/vector.lua250
-rw-r--r--builtin/fstk/tabview.lua50
-rw-r--r--builtin/fstk/ui.lua2
-rw-r--r--builtin/game/auth.lua7
-rw-r--r--builtin/game/chat.lua853
-rw-r--r--builtin/game/falling.lua93
-rw-r--r--builtin/game/features.lua3
-rw-r--r--builtin/game/forceloading.lua9
-rw-r--r--builtin/game/init.lua2
-rw-r--r--builtin/game/item.lua164
-rw-r--r--builtin/game/misc.lua86
-rw-r--r--builtin/game/privileges.lua47
-rw-r--r--builtin/game/register.lua12
-rw-r--r--builtin/game/voxelarea.lua14
-rw-r--r--builtin/init.lua1
-rw-r--r--builtin/locale/__builtin.de.tr245
-rw-r--r--builtin/locale/__builtin.it.tr258
-rw-r--r--builtin/locale/template.txt245
-rw-r--r--builtin/mainmenu/common.lua134
-rw-r--r--builtin/mainmenu/dlg_contentstore.lua135
-rw-r--r--builtin/mainmenu/dlg_create_world.lua266
-rw-r--r--builtin/mainmenu/dlg_settings_advanced.lua6
-rw-r--r--builtin/mainmenu/game_theme.lua (renamed from builtin/mainmenu/textures.lua)98
-rw-r--r--builtin/mainmenu/init.lua12
-rw-r--r--builtin/mainmenu/pkgmgr.lua88
-rw-r--r--builtin/mainmenu/tab_about.lua (renamed from builtin/mainmenu/tab_credits.lua)61
-rw-r--r--builtin/mainmenu/tab_content.lua15
-rw-r--r--builtin/mainmenu/tab_local.lua180
-rw-r--r--builtin/mainmenu/tab_online.lua456
-rw-r--r--builtin/mainmenu/tab_settings.lua64
-rw-r--r--builtin/mainmenu/tests/serverlistmgr_spec.lua1
-rw-r--r--builtin/profiler/init.lua18
-rw-r--r--builtin/profiler/instrumentation.lua5
-rw-r--r--builtin/profiler/reporter.lua19
-rw-r--r--builtin/settingtypes.txt170
46 files changed, 3122 insertions, 1546 deletions
diff --git a/builtin/async/init.lua b/builtin/async/init.lua
index 1b2549685..3803994d6 100644
--- a/builtin/async/init.lua
+++ b/builtin/async/init.lua
@@ -1,16 +1,10 @@
core.log("info", "Initializing Asynchronous environment")
-function core.job_processor(serialized_func, serialized_param)
- local func = loadstring(serialized_func)
+function core.job_processor(func, serialized_param)
local param = core.deserialize(serialized_param)
- local retval = nil
- if type(func) == "function" then
- retval = core.serialize(func(param))
- else
- core.log("error", "ASYNC WORKER: Unable to deserialize function")
- end
+ local retval = core.serialize(func(param))
return retval or core.serialize(nil)
end
diff --git a/builtin/client/chatcommands.lua b/builtin/client/chatcommands.lua
index 0e8d4dd03..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,7 @@ 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
@@ -36,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
@@ -66,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..c25c799ab 100644
--- a/builtin/client/death_formspec.lua
+++ b/builtin/client/death_formspec.lua
@@ -2,7 +2,6 @@
-- handled by the engine.
core.register_on_death(function()
- core.display_chat_message("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/init.lua b/builtin/client/init.lua
index 9633a7c71..589fe8f24 100644
--- a/builtin/client/init.lua
+++ b/builtin/client/init.lua
@@ -7,5 +7,4 @@ dofile(clientpath .. "register.lua")
dofile(commonpath .. "after.lua")
dofile(commonpath .. "chatcommands.lua")
dofile(clientpath .. "chatcommands.lua")
-dofile(commonpath .. "vector.lua")
dofile(clientpath .. "death_formspec.lua")
diff --git a/builtin/common/after.lua b/builtin/common/after.lua
index e20f292f0..bce262537 100644
--- a/builtin/common/after.lua
+++ b/builtin/common/after.lua
@@ -37,7 +37,14 @@ function core.after(after, func, ...)
arg = {...},
mod_origin = core.get_last_run_mod(),
}
+
jobs[#jobs + 1] = new_job
time_next = math.min(time_next, expire)
- return { cancel = function() new_job.func = function() end end }
+
+ return {
+ cancel = function()
+ new_job.func = function() end
+ new_job.args = {}
+ end
+ }
end
diff --git a/builtin/common/chatcommands.lua b/builtin/common/chatcommands.lua
index 52edda659..7c3da0601 100644
--- a/builtin/common/chatcommands.lua
+++ b/builtin/common/chatcommands.lua
@@ -1,7 +1,47 @@
-- 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 = {}
+-- Interpret the parameters of a command, separating options and arguments.
+-- Input: command, param
+-- command: name of command
+-- param: parameters of command
+-- Returns: opts, args
+-- opts is a string of option letters, or false on error
+-- args is an array with the non-option arguments in order, or an error message
+-- Example: for this command line:
+-- /command a b -cd e f -g
+-- the function would receive:
+-- a b -cd e f -g
+-- and it would return:
+-- "cdg", {"a", "b", "e", "f"}
+-- Negative numbers are taken as arguments. Long options (--option) are
+-- currently rejected as reserved.
+local function getopts(command, param)
+ local opts = ""
+ local args = {}
+ for match in param:gmatch("%S+") do
+ if match:byte(1) == 45 then -- 45 = '-'
+ local second = match:byte(2)
+ if second == 45 then
+ return false, S("Invalid parameters (see /help @1).", command)
+ elseif second and (second < 48 or second > 57) then -- 48 = '0', 57 = '9'
+ opts = opts .. match:sub(2)
+ else
+ -- numeric, add it to args
+ args[#args + 1] = match
+ end
+ else
+ args[#args + 1] = match
+ end
+ end
+ return opts, args
+end
+
function core.register_chatcommand(cmd, def)
def = def or {}
def.params = def.params or ""
@@ -29,35 +69,30 @@ 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
+local function format_help_line(cmd, def)
+ local cmd_marker = INIT == "client" and "." or "/"
+ local msg = core.colorize("#00ffff", cmd_marker .. cmd)
+ if def.params and def.params ~= "" then
+ msg = msg .. " " .. def.params
+ end
+ if def.description and def.description ~= "" then
+ msg = msg .. ": " .. def.description
+ end
+ return msg
end
local function do_help_cmd(name, param)
- local function format_help_line(cmd, def)
- local msg = core.colorize("#00ffff", cmd_marker .. cmd)
- if def.params and def.params ~= "" then
- msg = msg .. " " .. def.params
- end
- if def.description and def.description ~= "" then
- msg = msg .. ": " .. def.description
- end
- return msg
+ local opts, args = getopts("help", param)
+ if not opts then
+ return false, args
+ end
+ if #args > 1 then
+ return false, S("Too many arguments, try using just /help <command>")
end
- if param == "" then
+ local use_gui = INIT ~= "client" and core.get_player_by_name(name)
+ use_gui = use_gui and not opts:find("t")
+
+ if #args == 0 and not use_gui then
local cmds = {}
for cmd, def in pairs(core.registered_chatcommands) do
if INIT == "client" or core.check_player_privs(name, def.privs) then
@@ -65,10 +100,25 @@ 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)
- elseif param == "all" then
+ 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 #args == 0 or (args[1] == "all" and use_gui) then
+ core.show_general_help_formspec(name)
+ return true
+ elseif args[1] == "all" then
local cmds = {}
for cmd, def in pairs(core.registered_chatcommands) do
if INIT == "client" or core.check_player_privs(name, def.privs) then
@@ -76,19 +126,35 @@ local function do_help_cmd(name, param)
end
end
table.sort(cmds)
- return true, gettext("Available commands:").."\n"..table.concat(cmds, "\n")
- elseif INIT == "game" and param == "privs" then
+ 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 args[1] == "privs" then
+ if use_gui then
+ core.show_privs_help_formspec(name)
+ return true
+ end
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 cmd = args[1]
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 +163,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>] [-t]"),
+ description = S("Get help for commands or list privileges (-t: output in chat)"),
func = do_help_cmd,
})
end
diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua
index 3e2f1f079..3405263bf 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
@@ -123,30 +125,12 @@ core.register_on_player_receive_fields(function(player, formname, fields)
end
end)
-
-local help_command = core.registered_chatcommands["help"]
-local old_help_func = help_command.func
-
-help_command.func = function(name, param)
- local admin = core.settings:get("name")
-
- -- If the admin ran help, put the output in the chat buffer as well to
- -- work with the server terminal
- if param == "privs" then
- core.show_formspec(name, "__builtin:help_privs",
- build_privs_formspec(name))
- if name ~= admin then
- return true
- end
- end
- if param == "" or param == "all" then
- core.show_formspec(name, "__builtin:help_cmds",
- build_chatcommands_formspec(name))
- if name ~= admin then
- return true
- end
- end
-
- return old_help_func(name, param)
+function core.show_general_help_formspec(name)
+ core.show_formspec(name, "__builtin:help_cmds",
+ build_chatcommands_formspec(name))
end
+function core.show_privs_help_formspec(name)
+ core.show_formspec(name, "__builtin:help_privs",
+ build_privs_formspec(name))
+end
diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua
index 0f3897f47..f5f89acd7 100644
--- a/builtin/common/misc_helpers.lua
+++ b/builtin/common/misc_helpers.lua
@@ -209,14 +209,7 @@ end
--------------------------------------------------------------------------------
function math.hypot(x, y)
- local t
- x = math.abs(x)
- y = math.abs(y)
- t = math.min(x, y)
- x = math.max(x, y)
- if x == 0 then return 0 end
- t = t / x
- return x * math.sqrt(1 + t * t)
+ return math.sqrt(x * x + y * y)
end
--------------------------------------------------------------------------------
@@ -244,6 +237,15 @@ function math.factorial(x)
return v
end
+
+function math.round(x)
+ if x >= 0 then
+ return math.floor(x + 0.5)
+ end
+ return math.ceil(x - 0.5)
+end
+
+
function core.formspec_escape(text)
if text ~= nil then
text = string.gsub(text,"\\","\\\\")
@@ -423,21 +425,19 @@ function core.string_to_pos(value)
return nil
end
- local p = {}
- p.x, p.y, p.z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
- if p.x and p.y and p.z then
- p.x = tonumber(p.x)
- p.y = tonumber(p.y)
- p.z = tonumber(p.z)
- return p
+ local x, y, z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
+ if x and y and z then
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ return vector.new(x, y, z)
end
- p = {}
- p.x, p.y, p.z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
- if p.x and p.y and p.z then
- p.x = tonumber(p.x)
- p.y = tonumber(p.y)
- p.z = tonumber(p.z)
- return p
+ x, y, z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
+ if x and y and z then
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ return vector.new(x, y, z)
end
return nil
end
@@ -532,7 +532,7 @@ if INIT == "mainmenu" then
end
end
-if INIT == "client" or INIT == "mainmenu" then
+if core.gettext then -- for client and mainmenu
function fgettext_ne(text, ...)
text = core.gettext(text)
local arg = {n=select('#', ...), ...}
diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua
index bb9d13e7f..b16987f0b 100644
--- a/builtin/common/tests/misc_helpers_spec.lua
+++ b/builtin/common/tests/misc_helpers_spec.lua
@@ -1,4 +1,5 @@
_G.core = {}
+dofile("builtin/common/vector.lua")
dofile("builtin/common/misc_helpers.lua")
describe("string", function()
@@ -55,8 +56,8 @@ end)
describe("pos", function()
it("from string", function()
- assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("10.0, 5.1, -2"))
- assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("( 10.0, 5.1, -2)"))
+ assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("10.0, 5.1, -2"))
+ assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("( 10.0, 5.1, -2)"))
assert.is_nil(core.string_to_pos("asd, 5, -2)"))
end)
diff --git a/builtin/common/tests/serialize_spec.lua b/builtin/common/tests/serialize_spec.lua
index 17c6a60f7..e46b7dcc5 100644
--- a/builtin/common/tests/serialize_spec.lua
+++ b/builtin/common/tests/serialize_spec.lua
@@ -3,6 +3,7 @@ _G.core = {}
_G.setfenv = require 'busted.compatibility'.setfenv
dofile("builtin/common/serialize.lua")
+dofile("builtin/common/vector.lua")
describe("serialize", function()
it("works", function()
@@ -53,4 +54,16 @@ describe("serialize", function()
assert.is_nil(test_out.func)
assert.equals(test_out.foo, "bar")
end)
+
+ it("vectors work", function()
+ local v = vector.new(1, 2, 3)
+ assert.same({{x = 1, y = 2, z = 3}}, core.deserialize(core.serialize({v})))
+ assert.same({x = 1, y = 2, z = 3}, core.deserialize(core.serialize(v)))
+
+ -- abuse
+ v = vector.new(1, 2, 3)
+ v.a = "bla"
+ assert.same({x = 1, y = 2, z = 3, a = "bla"},
+ core.deserialize(core.serialize(v)))
+ end)
end)
diff --git a/builtin/common/tests/vector_spec.lua b/builtin/common/tests/vector_spec.lua
index 0f287363a..2f72f3383 100644
--- a/builtin/common/tests/vector_spec.lua
+++ b/builtin/common/tests/vector_spec.lua
@@ -4,14 +4,20 @@ dofile("builtin/common/vector.lua")
describe("vector", function()
describe("new()", function()
it("constructs", function()
- assert.same({ x = 0, y = 0, z = 0 }, vector.new())
- assert.same({ x = 1, y = 2, z = 3 }, vector.new(1, 2, 3))
- assert.same({ x = 3, y = 2, z = 1 }, vector.new({ x = 3, y = 2, z = 1 }))
+ assert.same({x = 0, y = 0, z = 0}, vector.new())
+ assert.same({x = 1, y = 2, z = 3}, vector.new(1, 2, 3))
+ assert.same({x = 3, y = 2, z = 1}, vector.new({x = 3, y = 2, z = 1}))
+
+ assert.is_true(vector.check(vector.new()))
+ assert.is_true(vector.check(vector.new(1, 2, 3)))
+ assert.is_true(vector.check(vector.new({x = 3, y = 2, z = 1})))
local input = vector.new({ x = 3, y = 2, z = 1 })
local output = vector.new(input)
assert.same(input, output)
- assert.are_not.equal(input, output)
+ assert.equal(input, output)
+ assert.is_false(rawequal(input, output))
+ assert.equal(input, input:new())
end)
it("throws on invalid input", function()
@@ -25,27 +31,286 @@ describe("vector", function()
end)
end)
- it("equal()", function()
- local function assertE(a, b)
- assert.is_true(vector.equals(a, b))
- end
- local function assertNE(a, b)
- assert.is_false(vector.equals(a, b))
- end
+ it("zero()", function()
+ assert.same({x = 0, y = 0, z = 0}, vector.zero())
+ assert.same(vector.new(), vector.zero())
+ assert.equal(vector.new(), vector.zero())
+ assert.is_true(vector.check(vector.zero()))
+ end)
- assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
- assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1})
- local a = { x = 2, y = 4, z = -10 }
- assertE(a, a)
- assertNE({x = -1, y = 0, z = 1}, a)
+ it("copy()", function()
+ local v = vector.new(1, 2, 3)
+ assert.same(v, vector.copy(v))
+ assert.same(vector.new(v), vector.copy(v))
+ assert.equal(vector.new(v), vector.copy(v))
+ assert.is_true(vector.check(vector.copy(v)))
end)
- it("add()", function()
- assert.same({ x = 2, y = 4, z = 6 }, vector.add(vector.new(1, 2, 3), { x = 1, y = 2, z = 3 }))
+ it("indexes", function()
+ local some_vector = vector.new(24, 42, 13)
+ assert.equal(24, some_vector[1])
+ assert.equal(24, some_vector.x)
+ assert.equal(42, some_vector[2])
+ assert.equal(42, some_vector.y)
+ assert.equal(13, some_vector[3])
+ assert.equal(13, some_vector.z)
+
+ some_vector[1] = 100
+ assert.equal(100, some_vector.x)
+ some_vector.x = 101
+ assert.equal(101, some_vector[1])
+
+ some_vector[2] = 100
+ assert.equal(100, some_vector.y)
+ some_vector.y = 102
+ assert.equal(102, some_vector[2])
+
+ some_vector[3] = 100
+ assert.equal(100, some_vector.z)
+ some_vector.z = 103
+ assert.equal(103, some_vector[3])
+ end)
+
+ it("direction()", function()
+ local a = vector.new(1, 0, 0)
+ local b = vector.new(1, 42, 0)
+ assert.equal(vector.new(0, 1, 0), vector.direction(a, b))
+ assert.equal(vector.new(0, 1, 0), a:direction(b))
+ end)
+
+ it("distance()", function()
+ local a = vector.new(1, 0, 0)
+ local b = vector.new(3, 42, 9)
+ assert.is_true(math.abs(43 - vector.distance(a, b)) < 1.0e-12)
+ assert.is_true(math.abs(43 - a:distance(b)) < 1.0e-12)
+ assert.equal(0, vector.distance(a, a))
+ assert.equal(0, b:distance(b))
+ end)
+
+ it("length()", function()
+ local a = vector.new(0, 0, -23)
+ assert.equal(0, vector.length(vector.new()))
+ assert.equal(23, vector.length(a))
+ assert.equal(23, a:length())
+ end)
+
+ it("normalize()", function()
+ local a = vector.new(0, 0, -23)
+ assert.equal(vector.new(0, 0, -1), vector.normalize(a))
+ assert.equal(vector.new(0, 0, -1), a:normalize())
+ assert.equal(vector.new(), vector.normalize(vector.new()))
+ end)
+
+ it("floor()", function()
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(0, 0, -1), vector.floor(a))
+ assert.equal(vector.new(0, 0, -1), a:floor())
+ end)
+
+ it("round()", function()
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(0, 1, -1), vector.round(a))
+ assert.equal(vector.new(0, 1, -1), a:round())
+ end)
+
+ it("apply()", function()
+ local i = 0
+ local f = function(x)
+ i = i + 1
+ return x + i
+ end
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(1, 1, 0), vector.apply(a, math.ceil))
+ assert.equal(vector.new(1, 1, 0), a:apply(math.ceil))
+ assert.equal(vector.new(0.1, 0.9, 0.5), vector.apply(a, math.abs))
+ assert.equal(vector.new(0.1, 0.9, 0.5), a:apply(math.abs))
+ assert.equal(vector.new(1.1, 2.9, 2.5), vector.apply(a, f))
+ assert.equal(vector.new(4.1, 5.9, 5.5), a:apply(f))
+ end)
+
+ it("equals()", function()
+ local function assertE(a, b)
+ assert.is_true(vector.equals(a, b))
+ end
+ local function assertNE(a, b)
+ assert.is_false(vector.equals(a, b))
+ end
+
+ assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
+ assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1})
+ assertE({x = -1, y = 0, z = 1}, vector.new(-1, 0, 1))
+ local a = {x = 2, y = 4, z = -10}
+ assertE(a, a)
+ assertNE({x = -1, y = 0, z = 1}, a)
+
+ assert.equal(vector.new(1, 2, 3), vector.new(1, 2, 3))
+ assert.is_true(vector.new(1, 2, 3):equals(vector.new(1, 2, 3)))
+ assert.not_equal(vector.new(1, 2, 3), vector.new(1, 2, 4))
+ assert.is_true(vector.new(1, 2, 3) == vector.new(1, 2, 3))
+ assert.is_false(vector.new(1, 2, 3) == vector.new(1, 3, 3))
+ end)
+
+ it("metatable is same", function()
+ local a = vector.new()
+ local b = vector.new(1, 2, 3)
+
+ assert.equal(true, vector.check(a))
+ assert.equal(true, vector.check(b))
+
+ assert.equal(vector.metatable, getmetatable(a))
+ assert.equal(vector.metatable, getmetatable(b))
+ assert.equal(vector.metatable, a.metatable)
+ end)
+
+ it("sort()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(0.5, 232, -2)
+ local sorted = {vector.new(0.5, 2, -2), vector.new(1, 232, 3)}
+ assert.same(sorted, {vector.sort(a, b)})
+ assert.same(sorted, {a:sort(b)})
+ end)
+
+ it("angle()", function()
+ assert.equal(math.pi, vector.angle(vector.new(-1, -2, -3), vector.new(1, 2, 3)))
+ assert.equal(math.pi/2, vector.new(0, 1, 0):angle(vector.new(1, 0, 0)))
+ end)
+
+ it("dot()", function()
+ assert.equal(-14, vector.dot(vector.new(-1, -2, -3), vector.new(1, 2, 3)))
+ assert.equal(0, vector.new():dot(vector.new(1, 2, 3)))
+ end)
+
+ it("cross()", function()
+ local a = vector.new(-1, -2, 0)
+ local b = vector.new(1, 2, 3)
+ assert.equal(vector.new(-6, 3, 0), vector.cross(a, b))
+ assert.equal(vector.new(-6, 3, 0), a:cross(b))
end)
it("offset()", function()
- assert.same({ x = 41, y = 52, z = 63 }, vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.same({x = 41, y = 52, z = 63}, vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.equal(vector.new(41, 52, 63), vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.equal(vector.new(41, 52, 63), vector.new(1, 2, 3):offset(40, 50, 60))
+ end)
+
+ it("is()", function()
+ local some_table1 = {foo = 13, [42] = 1, "bar", 2}
+ local some_table2 = {1, 2, 3}
+ local some_table3 = {x = 1, 2, 3}
+ local some_table4 = {1, 2, z = 3}
+ local old = {x = 1, y = 2, z = 3}
+ local real = vector.new(1, 2, 3)
+
+ assert.is_false(vector.check(nil))
+ assert.is_false(vector.check(1))
+ assert.is_false(vector.check(true))
+ assert.is_false(vector.check("foo"))
+ assert.is_false(vector.check(some_table1))
+ assert.is_false(vector.check(some_table2))
+ assert.is_false(vector.check(some_table3))
+ assert.is_false(vector.check(some_table4))
+ assert.is_false(vector.check(old))
+ assert.is_true(vector.check(real))
+ assert.is_true(real:check())
+ end)
+
+ it("global pairs", function()
+ local out = {}
+ local vec = vector.new(10, 20, 30)
+ for k, v in pairs(vec) do
+ out[k] = v
+ end
+ assert.same({x = 10, y = 20, z = 30}, out)
+ end)
+
+ it("abusing works", function()
+ local v = vector.new(1, 2, 3)
+ v.a = 1
+ assert.equal(1, v.a)
+
+ local a_is_there = false
+ for key, value in pairs(v) do
+ if key == "a" then
+ a_is_there = true
+ assert.equal(value, 1)
+ break
+ end
+ end
+ assert.is_true(a_is_there)
+ end)
+
+ it("add()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(1, 4, 3)
+ local c = vector.new(2, 6, 6)
+ assert.equal(c, vector.add(a, {x = 1, y = 4, z = 3}))
+ assert.equal(c, vector.add(a, b))
+ assert.equal(c, a:add(b))
+ assert.equal(c, a + b)
+ assert.equal(c, b + a)
+ end)
+
+ it("subtract()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(-1, -2, 0)
+ assert.equal(c, vector.subtract(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.subtract(a, b))
+ assert.equal(c, a:subtract(b))
+ assert.equal(c, a - b)
+ assert.equal(c, -b + a)
+ end)
+
+ it("multiply()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(2, 8, 9)
+ local s = 2
+ local d = vector.new(2, 4, 6)
+ assert.equal(c, vector.multiply(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.multiply(a, b))
+ assert.equal(d, vector.multiply(a, s))
+ assert.equal(d, a:multiply(s))
+ assert.equal(d, a * s)
+ assert.equal(d, s * a)
+ assert.equal(-a, -1 * a)
+ end)
+
+ it("divide()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(0.5, 0.5, 1)
+ local s = 2
+ local d = vector.new(0.5, 1, 1.5)
+ assert.equal(c, vector.divide(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.divide(a, b))
+ assert.equal(d, vector.divide(a, s))
+ assert.equal(d, a:divide(s))
+ assert.equal(d, a / s)
+ assert.equal(d, 1/s * a)
+ assert.equal(-a, a / -1)
+ end)
+
+ it("to_string()", function()
+ local v = vector.new(1, 2, 3.14)
+ assert.same("(1, 2, 3.14)", vector.to_string(v))
+ assert.same("(1, 2, 3.14)", v:to_string())
+ assert.same("(1, 2, 3.14)", tostring(v))
+ end)
+
+ it("from_string()", function()
+ local v = vector.new(1, 2, 3.14)
+ assert.is_true(vector.check(vector.from_string("(1, 2, 3.14)")))
+ assert.same({v, 13}, {vector.from_string("(1, 2, 3.14)")})
+ assert.same({v, 12}, {vector.from_string("(1,2 ,3.14)")})
+ assert.same({v, 12}, {vector.from_string("(1,2,3.14,)")})
+ assert.same({v, 11}, {vector.from_string("(1 2 3.14)")})
+ assert.same({v, 15}, {vector.from_string("( 1, 2, 3.14 )")})
+ assert.same({v, 15}, {vector.from_string(" ( 1, 2, 3.14) ")})
+ assert.same({vector.new(), 8}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ")})
+ assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 8)})
+ assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 9)})
+ assert.same(nil, vector.from_string("nothing"))
end)
-- This function is needed because of floating point imprecision.
diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua
index d6437deda..581d014e0 100644
--- a/builtin/common/vector.lua
+++ b/builtin/common/vector.lua
@@ -1,73 +1,126 @@
+--[[
+Vector helpers
+Note: The vector.*-functions must be able to accept old vectors that had no metatables
+]]
+
+-- localize functions
+local setmetatable = setmetatable
vector = {}
+local metatable = {}
+vector.metatable = metatable
+
+local xyz = {"x", "y", "z"}
+
+-- only called when rawget(v, key) returns nil
+function metatable.__index(v, key)
+ return rawget(v, xyz[key]) or vector[key]
+end
+
+-- only called when rawget(v, key) returns nil
+function metatable.__newindex(v, key, value)
+ rawset(v, xyz[key] or key, value)
+end
+
+-- constructors
+
+local function fast_new(x, y, z)
+ return setmetatable({x = x, y = y, z = z}, metatable)
+end
+
function vector.new(a, b, c)
+ if a and b and c then
+ return fast_new(a, b, c)
+ end
+
+ -- deprecated, use vector.copy and vector.zero directly
if type(a) == "table" then
- assert(a.x and a.y and a.z, "Invalid vector passed to vector.new()")
- return {x=a.x, y=a.y, z=a.z}
- elseif a then
- assert(b and c, "Invalid arguments for vector.new()")
- return {x=a, y=b, z=c}
+ return vector.copy(a)
+ else
+ assert(not a, "Invalid arguments for vector.new()")
+ return vector.zero()
+ end
+end
+
+function vector.zero()
+ return fast_new(0, 0, 0)
+end
+
+function vector.copy(v)
+ assert(v.x and v.y and v.z, "Invalid vector passed to vector.copy()")
+ return fast_new(v.x, v.y, v.z)
+end
+
+function vector.from_string(s, init)
+ local x, y, z, np = string.match(s, "^%s*%(%s*([^%s,]+)%s*[,%s]%s*([^%s,]+)%s*[,%s]" ..
+ "%s*([^%s,]+)%s*[,%s]?%s*%)()", init)
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ if not (x and y and z) then
+ return nil
end
- return {x=0, y=0, z=0}
+ return fast_new(x, y, z), np
+end
+
+function vector.to_string(v)
+ return string.format("(%g, %g, %g)", v.x, v.y, v.z)
end
+metatable.__tostring = vector.to_string
function vector.equals(a, b)
return a.x == b.x and
a.y == b.y and
a.z == b.z
end
+metatable.__eq = vector.equals
+
+-- unary operations
function vector.length(v)
- return math.hypot(v.x, math.hypot(v.y, v.z))
+ return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
end
+-- Note: we can not use __len because it is already used for primitive table length
function vector.normalize(v)
local len = vector.length(v)
if len == 0 then
- return {x=0, y=0, z=0}
+ return fast_new(0, 0, 0)
else
return vector.divide(v, len)
end
end
function vector.floor(v)
- return {
- x = math.floor(v.x),
- y = math.floor(v.y),
- z = math.floor(v.z)
- }
+ return vector.apply(v, math.floor)
end
function vector.round(v)
- return {
- x = math.floor(v.x + 0.5),
- y = math.floor(v.y + 0.5),
- z = math.floor(v.z + 0.5)
- }
+ return fast_new(
+ math.round(v.x),
+ math.round(v.y),
+ math.round(v.z)
+ )
end
function vector.apply(v, func)
- return {
- x = func(v.x),
- y = func(v.y),
- z = func(v.z)
- }
+ return fast_new(
+ func(v.x),
+ func(v.y),
+ func(v.z)
+ )
end
function vector.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y
local z = a.z - b.z
- return math.hypot(x, math.hypot(y, z))
+ return math.sqrt(x * x + y * y + z * z)
end
function vector.direction(pos1, pos2)
- return vector.normalize({
- x = pos2.x - pos1.x,
- y = pos2.y - pos1.y,
- z = pos2.z - pos1.z
- })
+ return vector.subtract(pos2, pos1):normalize()
end
function vector.angle(a, b)
@@ -82,70 +135,137 @@ function vector.dot(a, b)
end
function vector.cross(a, b)
- return {
- x = a.y * b.z - a.z * b.y,
- y = a.z * b.x - a.x * b.z,
- z = a.x * b.y - a.y * b.x
- }
+ return fast_new(
+ a.y * b.z - a.z * b.y,
+ a.z * b.x - a.x * b.z,
+ a.x * b.y - a.y * b.x
+ )
end
+function metatable.__unm(v)
+ return fast_new(-v.x, -v.y, -v.z)
+end
+
+-- add, sub, mul, div operations
+
function vector.add(a, b)
if type(b) == "table" then
- return {x = a.x + b.x,
- y = a.y + b.y,
- z = a.z + b.z}
+ return fast_new(
+ a.x + b.x,
+ a.y + b.y,
+ a.z + b.z
+ )
else
- return {x = a.x + b,
- y = a.y + b,
- z = a.z + b}
+ return fast_new(
+ a.x + b,
+ a.y + b,
+ a.z + b
+ )
end
end
+function metatable.__add(a, b)
+ return fast_new(
+ a.x + b.x,
+ a.y + b.y,
+ a.z + b.z
+ )
+end
function vector.subtract(a, b)
if type(b) == "table" then
- return {x = a.x - b.x,
- y = a.y - b.y,
- z = a.z - b.z}
+ return fast_new(
+ a.x - b.x,
+ a.y - b.y,
+ a.z - b.z
+ )
else
- return {x = a.x - b,
- y = a.y - b,
- z = a.z - b}
+ return fast_new(
+ a.x - b,
+ a.y - b,
+ a.z - b
+ )
end
end
+function metatable.__sub(a, b)
+ return fast_new(
+ a.x - b.x,
+ a.y - b.y,
+ a.z - b.z
+ )
+end
function vector.multiply(a, b)
if type(b) == "table" then
- return {x = a.x * b.x,
- y = a.y * b.y,
- z = a.z * b.z}
+ return fast_new(
+ a.x * b.x,
+ a.y * b.y,
+ a.z * b.z
+ )
+ else
+ return fast_new(
+ a.x * b,
+ a.y * b,
+ a.z * b
+ )
+ end
+end
+function metatable.__mul(a, b)
+ if type(a) == "table" then
+ return fast_new(
+ a.x * b,
+ a.y * b,
+ a.z * b
+ )
else
- return {x = a.x * b,
- y = a.y * b,
- z = a.z * b}
+ return fast_new(
+ a * b.x,
+ a * b.y,
+ a * b.z
+ )
end
end
function vector.divide(a, b)
if type(b) == "table" then
- return {x = a.x / b.x,
- y = a.y / b.y,
- z = a.z / b.z}
+ return fast_new(
+ a.x / b.x,
+ a.y / b.y,
+ a.z / b.z
+ )
else
- return {x = a.x / b,
- y = a.y / b,
- z = a.z / b}
+ return fast_new(
+ a.x / b,
+ a.y / b,
+ a.z / b
+ )
end
end
+function metatable.__div(a, b)
+ -- scalar/vector makes no sense
+ return fast_new(
+ a.x / b,
+ a.y / b,
+ a.z / b
+ )
+end
+
+-- misc stuff
function vector.offset(v, x, y, z)
- return {x = v.x + x,
- y = v.y + y,
- z = v.z + z}
+ return fast_new(
+ v.x + x,
+ v.y + y,
+ v.z + z
+ )
end
function vector.sort(a, b)
- return {x = math.min(a.x, b.x), y = math.min(a.y, b.y), z = math.min(a.z, b.z)},
- {x = math.max(a.x, b.x), y = math.max(a.y, b.y), z = math.max(a.z, b.z)}
+ return fast_new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)),
+ fast_new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z))
+end
+
+function vector.check(v)
+ return getmetatable(v) == metatable
end
local function sin(x)
@@ -213,7 +333,7 @@ end
function vector.dir_to_rotation(forward, up)
forward = vector.normalize(forward)
- local rot = {x = math.asin(forward.y), y = -math.atan2(forward.x, forward.z), z = 0}
+ local rot = vector.new(math.asin(forward.y), -math.atan2(forward.x, forward.z), 0)
if not up then
return rot
end
@@ -221,7 +341,7 @@ function vector.dir_to_rotation(forward, up)
"Invalid vectors passed to vector.dir_to_rotation().")
up = vector.normalize(up)
-- Calculate vector pointing up with roll = 0, just based on forward vector.
- local forwup = vector.rotate({x = 0, y = 1, z = 0}, rot)
+ local forwup = vector.rotate(vector.new(0, 1, 0), rot)
-- 'forwup' and 'up' are now in a plane with 'forward' as normal.
-- The angle between them is the absolute of the roll value we're looking for.
rot.z = vector.angle(forwup, up)
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 976659ed3..13f9cbec2 100644
--- a/builtin/fstk/ui.lua
+++ b/builtin/fstk/ui.lua
@@ -63,7 +63,7 @@ function ui.update()
-- handle errors
if gamedata ~= nil and gamedata.reconnect_requested then
local error_message = core.formspec_escape(
- gamedata.errormessage or "<none available>")
+ gamedata.errormessage or fgettext("<none available>"))
formspec = {
"size[14,8]",
"real_coordinates[true]",
diff --git a/builtin/game/auth.lua b/builtin/game/auth.lua
index fc061666c..e7d502bb3 100644
--- a/builtin/game/auth.lua
+++ b/builtin/game/auth.lua
@@ -87,6 +87,10 @@ core.builtin_auth_handler = {
core.settings:get("default_password")))
end
+ auth_entry.privileges = privileges
+
+ core_auth.save(auth_entry)
+
-- Run grant callbacks
for priv, _ in pairs(privileges) do
if not auth_entry.privileges[priv] then
@@ -100,9 +104,6 @@ core.builtin_auth_handler = {
core.run_priv_callbacks(name, priv, nil, "revoke")
end
end
-
- auth_entry.privileges = privileges
- core_auth.save(auth_entry)
core.notify_authentication_modified(name)
end,
reload = function()
diff --git a/builtin/game/chat.lua b/builtin/game/chat.lua
index 945707623..b73e32876 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,7 +56,7 @@ 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
@@ -65,15 +69,17 @@ core.register_on_chat_message(function(name, message)
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 = core.get_us_time()
local success, result = cmd_def.func(name, param)
+ local delay = (core.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)
@@ -81,13 +87,27 @@ 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 .. core.colorize("#f3d2ff", S(" (@1 s)",
+ string.format("%.5f", delay)))
+ else
+ result = core.colorize("#f3d2ff", S(
+ "Command execution took @1 s",
+ string.format("%.5f", 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)
@@ -107,12 +127,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
@@ -123,9 +144,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)
@@ -134,43 +155,54 @@ 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,
})
+local function privileges_of(name, privs)
+ if not privs then
+ privs = core.get_player_privs(name)
+ end
+ local privstr = core.privs_to_string(privs, ", ")
+ if privstr == "" then
+ return S("@1 does not have any privileges.", name)
+ else
+ return S("Privileges of @1: @2", name, privstr)
+ 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, privileges_of(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 = {}
@@ -180,19 +212,25 @@ 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, ", ")
+ if #players_with_priv == 0 then
+ return true, S("No online player has the \"@1\" privilege.",
+ param)
+ else
+ return true, S("Players online with the \"@1\" privilege: @2",
+ param,
+ table.concat(players_with_priv, ", "))
+ end
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
@@ -204,50 +242,51 @@ 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. "..
+ "'@1' only allows you to grant: @2",
+ "basic_privs",
+ core.privs_to_string(basic_privs, ', '))
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
if privs_unknown ~= "" then
return false, privs_unknown
end
+ core.set_player_privs(grantname, privs)
for priv, _ in pairs(grantprivs) do
-- call the on_grant callbacks
core.run_priv_callbacks(grantname, priv, caller, "grant")
end
- 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, privileges_of(grantname)
end
core.register_chatcommand("grant", {
- params = "<name> (<privilege> | all)",
- description = "Give privileges to player",
+ params = S("<name> (<privilege> [, <privilege2> [<...>]] | 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> [, <privilege2> [<...>]] | 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,
@@ -256,23 +295,20 @@ 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)
local privs = core.get_player_privs(revokename)
- local basic_privs =
- 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."
- end
- end
+ local revokeprivs = core.string_to_privs(revokeprivstr)
+ local is_singleplayer = core.is_singleplayer()
+ local is_admin = not is_singleplayer
+ and revokename == core.settings:get("name")
+ and revokename ~= ""
if revokeprivstr == "all" then
revokeprivs = privs
privs = {}
@@ -282,53 +318,99 @@ local function handle_revoke_command(caller, revokename, revokeprivstr)
end
end
+ local privs_unknown = ""
+ local basic_privs =
+ core.string_to_privs(core.settings:get("basic_privs") or "interact,shout")
+ local irrevokable = {}
+ local has_irrevokable_priv = false
+ for priv, _ in pairs(revokeprivs) do
+ if not basic_privs[priv] and not caller_privs.privs then
+ return false, S("Your privileges are insufficient. "..
+ "'@1' only allows you to revoke: @2",
+ "basic_privs",
+ core.privs_to_string(basic_privs, ', '))
+ end
+ local def = core.registered_privileges[priv]
+ if not def then
+ privs_unknown = privs_unknown .. S("Unknown privilege: @1", priv) .. "\n"
+ elseif is_singleplayer and def.give_to_singleplayer then
+ irrevokable[priv] = true
+ elseif is_admin and def.give_to_admin then
+ irrevokable[priv] = true
+ end
+ end
+ for priv, _ in pairs(irrevokable) do
+ revokeprivs[priv] = nil
+ has_irrevokable_priv = true
+ end
+ if privs_unknown ~= "" then
+ return false, privs_unknown
+ end
+ if has_irrevokable_priv then
+ if is_singleplayer then
+ core.chat_send_player(caller,
+ S("Note: Cannot revoke in singleplayer: @1",
+ core.privs_to_string(irrevokable, ', ')))
+ elseif is_admin then
+ core.chat_send_player(caller,
+ S("Note: Cannot revoke from admin: @1",
+ core.privs_to_string(irrevokable, ', ')))
+ end
+ end
+
+ local revokecount = 0
+
+ core.set_player_privs(revokename, privs)
for priv, _ in pairs(revokeprivs) do
-- call the on_revoke callbacks
core.run_priv_callbacks(revokename, priv, caller, "revoke")
+ revokecount = revokecount + 1
+ end
+ local new_privs = core.get_player_privs(revokename)
+
+ if revokecount == 0 then
+ return false, S("No privileges were revoked.")
end
- core.set_player_privs(revokename, privs)
core.log("action", caller..' revoked ('
..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, privileges_of(revokename, new_privs)
end
core.register_chatcommand("revoke", {
- params = "<name> (<privilege> | all)",
- description = "Remove privileges from player",
+ params = S("<name> (<privilege> [, <privilege2> [<...>]] | 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> [, <privilege2> [<...>]] | 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, "^([^ ]+) +(.+)$")
@@ -338,207 +420,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 = {
+ vector.new( 1, 0, 0),
+ vector.new(-1, 0, 0),
+ vector.new( 0, 0, 1),
+ vector.new( 0, 0, -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 = 31007 -- equals MAX_MAP_GENERATION_LIMIT in C++
+ 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]) ([^ ]+) (.+)")
@@ -550,22 +622,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,
})
@@ -578,26 +651,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)
@@ -615,15 +689,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)
@@ -632,18 +706,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)
@@ -652,20 +728,26 @@ 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(), ", ")
+ local mods = core.get_modnames()
+ if #mods == 0 then
+ return true, S("No mods installed.")
+ else
+ return true, table.concat(core.get_modnames(), ", ")
+ end
end,
})
@@ -674,117 +756,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,
})
@@ -800,14 +901,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*)")
@@ -815,30 +917,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,
@@ -847,189 +949,235 @@ 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."
+ local new_time = tonumber(param) or -1
+ if new_time ~= new_time or new_time < 0 or new_time > 24000 then
+ return false, S("Invalid time (must be between 0 and 24000).")
end
- -- Backward compatibility.
- core.set_timeofday((new_time % 24000) / 24000)
+ core.set_timeofday(new_time / 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
})
+local function parse_shutdown_param(param)
+ local delay, reconnect, message
+ local one, two, three
+ one, two, three = param:match("^(%S+) +(%-r) +(.*)")
+ if one and two and three then
+ -- 3 arguments: delay, reconnect and message
+ return one, two, three
+ end
+ -- 2 arguments
+ one, two = param:match("^(%S+) +(.*)")
+ if one and two then
+ if tonumber(one) then
+ delay = one
+ if two == "-r" then
+ reconnect = two
+ else
+ message = two
+ end
+ elseif one == "-r" then
+ reconnect, message = one, two
+ end
+ return delay, reconnect, message
+ end
+ -- 1 argument
+ one = param:match("(.*)")
+ if tonumber(one) then
+ delay = one
+ elseif one == "-r" then
+ reconnect = one
+ else
+ message = one
+ end
+ return delay, reconnect, message
+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] [-r] [<message>]"),
+ description = S("Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)"),
privs = {server=true},
func = function(name, param)
- local delay, reconnect, message
- delay, param = param:match("^%s*(%S+)(.*)")
- if param then
- reconnect, param = param:match("^%s*(%S+)(.*)")
+ local delay, reconnect, message = parse_shutdown_param(param)
+ local bool_reconnect = reconnect == "-r"
+ if not message then
+ message = ""
end
- message = param and param:match("^%s*(.+)") or ""
delay = tonumber(delay) or 0
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)
+ core.request_shutdown(message:trim(), bool_reconnect, delay)
return true
end,
})
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 = {}
@@ -1038,45 +1186,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
@@ -1084,25 +1231,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
@@ -1112,25 +1261,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
@@ -1138,12 +1287,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/falling.lua b/builtin/game/falling.lua
index 057d0d0ed..29cb56aae 100644
--- a/builtin/game/falling.lua
+++ b/builtin/game/falling.lua
@@ -39,7 +39,7 @@ local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
core.register_entity(":__builtin:falling_node", {
initial_properties = {
visual = "item",
- visual_size = {x = SCALE, y = SCALE, z = SCALE},
+ visual_size = vector.new(SCALE, SCALE, SCALE),
textures = {},
physical = true,
is_visible = false,
@@ -84,9 +84,6 @@ 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
@@ -99,7 +96,7 @@ core.register_entity(":__builtin:falling_node", {
local vsize
if def.visual_scale then
local s = def.visual_scale
- vsize = {x = s, y = s, z = s}
+ vsize = vector.new(s, s, s)
end
self.object:set_properties({
is_visible = true,
@@ -114,15 +111,21 @@ core.register_entity(":__builtin:falling_node", {
itemstring = core.itemstring_with_palette(itemstring, node.param2)
end
-- FIXME: solution needed for paramtype2 == "leveled"
- local vsize
- if def.visual_scale then
- local s = def.visual_scale * SCALE
- vsize = {x = s, y = s, z = s}
+ -- Calculate size of falling node
+ local s = {}
+ s.x = (def.visual_scale or 1) * SCALE
+ s.y = s.x
+ s.z = s.x
+ -- Compensate for wield_scale
+ if def.wield_scale then
+ s.x = s.x / def.wield_scale.x
+ s.y = s.y / def.wield_scale.y
+ s.z = s.z / def.wield_scale.z
end
self.object:set_properties({
is_visible = true,
wield_item = itemstring,
- visual_size = vsize,
+ visual_size = s,
glow = def.light_source,
})
end
@@ -147,11 +150,7 @@ core.register_entity(":__builtin:falling_node", {
-- Rotate entity
if def.drawtype == "torchlike" then
- if def.paramtype2 == "wallmounted" then
- self.object:set_yaw(math.pi*0.25)
- else
- self.object:set_yaw(-math.pi*0.25)
- end
+ self.object:set_yaw(math.pi*0.25)
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"
@@ -165,8 +164,13 @@ core.register_entity(":__builtin:falling_node", {
if euler then
self.object:set_rotation(euler)
end
- elseif (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted") then
+ elseif (def.drawtype ~= "plantlike" and def.drawtype ~= "plantlike_rooted" and
+ (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted" or def.drawtype == "signlike")) then
local rot = node.param2 % 8
+ if (def.drawtype == "signlike" and def.paramtype2 ~= "wallmounted" and def.paramtype2 ~= "colorwallmounted") then
+ -- Change rotation to "floor" by default for non-wallmounted paramtype2
+ rot = 1
+ end
local pitch, yaw, roll = 0, 0, 0
if def.drawtype == "nodebox" or def.drawtype == "mesh" then
if rot == 0 then
@@ -208,6 +212,14 @@ core.register_entity(":__builtin:falling_node", {
end
end
self.object:set_rotation({x=pitch, y=yaw, z=roll})
+ elseif (def.drawtype == "mesh" and def.paramtype2 == "degrotate") then
+ local p2 = (node.param2 - (def.place_param2 or 0)) % 240
+ local yaw = (p2 / 240) * (math.pi * 2)
+ self.object:set_yaw(yaw)
+ elseif (def.drawtype == "mesh" and def.paramtype2 == "colordegrotate") then
+ local p2 = (node.param2 % 32 - (def.place_param2 or 0) % 32) % 24
+ local yaw = (p2 / 24) * (math.pi * 2)
+ self.object:set_yaw(yaw)
end
end
end,
@@ -222,7 +234,7 @@ core.register_entity(":__builtin:falling_node", {
on_activate = function(self, staticdata)
self.object:set_armor_groups({immortal = 1})
- self.object:set_acceleration({x = 0, y = -gravity, z = 0})
+ self.object:set_acceleration(vector.new(0, -gravity, 0))
local ds = core.deserialize(staticdata)
if ds and ds.node then
@@ -298,7 +310,7 @@ core.register_entity(":__builtin:falling_node", {
if self.floats then
local pos = self.object:get_pos()
- local bcp = vector.round({x = pos.x, y = pos.y - 0.7, z = pos.z})
+ local bcp = pos:offset(0, -0.7, 0):round()
local bcn = core.get_node(bcp)
local bcd = core.registered_nodes[bcn.name]
@@ -339,13 +351,12 @@ core.register_entity(":__builtin:falling_node", {
-- TODO: this hack could be avoided in the future if objects
-- could choose who to collide with
local vel = self.object:get_velocity()
- self.object:set_velocity({
- x = vel.x,
- y = player_collision.old_velocity.y,
- z = vel.z
- })
- self.object:set_pos(vector.add(self.object:get_pos(),
- {x = 0, y = -0.5, z = 0}))
+ self.object:set_velocity(vector.new(
+ vel.x,
+ player_collision.old_velocity.y,
+ vel.z
+ ))
+ self.object:set_pos(self.object:get_pos():offset(0, -0.5, 0))
end
return
elseif bcn.name == "ignore" then
@@ -407,7 +418,7 @@ local function convert_to_falling_node(pos, node)
obj:get_luaentity():set_node(node, metatable)
core.remove_node(pos)
- return true
+ return true, obj
end
function core.spawn_falling_node(pos)
@@ -425,7 +436,7 @@ local function drop_attached_node(p)
if def and def.preserve_metadata then
local oldmeta = core.get_meta(p):to_table().fields
-- Copy pos and node because the callback can modify them.
- local pos_copy = {x=p.x, y=p.y, z=p.z}
+ local pos_copy = vector.new(p)
local node_copy = {name=n.name, param1=n.param1, param2=n.param2}
local drop_stacks = {}
for k, v in pairs(drops) do
@@ -450,14 +461,14 @@ end
function builtin_shared.check_attached_node(p, n)
local def = core.registered_nodes[n.name]
- local d = {x = 0, y = 0, z = 0}
+ local d = vector.new()
if def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted" then
-- The fallback vector here is in case 'wallmounted to dir' is nil due
-- to voxelmanip placing a wallmounted node without resetting a
-- pre-existing param2 value that is out-of-range for wallmounted.
-- The fallback vector corresponds to param2 = 0.
- d = core.wallmounted_to_dir(n.param2) or {x = 0, y = 1, z = 0}
+ d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0)
else
d.y = -1
end
@@ -477,7 +488,7 @@ end
function core.check_single_for_falling(p)
local n = core.get_node(p)
if core.get_item_group(n.name, "falling_node") ~= 0 then
- local p_bottom = {x = p.x, y = p.y - 1, z = p.z}
+ local p_bottom = vector.offset(p, 0, -1, 0)
-- Only spawn falling node if node below is loaded
local n_bottom = core.get_node_or_nil(p_bottom)
local d_bottom = n_bottom and core.registered_nodes[n_bottom.name]
@@ -516,17 +527,17 @@ end
-- Down first as likely case, but always before self. The same with sides.
-- Up must come last, so that things above self will also fall all at once.
local check_for_falling_neighbors = {
- {x = -1, y = -1, z = 0},
- {x = 1, y = -1, z = 0},
- {x = 0, y = -1, z = -1},
- {x = 0, y = -1, z = 1},
- {x = 0, y = -1, z = 0},
- {x = -1, y = 0, z = 0},
- {x = 1, y = 0, z = 0},
- {x = 0, y = 0, z = 1},
- {x = 0, y = 0, z = -1},
- {x = 0, y = 0, z = 0},
- {x = 0, y = 1, z = 0},
+ vector.new(-1, -1, 0),
+ vector.new( 1, -1, 0),
+ vector.new( 0, -1, -1),
+ vector.new( 0, -1, 1),
+ vector.new( 0, -1, 0),
+ vector.new(-1, 0, 0),
+ vector.new( 1, 0, 0),
+ vector.new( 0, 0, 1),
+ vector.new( 0, 0, -1),
+ vector.new( 0, 0, 0),
+ vector.new( 0, 1, 0),
}
function core.check_for_falling(p)
diff --git a/builtin/game/features.lua b/builtin/game/features.lua
index 36ff1f0b0..583ef5092 100644
--- a/builtin/game/features.lua
+++ b/builtin/game/features.lua
@@ -19,6 +19,9 @@ core.features = {
object_step_has_moveresult = true,
direct_velocity_on_players = true,
use_texture_alpha_string_modes = true,
+ degrotate_240_steps = true,
+ abm_min_max_y = true,
+ dynamic_add_media_table = true,
}
function core.has_feature(arg)
diff --git a/builtin/game/forceloading.lua b/builtin/game/forceloading.lua
index e1e00920c..8043e5dea 100644
--- a/builtin/game/forceloading.lua
+++ b/builtin/game/forceloading.lua
@@ -86,12 +86,6 @@ local function read_file(filename)
return core.deserialize(t) or {}
end
-local function write_file(filename, table)
- local f = io.open(filename, "w")
- f:write(core.serialize(table))
- f:close()
-end
-
blocks_forceloaded = read_file(wpath.."/force_loaded.txt")
for _, __ in pairs(blocks_forceloaded) do
total_forceloaded = total_forceloaded + 1
@@ -106,7 +100,8 @@ end)
-- persists the currently forceloaded blocks to disk
local function persist_forceloaded_blocks()
- write_file(wpath.."/force_loaded.txt", blocks_forceloaded)
+ local data = core.serialize(blocks_forceloaded)
+ core.safe_file_write(wpath.."/force_loaded.txt", data)
end
-- periodical forceload persistence
diff --git a/builtin/game/init.lua b/builtin/game/init.lua
index 1d62be019..bb007fabd 100644
--- a/builtin/game/init.lua
+++ b/builtin/game/init.lua
@@ -7,8 +7,6 @@ local gamepath = scriptpath .. "game".. DIR_DELIM
-- not exposed to outer context
local builtin_shared = {}
-dofile(commonpath .. "vector.lua")
-
dofile(gamepath .. "constants.lua")
assert(loadfile(gamepath .. "item.lua"))(builtin_shared)
dofile(gamepath .. "register.lua")
diff --git a/builtin/game/item.lua b/builtin/game/item.lua
index b68177c22..5a83eafd2 100644
--- a/builtin/game/item.lua
+++ b/builtin/game/item.lua
@@ -92,12 +92,12 @@ end
-- Table of possible dirs
local facedir_to_dir = {
- {x= 0, y=0, z= 1},
- {x= 1, y=0, z= 0},
- {x= 0, y=0, z=-1},
- {x=-1, y=0, z= 0},
- {x= 0, y=-1, z= 0},
- {x= 0, y=1, z= 0},
+ vector.new( 0, 0, 1),
+ vector.new( 1, 0, 0),
+ vector.new( 0, 0, -1),
+ vector.new(-1, 0, 0),
+ vector.new( 0, -1, 0),
+ vector.new( 0, 1, 0),
}
-- Mapping from facedir value to index in facedir_to_dir.
local facedir_to_dir_map = {
@@ -136,12 +136,12 @@ end
-- table of dirs in wallmounted order
local wallmounted_to_dir = {
- [0] = {x = 0, y = 1, z = 0},
- {x = 0, y = -1, z = 0},
- {x = 1, y = 0, z = 0},
- {x = -1, y = 0, z = 0},
- {x = 0, y = 0, z = 1},
- {x = 0, y = 0, z = -1},
+ [0] = vector.new( 0, 1, 0),
+ vector.new( 0, -1, 0),
+ vector.new( 1, 0, 0),
+ vector.new(-1, 0, 0),
+ vector.new( 0, 0, 1),
+ vector.new( 0, 0, -1),
}
function core.wallmounted_to_dir(wallmounted)
return wallmounted_to_dir[wallmounted % 8]
@@ -152,12 +152,12 @@ function core.dir_to_yaw(dir)
end
function core.yaw_to_dir(yaw)
- return {x = -math.sin(yaw), y = 0, z = math.cos(yaw)}
+ return vector.new(-math.sin(yaw), 0, math.cos(yaw))
end
function core.is_colored_paramtype(ptype)
return (ptype == "color") or (ptype == "colorfacedir") or
- (ptype == "colorwallmounted")
+ (ptype == "colorwallmounted") or (ptype == "colordegrotate")
end
function core.strip_param2_color(param2, paramtype2)
@@ -168,11 +168,25 @@ function core.strip_param2_color(param2, paramtype2)
param2 = math.floor(param2 / 32) * 32
elseif paramtype2 == "colorwallmounted" then
param2 = math.floor(param2 / 8) * 8
+ elseif paramtype2 == "colordegrotate" then
+ param2 = math.floor(param2 / 32) * 32
end
-- paramtype2 == "color" requires no modification.
return param2
end
+local function has_all_groups(tbl, required_groups)
+ if type(required_groups) == "string" then
+ return (tbl[required_groups] or 0) ~= 0
+ end
+ for _, group in ipairs(required_groups) do
+ if (tbl[group] or 0) == 0 then
+ return false
+ end
+ end
+ return true
+end
+
function core.get_node_drops(node, toolname)
-- Compatibility, if node is string
local nodename = node
@@ -212,7 +226,7 @@ function core.get_node_drops(node, toolname)
if item.rarity ~= nil then
good_rarity = item.rarity < 1 or math.random(item.rarity) == 1
end
- if item.tools ~= nil then
+ if item.tools ~= nil or item.tool_groups ~= nil then
good_tool = false
end
if item.tools ~= nil and toolname then
@@ -227,6 +241,27 @@ function core.get_node_drops(node, toolname)
end
end
end
+ if item.tool_groups ~= nil and toolname then
+ local tooldef = core.registered_items[toolname]
+ if tooldef ~= nil and type(tooldef.groups) == "table" then
+ if type(item.tool_groups) == "string" then
+ -- tool_groups can be a string which specifies the required group
+ good_tool = core.get_item_group(toolname, item.tool_groups) ~= 0
+ else
+ -- tool_groups can be a list of sufficient requirements.
+ -- i.e. if any item in the list can be satisfied then the tool is good
+ assert(type(item.tool_groups) == "table")
+ for _, required_groups in ipairs(item.tool_groups) do
+ -- required_groups can be either a string (a single group),
+ -- or an array of strings where all must be in tooldef.groups
+ good_tool = has_all_groups(tooldef.groups, required_groups)
+ if good_tool then
+ break
+ end
+ end
+ end
+ end
+ end
if good_rarity and good_tool then
got_count = got_count + 1
for _, add_item in ipairs(item.items) do
@@ -288,12 +323,12 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
end
-- Place above pointed node
- local place_to = {x = above.x, y = above.y, z = above.z}
+ local place_to = vector.new(above)
-- If node under is buildable_to, place into it instead (eg. snow)
if olddef_under.buildable_to then
log("info", "node under is buildable to")
- place_to = {x = under.x, y = under.y, z = under.z}
+ place_to = vector.new(under)
end
if core.is_protected(place_to, playername) then
@@ -313,22 +348,14 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
newnode.param2 = def.place_param2
elseif (def.paramtype2 == "wallmounted" or
def.paramtype2 == "colorwallmounted") and not param2 then
- local dir = {
- x = under.x - above.x,
- y = under.y - above.y,
- z = under.z - above.z
- }
+ local dir = vector.subtract(under, above)
newnode.param2 = core.dir_to_wallmounted(dir)
-- Calculate the direction for furnaces and chests and stuff
elseif (def.paramtype2 == "facedir" or
def.paramtype2 == "colorfacedir") and not param2 then
local placer_pos = placer and placer:get_pos()
if placer_pos then
- local dir = {
- x = above.x - placer_pos.x,
- y = above.y - placer_pos.y,
- z = above.z - placer_pos.z
- }
+ local dir = vector.subtract(above, placer_pos)
newnode.param2 = core.dir_to_facedir(dir)
log("info", "facedir: " .. newnode.param2)
end
@@ -345,6 +372,8 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
color_divisor = 8
elseif def.paramtype2 == "colorfacedir" then
color_divisor = 32
+ elseif def.paramtype2 == "colordegrotate" then
+ color_divisor = 32
end
if color_divisor then
local color = math.floor(metatable.palette_index / color_divisor)
@@ -380,7 +409,7 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
-- Run callback
if def.after_place_node and not prevent_after_place then
-- Deepcopy place_to and pointed_thing because callback can modify it
- local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z}
+ local place_to_copy = vector.new(place_to)
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
if def.after_place_node(place_to_copy, placer, itemstack,
pointed_thing_copy) then
@@ -391,7 +420,7 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2,
-- Run script hook
for _, callback in ipairs(core.registered_on_placenodes) do
-- Deepcopy pos, node and pointed_thing because callback can modify them
- local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z}
+ local place_to_copy = vector.new(place_to)
local newnode_copy = {name=newnode.name, param1=newnode.param1, param2=newnode.param2}
local oldnode_copy = {name=oldnode.name, param1=oldnode.param1, param2=oldnode.param2}
local pointed_thing_copy = copy_pointed_thing(pointed_thing)
@@ -470,34 +499,41 @@ function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed
return result
end
end
+ -- read definition before potentially emptying the stack
local def = itemstack:get_definition()
- if itemstack:take_item() ~= nil then
- user:set_hp(user:get_hp() + hp_change)
-
- if def and def.sound and def.sound.eat then
- core.sound_play(def.sound.eat, {
- pos = user:get_pos(),
- max_hear_distance = 16
- }, true)
- end
+ if itemstack:take_item():is_empty() then
+ return itemstack
+ end
+
+ if def and def.sound and def.sound.eat then
+ core.sound_play(def.sound.eat, {
+ pos = user:get_pos(),
+ max_hear_distance = 16
+ }, true)
+ end
- if replace_with_item then
- if itemstack:is_empty() then
- itemstack:add_item(replace_with_item)
+ -- Changing hp might kill the player causing mods to do who-knows-what to the
+ -- inventory, so do this before set_hp().
+ if replace_with_item then
+ if itemstack:is_empty() then
+ itemstack:add_item(replace_with_item)
+ else
+ local inv = user:get_inventory()
+ -- Check if inv is null, since non-players don't have one
+ if inv and inv:room_for_item("main", {name=replace_with_item}) then
+ inv:add_item("main", replace_with_item)
else
- local inv = user:get_inventory()
- -- Check if inv is null, since non-players don't have one
- if inv and inv:room_for_item("main", {name=replace_with_item}) then
- inv:add_item("main", replace_with_item)
- else
- local pos = user:get_pos()
- pos.y = math.floor(pos.y + 0.5)
- core.add_item(pos, replace_with_item)
- end
+ local pos = user:get_pos()
+ pos.y = math.floor(pos.y + 0.5)
+ core.add_item(pos, replace_with_item)
end
end
end
- return itemstack
+ user:set_wielded_item(itemstack)
+
+ user:set_hp(user:get_hp() + hp_change)
+
+ return nil -- don't overwrite wield item a second time
end
function core.item_eat(hp_change, replace_with_item)
@@ -537,11 +573,11 @@ function core.handle_node_drops(pos, drops, digger)
for _, dropped_item in pairs(drops) do
local left = give_item(dropped_item)
if not left:is_empty() then
- local p = {
- x = pos.x + math.random()/2-0.25,
- y = pos.y + math.random()/2-0.25,
- z = pos.z + math.random()/2-0.25,
- }
+ local p = vector.offset(pos,
+ math.random()/2-0.25,
+ math.random()/2-0.25,
+ math.random()/2-0.25
+ )
core.add_item(p, left)
end
end
@@ -578,7 +614,7 @@ function core.node_dig(pos, node, digger)
if wielded then
local wdef = wielded:get_definition()
local tp = wielded:get_tool_capabilities()
- local dp = core.get_dig_params(def and def.groups, tp)
+ local dp = core.get_dig_params(def and def.groups, tp, wielded:get_wear())
if wdef and wdef.after_use then
wielded = wdef.after_use(wielded, digger, node, dp) or wielded
else
@@ -600,7 +636,7 @@ function core.node_dig(pos, node, digger)
if def and def.preserve_metadata then
local oldmeta = core.get_meta(pos):to_table().fields
-- Copy pos and node because the callback can modify them.
- local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
+ local pos_copy = vector.new(pos)
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
local drop_stacks = {}
for k, v in pairs(drops) do
@@ -632,7 +668,7 @@ function core.node_dig(pos, node, digger)
-- Run callback
if def and def.after_dig_node then
-- Copy pos and node because callback can modify them
- local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
+ local pos_copy = vector.new(pos)
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
def.after_dig_node(pos_copy, node_copy, oldmetadata, digger)
end
@@ -645,7 +681,7 @@ function core.node_dig(pos, node, digger)
end
-- Copy pos and node because callback can modify them
- local pos_copy = {x=pos.x, y=pos.y, z=pos.z}
+ local pos_copy = vector.new(pos)
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
callback(pos_copy, node_copy, digger)
end
@@ -688,7 +724,7 @@ core.nodedef_default = {
groups = {},
inventory_image = "",
wield_image = "",
- wield_scale = {x=1,y=1,z=1},
+ wield_scale = vector.new(1, 1, 1),
stack_max = default_stack_max,
usable = false,
liquids_pointable = false,
@@ -747,7 +783,7 @@ core.craftitemdef_default = {
groups = {},
inventory_image = "",
wield_image = "",
- wield_scale = {x=1,y=1,z=1},
+ wield_scale = vector.new(1, 1, 1),
stack_max = default_stack_max,
liquids_pointable = false,
tool_capabilities = nil,
@@ -766,7 +802,7 @@ core.tooldef_default = {
groups = {},
inventory_image = "",
wield_image = "",
- wield_scale = {x=1,y=1,z=1},
+ wield_scale = vector.new(1, 1, 1),
stack_max = 1,
liquids_pointable = false,
tool_capabilities = nil,
@@ -785,7 +821,7 @@ core.noneitemdef_default = { -- This is used for the hand and unknown items
groups = {},
inventory_image = "",
wield_image = "",
- wield_scale = {x=1,y=1,z=1},
+ wield_scale = vector.new(1, 1, 1),
stack_max = default_stack_max,
liquids_pointable = false,
tool_capabilities = nil,
diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua
index b8c5e16a9..e86efc50c 100644
--- a/builtin/game/misc.lua
+++ b/builtin/game/misc.lua
@@ -1,9 +1,21 @@
-- Minetest: builtin/misc.lua
+local S = core.get_translator("__builtin")
+
--
-- Misc. API functions
--
+-- @spec core.kick_player(String, String) :: Boolean
+function core.kick_player(player_name, reason)
+ if type(reason) == "string" then
+ reason = "Kicked: " .. reason
+ else
+ reason = "Kicked."
+ end
+ return core.disconnect_player(player_name, reason)
+end
+
function core.check_player_privs(name, ...)
if core.is_player(name) then
name = name:get_player_name()
@@ -42,15 +54,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
@@ -117,13 +129,12 @@ end
function core.get_position_from_hash(hash)
- local pos = {}
- pos.x = (hash % 65536) - 32768
+ local x = (hash % 65536) - 32768
hash = math.floor(hash / 65536)
- pos.y = (hash % 65536) - 32768
+ local y = (hash % 65536) - 32768
hash = math.floor(hash / 65536)
- pos.z = (hash % 65536) - 32768
- return pos
+ local z = (hash % 65536) - 32768
+ return vector.new(x, y, z)
end
@@ -213,7 +224,7 @@ function core.is_area_protected(minp, maxp, player_name, interval)
local y = math.floor(yf + 0.5)
for xf = minp.x, maxp.x, d.x do
local x = math.floor(xf + 0.5)
- local pos = {x = x, y = y, z = z}
+ local pos = vector.new(x, y, z)
if core.is_protected(pos, player_name) then
return pos
end
@@ -239,7 +250,7 @@ end
-- HTTP callback interface
-function core.http_add_fetch(httpenv)
+core.set_http_api_lua(function(httpenv)
httpenv.fetch = function(req, callback)
local handle = httpenv.fetch_async(req)
@@ -255,7 +266,8 @@ function core.http_add_fetch(httpenv)
end
return httpenv
-end
+end)
+core.set_http_api_lua = nil
function core.close_formspec(player_name, formname)
@@ -268,24 +280,44 @@ function core.cancel_shutdown_requests()
end
--- Callback handling for dynamic_add_media
+-- Used for callback handling with dynamic_add_media
+core.dynamic_media_callbacks = {}
+
-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
+-- PNG encoder safety wrapper
+
+local o_encode_png = core.encode_png
+function core.encode_png(width, height, data, compression)
+ if type(width) ~= "number" then
+ error("Incorrect type for 'width', expected number, got " .. type(width))
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)
+ if type(height) ~= "number" then
+ error("Incorrect type for 'height', expected number, got " .. type(height))
+ end
+
+ local expected_byte_count = width * height * 4
+
+ if type(data) ~= "table" and type(data) ~= "string" then
+ error("Incorrect type for 'data', expected table or string, got " .. type(data))
+ end
+
+ local data_length = type(data) == "table" and #data * 4 or string.len(data)
+
+ if data_length ~= expected_byte_count then
+ error(string.format(
+ "Incorrect length of 'data', width and height imply %d bytes but %d were provided",
+ expected_byte_count,
+ data_length
+ ))
+ end
+
+ if type(data) == "table" then
+ local dataBuf = {}
+ for i = 1, #data do
+ dataBuf[i] = core.colorspec_to_bytes(data[i])
end
+ data = table.concat(dataBuf)
end
- return true
+
+ return o_encode_png(width, height, data, compression or 6)
end
diff --git a/builtin/game/privileges.lua b/builtin/game/privileges.lua
index c7417d2f4..2ff4c093c 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,71 +30,76 @@ 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"))
+
+local basic_privs =
+ core.string_to_privs((core.settings:get("basic_privs") or "shout,interact"))
+local basic_privs_desc = S("Can modify basic privileges (@1)",
+ core.privs_to_string(basic_privs, ', '))
+core.register_privilege("basic_privs", basic_privs_desc)
+
+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("Can enable wireframe"),
give_to_singleplayer = false,
- give_to_admin = true,
})
core.register_can_bypass_userlimit(function(name, ip)
diff --git a/builtin/game/register.lua b/builtin/game/register.lua
index 1cff85813..56e40c75c 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
--
@@ -326,7 +328,7 @@ end
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,
@@ -336,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",
@@ -353,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",
@@ -366,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,
})
@@ -607,6 +610,7 @@ core.registered_on_modchannel_message, core.register_on_modchannel_message = mak
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()
+core.registered_on_liquid_transformed, core.register_on_liquid_transformed = make_registration()
--
-- Compatibility for on_mapgen_init()
diff --git a/builtin/game/voxelarea.lua b/builtin/game/voxelarea.lua
index 724761414..64436bf1a 100644
--- a/builtin/game/voxelarea.lua
+++ b/builtin/game/voxelarea.lua
@@ -1,6 +1,6 @@
VoxelArea = {
- MinEdge = {x=1, y=1, z=1},
- MaxEdge = {x=0, y=0, z=0},
+ MinEdge = vector.new(1, 1, 1),
+ MaxEdge = vector.new(0, 0, 0),
ystride = 0,
zstride = 0,
}
@@ -19,11 +19,11 @@ end
function VoxelArea:getExtent()
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
- return {
- x = MaxEdge.x - MinEdge.x + 1,
- y = MaxEdge.y - MinEdge.y + 1,
- z = MaxEdge.z - MinEdge.z + 1,
- }
+ return vector.new(
+ MaxEdge.x - MinEdge.x + 1,
+ MaxEdge.y - MinEdge.y + 1,
+ MaxEdge.z - MinEdge.z + 1
+ )
end
function VoxelArea:getVolume()
diff --git a/builtin/init.lua b/builtin/init.lua
index 89b1fdc64..7a9b5c427 100644
--- a/builtin/init.lua
+++ b/builtin/init.lua
@@ -30,6 +30,7 @@ local clientpath = scriptdir .. "client" .. DIR_DELIM
local commonpath = scriptdir .. "common" .. DIR_DELIM
local asyncpath = scriptdir .. "async" .. DIR_DELIM
+dofile(commonpath .. "vector.lua")
dofile(commonpath .. "strict.lua")
dofile(commonpath .. "serialize.lua")
dofile(commonpath .. "misc_helpers.lua")
diff --git a/builtin/locale/__builtin.de.tr b/builtin/locale/__builtin.de.tr
new file mode 100644
index 000000000..1b29f81e7
--- /dev/null
+++ b/builtin/locale/__builtin.de.tr
@@ -0,0 +1,245 @@
+# textdomain: __builtin
+Empty command.=Leerer Befehl.
+Invalid command: @1=Ungültiger Befehl: @1
+Invalid command usage.=Ungültige Befehlsverwendung.
+ (@1 s)= (@1 s)
+Command execution took @1 s=Befehlsausführung brauchte @1 s
+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.
+@1 does not have any privileges.=@1 hat keine Privilegien.
+Privileges of @1: @2=Privilegien von @1: @2
+[<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.
+<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!
+No online player has the "@1" privilege.=Kein online spielender Spieler hat das „@1“-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.
+Your privileges are insufficient. '@1' only allows you to grant: @2=Ihre Privilegien sind unzureichend. Mit „@1“ können Sie nur folgendes gewähren: @2
+Unknown privilege: @1=Unbekanntes Privileg: @1
+@1 granted you privileges: @2=@1 gewährte Ihnen Privilegien: @2
+<name> (<privilege> [, <privilege2> [<...>]] | all)=<Name> (<Privileg> [, <Privileg2> [<...>]] | all)
+Give privileges to player=Privileg an Spieler vergeben
+Invalid parameters (see /help grant).=Ungültige Parameter (siehe „/help grant“).
+<privilege> [, <privilege2> [<...>]] | all=<Privileg> [, <Privileg2> [<...>]] | all
+Grant privileges to yourself=Privilegien an Ihnen selbst vergeben
+Invalid parameters (see /help grantme).=Ungültige Parameter (siehe „/help grantme“).
+Your privileges are insufficient. '@1' only allows you to revoke: @2=Ihre Privilegien sind unzureichend. Mit „@1“ können Sie nur folgendes entziehen: @2
+Note: Cannot revoke in singleplayer: @1=Anmerkung: Im Einzelspielermodus kann man folgendes nicht entziehen: @1
+Note: Cannot revoke from admin: @1=Anmerkung: Vom Admin kann man folgendes nicht entziehen: @1
+No privileges were revoked.=Es wurden keine Privilegien entzogen.
+@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
+No mods installed.=Es sind keine Mods installiert.
+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 (must be between 0 and 24000).=Ungültige Zeit (muss zwischen 0 und 24000 liegen).
+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] [-r] [<message>]=[<Verzögerung_in_Sekunden> | -1] [-r] [<Nachricht>]
+Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)=Server herunterfahren (-1 bricht einen verzögerten Abschaltvorgang ab, -r erlaubt Spielern, sich wiederzuverbinden)
+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)
+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
+Invalid parameters (see /help @1).=Ungültige Parameter (siehe „/help @1“).
+Too many arguments, try using just /help <command>=Zu viele Argumente. Probieren Sie es mit „/help <Befehl>“
+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>] [-t]=[all | privs | <Befehl>] [-t]
+Get help for commands or list privileges (-t: output in chat)=Hilfe für Befehle erhalten oder Privilegien auflisten (-t: Ausgabe im Chat)
+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).
+@1 joined the game.=@1 ist dem Spiel beigetreten.
+@1 left the game.=@1 hat das Spiel verlassen.
+@1 left the game (timed out).=@1 hat das Spiel verlassen (Netzwerkzeitüberschreitung).
+(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 basic privileges (@1)=Kann grundlegende Privilegien anpassen (@1)
+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
+Can view more debug info that might give a gameplay advantage=Kann zusätzliche Debuginformationen betrachten, welche einen spielerischen Vorteil geben könnten
+Can enable wireframe=Kann Drahtmodell aktivieren
+Unknown Item=Unbekannter Gegenstand
+Air=Luft
+Ignore=Ignorieren
+You can't place 'ignore' nodes!=Sie können keine „ignore“-Blöcke platzieren!
+Values below show absolute/relative times spend per server step by the instrumented function.=Die unten angegebenen Werte zeigen absolute/relative Zeitspannen, die je Server-Step von der instrumentierten Funktion in Anspruch genommen wurden.
+A total of @1 sample(s) were taken.=Es wurden insgesamt @1 Datenpunkt(e) aufgezeichnet.
+The output is limited to '@1'.=Die Ausgabe ist beschränkt auf „@1“.
+Saving of profile failed: @1=Speichern des Profils fehlgeschlagen: @1
+Profile saved to @1=Profil abgespeichert nach @1
diff --git a/builtin/locale/__builtin.it.tr b/builtin/locale/__builtin.it.tr
new file mode 100644
index 000000000..77f85c766
--- /dev/null
+++ b/builtin/locale/__builtin.it.tr
@@ -0,0 +1,258 @@
+# textdomain: __builtin
+Empty command.=Comando vuoto.
+Invalid command: @1=Comando non valido: @1
+Invalid command usage.=Utilizzo del comando non valido.
+ (@1 s)=
+Command execution took @1 s=
+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.
+@1 does not have any privileges.=
+Privileges of @1: @2=Privilegi di @1: @2
+[<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.
+<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!
+No online player has the "@1" privilege.=
+Players online with the "@1" privilege: @2=Giocatori connessi con il privilegio "@1": @2
+Your privileges are insufficient.=I tuoi privilegi sono insufficienti.
+Your privileges are insufficient. '@1' only allows you to grant: @2=
+Unknown privilege: @1=Privilegio sconosciuto: @1
+@1 granted you privileges: @2=@1 ti ha assegnato i seguenti privilegi: @2
+<name> (<privilege> [, <privilege2> [<...>]] | all)=
+Give privileges to player=Dà privilegi al giocatore
+Invalid parameters (see /help grant).=Parametri non validi (vedi /help grant).
+<privilege> [, <privilege2> [<...>]] | all=
+Grant privileges to yourself=Assegna dei privilegi a te stessǝ
+Invalid parameters (see /help grantme).=Parametri non validi (vedi /help grantme).
+Your privileges are insufficient. '@1' only allows you to revoke: @2=
+Note: Cannot revoke in singleplayer: @1=
+Note: Cannot revoke from admin: @1=
+No privileges were revoked.=
+@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
+No mods installed.=
+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 (must be between 0 and 24000).=
+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] [-r] [<message>]=
+Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)=
+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ǝ
+Invalid parameters (see /help @1).=
+Too many arguments, try using just /help <command>=
+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>] [-t]=
+Get help for commands or list privileges (-t: output in chat)=
+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).
+@1 joined the game.=
+@1 left the game.=
+@1 left the game (timed out).=
+(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 basic privileges (@1)=
+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
+Can view more debug info that might give a gameplay advantage=
+Can enable wireframe=
+Unknown Item=Oggetto sconosciuto
+Air=Aria
+Ignore=Ignora
+You can't place 'ignore' nodes!=Non puoi piazzare nodi 'ignore'!
+Values below show absolute/relative times spend per server step by the instrumented function.=
+A total of @1 sample(s) were taken.=
+The output is limited to '@1'.=
+Saving of profile failed: @1=
+Profile saved to @1=
+
+
+##### not used anymore #####
+
+Invalid time.=Orario non valido.
+[all | privs | <cmd>]=[all | privs | <comando>]
+Get help for commands or list privileges=Richiama la finestra d'aiuto dei comandi o dei privilegi
+Allows enabling various debug options that may affect gameplay=Permette di abilitare varie opzioni di debug che potrebbero influenzare l'esperienza di gioco
+[<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)
+<name> (<privilege> | all)=<nome> (<privilegio> | all)
+<privilege> | all=<privilegio> | all
+Can modify 'shout' and 'interact' privileges=Si possono modificare i privilegi 'shout' e 'interact'
diff --git a/builtin/locale/template.txt b/builtin/locale/template.txt
new file mode 100644
index 000000000..308d17f37
--- /dev/null
+++ b/builtin/locale/template.txt
@@ -0,0 +1,245 @@
+# textdomain: __builtin
+Empty command.=
+Invalid command: @1=
+Invalid command usage.=
+ (@1 s)=
+Command execution took @1 s=
+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.=
+@1 does not have any privileges.=
+Privileges of @1: @2=
+[<name>]=
+Show privileges of yourself or another player=
+Player @1 does not exist.=
+<privilege>=
+Return list of all online players with privilege=
+Invalid parameters (see /help haspriv).=
+Unknown privilege!=
+No online player has the "@1" privilege.=
+Players online with the "@1" privilege: @2=
+Your privileges are insufficient.=
+Your privileges are insufficient. '@1' only allows you to grant: @2=
+Unknown privilege: @1=
+@1 granted you privileges: @2=
+<name> (<privilege> [, <privilege2> [<...>]] | all)=
+Give privileges to player=
+Invalid parameters (see /help grant).=
+<privilege> [, <privilege2> [<...>]] | all=
+Grant privileges to yourself=
+Invalid parameters (see /help grantme).=
+Your privileges are insufficient. '@1' only allows you to revoke: @2=
+Note: Cannot revoke in singleplayer: @1=
+Note: Cannot revoke from admin: @1=
+No privileges were revoked.=
+@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=
+No mods installed.=
+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 (must be between 0 and 24000).=
+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] [-r] [<message>]=
+Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)=
+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=
+Invalid parameters (see /help @1).=
+Too many arguments, try using just /help <command>=
+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>] [-t]=
+Get help for commands or list privileges (-t: output in chat)=
+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).=
+@1 joined the game.=
+@1 left the game.=
+@1 left the game (timed out).=
+(no description)=
+Can interact with things and modify the world=
+Can speak in chat=
+Can modify basic privileges (@1)=
+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=
+Can view more debug info that might give a gameplay advantage=
+Can enable wireframe=
+Unknown Item=
+Air=
+Ignore=
+You can't place 'ignore' nodes!=
+Values below show absolute/relative times spend per server step by the instrumented function.=
+A total of @1 sample(s) were taken.=
+The output is limited to '@1'.=
+Saving of profile failed: @1=
+Profile saved to @1=
diff --git a/builtin/mainmenu/common.lua b/builtin/mainmenu/common.lua
index cd896f9ec..8db8bb8d1 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,21 +45,7 @@ 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 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())
@@ -75,31 +56,29 @@ function render_serverlist_row(spec, is_favorite)
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
@@ -110,68 +89,59 @@ 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
-end
+ table.insert(details, color)
+ table.insert(details, text)
---------------------------------------------------------------------------------
-os.tempfolder = function()
- local temp = core.get_temp_path()
- return temp .. DIR_DELIM .. "MT_" .. math.random(0, 10000)
+ return table.concat(details, ",")
end
-
---------------------------------------------------------------------------------
+---------------------------------------------------------------------------------
os.tmpname = function()
- local path = os.tempfolder()
- io.open(path, "w"):close()
- return path
+ error('do not use') -- instead use core.get_temp_path()
end
-
--------------------------------------------------------------------------------
-function menu_render_worldlist()
- local retval = ""
+
+function menu_render_worldlist(show_gameid)
+ local retval = {}
local current_worldlist = menudata.worldlist:get_list()
+ local row
for i, v in ipairs(current_worldlist) do
- if retval ~= "" then retval = retval .. "," end
- retval = retval .. core.formspec_escape(v.name) ..
- " \\[" .. core.formspec_escape(v.gameid) .. "\\]"
+ row = v.name
+ if show_gameid == nil or show_gameid == true then
+ row = row .. " [" .. v.gameid .. "]"
+ end
+ retval[#retval+1] = core.formspec_escape(row)
+
end
- return retval
+ return table.concat(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
@@ -188,7 +158,6 @@ function menu_handle_key_up_down(fields, textlist, settingname)
return false
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 ..
@@ -206,7 +175,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.
@@ -214,7 +182,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
@@ -242,7 +210,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_contentstore.lua b/builtin/mainmenu/dlg_contentstore.lua
index b0736a4fd..9db67cf57 100644
--- a/builtin/mainmenu/dlg_contentstore.lua
+++ b/builtin/mainmenu/dlg_contentstore.lua
@@ -57,34 +57,77 @@ local filter_types_type = {
"txp",
}
+local REASON_NEW = "new"
+local REASON_UPDATE = "update"
+local REASON_DEPENDENCY = "dependency"
-local function download_package(param)
- if core.download_file(param.package.url, param.filename) then
+
+-- encodes for use as URL parameter or path component
+local function urlencode(str)
+ return str:gsub("[^%a%d()._~-]", function(char)
+ return string.format("%%%02X", string.byte(char))
+ end)
+end
+assert(urlencode("sample text?") == "sample%20text%3F")
+
+
+local function get_download_url(package, reason)
+ local base_url = core.settings:get("contentdb_url")
+ local ret = base_url .. ("/packages/%s/releases/%d/download/"):format(
+ package.url_part, package.release)
+ if reason then
+ ret = ret .. "?reason=" .. reason
+ end
+ return ret
+end
+
+
+local function download_and_extract(param)
+ local package = param.package
+
+ local filename = core.get_temp_path(true)
+ if filename == "" or not core.download_file(param.url, filename) then
+ core.log("error", "Downloading " .. dump(param.url) .. " failed")
return {
- filename = param.filename,
- successful = true,
+ msg = fgettext("Failed to download $1", package.name)
}
+ end
+
+ local tempfolder = core.get_temp_path()
+ if tempfolder ~= "" then
+ tempfolder = tempfolder .. DIR_DELIM .. "MT_" .. math.random(1, 1024000)
+ if not core.extract_zip(filename, tempfolder) then
+ tempfolder = nil
+ end
else
- core.log("error", "downloading " .. dump(param.package.url) .. " failed")
+ tempfolder = nil
+ end
+ os.remove(filename)
+ if not tempfolder then
return {
- successful = false,
+ msg = fgettext("Install: Unsupported file type or broken archive"),
}
end
+
+ return {
+ path = tempfolder
+ }
end
-local function start_install(package)
+local function start_install(package, reason)
local params = {
package = package,
- filename = os.tempfolder() .. "_MODNAME_" .. package.name .. ".zip",
+ url = get_download_url(package, reason),
}
number_downloading = number_downloading + 1
local function callback(result)
- if result.successful then
- local path, msg = pkgmgr.install(package.type,
- result.filename, package.name,
- package.path)
+ if result.msg then
+ gamedata.errormessage = result.msg
+ else
+ local path, msg = pkgmgr.install_dir(package.type, result.path, package.name, package.path)
+ core.delete_dir(result.path)
if not path then
gamedata.errormessage = msg
else
@@ -122,9 +165,6 @@ local function start_install(package)
conf:write()
end
end
- os.remove(result.filename)
- else
- gamedata.errormessage = fgettext("Failed to download $1", package.name)
end
package.downloading = false
@@ -135,7 +175,7 @@ local function start_install(package)
if next then
table.remove(download_queue, 1)
- start_install(next)
+ start_install(next.package, next.reason)
end
ui.update()
@@ -144,19 +184,19 @@ local function start_install(package)
package.queued = false
package.downloading = true
- if not core.handle_async(download_package, params, callback) then
+ if not core.handle_async(download_and_extract, params, callback) then
core.log("error", "ERROR: async event failed")
gamedata.errormessage = fgettext("Failed to download $1", package.name)
return
end
end
-local function queue_download(package)
+local function queue_download(package, reason)
local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads"))
if number_downloading < max_concurrent_downloads then
- start_install(package)
+ start_install(package, reason)
else
- table.insert(download_queue, package)
+ table.insert(download_queue, { package = package, reason = reason })
package.queued = true
end
end
@@ -169,7 +209,7 @@ local function get_raw_dependencies(package)
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 url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string))
local response = http.fetch_sync({ url = url })
if not response.succeeded then
@@ -407,12 +447,12 @@ function install_dialog.handle_submit(this, fields)
end
if fields.install_all then
- queue_download(install_dialog.package)
+ queue_download(install_dialog.package, REASON_NEW)
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)
+ queue_download(dep.package, REASON_DEPENDENCY)
end
end
end
@@ -544,33 +584,43 @@ function store.load()
local base_url = core.settings:get("contentdb_url")
local url = base_url ..
"/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
- core.get_max_supp_proto() .. "&engine_version=" .. version.string
+ core.get_max_supp_proto() .. "&engine_version=" .. urlencode(version.string)
for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
item = item:trim()
if item ~= "" then
- url = url .. "&hide=" .. item
+ url = url .. "&hide=" .. urlencode(item)
end
end
- local timeout = tonumber(core.settings:get("curl_file_download_timeout"))
- local response = http.fetch_sync({ url = url, timeout = timeout })
+ local response = http.fetch_sync({ url = url })
if not response.succeeded then
return
end
store.packages_full = core.parse_json(response.data) or {}
+ store.aliases = {}
for _, package in pairs(store.packages_full) do
- package.url = base_url .. "/packages/" ..
- package.author .. "/" .. package.name ..
- "/releases/" .. package.release .. "/download/"
-
local name_len = #package.name
+ -- This must match what store.update_paths() does!
+ package.id = package.author:lower() .. "/"
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
- package.id = package.author:lower() .. "/" .. package.name:sub(1, name_len - 5)
+ package.id = package.id .. package.name:sub(1, name_len - 5)
else
- package.id = package.author:lower() .. "/" .. package.name
+ package.id = package.id .. package.name
+ end
+
+ package.url_part = urlencode(package.author) .. "/" .. urlencode(package.name)
+
+ if package.aliases then
+ for _, alias in ipairs(package.aliases) do
+ -- We currently don't support name changing
+ local suffix = "/" .. package.name
+ if alias:sub(-#suffix) == suffix then
+ store.aliases[alias:lower()] = package.id
+ end
+ end
end
end
@@ -584,7 +634,8 @@ function store.update_paths()
pkgmgr.refresh_globals()
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
if mod.author and mod.release > 0 then
- mod_hash[mod.author:lower() .. "/" .. mod.name] = mod
+ local id = mod.author:lower() .. "/" .. mod.name
+ mod_hash[store.aliases[id] or id] = mod
end
end
@@ -592,14 +643,16 @@ function store.update_paths()
pkgmgr.update_gamelist()
for _, game in pairs(pkgmgr.games) do
if game.author ~= "" and game.release > 0 then
- game_hash[game.author:lower() .. "/" .. game.id] = game
+ local id = game.author:lower() .. "/" .. game.id
+ game_hash[store.aliases[id] or id] = game
end
end
local txp_hash = {}
for _, txp in pairs(pkgmgr.get_texture_packs()) do
if txp.author and txp.release > 0 then
- txp_hash[txp.author:lower() .. "/" .. txp.name] = txp
+ local id = txp.author:lower() .. "/" .. txp.name
+ txp_hash[store.aliases[id] or id] = txp
end
end
@@ -915,7 +968,7 @@ function store.handle_submit(this, fields)
local package = store.packages_full[i]
if package.path and package.installed_release < package.release and
not (package.downloading or package.queued) then
- queue_download(package)
+ queue_download(package, REASON_UPDATE)
end
end
return true
@@ -948,7 +1001,7 @@ function store.handle_submit(this, fields)
this:hide()
dlg:show()
else
- queue_download(package)
+ queue_download(package, package.path and REASON_UPDATE or REASON_NEW)
end
end
@@ -973,9 +1026,9 @@ function store.handle_submit(this, fields)
end
if fields["view_" .. i] then
- local url = ("%s/packages/%s/%s?protocol_version=%d"):format(
- core.settings:get("contentdb_url"),
- package.author, package.name, core.get_max_supp_proto())
+ local url = ("%s/packages/%s?protocol_version=%d"):format(
+ core.settings:get("contentdb_url"), package.url_part,
+ core.get_max_supp_proto())
core.open_url(url)
return true
end
diff --git a/builtin/mainmenu/dlg_create_world.lua b/builtin/mainmenu/dlg_create_world.lua
index 1938747fe..8d1509f33 100644
--- a/builtin/mainmenu/dlg_create_world.lua
+++ b/builtin/mainmenu/dlg_create_world.lua
@@ -15,7 +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 worldname = ""
+-- cf. tab_local, the gamebar already provides game selection so we hide the list from here
+local hide_gamelist = PLATFORM ~= "Android"
local function table_to_flags(ftable)
-- Convert e.g. { jungles = true, caves = false } to "jungles,nocaves"
@@ -31,9 +32,8 @@ local function strflag(flags, flag)
return (flags[flag] == true) and "true" or "false"
end
-local cb_caverns = { "caverns", fgettext("Caverns"), "caverns",
+local cb_caverns = { "caverns", fgettext("Caverns"),
fgettext("Very large caverns deep in the underground") }
-local tt_sea_rivers = fgettext("Sea level rivers")
local flag_checkboxes = {
v5 = {
@@ -41,39 +41,38 @@ local flag_checkboxes = {
},
v7 = {
cb_caverns,
- { "ridges", fgettext("Rivers"), "ridges", tt_sea_rivers },
- { "mountains", fgettext("Mountains"), "mountains" },
- { "floatlands", fgettext("Floatlands (experimental)"), "floatlands",
+ { "ridges", fgettext("Rivers"), fgettext("Sea level rivers") },
+ { "mountains", fgettext("Mountains") },
+ { "floatlands", fgettext("Floatlands (experimental)"),
fgettext("Floating landmasses in the sky") },
},
carpathian = {
cb_caverns,
- { "rivers", fgettext("Rivers"), "rivers", tt_sea_rivers },
+ { "rivers", fgettext("Rivers"), fgettext("Sea level rivers") },
},
valleys = {
- { "altitude-chill", fgettext("Altitude chill"), "altitude_chill",
+ { "altitude_chill", fgettext("Altitude chill"),
fgettext("Reduces heat with altitude") },
- { "altitude-dry", fgettext("Altitude dry"), "altitude_dry",
+ { "altitude_dry", fgettext("Altitude dry"),
fgettext("Reduces humidity with altitude") },
- { "humid-rivers", fgettext("Humid rivers"), "humid_rivers",
+ { "humid_rivers", fgettext("Humid rivers"),
fgettext("Increases humidity around rivers") },
- { "vary-river-depth", fgettext("Vary river depth"), "vary_river_depth",
+ { "vary_river_depth", fgettext("Vary river depth"),
fgettext("Low humidity and high heat causes shallow or dry rivers") },
},
flat = {
cb_caverns,
- { "hills", fgettext("Hills"), "hills" },
- { "lakes", fgettext("Lakes"), "lakes" },
+ { "hills", fgettext("Hills") },
+ { "lakes", fgettext("Lakes") },
},
fractal = {
- { "terrain", fgettext("Additional terrain"), "terrain",
+ { "terrain", fgettext("Additional terrain"),
fgettext("Generate non-fractal terrain: Oceans and underground") },
},
v6 = {
- { "trees", fgettext("Trees and jungle grass"), "trees" },
- { "flat", fgettext("Flat terrain"), "flat" },
- { "mudflow", fgettext("Mud flow"), "mudflow",
- fgettext("Terrain surface erosion") },
+ { "trees", fgettext("Trees and jungle grass") },
+ { "flat", fgettext("Flat terrain") },
+ { "mudflow", fgettext("Mud flow"), fgettext("Terrain surface erosion") },
-- Biome settings are in mgv6_biomes below
},
}
@@ -105,38 +104,26 @@ local function create_world_formspec(dialogdata)
"button[4.75,2.5;3,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
end
+ local current_mg = dialogdata.mg
local mapgens = core.get_mapgen_names()
- local current_seed = core.settings:get("fixed_map_seed") or ""
- local current_mg = core.settings:get("mg_name")
local gameid = core.settings:get("menu_last_game")
- local flags = {
- main = core.settings:get_flags("mg_flags"),
- v5 = core.settings:get_flags("mgv5_spflags"),
- v6 = core.settings:get_flags("mgv6_spflags"),
- v7 = core.settings:get_flags("mgv7_spflags"),
- fractal = core.settings:get_flags("mgfractal_spflags"),
- carpathian = core.settings:get_flags("mgcarpathian_spflags"),
- valleys = core.settings:get_flags("mgvalleys_spflags"),
- flat = core.settings:get_flags("mgflat_spflags"),
- }
-
- local gameidx = 0
- if gameid ~= nil then
- local _
- _, gameidx = pkgmgr.find_by_gameid(gameid)
+ local flags = dialogdata.flags
- if gameidx == nil then
- gameidx = 0
- end
+ local game, gameidx = pkgmgr.find_by_gameid(gameid)
+ if game == nil and hide_gamelist then
+ -- should never happen but just pick the first game
+ game = pkgmgr.get_game(1)
+ gameidx = 1
+ core.settings:set("menu_last_game", game.id)
+ elseif game == nil then
+ gameidx = 0
end
- local game_by_gameidx = core.get_game(gameidx)
local disallowed_mapgen_settings = {}
- if game_by_gameidx ~= nil then
- local gamepath = game_by_gameidx.path
- local gameconfig = Settings(gamepath.."/game.conf")
+ if game ~= nil then
+ local gameconfig = Settings(game.path.."/game.conf")
local allowed_mapgens = (gameconfig:get("allowed_mapgens") or ""):split()
for key, value in pairs(allowed_mapgens) do
@@ -156,7 +143,7 @@ local function create_world_formspec(dialogdata)
end
end
- if disallowed_mapgens then
+ if #disallowed_mapgens > 0 then
for i = #mapgens, 1, -1 do
if table.indexof(disallowed_mapgens, mapgens[i]) > 0 then
table.remove(mapgens, i)
@@ -172,23 +159,29 @@ local function create_world_formspec(dialogdata)
local mglist = ""
local selindex
- local i = 1
- local first_mg
- for k,v in pairs(mapgens) do
- if not first_mg then
- first_mg = v
+ do -- build the list of mapgens
+ local i = 1
+ local first_mg
+ for k, v in pairs(mapgens) do
+ if not first_mg then
+ first_mg = v
+ end
+ if current_mg == v then
+ selindex = i
+ end
+ i = i + 1
+ mglist = mglist .. core.formspec_escape(v) .. ","
end
- if current_mg == v then
- selindex = i
+ if not selindex then
+ selindex = 1
+ current_mg = first_mg
end
- i = i + 1
- mglist = mglist .. v .. ","
- end
- if not selindex then
- selindex = 1
- current_mg = first_mg
+ mglist = mglist:sub(1, -2)
end
- mglist = mglist:sub(1, -2)
+
+ -- The logic of the flag element IDs is as follows:
+ -- "flag_main_foo-bar-baz" controls dialogdata.flags["main"]["foo_bar_baz"]
+ -- see the buttonhandler for the implementation of this
local mg_main_flags = function(mapgen, y)
if mapgen == "singlenode" then
@@ -198,11 +191,11 @@ local function create_world_formspec(dialogdata)
return "", y
end
- local form = "checkbox[0," .. y .. ";flag_mg_caves;" ..
+ local form = "checkbox[0," .. y .. ";flag_main_caves;" ..
fgettext("Caves") .. ";"..strflag(flags.main, "caves").."]"
y = y + 0.5
- form = form .. "checkbox[0,"..y..";flag_mg_dungeons;" ..
+ form = form .. "checkbox[0,"..y..";flag_main_dungeons;" ..
fgettext("Dungeons") .. ";"..strflag(flags.main, "dungeons").."]"
y = y + 0.5
@@ -213,7 +206,7 @@ local function create_world_formspec(dialogdata)
else
d_tt = fgettext("Structures appearing on the terrain, typically trees and plants")
end
- form = form .. "checkbox[0,"..y..";flag_mg_decorations;" ..
+ form = form .. "checkbox[0,"..y..";flag_main_decorations;" ..
d_name .. ";" ..
strflag(flags.main, "decorations").."]" ..
"tooltip[flag_mg_decorations;" ..
@@ -221,7 +214,7 @@ local function create_world_formspec(dialogdata)
"]"
y = y + 0.5
- form = form .. "tooltip[flag_mg_caves;" ..
+ form = form .. "tooltip[flag_main_caves;" ..
fgettext("Network of tunnels and caves")
.. "]"
return form, y
@@ -235,13 +228,13 @@ local function create_world_formspec(dialogdata)
return "", y
end
local form = ""
- for _,tab in pairs(flag_checkboxes[mapgen]) do
- local id = "flag_mg"..mapgen.."_"..tab[1]
+ for _, tab in pairs(flag_checkboxes[mapgen]) do
+ local id = "flag_"..mapgen.."_"..tab[1]:gsub("_", "-")
form = form .. ("checkbox[0,%f;%s;%s;%s]"):
- format(y, id, tab[2], strflag(flags[mapgen], tab[3]))
+ format(y, id, tab[2], strflag(flags[mapgen], tab[1]))
- if tab[4] then
- form = form .. "tooltip["..id..";"..tab[4].."]"
+ if tab[3] then
+ form = form .. "tooltip["..id..";"..tab[3].."]"
end
y = y + 0.5
end
@@ -277,16 +270,14 @@ local function create_world_formspec(dialogdata)
-- biomeblend
y = y + 0.55
- form = form .. "checkbox[0,"..y..";flag_mgv6_biomeblend;" ..
+ form = form .. "checkbox[0,"..y..";flag_v6_biomeblend;" ..
fgettext("Biome blending") .. ";"..strflag(flags.v6, "biomeblend").."]" ..
- "tooltip[flag_mgv6_biomeblend;" ..
+ "tooltip[flag_v6_biomeblend;" ..
fgettext("Smooth transition between biomes") .. "]"
return form, y
end
- current_seed = core.formspec_escape(current_seed)
-
local y_start = 0.0
local y = y_start
local str_flags, str_spflags
@@ -323,21 +314,27 @@ local function create_world_formspec(dialogdata)
"container[0,0]"..
"field[0.3,0.6;6,0.5;te_world_name;" ..
fgettext("World name") ..
- ";" .. core.formspec_escape(worldname) .. "]" ..
+ ";" .. core.formspec_escape(dialogdata.worldname) .. "]" ..
+ "set_focus[te_world_name;false]" ..
"field[0.3,1.7;6,0.5;te_seed;" ..
fgettext("Seed") ..
- ";".. current_seed .. "]" ..
+ ";".. core.formspec_escape(dialogdata.seed) .. "]" ..
"label[0,2;" .. fgettext("Mapgen") .. "]"..
- "dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]" ..
+ "dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]"
+
+ if not hide_gamelist or devtest_only ~= "" then
+ retval = retval ..
+ "label[0,3.35;" .. fgettext("Game") .. "]"..
+ "textlist[0,3.85;5.8,"..gamelist_height..";games;" ..
+ pkgmgr.gamelist() .. ";" .. gameidx .. ";false]" ..
+ "container[0,4.5]" ..
+ devtest_only ..
+ "container_end[]"
+ end
- "label[0,3.35;" .. fgettext("Game") .. "]"..
- "textlist[0,3.85;5.8,"..gamelist_height..";games;" ..
- pkgmgr.gamelist() .. ";" .. gameidx .. ";false]" ..
- "container[0,4.5]" ..
- devtest_only ..
- "container_end[]" ..
+ retval = retval ..
"container_end[]" ..
-- Right side
@@ -360,9 +357,20 @@ local function create_world_buttonhandler(this, fields)
fields["key_enter"] then
local worldname = fields["te_world_name"]
- local gameindex = core.get_textlist_index("games")
+ local game, gameindex
+ if hide_gamelist then
+ game, gameindex = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
+ else
+ gameindex = core.get_textlist_index("games")
+ game = pkgmgr.get_game(gameindex)
+ end
- if gameindex ~= nil then
+ local message
+ if game == nil then
+ message = fgettext("No game selected")
+ end
+
+ if message == 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.
@@ -377,36 +385,48 @@ local function create_world_buttonhandler(this, fields)
worldname = "world" .. worldnum_max + 1
end
- core.settings:set("fixed_map_seed", fields["te_seed"])
-
- local message
- if not menudata.worldlist:uid_exists_raw(worldname) then
- core.settings:set("mg_name",fields["dd_mapgen"])
- message = core.create_world(worldname,gameindex)
- else
+ if menudata.worldlist:uid_exists_raw(worldname) then
message = fgettext("A world named \"$1\" already exists", worldname)
end
+ end
- if message ~= nil then
- gamedata.errormessage = message
- else
- core.settings:set("menu_last_game",pkgmgr.games[gameindex].id)
- if this.data.update_worldlist_filter then
- menudata.worldlist:set_filtercriteria(pkgmgr.games[gameindex].id)
- mm_texture.update("singleplayer", pkgmgr.games[gameindex].id)
- end
- menudata.worldlist:refresh()
- core.settings:set("mainmenu_last_selected_world",
- menudata.worldlist:raw_index_by_uid(worldname))
+ if message == nil then
+ this.data.seed = fields["te_seed"]
+ this.data.mg = fields["dd_mapgen"]
+
+ -- actual names as used by engine
+ local settings = {
+ fixed_map_seed = this.data.seed,
+ mg_name = this.data.mg,
+ mg_flags = table_to_flags(this.data.flags.main),
+ mgv5_spflags = table_to_flags(this.data.flags.v5),
+ mgv6_spflags = table_to_flags(this.data.flags.v6),
+ mgv7_spflags = table_to_flags(this.data.flags.v7),
+ mgfractal_spflags = table_to_flags(this.data.flags.fractal),
+ mgcarpathian_spflags = table_to_flags(this.data.flags.carpathian),
+ mgvalleys_spflags = table_to_flags(this.data.flags.valleys),
+ mgflat_spflags = table_to_flags(this.data.flags.flat),
+ }
+ message = core.create_world(worldname, gameindex, settings)
+ end
+
+ if message == nil then
+ core.settings:set("menu_last_game", game.id)
+ if this.data.update_worldlist_filter then
+ menudata.worldlist:set_filtercriteria(game.id)
end
- else
- gamedata.errormessage = fgettext("No game selected")
+ menudata.worldlist:refresh()
+ core.settings:set("mainmenu_last_selected_world",
+ menudata.worldlist:raw_index_by_uid(worldname))
end
+
+ gamedata.errormessage = message
this:delete()
return true
end
- worldname = fields.te_world_name
+ this.data.worldname = fields["te_world_name"]
+ this.data.seed = fields["te_seed"]
if fields["games"] then
local gameindex = core.get_textlist_index("games")
@@ -417,22 +437,11 @@ local function create_world_buttonhandler(this, fields)
for k,v in pairs(fields) do
local split = string.split(k, "_", nil, 3)
if split and split[1] == "flag" then
- local setting
- if split[2] == "mg" then
- setting = "mg_flags"
- else
- setting = split[2].."_spflags"
- end
-- We replaced the underscore of flag names with a dash.
local flag = string.gsub(split[3], "-", "_")
- local ftable = core.settings:get_flags(setting)
- if v == "true" then
- ftable[flag] = true
- else
- ftable[flag] = false
- end
- local flags = table_to_flags(ftable)
- core.settings:set(setting, flags)
+ local ftable = this.data.flags[split[2]]
+ assert(ftable)
+ ftable[flag] = v == "true"
return true
end
end
@@ -446,18 +455,16 @@ local function create_world_buttonhandler(this, fields)
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")
+ local ftable = this.data.flags.v6
ftable.jungles = mgv6_biomes[b][2].jungles
ftable.snowbiomes = mgv6_biomes[b][2].snowbiomes
- local flags = table_to_flags(ftable)
- core.settings:set("mgv6_spflags", flags)
return true
end
end
end
if fields["dd_mapgen"] then
- core.settings:set("mg_name", fields["dd_mapgen"])
+ this.data.mg = fields["dd_mapgen"]
return true
end
@@ -466,12 +473,27 @@ end
function create_create_world_dlg(update_worldlistfilter)
- worldname = ""
local retval = dialog_create("sp_create_world",
create_world_formspec,
create_world_buttonhandler,
nil)
- retval.update_worldlist_filter = update_worldlistfilter
+ retval.data = {
+ update_worldlist_filter = update_worldlistfilter,
+ worldname = "",
+ -- settings the world is created with:
+ seed = core.settings:get("fixed_map_seed") or "",
+ mg = core.settings:get("mg_name"),
+ flags = {
+ main = core.settings:get_flags("mg_flags"),
+ v5 = core.settings:get_flags("mgv5_spflags"),
+ v6 = core.settings:get_flags("mgv6_spflags"),
+ v7 = core.settings:get_flags("mgv7_spflags"),
+ fractal = core.settings:get_flags("mgfractal_spflags"),
+ carpathian = core.settings:get_flags("mgcarpathian_spflags"),
+ valleys = core.settings:get_flags("mgvalleys_spflags"),
+ flat = core.settings:get_flags("mgflat_spflags"),
+ }
+ }
return retval
end
diff --git a/builtin/mainmenu/dlg_settings_advanced.lua b/builtin/mainmenu/dlg_settings_advanced.lua
index c16e4aad0..06fd32d84 100644
--- a/builtin/mainmenu/dlg_settings_advanced.lua
+++ b/builtin/mainmenu/dlg_settings_advanced.lua
@@ -31,6 +31,10 @@ end
-- returns error message, or nil
local function parse_setting_line(settings, line, read_all, base_level, allow_secure)
+
+ -- strip carriage returns (CR, /r)
+ line = line:gsub("\r", "")
+
-- comment
local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$")
if comment then
@@ -620,7 +624,7 @@ local function create_change_setting_formspec(dialogdata)
-- Third row
add_field(0.3, "te_octaves", fgettext("Octaves"), t[7])
- add_field(3.6, "te_persist", fgettext("Persistance"), t[8])
+ add_field(3.6, "te_persist", fgettext("Persistence"), t[8])
add_field(6.9, "te_lacun", fgettext("Lacunarity"), t[9])
height = height + 1.1
diff --git a/builtin/mainmenu/textures.lua b/builtin/mainmenu/game_theme.lua
index a3acbbdec..89e1b66c8 100644
--- a/builtin/mainmenu/textures.lua
+++ b/builtin/mainmenu/game_theme.lua
@@ -16,23 +16,25 @@
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-mm_texture = {}
+mm_game_theme = {}
--------------------------------------------------------------------------------
-function mm_texture.init()
- mm_texture.defaulttexturedir = core.get_texturepath() .. DIR_DELIM .. "base" ..
+function mm_game_theme.init()
+ mm_game_theme.defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
DIR_DELIM .. "pack" .. DIR_DELIM
- mm_texture.basetexturedir = mm_texture.defaulttexturedir
+ mm_game_theme.basetexturedir = mm_game_theme.defaulttexturedir
- mm_texture.texturepack = core.settings:get("texture_path")
+ mm_game_theme.texturepack = core.settings:get("texture_path")
- mm_texture.gameid = nil
+ mm_game_theme.gameid = nil
+
+ mm_game_theme.music_handle = nil
end
--------------------------------------------------------------------------------
-function mm_texture.update(tab,gamedetails)
+function mm_game_theme.update(tab,gamedetails)
if tab ~= "singleplayer" then
- mm_texture.reset()
+ mm_game_theme.reset()
return
end
@@ -40,50 +42,54 @@ function mm_texture.update(tab,gamedetails)
return
end
- mm_texture.update_game(gamedetails)
+ mm_game_theme.update_game(gamedetails)
end
--------------------------------------------------------------------------------
-function mm_texture.reset()
- mm_texture.gameid = nil
+function mm_game_theme.reset()
+ mm_game_theme.gameid = nil
local have_bg = false
- local have_overlay = mm_texture.set_generic("overlay")
+ local have_overlay = mm_game_theme.set_generic("overlay")
if not have_overlay then
- have_bg = mm_texture.set_generic("background")
+ have_bg = mm_game_theme.set_generic("background")
end
- mm_texture.clear("header")
- mm_texture.clear("footer")
+ mm_game_theme.clear("header")
+ mm_game_theme.clear("footer")
core.set_clouds(false)
- mm_texture.set_generic("footer")
- mm_texture.set_generic("header")
+ mm_game_theme.set_generic("footer")
+ mm_game_theme.set_generic("header")
if not have_bg then
if core.settings:get_bool("menu_clouds") then
core.set_clouds(true)
else
- mm_texture.set_dirt_bg()
+ mm_game_theme.set_dirt_bg()
end
end
+
+ if mm_game_theme.music_handle ~= nil then
+ core.sound_stop(mm_game_theme.music_handle)
+ end
end
--------------------------------------------------------------------------------
-function mm_texture.update_game(gamedetails)
- if mm_texture.gameid == gamedetails.id then
+function mm_game_theme.update_game(gamedetails)
+ if mm_game_theme.gameid == gamedetails.id then
return
end
local have_bg = false
- local have_overlay = mm_texture.set_game("overlay",gamedetails)
+ local have_overlay = mm_game_theme.set_game("overlay",gamedetails)
if not have_overlay then
- have_bg = mm_texture.set_game("background",gamedetails)
+ have_bg = mm_game_theme.set_game("background",gamedetails)
end
- mm_texture.clear("header")
- mm_texture.clear("footer")
+ mm_game_theme.clear("header")
+ mm_game_theme.clear("footer")
core.set_clouds(false)
if not have_bg then
@@ -91,34 +97,34 @@ function mm_texture.update_game(gamedetails)
if core.settings:get_bool("menu_clouds") then
core.set_clouds(true)
else
- mm_texture.set_dirt_bg()
+ mm_game_theme.set_dirt_bg()
end
end
- mm_texture.set_game("footer",gamedetails)
- mm_texture.set_game("header",gamedetails)
+ mm_game_theme.set_game("footer",gamedetails)
+ mm_game_theme.set_game("header",gamedetails)
- mm_texture.gameid = gamedetails.id
+ mm_game_theme.gameid = gamedetails.id
end
--------------------------------------------------------------------------------
-function mm_texture.clear(identifier)
+function mm_game_theme.clear(identifier)
core.set_background(identifier,"")
end
--------------------------------------------------------------------------------
-function mm_texture.set_generic(identifier)
+function mm_game_theme.set_generic(identifier)
--try texture pack first
- if mm_texture.texturepack ~= nil then
- local path = mm_texture.texturepack .. DIR_DELIM .."menu_" ..
+ if mm_game_theme.texturepack ~= nil then
+ local path = mm_game_theme.texturepack .. DIR_DELIM .."menu_" ..
identifier .. ".png"
if core.set_background(identifier,path) then
return true
end
end
- if mm_texture.defaulttexturedir ~= nil then
- local path = mm_texture.defaulttexturedir .. DIR_DELIM .."menu_" ..
+ if mm_game_theme.defaulttexturedir ~= nil then
+ local path = mm_game_theme.defaulttexturedir .. DIR_DELIM .."menu_" ..
identifier .. ".png"
if core.set_background(identifier,path) then
return true
@@ -129,14 +135,16 @@ function mm_texture.set_generic(identifier)
end
--------------------------------------------------------------------------------
-function mm_texture.set_game(identifier, gamedetails)
+function mm_game_theme.set_game(identifier, gamedetails)
if gamedetails == nil then
return false
end
- if mm_texture.texturepack ~= nil then
- local path = mm_texture.texturepack .. DIR_DELIM ..
+ mm_game_theme.set_music(gamedetails)
+
+ if mm_game_theme.texturepack ~= nil then
+ local path = mm_game_theme.texturepack .. DIR_DELIM ..
gamedetails.id .. "_menu_" .. identifier .. ".png"
if core.set_background(identifier, path) then
return true
@@ -171,9 +179,10 @@ function mm_texture.set_game(identifier, gamedetails)
return false
end
-function mm_texture.set_dirt_bg()
- if mm_texture.texturepack ~= nil then
- local path = mm_texture.texturepack .. DIR_DELIM .."default_dirt.png"
+--------------------------------------------------------------------------------
+function mm_game_theme.set_dirt_bg()
+ if mm_game_theme.texturepack ~= nil then
+ local path = mm_game_theme.texturepack .. DIR_DELIM .."default_dirt.png"
if core.set_background("background", path, true, 128) then
return true
end
@@ -183,3 +192,12 @@ function mm_texture.set_dirt_bg()
local minimalpath = defaulttexturedir .. "menu_bg.png"
core.set_background("background", minimalpath, true, 128)
end
+
+--------------------------------------------------------------------------------
+function mm_game_theme.set_music(gamedetails)
+ if mm_game_theme.music_handle ~= nil then
+ core.sound_stop(mm_game_theme.music_handle)
+ end
+ local music_path = gamedetails.path .. DIR_DELIM .. "menu" .. DIR_DELIM .. "theme"
+ mm_game_theme.music_handle = core.sound_play(music_path, true)
+end
diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua
index 45089c7c9..8e716c2eb 100644
--- a/builtin/mainmenu/init.lua
+++ b/builtin/mainmenu/init.lua
@@ -35,7 +35,7 @@ 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 .. "game_theme.lua")
dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua")
@@ -49,7 +49,7 @@ local tabs = {}
tabs.settings = dofile(menupath .. DIR_DELIM .. "tab_settings.lua")
tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.lua")
-tabs.credits = dofile(menupath .. DIR_DELIM .. "tab_credits.lua")
+tabs.about = dofile(menupath .. DIR_DELIM .. "tab_about.lua")
tabs.local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua")
tabs.play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua")
@@ -87,7 +87,7 @@ local function init_globals()
core.settings:set("menu_last_game", default_game)
end
- mm_texture.init()
+ mm_game_theme.init()
-- Create main tabview
local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0})
@@ -98,7 +98,7 @@ local function init_globals()
tv_main:add(tabs.content)
tv_main:add(tabs.settings)
- tv_main:add(tabs.credits)
+ tv_main:add(tabs.about)
tv_main:set_global_event_handler(main_event_handler)
tv_main:set_fixed_size(false)
@@ -113,7 +113,7 @@ local function init_globals()
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()
+ mm_game_theme.reset()
end
end
@@ -121,8 +121,6 @@ local function init_globals()
tv_main:show()
ui.update()
-
- core.sound_play("main_menu", true)
end
init_globals()
diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/pkgmgr.lua
index 787936e31..6de671529 100644
--- a/builtin/mainmenu/pkgmgr.lua
+++ b/builtin/mainmenu/pkgmgr.lua
@@ -181,21 +181,6 @@ function pkgmgr.get_texture_packs()
end
--------------------------------------------------------------------------------
-function pkgmgr.extract(modfile)
- if modfile.type == "zip" then
- local tempfolder = os.tempfolder()
-
- if tempfolder ~= nil and
- tempfolder ~= "" then
- core.create_dir(tempfolder)
- if core.extract_zip(modfile.name,tempfolder) then
- return tempfolder
- end
- end
- end
- return nil
-end
-
function pkgmgr.get_folder_type(path)
local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
if testfile ~= nil then
@@ -450,6 +435,7 @@ function pkgmgr.enable_mod(this, toset)
local toggled_mods = {}
local enabled_mods = {}
toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
+ toset = mod.enabled -- Update if toggled
if not toset then
-- Mod(s) were disabled, so no dependencies need to be enabled
@@ -561,11 +547,10 @@ function pkgmgr.install_dir(type, path, basename, targetpath)
local from = basefolder and basefolder.path or path
if targetpath then
core.delete_dir(targetpath)
- core.create_dir(targetpath)
else
targetpath = core.get_texturepath() .. DIR_DELIM .. basename
end
- if not core.copy_dir(from, targetpath) then
+ if not core.copy_dir(from, targetpath, false) then
return nil,
fgettext("Failed to install $1 to $2", basename, targetpath)
end
@@ -586,7 +571,6 @@ function pkgmgr.install_dir(type, path, basename, targetpath)
-- Get destination name for modpack
if targetpath then
core.delete_dir(targetpath)
- core.create_dir(targetpath)
else
local clean_path = nil
if basename ~= nil then
@@ -610,7 +594,6 @@ function pkgmgr.install_dir(type, path, basename, targetpath)
if targetpath then
core.delete_dir(targetpath)
- core.create_dir(targetpath)
else
local targetfolder = basename
if targetfolder == nil then
@@ -636,14 +619,13 @@ function pkgmgr.install_dir(type, path, basename, targetpath)
if targetpath then
core.delete_dir(targetpath)
- core.create_dir(targetpath)
else
targetpath = core.get_gamepath() .. DIR_DELIM .. basename
end
end
-- Copy it
- if not core.copy_dir(basefolder.path, targetpath) then
+ if not core.copy_dir(basefolder.path, targetpath, false) then
return nil,
fgettext("Failed to install $1 to $2", basename, targetpath)
end
@@ -658,23 +640,6 @@ function pkgmgr.install_dir(type, path, basename, targetpath)
end
--------------------------------------------------------------------------------
-function pkgmgr.install(type, modfilename, basename, dest)
- local archive_info = pkgmgr.identify_filetype(modfilename)
- local path = pkgmgr.extract(archive_info)
-
- if path == nil then
- return nil,
- fgettext("Install: file: \"$1\"", archive_info.name) .. "\n" ..
- fgettext("Install: Unsupported file type \"$1\" or broken archive",
- archive_info.type)
- end
-
- local targetpath, msg = pkgmgr.install_dir(type, path, basename, dest)
- core.delete_dir(path)
- return targetpath, msg
-end
-
---------------------------------------------------------------------------------
function pkgmgr.preparemodlist(data)
local retval = {}
@@ -682,11 +647,9 @@ function pkgmgr.preparemodlist(data)
local game_mods = {}
--read global mods
- local modpath = core.get_modpath()
-
- if modpath ~= nil and
- modpath ~= "" then
- get_mods(modpath,global_mods)
+ local modpaths = core.get_modpaths()
+ for _, modpath in ipairs(modpaths) do
+ get_mods(modpath, global_mods)
end
for i=1,#global_mods,1 do
@@ -820,45 +783,6 @@ function pkgmgr.refresh_globals()
end
--------------------------------------------------------------------------------
-function pkgmgr.identify_filetype(name)
-
- if name:sub(-3):lower() == "zip" then
- return {
- name = name,
- type = "zip"
- }
- end
-
- if name:sub(-6):lower() == "tar.gz" or
- name:sub(-3):lower() == "tgz"then
- return {
- name = name,
- type = "tgz"
- }
- end
-
- if name:sub(-6):lower() == "tar.bz2" then
- return {
- name = name,
- type = "tbz"
- }
- end
-
- if name:sub(-2):lower() == "7z" then
- return {
- name = name,
- type = "7z"
- }
- end
-
- return {
- name = name,
- type = "ukn"
- }
-end
-
-
---------------------------------------------------------------------------------
function pkgmgr.find_by_gameid(gameid)
for i=1,#pkgmgr.games,1 do
if pkgmgr.games[i].id == gameid then
diff --git a/builtin/mainmenu/tab_credits.lua b/builtin/mainmenu/tab_about.lua
index a34dd58bb..ba258fd2d 100644
--- a/builtin/mainmenu/tab_credits.lua
+++ b/builtin/mainmenu/tab_about.lua
@@ -28,32 +28,35 @@ local core_developers = {
"Lars Hofhansl <larsh@apache.org>",
"Pierre-Yves Rollo <dev@pyrollo.com>",
"v-rob <robinsonvincent89@gmail.com>",
+ "hecks",
+ "Hugues Ross <hugues.ross@gmail.com>",
+ "Dmitry Kostenko (x2048) <codeforsmile@gmail.com>",
}
-- For updating active/previous contributors, see the script in ./util/gather_git_credits.py
local active_contributors = {
- "Wuzzy [devtest game, visual corrections]",
- "Zughy [Visual improvements, various fixes]",
- "Maksim (MoNTE48) [Android]",
+ "Wuzzy [I18n for builtin, liquid features, fixes]",
+ "Zughy [Various features and fixes]",
"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]",
+ "Desour [Internal fixes, Clipboard on X11]",
+ "Lars Müller [Various internal fixes]",
+ "JosiahWI [CMake, cleanups and fixes]",
+ "HybridDog [builtin, documentation]",
+ "Jude Melton-Houghton [Database implementation]",
+ "savilli [Fixes]",
+ "Liso [Shadow Mapping]",
+ "MoNTE48 [Build fix]",
+ "Jean-Patrick Guerrero (kilbith) [Fixes]",
+ "ROllerozxa [Code cleanups]",
+ "Lejo [bitop library integration]",
+ "LoneWolfHT [Build fixes]",
+ "NeroBurner [Joystick]",
+ "Elias Fleckenstein [Internal fixes]",
"David CARLIER [Unix & Haiku build fixes]",
- "dcbrwn [Object shading]",
- "Elias Fleckenstein [API features/fixes]",
- "Jean-Patrick Guerrero (kilbith) [model element, visual fixes]",
- "k.h.lai [Memory leak fixes, documentation]",
+ "pecksin [Clickable web links]",
+ "srfqi [Android & rendering fixes]",
+ "EvidenceB [Formspec]",
}
local previous_core_developers = {
@@ -70,6 +73,7 @@ local previous_core_developers = {
"Zeno",
"ShadowNinja <shadowninja@minetest.net>",
"Auke Kok (sofar) <sofar@foo-projects.org>",
+ "Aaron Suen <warr1024@gmail.com>",
}
local previous_contributors = {
@@ -80,10 +84,10 @@ local previous_contributors = {
"MirceaKitsune <mirceakitsune@gmail.com>",
"Constantin Wenger (SpeedProg)",
"Ciaran Gultnieks (CiaranG)",
- "stujones11 [Android UX improvements]",
- "Rogier <rogier777@gmail.com> [Fixes]",
- "Gregory Currie (gregorycu) [optimisation]",
- "srifqi [Fixes]",
+ "Paul Ouellette (pauloue)",
+ "stujones11",
+ "Rogier <rogier777@gmail.com>",
+ "Gregory Currie (gregorycu)",
"JacobF",
"Jeija <jeija@mesecons.net> [HTTP, particles]",
}
@@ -97,8 +101,8 @@ local function buildCreditList(source)
end
return {
- name = "credits",
- caption = fgettext("Credits"),
+ name = "about",
+ caption = fgettext("About"),
cbf_formspec = function(tabview, name, tabdata)
local logofile = defaulttexturedir .. "logo.png"
local version = core.get_version()
@@ -119,11 +123,16 @@ return {
buildCreditList(previous_contributors) .. "," ..
";1]"
+ -- Render information
+ fs = fs .. "label[0.75,4.9;" ..
+ fgettext("Active renderer:") .. "\n" ..
+ core.formspec_escape(core.get_screen_info().render_info) .. "]"
+
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") .. "]"
+ fs = fs .. "button[0,4;3.5,1;userdata;" .. fgettext("Open User Data Directory") .. "]"
end
return fs
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_local.lua b/builtin/mainmenu/tab_local.lua
index 0e06c3bef..e77c6f04d 100644
--- a/builtin/mainmenu/tab_local.lua
+++ b/builtin/mainmenu/tab_local.lua
@@ -18,8 +18,14 @@
local enable_gamebar = PLATFORM ~= "Android"
local current_game, singleplayer_refresh_gamebar
+local valid_disabled_settings = {
+ ["enable_damage"]=true,
+ ["creative_mode"]=true,
+ ["enable_server"]=true,
+}
if enable_gamebar then
+ -- Currently chosen game in gamebar for theming and filtering
function current_game()
local last_game_id = core.settings:get("menu_last_game")
local game = pkgmgr.find_by_gameid(last_game_id)
@@ -27,10 +33,30 @@ if enable_gamebar then
return game
end
+ -- Apply menu changes from given game
+ function apply_game(game)
+ core.set_topleft_text(game.name)
+ core.settings:set("menu_last_game", game.id)
+ menudata.worldlist:set_filtercriteria(game.id)
+
+ mm_game_theme.update("singleplayer", game) -- this refreshes the formspec
+
+ local index = filterlist.get_current_index(menudata.worldlist,
+ tonumber(core.settings:get("mainmenu_last_selected_world")))
+ if not index or index < 1 then
+ local selected = core.get_textlist_index("sp_worlds")
+ if selected ~= nil and selected < #menudata.worldlist:get_list() then
+ index = selected
+ else
+ index = #menudata.worldlist:get_list()
+ end
+ end
+ menu_worldmt_legacy(index)
+ end
+
function singleplayer_refresh_gamebar()
local old_bar = ui.find_by_name("game_button_bar")
-
if old_bar ~= nil then
old_bar:delete()
end
@@ -45,26 +71,10 @@ if enable_gamebar then
return true
end
- for key,value in pairs(fields) do
- for j=1,#pkgmgr.games,1 do
- if ("game_btnbar_" .. pkgmgr.games[j].id == key) then
- mm_texture.update("singleplayer", pkgmgr.games[j])
- core.set_topleft_text(pkgmgr.games[j].name)
- core.settings:set("menu_last_game",pkgmgr.games[j].id)
- menudata.worldlist:set_filtercriteria(pkgmgr.games[j].id)
- local index = filterlist.get_current_index(menudata.worldlist,
- tonumber(core.settings:get("mainmenu_last_selected_world")))
- if not index or index < 1 then
- local selected = core.get_textlist_index("sp_worlds")
- if selected ~= nil and selected < #menudata.worldlist:get_list() then
- index = selected
- else
- index = #menudata.worldlist:get_list()
- end
- end
- menu_worldmt_legacy(index)
- return true
- end
+ for _, game in ipairs(pkgmgr.games) do
+ if fields["game_btnbar_" .. game.id] then
+ apply_game(game)
+ return true
end
end
end
@@ -73,25 +83,22 @@ if enable_gamebar then
game_buttonbar_button_handler,
{x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15})
- for i=1,#pkgmgr.games,1 do
- local btn_name = "game_btnbar_" .. pkgmgr.games[i].id
+ for _, game in ipairs(pkgmgr.games) do
+ local btn_name = "game_btnbar_" .. game.id
local image = nil
local text = nil
- local tooltip = core.formspec_escape(pkgmgr.games[i].name)
+ local tooltip = core.formspec_escape(game.name)
- if pkgmgr.games[i].menuicon_path ~= nil and
- pkgmgr.games[i].menuicon_path ~= "" then
- image = core.formspec_escape(pkgmgr.games[i].menuicon_path)
+ if (game.menuicon_path or "") ~= "" then
+ image = core.formspec_escape(game.menuicon_path)
else
-
- local part1 = pkgmgr.games[i].id:sub(1,5)
- local part2 = pkgmgr.games[i].id:sub(6,10)
- local part3 = pkgmgr.games[i].id:sub(11)
+ local part1 = game.id:sub(1,5)
+ local part2 = game.id:sub(6,10)
+ local part3 = game.id:sub(11)
text = part1 .. "\n" .. part2
- if part3 ~= nil and
- part3 ~= "" then
+ if part3 ~= "" then
text = text .. "\n" .. part3
end
end
@@ -102,37 +109,91 @@ if enable_gamebar then
btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB"))
end
else
+ -- Currently chosen game in gamebar: no gamebar -> no "current" game
function current_game()
return nil
end
end
+local function get_disabled_settings(game)
+ if not game then
+ return {}
+ end
+
+ local gameconfig = Settings(game.path .. "/game.conf")
+ local disabled_settings = {}
+ if gameconfig then
+ local disabled_settings_str = (gameconfig:get("disabled_settings") or ""):split()
+ for _, value in pairs(disabled_settings_str) do
+ local state = false
+ value = value:trim()
+ if string.sub(value, 1, 1) == "!" then
+ state = true
+ value = string.sub(value, 2)
+ end
+ if valid_disabled_settings[value] then
+ disabled_settings[value] = state
+ else
+ core.log("error", "Invalid disabled setting in game.conf: "..tostring(value))
+ end
+ end
+ end
+ return disabled_settings
+end
+
local function get_formspec(tabview, name, tabdata)
local retval = ""
local index = filterlist.get_current_index(menudata.worldlist,
- tonumber(core.settings:get("mainmenu_last_selected_world"))
- )
+ tonumber(core.settings:get("mainmenu_last_selected_world")))
+ local list = menudata.worldlist:get_list()
+ local world = list and index and list[index]
+ local game
+ if world then
+ game = pkgmgr.find_by_gameid(world.gameid)
+ else
+ game = current_game()
+ end
+ local disabled_settings = get_disabled_settings(game)
+
+ local creative, damage, host = "", "", ""
+
+ -- Y offsets for game settings checkboxes
+ local y = -0.2
+ local yo = 0.45
+
+ if disabled_settings["creative_mode"] == nil then
+ creative = "checkbox[0,"..y..";cb_creative_mode;".. fgettext("Creative Mode") .. ";" ..
+ dump(core.settings:get_bool("creative_mode")) .. "]"
+ y = y + yo
+ end
+ if disabled_settings["enable_damage"] == nil then
+ damage = "checkbox[0,"..y..";cb_enable_damage;".. fgettext("Enable Damage") .. ";" ..
+ dump(core.settings:get_bool("enable_damage")) .. "]"
+ y = y + yo
+ end
+ if disabled_settings["enable_server"] == nil then
+ host = "checkbox[0,"..y..";cb_server;".. fgettext("Host Server") ..";" ..
+ dump(core.settings:get_bool("enable_server")) .. "]"
+ y = y + yo
+ end
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("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") .. ";" ..
- dump(core.settings:get_bool("creative_mode")) .. "]"..
- "checkbox[0,0.25;cb_enable_damage;".. fgettext("Enable Damage") .. ";" ..
- dump(core.settings:get_bool("enable_damage")) .. "]"..
- "checkbox[0,0.7;cb_server;".. fgettext("Host Server") ..";" ..
- dump(core.settings:get_bool("enable_server")) .. "]" ..
+ creative ..
+ damage ..
+ host ..
"textlist[3.9,0.4;7.9,3.45;sp_worlds;" ..
- menu_render_worldlist() ..
+ menu_render_worldlist(not enable_gamebar) ..
";" .. index .. "]"
- if core.settings:get_bool("enable_server") then
+ if core.settings:get_bool("enable_server") and disabled_settings["enable_server"] == nil then
retval = retval ..
"button[7.9,4.75;4.1,1;play;".. fgettext("Host Game") .. "]" ..
- "checkbox[0,1.15;cb_server_announce;" .. fgettext("Announce Server") .. ";" ..
+ "checkbox[0,"..y..";cb_server_announce;" .. fgettext("Announce Server") .. ";" ..
dump(core.settings:get_bool("server_announce")) .. "]" ..
"field[0.3,2.85;3.8,0.5;te_playername;" .. fgettext("Name") .. ";" ..
core.formspec_escape(core.settings:get("name")) .. "]" ..
@@ -227,9 +288,21 @@ local function main_button_handler(this, fields, name, tabdata)
-- Update last game
local world = menudata.worldlist:get_raw_element(gamedata.selected_world)
+ local game_obj
if world then
- local game = pkgmgr.find_by_gameid(world.gameid)
- core.settings:set("menu_last_game", game.id)
+ game_obj = pkgmgr.find_by_gameid(world.gameid)
+ core.settings:set("menu_last_game", game_obj.id)
+ end
+
+ local disabled_settings = get_disabled_settings(game_obj)
+ for k, _ in pairs(valid_disabled_settings) do
+ local v = disabled_settings[k]
+ if v ~= nil then
+ if k == "enable_server" and v == true then
+ error("Setting 'enable_server' cannot be force-enabled! The game.conf needs to be fixed.")
+ end
+ core.settings:set_bool(k, disabled_settings[k])
+ end
end
if core.settings:get_bool("enable_server") then
@@ -251,11 +324,11 @@ local function main_button_handler(this, fields, name, tabdata)
end
if fields["world_create"] ~= nil then
- local create_world_dlg = create_create_world_dlg(true)
+ local create_world_dlg = create_create_world_dlg(enable_gamebar)
create_world_dlg:set_parent(this)
this:hide()
create_world_dlg:show()
- mm_texture.update("singleplayer", current_game())
+ mm_game_theme.update("singleplayer", current_game())
return true
end
@@ -272,7 +345,7 @@ local function main_button_handler(this, fields, name, tabdata)
delete_world_dlg:set_parent(this)
this:hide()
delete_world_dlg:show()
- mm_texture.update("singleplayer",current_game())
+ mm_game_theme.update("singleplayer",current_game())
end
end
@@ -290,7 +363,7 @@ local function main_button_handler(this, fields, name, tabdata)
configdialog:set_parent(this)
this:hide()
configdialog:show()
- mm_texture.update("singleplayer",current_game())
+ mm_game_theme.update("singleplayer",current_game())
end
end
@@ -303,11 +376,8 @@ if enable_gamebar then
function on_change(type, old_tab, new_tab)
if (type == "ENTER") then
local game = current_game()
-
if game then
- menudata.worldlist:set_filtercriteria(game.id)
- core.set_topleft_text(game.name)
- mm_texture.update("singleplayer",game)
+ apply_game(game)
end
singleplayer_refresh_gamebar()
@@ -319,7 +389,7 @@ if enable_gamebar then
gamebar:hide()
end
core.set_topleft_text("")
- mm_texture.update(new_tab,nil)
+ mm_game_theme.update(new_tab,nil)
end
end
end
diff --git a/builtin/mainmenu/tab_online.lua b/builtin/mainmenu/tab_online.lua
index e6748ed88..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 selected
- if menudata.search_result then
- selected = menudata.search_result[tabdata.selected]
- else
- selected = serverlistmgr.servers[tabdata.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) .. "]" ..
- "image_button[5.63,-.165;.83,.83;" .. core.formspec_escape(defaulttexturedir .. "search.png") .. ";btn_mp_search;]" ..
- "image_button[6.3,-.165;.83,.83;" .. core.formspec_escape(defaulttexturedir .. "clear.png") .. ";btn_mp_clear;]" ..
- "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.selected and 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 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
- --favorites
+ 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;favorites;"
-
- if menudata.search_result then
- local favs = serverlistmgr.get_favorites()
- for i = 1, #menudata.search_result do
- 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
- end
-
- if i ~= 1 then
- retval = retval .. ","
- end
-
- retval = retval .. render_serverlist_row(server, server.is_favorite)
- end
- elseif #serverlistmgr.servers > 0 then
- local favs = serverlistmgr.get_favorites()
- if #favs > 0 then
- for i = 1, #favs do
- for j = 1, #serverlistmgr.servers do
- if serverlistmgr.servers[j].address == favs[i].address and
- serverlistmgr.servers[j].port == favs[i].port then
- table.insert(serverlistmgr.servers, i, table.remove(serverlistmgr.servers, j))
- end
- end
- if favs[i].address ~= serverlistmgr.servers[i].address then
- table.insert(serverlistmgr.servers, i, favs[i])
- 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
-
- retval = retval .. render_serverlist_row(serverlistmgr.servers[1], (#favs > 0))
- for i = 2, #serverlistmgr.servers do
- retval = retval .. "," .. render_serverlist_row(serverlistmgr.servers[i], (i <= #favs))
- end
end
+ retval = retval .. table.concat(rows, ",")
+
if tabdata.selected then
retval = retval .. ";" .. tabdata.selected .. "]"
else
retval = retval .. ";0]"
end
- return retval
+ return retval, "size[15.5,7,false]real_coordinates[true]"
end
--------------------------------------------------------------------------------
-local function main_button_handler(tabview, fields, name, tabdata)
- local serverlist = menudata.search_result or serverlistmgr.servers
+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 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 = (#serverlistmgr.servers - i) / 5 + found
+ server.points = points
+ table.insert(search_result, server)
+ end
+ end
+
+ if #search_result == 0 then
+ return
+ end
+
+ table.sort(search_result, function(a, b)
+ return a.points > b.points
+ end)
+ menudata.search_result = search_result
+end
+
+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.favorites then
- local event = core.explode_table_event(fields.favorites)
- 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 server then
+ if event.type == "DCL" then
if not is_server_protocol_compat_or_error(
- fav.proto_min, fav.proto_max) then
+ 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,84 +288,32 @@ 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 = serverlistmgr.get_favorites()
- 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.selected = event.row
- end
- return true
- end
- end
-
- if fields.key_up or fields.key_down then
- local fav_idx = core.get_table_index("favorites")
- 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 < #serverlistmgr.servers then
- fav_idx = fav_idx + 1
+ if event.type == "CHG" then
+ set_selected_server(tabdata, event.row, server)
+ return true
end
- else
- fav_idx = 1
- end
-
- if not serverlistmgr.servers or not fav then
- tabdata.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.selected = fav_idx
- return true
end
if fields.btn_delete_favorite then
- local current_favorite = core.get_table_index("favorites")
- if not current_favorite then return end
-
- serverlistmgr.delete_favorite(serverlistmgr.servers[current_favorite])
- serverlistmgr.sync()
- tabdata.selected = nil
-
- core.settings:set("address", "")
- core.settings:set("remote_port", "30000")
+ 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
@@ -250,63 +324,13 @@ local function main_button_handler(tabview, fields, name, tabdata)
end
if fields.btn_mp_search or fields.key_enter_field == "te_search" then
- tabdata.selected = 1
- local input = fields.te_search:lower()
tabdata.search_for = fields.te_search
-
- if #serverlistmgr.servers < 2 then
- return true
- 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)
+ 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
- if #keywords == 0 then
- menudata.search_result = nil
- return true
- end
-
- -- 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 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 = (#serverlistmgr.servers - 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
@@ -322,20 +346,22 @@ local function main_button_handler(tabview, fields, name, tabdata)
gamedata.address = fields.te_address
gamedata.port = tonumber(fields.te_port)
gamedata.selected_world = 0
- local fav_idx = core.get_table_index("favorites")
- local fav = serverlist[fav_idx]
- if fav_idx and fav_idx <= #serverlist and
- fav.address == gamedata.address and
- fav.port == gamedata.port then
+ local idx = core.get_table_index("servers")
+ local server = idx and tabdata.lookup[idx]
+
+ set_selected_server(tabdata)
- serverlistmgr.add_favorite(fav)
+ if server and server.address == gamedata.address and
+ server.port == gamedata.port then
- gamedata.servername = fav.name
- gamedata.serverdescription = fav.description
+ serverlistmgr.add_favorite(server)
+
+ gamedata.servername = server.name
+ gamedata.serverdescription = server.description
if not is_server_protocol_compat_or_error(
- fav.proto_min, fav.proto_max) then
+ server.proto_min, server.proto_max) then
return true
end
else
@@ -354,6 +380,7 @@ local function main_button_handler(tabview, fields, name, tabdata)
core.start()
return true
end
+
return false
end
@@ -362,7 +389,6 @@ local function on_change(type, old_tab, new_tab)
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 29744048a..700b7390f 100644
--- a/builtin/mainmenu/tab_settings.lua
+++ b/builtin/mainmenu/tab_settings.lua
@@ -43,6 +43,14 @@ local labels = {
fgettext("2x"),
fgettext("4x"),
fgettext("8x")
+ },
+ shadow_levels = {
+ fgettext("Disabled"),
+ fgettext("Very Low"),
+ fgettext("Low"),
+ fgettext("Medium"),
+ fgettext("High"),
+ fgettext("Ultra High")
}
}
@@ -66,6 +74,10 @@ local dd_options = {
antialiasing = {
table.concat(labels.antialiasing, ","),
{"0", "2", "4", "8"}
+ },
+ shadow_levels = {
+ table.concat(labels.shadow_levels, ","),
+ { "0", "1", "2", "3", "4", "5" }
}
}
@@ -110,6 +122,15 @@ local getSettingIndex = {
end
end
return 1
+ end,
+ ShadowMapping = function()
+ local shadow_setting = core.settings:get("shadow_levels")
+ for i = 1, #dd_options.shadow_levels[2] do
+ if shadow_setting == dd_options.shadow_levels[2][i] then
+ return i
+ end
+ end
+ return 1
end
}
@@ -198,6 +219,9 @@ local function formspec(tabview, name, tabdata)
.. dump(core.settings:get_bool("enable_waving_leaves")) .. "]" ..
"checkbox[8.25,2;cb_waving_plants;" .. fgettext("Waving Plants") .. ";"
.. dump(core.settings:get_bool("enable_waving_plants")) .. "]"
+ --"label[8.25,3.0;" .. fgettext("Dynamic shadows: ") .. "]" ..
+ --"dropdown[8.25,3.5;3.5;dd_shadows;" .. dd_options.shadow_levels[1] .. ";"
+ -- .. getSettingIndex.ShadowMapping() .. "]"
else
tab_string = tab_string ..
"label[8.38,0.7;" .. core.colorize("#888888",
@@ -208,6 +232,8 @@ local function formspec(tabview, name, tabdata)
fgettext("Waving Leaves")) .. "]" ..
"label[8.38,2.2;" .. core.colorize("#888888",
fgettext("Waving Plants")) .. "]"
+ --"label[8.38,2.7;" .. core.colorize("#888888",
+ -- fgettext("Dynamic shadows")) .. "]"
end
return tab_string
@@ -221,7 +247,7 @@ local function handle_settings_buttons(this, fields, tabname, tabdata)
adv_settings_dlg:set_parent(this)
this:hide()
adv_settings_dlg:show()
- --mm_texture.update("singleplayer", current_game())
+ --mm_game_theme.update("singleplayer", current_game())
return true
end
if fields["cb_smooth_lighting"] then
@@ -249,13 +275,7 @@ local function handle_settings_buttons(this, fields, tabname, tabdata)
return true
end
if fields["cb_shaders"] then
- if (core.settings:get("video_driver") == "direct3d8" or
- core.settings:get("video_driver") == "direct3d9") then
- core.settings:set("enable_shaders", "false")
- gamedata.errormessage = fgettext("To enable shaders the OpenGL driver needs to be used.")
- else
- core.settings:set("enable_shaders", fields["cb_shaders"])
- end
+ core.settings:set("enable_shaders", fields["cb_shaders"])
return true
end
if fields["cb_tonemapping"] then
@@ -333,6 +353,34 @@ local function handle_settings_buttons(this, fields, tabname, tabdata)
ddhandled = true
end
+ for i = 1, #labels.shadow_levels do
+ if fields["dd_shadows"] == labels.shadow_levels[i] then
+ core.settings:set("shadow_levels", dd_options.shadow_levels[2][i])
+ ddhandled = true
+ end
+ end
+
+ if fields["dd_shadows"] == labels.shadow_levels[1] then
+ core.settings:set("enable_dynamic_shadows", "false")
+ else
+ local shadow_presets = {
+ [2] = { 80, 512, "true", 0, "false" },
+ [3] = { 120, 1024, "true", 1, "false" },
+ [4] = { 350, 2048, "true", 1, "false" },
+ [5] = { 350, 2048, "true", 2, "true" },
+ [6] = { 450, 4096, "true", 2, "true" },
+ }
+ local s = shadow_presets[table.indexof(labels.shadow_levels, fields["dd_shadows"])]
+ if s then
+ core.settings:set("enable_dynamic_shadows", "true")
+ core.settings:set("shadow_map_max_distance", s[1])
+ core.settings:set("shadow_map_texture_size", s[2])
+ core.settings:set("shadow_map_texture_32bit", s[3])
+ core.settings:set("shadow_filters", s[4])
+ core.settings:set("shadow_map_color", s[5])
+ end
+ end
+
return ddhandled
end
diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua
index 148e9b794..a091959fb 100644
--- a/builtin/mainmenu/tests/serverlistmgr_spec.lua
+++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua
@@ -2,6 +2,7 @@ _G.core = {}
_G.unpack = table.unpack
_G.serverlistmgr = {}
+dofile("builtin/common/vector.lua")
dofile("builtin/common/misc_helpers.lua")
dofile("builtin/mainmenu/serverlistmgr.lua")
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 6b951a2c2..f80314b32 100644
--- a/builtin/profiler/instrumentation.lua
+++ b/builtin/profiler/instrumentation.lua
@@ -102,8 +102,9 @@ local function instrument(def)
-- also called https://en.wikipedia.org/wiki/Continuation_passing_style
-- Compared to table creation and unpacking it won't lose `nil` returns
-- and is expected to be faster
- -- `measure` will be executed after time() and func(...)
- return measure(modname, instrument_name, time(), func(...))
+ -- `measure` will be executed after func(...)
+ local start = time()
+ return measure(modname, instrument_name, start, func(...))
end
end
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 f800f71ab..42b45aa00 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
@@ -146,17 +146,17 @@ enable_joysticks (Enable joysticks) bool false
joystick_id (Joystick ID) int 0
# The type of joystick
-joystick_type (Joystick type) enum auto auto,generic,xbox
+joystick_type (Joystick type) enum auto auto,generic,xbox,dragonrise_gamecube
# The time in seconds it takes between repeated events
# 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 dead zone of the joystick
+joystick_deadzone (Joystick dead zone) int 2048
# The sensitivity of the joystick axes for moving the
-# ingame view frustum around.
+# in-game view frustum around.
joystick_frustum_sensitivity (Joystick frustum sensitivity) float 170
# Key for moving the player forward.
@@ -199,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
@@ -451,9 +451,9 @@ keymap_decrease_viewing_range_min (View range decrease key) key -
[**Basic]
-# Whether nametag backgrounds should be shown by default.
+# Whether name tag backgrounds should be shown by default.
# Mods may still set a background.
-show_nametag_backgrounds (Show nametag backgrounds by default) bool true
+show_nametag_backgrounds (Show name tag backgrounds by default) bool true
# Enable vertex buffer objects.
# This should greatly improve graphics performance.
@@ -475,6 +475,10 @@ connected_glass (Connect glass) bool false
# Disable for speed or for different looks.
smooth_lighting (Smooth lighting) bool true
+# Enables tradeoffs that reduce CPU load or increase rendering performance
+# at the expense of minor visual glitches that do not impact game playability.
+performance_tradeoffs (Tradeoffs for performance) bool false
+
# Clouds are a client side effect.
enable_clouds (Clouds) bool true
@@ -489,7 +493,7 @@ enable_particles (Digging particles) bool true
[**Filtering]
-# Use mip mapping to scale textures. May slightly increase performance,
+# Use mipmapping to scale textures. May slightly increase performance,
# especially when using a high resolution texture pack.
# Gamma correct downscaling is not supported.
mip_map (Mipmapping) bool false
@@ -504,18 +508,17 @@ bilinear_filter (Bilinear filtering) bool false
trilinear_filter (Trilinear filtering) bool false
# Filtered textures can blend RGB values with fully-transparent neighbors,
-# which PNG optimizers usually discard, sometimes resulting in a dark or
-# light edge to transparent textures. Apply this filter to clean that up
-# at texture load time.
+# which PNG optimizers usually discard, often resulting in dark or
+# light edges to transparent textures. Apply a filter to clean that up
+# at texture load time. This is automatically enabled if mipmapping is enabled.
texture_clean_transparent (Clean transparent textures) bool false
# When using bilinear/trilinear/anisotropic filters, low-resolution textures
# can be blurred, so automatically upscale them with nearest-neighbor
# interpolation to preserve crisp pixels. This sets the minimum texture size
# for the upscaled textures; higher values look sharper, but require more
-# memory. Powers of 2 are recommended. Setting this higher than 1 may not
-# have a visible effect unless bilinear/trilinear/anisotropic filtering is
-# enabled.
+# memory. Powers of 2 are recommended. This setting is ONLY applied if
+# bilinear/trilinear/anisotropic filtering is enabled.
# This is also used as the base node texture size for world-aligned
# texture autoscaling.
texture_min_size (Minimum texture size) int 64
@@ -609,10 +612,10 @@ viewing_range (Viewing range) int 190 20 4000
# 0.1 = Default, 0.25 = Good value for weaker tablets.
near_plane (Near plane) float 0.1 0 0.25
-# Width component of the initial window size.
+# Width component of the initial window size. Ignored in fullscreen mode.
screen_w (Screen width) int 1024 1
-# Height component of the initial window size.
+# Height component of the initial window size. Ignored in fullscreen mode.
screen_h (Screen height) int 600 1
# Save window size automatically when modified.
@@ -621,9 +624,6 @@ autosave_screensize (Autosave screen size) bool true
# Fullscreen mode.
fullscreen (Full screen) bool false
-# Bits per pixel (aka color depth) in fullscreen mode.
-fullscreen_bpp (Full screen BPP) int 24
-
# Vertical screen synchronization.
vsync (VSync) bool false
@@ -662,12 +662,12 @@ lighting_boost_spread (Light curve boost spread) float 0.2 0.0 0.4
# Path to texture directory. All textures are first searched from here.
texture_path (Texture path) path
-# The rendering back-end for Irrlicht.
+# The rendering back-end.
# 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.
# Shaders are supported by OpenGL (desktop only) and OGLES2 (experimental)
-video_driver (Video driver) enum opengl null,software,burningsvideo,direct3d8,direct3d9,opengl,ogles1,ogles2
+video_driver (Video driver) enum opengl opengl,ogles1,ogles2
# Radius of cloud area stated in number of 64 node cloud squares.
# Values larger than 26 will start to produce sharp cutoffs at cloud area corners.
@@ -728,7 +728,7 @@ selectionbox_width (Selection box width) int 2 1 5
crosshair_color (Crosshair color) string (255,255,255)
# Crosshair alpha (opaqueness, between 0 and 255).
-# Also controls the object crosshair color
+# This also applies to the object crosshair.
crosshair_alpha (Crosshair alpha) int 255 0 255
# Maximum number of recent chat messages to show
@@ -741,7 +741,7 @@ desynchronize_mapblock_texture_animation (Desynchronize block animation) bool tr
# Useful if there's something to be displayed right or left of hotbar.
hud_hotbar_max_width (Maximum hotbar width) float 1.0
-# Modifies the size of the hudbar elements.
+# Modifies the size of the HUD elements.
hud_scaling (HUD scale factor) float 1.0
# Enables caching of facedir rotated meshes.
@@ -834,10 +834,6 @@ tooltip_show_delay (Tooltip delay) int 400
# Append item name to tooltip.
tooltip_append_itemname (Append item name) bool false
-# Whether FreeType fonts are used, requires FreeType support to be compiled in.
-# If disabled, bitmap and XML vectors fonts are used instead.
-freetype (FreeType fonts) bool true
-
font_bold (Font bold by default) bool false
font_italic (Font italic by default) bool false
@@ -848,44 +844,41 @@ font_shadow (Font shadow) int 1
# Opaqueness (alpha) of the shadow behind the default font, between 0 and 255.
font_shadow_alpha (Font shadow alpha) int 127 0 255
-# Font size of the default font in point (pt).
+# Font size of the default font where 1 unit = 1 pixel at 96 DPI
font_size (Font size) int 16 1
-# Path to the default font.
-# If “freetype” setting is enabled: Must be a TrueType font.
-# If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+# For pixel-style fonts that do not scale well, this ensures that font sizes used
+# with this font will always be divisible by this value, in pixels. For instance,
+# a pixel font 16 pixels tall should have this set to 16, so it will only ever be
+# sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32.
+font_size_divisible_by (Font size divisible by) int 1 1
+
+# Path to the default font. Must be a TrueType font.
# The fallback font will be used if the font cannot be loaded.
font_path (Regular font path) filepath fonts/Arimo-Regular.ttf
font_path_bold (Bold font path) filepath fonts/Arimo-Bold.ttf
font_path_italic (Italic font path) filepath fonts/Arimo-Italic.ttf
-font_path_bolditalic (Bold and italic font path) filepath fonts/Arimo-BoldItalic.ttf
+font_path_bold_italic (Bold and italic font path) filepath fonts/Arimo-BoldItalic.ttf
-# Font size of the monospace font in point (pt).
-mono_font_size (Monospace font size) int 15 1
+# Font size of the monospace font where 1 unit = 1 pixel at 96 DPI
+mono_font_size (Monospace font size) int 16 1
-# Path to the monospace font.
-# If “freetype” setting is enabled: Must be a TrueType font.
-# If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+# For pixel-style fonts that do not scale well, this ensures that font sizes used
+# with this font will always be divisible by this value, in pixels. For instance,
+# a pixel font 16 pixels tall should have this set to 16, so it will only ever be
+# sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32.
+mono_font_size_divisible_by (Monospace font size divisible by) int 1 1
+
+# Path to the monospace font. Must be a TrueType font.
# This font is used for e.g. the console and profiler screen.
mono_font_path (Monospace font path) filepath fonts/Cousine-Regular.ttf
mono_font_path_bold (Bold monospace font path) filepath fonts/Cousine-Bold.ttf
mono_font_path_italic (Italic monospace font path) filepath fonts/Cousine-Italic.ttf
-mono_font_path_bolditalic (Bold and italic monospace font path) filepath fonts/Cousine-BoldItalic.ttf
-
-# Font size of the fallback font in point (pt).
-fallback_font_size (Fallback font size) int 15 1
+mono_font_path_bold_italic (Bold and italic monospace font path) filepath fonts/Cousine-BoldItalic.ttf
-# Shadow offset (in pixels) of the fallback font. If 0, then shadow will not be drawn.
-fallback_font_shadow (Fallback font shadow) int 1
-
-# Opaqueness (alpha) of the shadow behind the fallback font, between 0 and 255.
-fallback_font_shadow_alpha (Fallback font shadow alpha) int 128 0 255
-
-# Path of the fallback font.
-# If “freetype” setting is enabled: Must be a TrueType font.
-# If “freetype” setting is disabled: Must be a bitmap or XML vectors font.
+# Path of the fallback font. Must be a TrueType font.
# This font will be used for certain languages or if the default font is unavailable.
fallback_font_path (Fallback font path) filepath fonts/DroidSansFallbackFull.ttf
@@ -898,7 +891,7 @@ chat_font_size (Chat font size) int 0
screenshot_path (Screenshot folder) path screenshots
# Format of screenshots.
-screenshot_format (Screenshot format) enum png png,jpg,bmp,pcx,ppm,tga
+screenshot_format (Screenshot format) enum png png,jpg
# Screenshot quality. Only used for JPEG format.
# 1 means worst quality; 100 means best quality.
@@ -910,6 +903,9 @@ screenshot_quality (Screenshot quality) int 0 0 100
# Adjust dpi configuration to your screen (non X11/Android only) e.g. for 4k screens.
screen_dpi (DPI) int 72 1
+# Adjust the detected display density, used for scaling UI elements.
+display_density_factor (Display Density Scaling Factor) float 1
+
# Windows systems only: Start Minetest with the command line window in the background.
# Contains the same information as the file debug.txt (default name).
enable_console (Enable console window) bool false
@@ -934,6 +930,12 @@ mute_sound (Mute sound) bool false
[Client]
+# Clickable weblinks (middle-click or Ctrl+left-click) enabled in chat console output.
+clickable_chat_weblinks (Chat weblinks) bool false
+
+# Optional override for chat weblink color.
+chat_weblink_color (Weblink color) string
+
[*Network]
# Address to connect to.
@@ -946,9 +948,9 @@ address (Server address) string
remote_port (Remote port) int 30000 1 65535
# Prometheus listener address.
-# If minetest is compiled with ENABLE_PROMETHEUS option enabled,
+# If Minetest is compiled with ENABLE_PROMETHEUS option enabled,
# enable metrics listener for Prometheus on that address.
-# Metrics can be fetch on http://127.0.0.1:30000/metrics
+# Metrics can be fetched on http://127.0.0.1:30000/metrics
prometheus_listener_address (Prometheus listener address) string 127.0.0.1:30000
# Save the map received by the client on disk.
@@ -1055,11 +1057,10 @@ 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
+# Compression level to use when sending mapblocks to the client.
+# -1 - use default compression level
+# 0 - least compression, 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]
@@ -1136,6 +1137,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.
@@ -1240,7 +1245,7 @@ movement_gravity (Gravity) float 9.81
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
+# This is a trade-off between SQLite transaction overhead and
# memory consumption (4096=100MB, as a rule of thumb).
max_clearobjects_extra_loaded_blocks (Max. clearobjects extra blocks) int 4096
@@ -1254,12 +1259,11 @@ 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
+# Compression level to use when saving mapblocks to disk.
+# -1 - use default compression level
+# 0 - least compression, 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
+map_compression_level_disk (Map Compression Level for Disk Storage) int -1 -1 9
# Length of a server tick and the interval at which objects are generally updated over
# network.
@@ -1364,8 +1368,8 @@ instrument.abm (Active Block Modifiers) bool true
# Instrument the action function of Loading Block Modifiers on registration.
instrument.lbm (Loading Block Modifiers) bool true
-# Instrument chatcommands on registration.
-instrument.chatcommand (Chatcommands) bool true
+# Instrument chat commands on registration.
+instrument.chatcommand (Chat commands) bool true
# Instrument global callback functions on registration.
# (anything you pass to a minetest.register_*() function)
@@ -1391,7 +1395,7 @@ name (Player name) string
# Set the language. Leave empty to use the system language.
# A restart is required after changing this.
-language (Language) enum ,ar,ca,cs,da,de,dv,el,en,eo,es,et,eu,fil,fr,hu,id,it,ja,ja_KS,jbo,kk,kn,lo,lt,ms,my,nb,nl,nn,pl,pt,pt_BR,ro,ru,sl,sr_Cyrl,sv,sw,th,tr,uk,vi
+language (Language) enum ,be,bg,ca,cs,da,de,el,en,eo,es,et,eu,fi,fr,gd,gl,hu,id,it,ja,jbo,kk,ko,lt,lv,ms,nb,nl,nn,pl,pt,pt_BR,ro,ru,sk,sl,sr_Cyrl,sr_Latn,sv,sw,tr,uk,vi,zh_CN,zh_TW
# Level of logging to be written to debug.txt:
# - <nothing> (no logging)
@@ -1418,9 +1422,8 @@ enable_ipv6 (IPv6) bool true
[*Advanced]
-# Default timeout for cURL, stated in milliseconds.
-# Only has an effect if compiled with cURL.
-curl_timeout (cURL timeout) int 5000
+# Maximum time an interactive request (e.g. server list fetch) may take, stated in milliseconds.
+curl_timeout (cURL interactive timeout) int 20000
# Limits number of parallel HTTP requests. Affects:
# - Media fetch if server uses remote_media setting.
@@ -1429,12 +1432,9 @@ curl_timeout (cURL timeout) int 5000
# Only has an effect if compiled with cURL.
curl_parallel_limit (cURL parallel limit) int 8
-# Maximum time in ms a file download (e.g. a mod download) may take.
+# Maximum time a file download (e.g. a mod download) may take, stated in milliseconds.
curl_file_download_timeout (cURL file download timeout) int 300000
-# Makes DirectX work with LuaJIT. Disable if it causes troubles.
-high_precision_fpu (High-precision FPU) bool true
-
# Replaces the default main menu with a custom one.
main_menu_script (Main menu script) string
@@ -1459,11 +1459,11 @@ 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.
# Value is stored per-world.
-mapgen_limit (Map generation limit) int 31000 0 31000
+mapgen_limit (Map generation limit) int 31007 0 31007
# Global map generation attributes.
# In Mapgen v6 the 'decorations' flag controls all decorations except trees
-# and junglegrass, in all other mapgens this flag controls all decorations.
+# and jungle grass, in all other mapgens this flag controls all decorations.
mg_flags (Mapgen flags) flags caves,dungeons,light,decorations,biomes,ores caves,dungeons,light,decorations,biomes,ores,nocaves,nodungeons,nolight,nodecorations,nobiomes,noores
[*Biome API temperature and humidity noise parameters]
@@ -2173,15 +2173,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 1024
+emergequeue_limit_total (Absolute limit of queued blocks to emerge) int 1024 1 1000000
# 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 128
+emergequeue_limit_diskonly (Per-player limit of queued blocks load from disk) int 128 1 1000000
# 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 128
+emergequeue_limit_generate (Per-player limit of queued blocks to generate) int 128 1 1000000
# Number of emerge threads to use.
# Value 0: