mirror of
https://github.com/MCLx86/xtreemtest.git
synced 2025-12-06 15:35:32 +01:00
Initial commit
This commit is contained in:
59
builtin/async/game.lua
Normal file
59
builtin/async/game.lua
Normal file
@@ -0,0 +1,59 @@
|
||||
core.log("info", "Initializing asynchronous environment (game)")
|
||||
|
||||
local function pack2(...)
|
||||
return {n=select('#', ...), ...}
|
||||
end
|
||||
|
||||
-- Entrypoint to run async jobs, called by C++
|
||||
function core.job_processor(func, params)
|
||||
local retval = pack2(func(unpack(params, 1, params.n)))
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
-- Import a bunch of individual files from builtin/game/
|
||||
local gamepath = core.get_builtin_path() .. "game" .. DIR_DELIM
|
||||
|
||||
dofile(gamepath .. "constants.lua")
|
||||
dofile(gamepath .. "item_s.lua")
|
||||
dofile(gamepath .. "misc_s.lua")
|
||||
dofile(gamepath .. "features.lua")
|
||||
dofile(gamepath .. "voxelarea.lua")
|
||||
|
||||
-- Transfer of globals
|
||||
do
|
||||
local all = assert(core.transferred_globals)
|
||||
core.transferred_globals = nil
|
||||
|
||||
all.registered_nodes = {}
|
||||
all.registered_craftitems = {}
|
||||
all.registered_tools = {}
|
||||
for k, v in pairs(all.registered_items) do
|
||||
-- Disable further modification
|
||||
setmetatable(v, {__newindex = {}})
|
||||
-- Reassemble the other tables
|
||||
if v.type == "node" then
|
||||
all.registered_nodes[k] = v
|
||||
elseif v.type == "craftitem" then
|
||||
all.registered_craftitems[k] = v
|
||||
elseif v.type == "tool" then
|
||||
all.registered_tools[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in pairs(all) do
|
||||
core[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- For tables that are indexed by item name:
|
||||
-- If table[X] does not exist, default to table[core.registered_aliases[X]]
|
||||
local alias_metatable = {
|
||||
__index = function(t, name)
|
||||
return rawget(t, core.registered_aliases[name])
|
||||
end
|
||||
}
|
||||
setmetatable(core.registered_items, alias_metatable)
|
||||
setmetatable(core.registered_nodes, alias_metatable)
|
||||
setmetatable(core.registered_craftitems, alias_metatable)
|
||||
setmetatable(core.registered_tools, alias_metatable)
|
||||
9
builtin/async/mainmenu.lua
Normal file
9
builtin/async/mainmenu.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
core.log("info", "Initializing asynchronous environment")
|
||||
|
||||
function core.job_processor(func, serialized_param)
|
||||
local param = core.deserialize(serialized_param)
|
||||
|
||||
local retval = core.serialize(func(param))
|
||||
|
||||
return retval or core.serialize(nil)
|
||||
end
|
||||
74
builtin/client/chatcommands.lua
Normal file
74
builtin/client/chatcommands.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
-- Minetest: builtin/client/chatcommands.lua
|
||||
|
||||
core.register_on_sending_chat_message(function(message)
|
||||
if message:sub(1,2) == ".." then
|
||||
return false
|
||||
end
|
||||
|
||||
local first_char = message:sub(1,1)
|
||||
if first_char == "/" or first_char == "." then
|
||||
core.display_chat_message(core.gettext("Issued command: ") .. message)
|
||||
end
|
||||
|
||||
if first_char ~= "." then
|
||||
return false
|
||||
end
|
||||
|
||||
local cmd, param = string.match(message, "^%.([^ ]+) *(.*)")
|
||||
param = param or ""
|
||||
|
||||
if not cmd then
|
||||
core.display_chat_message("-!- " .. core.gettext("Empty command."))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Run core.registered_on_chatcommand callbacks.
|
||||
if core.run_callbacks(core.registered_on_chatcommand, 5, cmd, param) then
|
||||
return true
|
||||
end
|
||||
|
||||
local cmd_def = core.registered_chatcommands[cmd]
|
||||
if cmd_def then
|
||||
core.set_last_run_mod(cmd_def.mod_origin)
|
||||
local _, result = cmd_def.func(param)
|
||||
if result then
|
||||
core.display_chat_message(result)
|
||||
end
|
||||
else
|
||||
core.display_chat_message("-!- " .. core.gettext("Invalid command: ") .. cmd)
|
||||
end
|
||||
|
||||
return true
|
||||
end)
|
||||
|
||||
core.register_chatcommand("list_players", {
|
||||
description = core.gettext("List online players"),
|
||||
func = function(param)
|
||||
local player_names = core.get_player_names()
|
||||
if not player_names then
|
||||
return false, core.gettext("This command is disabled by server.")
|
||||
end
|
||||
|
||||
local players = table.concat(player_names, ", ")
|
||||
return true, core.gettext("Online players: ") .. players
|
||||
end
|
||||
})
|
||||
|
||||
core.register_chatcommand("disconnect", {
|
||||
description = core.gettext("Exit to main menu"),
|
||||
func = function(param)
|
||||
core.disconnect()
|
||||
end,
|
||||
})
|
||||
|
||||
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.")
|
||||
end,
|
||||
})
|
||||
|
||||
function core.run_server_chatcommand(cmd, param)
|
||||
core.send_chat_message("/" .. cmd .. " " .. param)
|
||||
end
|
||||
15
builtin/client/death_formspec.lua
Normal file
15
builtin/client/death_formspec.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- CSM death formspec. Only used when clientside modding is enabled, otherwise
|
||||
-- handled by the engine.
|
||||
|
||||
core.register_on_death(function()
|
||||
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") .."]"
|
||||
core.show_formspec("bultin:death", formspec)
|
||||
end)
|
||||
|
||||
core.register_on_formspec_input(function(formname, fields)
|
||||
if formname == "bultin:death" then
|
||||
core.send_respawn()
|
||||
end
|
||||
end)
|
||||
12
builtin/client/init.lua
Normal file
12
builtin/client/init.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Minetest: builtin/client/init.lua
|
||||
local scriptpath = core.get_builtin_path()
|
||||
local clientpath = scriptpath.."client"..DIR_DELIM
|
||||
local commonpath = scriptpath.."common"..DIR_DELIM
|
||||
|
||||
dofile(clientpath .. "register.lua")
|
||||
dofile(commonpath .. "after.lua")
|
||||
dofile(commonpath .. "mod_storage.lua")
|
||||
dofile(commonpath .. "chatcommands.lua")
|
||||
dofile(clientpath .. "chatcommands.lua")
|
||||
dofile(clientpath .. "death_formspec.lua")
|
||||
dofile(clientpath .. "misc.lua")
|
||||
7
builtin/client/misc.lua
Normal file
7
builtin/client/misc.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
function core.setting_get_pos(name)
|
||||
local value = core.settings:get(name)
|
||||
if not value then
|
||||
return nil
|
||||
end
|
||||
return core.string_to_pos(value)
|
||||
end
|
||||
83
builtin/client/register.lua
Normal file
83
builtin/client/register.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
core.callback_origins = {}
|
||||
|
||||
local getinfo = debug.getinfo
|
||||
debug.getinfo = nil
|
||||
|
||||
--- Runs given callbacks.
|
||||
--
|
||||
-- Note: this function is also called from C++
|
||||
-- @tparam table callbacks a table with registered callbacks, like `core.registered_on_*`
|
||||
-- @tparam number mode a RunCallbacksMode, as defined in src/script/common/c_internal.h
|
||||
-- @param ... arguments for the callback
|
||||
-- @return depends on mode
|
||||
function core.run_callbacks(callbacks, mode, ...)
|
||||
assert(type(callbacks) == "table")
|
||||
local cb_len = #callbacks
|
||||
if cb_len == 0 then
|
||||
if mode == 2 or mode == 3 then
|
||||
return true
|
||||
elseif mode == 4 or mode == 5 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local ret
|
||||
for i = 1, cb_len do
|
||||
local cb_ret = callbacks[i](...)
|
||||
|
||||
if mode == 0 and i == 1 or mode == 1 and i == cb_len then
|
||||
ret = cb_ret
|
||||
elseif mode == 2 then
|
||||
if not cb_ret or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 3 then
|
||||
if cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
ret = cb_ret
|
||||
elseif mode == 4 then
|
||||
if (cb_ret and not ret) or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 5 and cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--
|
||||
-- Callback registration
|
||||
--
|
||||
|
||||
local function make_registration()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
t[#t + 1] = func
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
core.registered_globalsteps, core.register_globalstep = make_registration()
|
||||
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
|
||||
core.registered_on_shutdown, core.register_on_shutdown = make_registration()
|
||||
core.registered_on_receiving_chat_message, core.register_on_receiving_chat_message = make_registration()
|
||||
core.registered_on_sending_chat_message, core.register_on_sending_chat_message = make_registration()
|
||||
core.registered_on_chatcommand, core.register_on_chatcommand = make_registration()
|
||||
core.registered_on_death, core.register_on_death = make_registration()
|
||||
core.registered_on_hp_modification, core.register_on_hp_modification = make_registration()
|
||||
core.registered_on_damage_taken, core.register_on_damage_taken = make_registration()
|
||||
core.registered_on_formspec_input, core.register_on_formspec_input = make_registration()
|
||||
core.registered_on_dignode, core.register_on_dignode = make_registration()
|
||||
core.registered_on_punchnode, core.register_on_punchnode = make_registration()
|
||||
core.registered_on_placenode, core.register_on_placenode = make_registration()
|
||||
core.registered_on_item_use, core.register_on_item_use = make_registration()
|
||||
core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
|
||||
core.registered_on_modchannel_signal, core.register_on_modchannel_signal = make_registration()
|
||||
core.registered_on_inventory_open, core.register_on_inventory_open = make_registration()
|
||||
50
builtin/common/after.lua
Normal file
50
builtin/common/after.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
local jobs = {}
|
||||
local time = 0.0
|
||||
local time_next = math.huge
|
||||
|
||||
core.register_globalstep(function(dtime)
|
||||
time = time + dtime
|
||||
|
||||
if time < time_next then
|
||||
return
|
||||
end
|
||||
|
||||
time_next = math.huge
|
||||
|
||||
-- Iterate backwards so that we miss any new timers added by
|
||||
-- a timer callback.
|
||||
for i = #jobs, 1, -1 do
|
||||
local job = jobs[i]
|
||||
if time >= job.expire then
|
||||
core.set_last_run_mod(job.mod_origin)
|
||||
job.func(unpack(job.arg))
|
||||
local jobs_l = #jobs
|
||||
jobs[i] = jobs[jobs_l]
|
||||
jobs[jobs_l] = nil
|
||||
elseif job.expire < time_next then
|
||||
time_next = job.expire
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function core.after(after, func, ...)
|
||||
assert(tonumber(after) and type(func) == "function",
|
||||
"Invalid minetest.after invocation")
|
||||
local expire = time + after
|
||||
local new_job = {
|
||||
func = func,
|
||||
expire = expire,
|
||||
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
|
||||
new_job.args = {}
|
||||
end
|
||||
}
|
||||
end
|
||||
178
builtin/common/chatcommands.lua
Normal file
178
builtin/common/chatcommands.lua
Normal file
@@ -0,0 +1,178 @@
|
||||
-- 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 ""
|
||||
def.description = def.description or ""
|
||||
def.privs = def.privs or {}
|
||||
def.mod_origin = core.get_current_modname() or "??"
|
||||
core.registered_chatcommands[cmd] = def
|
||||
end
|
||||
|
||||
function core.unregister_chatcommand(name)
|
||||
if core.registered_chatcommands[name] then
|
||||
core.registered_chatcommands[name] = nil
|
||||
else
|
||||
core.log("warning", "Not unregistering chatcommand " ..name..
|
||||
" because it doesn't exist.")
|
||||
end
|
||||
end
|
||||
|
||||
function core.override_chatcommand(name, redefinition)
|
||||
local chatcommand = core.registered_chatcommands[name]
|
||||
assert(chatcommand, "Attempt to override non-existent chatcommand "..name)
|
||||
for k, v in pairs(redefinition) do
|
||||
rawset(chatcommand, k, v)
|
||||
end
|
||||
core.registered_chatcommands[name] = chatcommand
|
||||
end
|
||||
|
||||
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 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
|
||||
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
|
||||
cmds[#cmds + 1] = cmd
|
||||
end
|
||||
end
|
||||
table.sort(cmds)
|
||||
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
|
||||
cmds[#cmds + 1] = format_help_line(cmd, def)
|
||||
end
|
||||
end
|
||||
table.sort(cmds)
|
||||
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, S("Available privileges:").."\n"..table.concat(privs, "\n")
|
||||
else
|
||||
local cmd = args[1]
|
||||
local def = core.registered_chatcommands[cmd]
|
||||
if not def then
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
if INIT == "client" then
|
||||
core.register_chatcommand("help", {
|
||||
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 = S("[all | privs | <cmd>] [-t]"),
|
||||
description = S("Get help for commands or list privileges (-t: output in chat)"),
|
||||
func = do_help_cmd,
|
||||
})
|
||||
end
|
||||
319
builtin/common/filterlist.lua
Normal file
319
builtin/common/filterlist.lua
Normal file
@@ -0,0 +1,319 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- TODO improve doc --
|
||||
-- TODO code cleanup --
|
||||
-- Generic implementation of a filter/sortable list --
|
||||
-- Usage: --
|
||||
-- Filterlist needs to be initialized on creation. To achieve this you need to --
|
||||
-- pass following functions: --
|
||||
-- raw_fct() (mandatory): --
|
||||
-- function returning a table containing the elements to be filtered --
|
||||
-- compare_fct(element1,element2) (mandatory): --
|
||||
-- function returning true/false if element1 is same element as element2 --
|
||||
-- uid_match_fct(element1,uid) (optional) --
|
||||
-- function telling if uid is attached to element1 --
|
||||
-- filter_fct(element,filtercriteria) (optional) --
|
||||
-- function returning true/false if filtercriteria met to element --
|
||||
-- fetch_param (optional) --
|
||||
-- parameter passed to raw_fct to aquire correct raw data --
|
||||
-- --
|
||||
--------------------------------------------------------------------------------
|
||||
filterlist = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.refresh(self)
|
||||
self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param)
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.create(raw_fct,compare_fct,uid_match_fct,filter_fct,fetch_param)
|
||||
|
||||
assert((raw_fct ~= nil) and (type(raw_fct) == "function"))
|
||||
assert((compare_fct ~= nil) and (type(compare_fct) == "function"))
|
||||
|
||||
local self = {}
|
||||
|
||||
self.m_raw_list_fct = raw_fct
|
||||
self.m_compare_fct = compare_fct
|
||||
self.m_filter_fct = filter_fct
|
||||
self.m_uid_match_fct = uid_match_fct
|
||||
|
||||
self.m_filtercriteria = nil
|
||||
self.m_fetch_param = fetch_param
|
||||
|
||||
self.m_sortmode = "none"
|
||||
self.m_sort_list = {}
|
||||
|
||||
self.m_processed_list = nil
|
||||
self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param)
|
||||
|
||||
self.add_sort_mechanism = filterlist.add_sort_mechanism
|
||||
self.set_filtercriteria = filterlist.set_filtercriteria
|
||||
self.get_filtercriteria = filterlist.get_filtercriteria
|
||||
self.set_sortmode = filterlist.set_sortmode
|
||||
self.get_list = filterlist.get_list
|
||||
self.get_raw_list = filterlist.get_raw_list
|
||||
self.get_raw_element = filterlist.get_raw_element
|
||||
self.get_raw_index = filterlist.get_raw_index
|
||||
self.get_current_index = filterlist.get_current_index
|
||||
self.size = filterlist.size
|
||||
self.uid_exists_raw = filterlist.uid_exists_raw
|
||||
self.raw_index_by_uid = filterlist.raw_index_by_uid
|
||||
self.refresh = filterlist.refresh
|
||||
|
||||
filterlist.process(self)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.add_sort_mechanism(self,name,fct)
|
||||
self.m_sort_list[name] = fct
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.set_filtercriteria(self,criteria)
|
||||
if criteria == self.m_filtercriteria and
|
||||
type(criteria) ~= "table" then
|
||||
return
|
||||
end
|
||||
self.m_filtercriteria = criteria
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_filtercriteria(self)
|
||||
return self.m_filtercriteria
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--supported sort mode "alphabetic|none"
|
||||
function filterlist.set_sortmode(self,mode)
|
||||
if (mode == self.m_sortmode) then
|
||||
return
|
||||
end
|
||||
self.m_sortmode = mode
|
||||
filterlist.process(self)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_list(self)
|
||||
return self.m_processed_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_list(self)
|
||||
return self.m_raw_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_element(self,idx)
|
||||
if type(idx) ~= "number" then
|
||||
idx = tonumber(idx)
|
||||
end
|
||||
|
||||
if idx ~= nil and idx > 0 and idx <= #self.m_raw_list then
|
||||
return self.m_raw_list[idx]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_raw_index(self,listindex)
|
||||
assert(self.m_processed_list ~= nil)
|
||||
|
||||
if listindex ~= nil and listindex > 0 and
|
||||
listindex <= #self.m_processed_list then
|
||||
local entry = self.m_processed_list[listindex]
|
||||
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
|
||||
if self.m_compare_fct(v,entry) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.get_current_index(self,listindex)
|
||||
assert(self.m_processed_list ~= nil)
|
||||
|
||||
if listindex ~= nil and listindex > 0 and
|
||||
listindex <= #self.m_raw_list then
|
||||
local entry = self.m_raw_list[listindex]
|
||||
|
||||
for i,v in ipairs(self.m_processed_list) do
|
||||
|
||||
if self.m_compare_fct(v,entry) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.process(self)
|
||||
assert(self.m_raw_list ~= nil)
|
||||
|
||||
if self.m_sortmode == "none" and
|
||||
self.m_filtercriteria == nil then
|
||||
self.m_processed_list = self.m_raw_list
|
||||
return
|
||||
end
|
||||
|
||||
self.m_processed_list = {}
|
||||
|
||||
for k,v in pairs(self.m_raw_list) do
|
||||
if self.m_filtercriteria == nil or
|
||||
self.m_filter_fct(v,self.m_filtercriteria) then
|
||||
self.m_processed_list[#self.m_processed_list + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
if self.m_sortmode == "none" then
|
||||
return
|
||||
end
|
||||
|
||||
if self.m_sort_list[self.m_sortmode] ~= nil and
|
||||
type(self.m_sort_list[self.m_sortmode]) == "function" then
|
||||
|
||||
self.m_sort_list[self.m_sortmode](self)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.size(self)
|
||||
if self.m_processed_list == nil then
|
||||
return 0
|
||||
end
|
||||
|
||||
return #self.m_processed_list
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.uid_exists_raw(self,uid)
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
if self.m_uid_match_fct(v,uid) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function filterlist.raw_index_by_uid(self, uid)
|
||||
local elementcount = 0
|
||||
local elementidx = 0
|
||||
for i,v in ipairs(self.m_raw_list) do
|
||||
if self.m_uid_match_fct(v,uid) then
|
||||
elementcount = elementcount +1
|
||||
elementidx = i
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- If there are more elements than one with same name uid can't decide which
|
||||
-- one is meant. self shouldn't be possible but just for sure.
|
||||
if elementcount > 1 then
|
||||
elementidx=0
|
||||
end
|
||||
|
||||
return elementidx
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- COMMON helper functions --
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function compare_worlds(world1,world2)
|
||||
if world1.path ~= world2.path then
|
||||
return false
|
||||
end
|
||||
|
||||
if world1.name ~= world2.name then
|
||||
return false
|
||||
end
|
||||
|
||||
if world1.gameid ~= world2.gameid then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function sort_worlds_alphabetic(self)
|
||||
|
||||
table.sort(self.m_processed_list, function(a, b)
|
||||
--fixes issue #857 (crash due to sorting nil in worldlist)
|
||||
if a == nil or b == nil then
|
||||
if a == nil and b ~= nil then return false end
|
||||
if b == nil and a ~= nil then return true end
|
||||
return false
|
||||
end
|
||||
if a.name:lower() == b.name:lower() then
|
||||
return a.name < b.name
|
||||
end
|
||||
return a.name:lower() < b.name:lower()
|
||||
end)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function sort_mod_list(self)
|
||||
|
||||
table.sort(self.m_processed_list, function(a, b)
|
||||
-- Show game mods at bottom
|
||||
if a.type ~= b.type or a.loc ~= b.loc then
|
||||
if b.type == "game" then
|
||||
return a.loc ~= "game"
|
||||
end
|
||||
return b.loc == "game"
|
||||
end
|
||||
-- If in same or no modpack, sort by name
|
||||
if a.modpack == b.modpack then
|
||||
if a.name:lower() == b.name:lower() then
|
||||
return a.name < b.name
|
||||
end
|
||||
return a.name:lower() < b.name:lower()
|
||||
-- Else compare name to modpack name
|
||||
else
|
||||
-- Always show modpack pseudo-mod on top of modpack mod list
|
||||
if a.name == b.modpack then
|
||||
return true
|
||||
elseif b.name == a.modpack then
|
||||
return false
|
||||
end
|
||||
|
||||
local name_a = a.modpack or a.name
|
||||
local name_b = b.modpack or b.name
|
||||
if name_a:lower() == name_b:lower() then
|
||||
return name_a < name_b
|
||||
end
|
||||
return name_a:lower() < name_b:lower()
|
||||
end
|
||||
end)
|
||||
end
|
||||
136
builtin/common/information_formspecs.lua
Normal file
136
builtin/common/information_formspecs.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
local COLOR_BLUE = "#7AF"
|
||||
local COLOR_GREEN = "#7F7"
|
||||
local COLOR_GRAY = "#BBB"
|
||||
|
||||
local LIST_FORMSPEC = [[
|
||||
size[13,6.5]
|
||||
label[0,-0.1;%s]
|
||||
tablecolumns[color;tree;text;text]
|
||||
table[0,0.5;12.8,5.5;list;%s;0]
|
||||
button_exit[5,6;3,1;quit;%s]
|
||||
]]
|
||||
|
||||
local LIST_FORMSPEC_DESCRIPTION = [[
|
||||
size[13,7.5]
|
||||
label[0,-0.1;%s]
|
||||
tablecolumns[color;tree;text;text]
|
||||
table[0,0.5;12.8,4.8;list;%s;%i]
|
||||
box[0,5.5;12.8,1.5;#000]
|
||||
textarea[0.3,5.5;13.05,1.9;;;%s]
|
||||
button_exit[5,7;3,1;quit;%s]
|
||||
]]
|
||||
|
||||
local F = core.formspec_escape
|
||||
local S = core.get_translator("__builtin")
|
||||
|
||||
|
||||
-- CHAT COMMANDS FORMSPEC
|
||||
|
||||
local mod_cmds = {}
|
||||
|
||||
local function load_mod_command_tree()
|
||||
mod_cmds = {}
|
||||
|
||||
for name, def in pairs(core.registered_chatcommands) do
|
||||
mod_cmds[def.mod_origin] = mod_cmds[def.mod_origin] or {}
|
||||
local cmds = mod_cmds[def.mod_origin]
|
||||
|
||||
-- Could be simplified, but avoid the priv checks whenever possible
|
||||
cmds[#cmds + 1] = { name, def }
|
||||
end
|
||||
local sorted_mod_cmds = {}
|
||||
for modname, cmds in pairs(mod_cmds) do
|
||||
table.sort(cmds, function(a, b) return a[1] < b[1] end)
|
||||
sorted_mod_cmds[#sorted_mod_cmds + 1] = { modname, cmds }
|
||||
end
|
||||
table.sort(sorted_mod_cmds, function(a, b) return a[1] < b[1] end)
|
||||
mod_cmds = sorted_mod_cmds
|
||||
end
|
||||
|
||||
core.after(0, load_mod_command_tree)
|
||||
|
||||
local function build_chatcommands_formspec(name, sel, copy)
|
||||
local rows = {}
|
||||
rows[1] = "#FFF,0,"..F(S("Command"))..","..F(S("Parameters"))
|
||||
|
||||
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.")
|
||||
|
||||
local privs = core.get_player_privs(name)
|
||||
for i, data in ipairs(mod_cmds) do
|
||||
rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. ","
|
||||
for j, cmds in ipairs(data[2]) do
|
||||
local has_priv = privs[cmds[2].privs]
|
||||
rows[#rows + 1] = ("%s,1,%s,%s"):format(
|
||||
has_priv and COLOR_GREEN or COLOR_GRAY,
|
||||
cmds[1], F(cmds[2].params))
|
||||
if sel == #rows then
|
||||
description = cmds[2].description
|
||||
if copy then
|
||||
core.chat_send_player(name, S("Command: @1 @2",
|
||||
core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return LIST_FORMSPEC_DESCRIPTION:format(
|
||||
F(S("Available commands: (see also: /help <cmd>)")),
|
||||
table.concat(rows, ","), sel or 0,
|
||||
F(description), F(S("Close"))
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
-- PRIVILEGES FORMSPEC
|
||||
|
||||
local function build_privs_formspec(name)
|
||||
local privs = {}
|
||||
for priv_name, def in pairs(core.registered_privileges) do
|
||||
privs[#privs + 1] = { priv_name, def }
|
||||
end
|
||||
table.sort(privs, function(a, b) return a[1] < b[1] end)
|
||||
|
||||
local rows = {}
|
||||
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], F(data[2].description))
|
||||
end
|
||||
|
||||
return LIST_FORMSPEC:format(
|
||||
F(S("Available privileges:")),
|
||||
table.concat(rows, ","),
|
||||
F(S("Close"))
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
-- DETAILED CHAT COMMAND INFORMATION
|
||||
|
||||
core.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= "__builtin:help_cmds" or fields.quit then
|
||||
return
|
||||
end
|
||||
|
||||
local event = core.explode_table_event(fields.list)
|
||||
if event.type ~= "INV" then
|
||||
local name = player:get_player_name()
|
||||
core.show_formspec(name, "__builtin:help_cmds",
|
||||
build_chatcommands_formspec(name, event.row, event.type == "DCL"))
|
||||
end
|
||||
end)
|
||||
|
||||
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
|
||||
770
builtin/common/misc_helpers.lua
Normal file
770
builtin/common/misc_helpers.lua
Normal file
@@ -0,0 +1,770 @@
|
||||
-- Minetest: builtin/misc_helpers.lua
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Localize functions to avoid table lookups (better performance).
|
||||
local string_sub, string_find = string.sub, string.find
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function basic_dump(o)
|
||||
local tp = type(o)
|
||||
if tp == "number" then
|
||||
return tostring(o)
|
||||
elseif tp == "string" then
|
||||
return string.format("%q", o)
|
||||
elseif tp == "boolean" then
|
||||
return tostring(o)
|
||||
elseif tp == "nil" then
|
||||
return "nil"
|
||||
-- Uncomment for full function dumping support.
|
||||
-- Not currently enabled because bytecode isn't very human-readable and
|
||||
-- dump's output is intended for humans.
|
||||
--elseif tp == "function" then
|
||||
-- return string.format("loadstring(%q)", string.dump(o))
|
||||
elseif tp == "userdata" then
|
||||
return tostring(o)
|
||||
else
|
||||
return string.format("<%s>", tp)
|
||||
end
|
||||
end
|
||||
|
||||
local keywords = {
|
||||
["and"] = true,
|
||||
["break"] = true,
|
||||
["do"] = true,
|
||||
["else"] = true,
|
||||
["elseif"] = true,
|
||||
["end"] = true,
|
||||
["false"] = true,
|
||||
["for"] = true,
|
||||
["function"] = true,
|
||||
["goto"] = true, -- Lua 5.2
|
||||
["if"] = true,
|
||||
["in"] = true,
|
||||
["local"] = true,
|
||||
["nil"] = true,
|
||||
["not"] = true,
|
||||
["or"] = true,
|
||||
["repeat"] = true,
|
||||
["return"] = true,
|
||||
["then"] = true,
|
||||
["true"] = true,
|
||||
["until"] = true,
|
||||
["while"] = true,
|
||||
}
|
||||
local function is_valid_identifier(str)
|
||||
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Dumps values in a line-per-value format.
|
||||
-- For example, {test = {"Testing..."}} becomes:
|
||||
-- _["test"] = {}
|
||||
-- _["test"][1] = "Testing..."
|
||||
-- This handles tables as keys and circular references properly.
|
||||
-- It also handles multiple references well, writing the table only once.
|
||||
-- The dumped argument is internal-only.
|
||||
|
||||
function dump2(o, name, dumped)
|
||||
name = name or "_"
|
||||
-- "dumped" is used to keep track of serialized tables to handle
|
||||
-- multiple references and circular tables properly.
|
||||
-- It only contains tables as keys. The value is the name that
|
||||
-- the table has in the dump, eg:
|
||||
-- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
|
||||
dumped = dumped or {}
|
||||
if type(o) ~= "table" then
|
||||
return string.format("%s = %s\n", name, basic_dump(o))
|
||||
end
|
||||
if dumped[o] then
|
||||
return string.format("%s = %s\n", name, dumped[o])
|
||||
end
|
||||
dumped[o] = name
|
||||
-- This contains a list of strings to be concatenated later (because
|
||||
-- Lua is slow at individual concatenation).
|
||||
local t = {}
|
||||
for k, v in pairs(o) do
|
||||
local keyStr
|
||||
if type(k) == "table" then
|
||||
if dumped[k] then
|
||||
keyStr = dumped[k]
|
||||
else
|
||||
-- Key tables don't have a name, so use one of
|
||||
-- the form _G["table: 0xFFFFFFF"]
|
||||
keyStr = string.format("_G[%q]", tostring(k))
|
||||
-- Dump key table
|
||||
t[#t + 1] = dump2(k, keyStr, dumped)
|
||||
end
|
||||
else
|
||||
keyStr = basic_dump(k)
|
||||
end
|
||||
local vname = string.format("%s[%s]", name, keyStr)
|
||||
t[#t + 1] = dump2(v, vname, dumped)
|
||||
end
|
||||
return string.format("%s = {}\n%s", name, table.concat(t))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- This dumps values in a one-statement format.
|
||||
-- For example, {test = {"Testing..."}} becomes:
|
||||
-- [[{
|
||||
-- test = {
|
||||
-- "Testing..."
|
||||
-- }
|
||||
-- }]]
|
||||
-- This supports tables as keys, but not circular references.
|
||||
-- It performs poorly with multiple references as it writes out the full
|
||||
-- table each time.
|
||||
-- The indent field specifies a indentation string, it defaults to a tab.
|
||||
-- Use the empty string to disable indentation.
|
||||
-- The dumped and level arguments are internal-only.
|
||||
|
||||
function dump(o, indent, nested, level)
|
||||
local t = type(o)
|
||||
if not level and t == "userdata" then
|
||||
-- when userdata (e.g. player) is passed directly, print its metatable:
|
||||
return "userdata metatable: " .. dump(getmetatable(o))
|
||||
end
|
||||
if t ~= "table" then
|
||||
return basic_dump(o)
|
||||
end
|
||||
|
||||
-- Contains table -> true/nil of currently nested tables
|
||||
nested = nested or {}
|
||||
if nested[o] then
|
||||
return "<circular reference>"
|
||||
end
|
||||
nested[o] = true
|
||||
indent = indent or "\t"
|
||||
level = level or 1
|
||||
|
||||
local ret = {}
|
||||
local dumped_indexes = {}
|
||||
for i, v in ipairs(o) do
|
||||
ret[#ret + 1] = dump(v, indent, nested, level + 1)
|
||||
dumped_indexes[i] = true
|
||||
end
|
||||
for k, v in pairs(o) do
|
||||
if not dumped_indexes[k] then
|
||||
if type(k) ~= "string" or not is_valid_identifier(k) then
|
||||
k = "["..dump(k, indent, nested, level + 1).."]"
|
||||
end
|
||||
v = dump(v, indent, nested, level + 1)
|
||||
ret[#ret + 1] = k.." = "..v
|
||||
end
|
||||
end
|
||||
nested[o] = nil
|
||||
if indent ~= "" then
|
||||
local indent_str = "\n"..string.rep(indent, level)
|
||||
local end_indent_str = "\n"..string.rep(indent, level - 1)
|
||||
return string.format("{%s%s%s}",
|
||||
indent_str,
|
||||
table.concat(ret, ","..indent_str),
|
||||
end_indent_str)
|
||||
end
|
||||
return "{"..table.concat(ret, ", ").."}"
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
|
||||
delim = delim or ","
|
||||
max_splits = max_splits or -2
|
||||
local items = {}
|
||||
local pos, len = 1, #str
|
||||
local plain = not sep_is_pattern
|
||||
max_splits = max_splits + 1
|
||||
repeat
|
||||
local np, npe = string_find(str, delim, pos, plain)
|
||||
np, npe = (np or (len+1)), (npe or (len+1))
|
||||
if (not np) or (max_splits == 1) then
|
||||
np = len + 1
|
||||
npe = np
|
||||
end
|
||||
local s = string_sub(str, pos, np - 1)
|
||||
if include_empty or (s ~= "") then
|
||||
max_splits = max_splits - 1
|
||||
items[#items + 1] = s
|
||||
end
|
||||
pos = npe + 1
|
||||
until (max_splits == 0) or (pos > (len + 1))
|
||||
return items
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function table.indexof(list, val)
|
||||
for i, v in ipairs(list) do
|
||||
if v == val then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function string:trim()
|
||||
return self:match("^%s*(.-)%s*$")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.hypot(x, y)
|
||||
return math.sqrt(x * x + y * y)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.sign(x, tolerance)
|
||||
tolerance = tolerance or 0
|
||||
if x > tolerance then
|
||||
return 1
|
||||
elseif x < -tolerance then
|
||||
return -1
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function math.factorial(x)
|
||||
assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer")
|
||||
if x >= 171 then
|
||||
-- 171! is greater than the biggest double, no need to calculate
|
||||
return math.huge
|
||||
end
|
||||
local v = 1
|
||||
for k = 2, x do
|
||||
v = v * k
|
||||
end
|
||||
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
|
||||
|
||||
local formspec_escapes = {
|
||||
["\\"] = "\\\\",
|
||||
["["] = "\\[",
|
||||
["]"] = "\\]",
|
||||
[";"] = "\\;",
|
||||
[","] = "\\,"
|
||||
}
|
||||
function core.formspec_escape(text)
|
||||
-- Use explicit character set instead of dot here because it doubles the performance
|
||||
return text and string.gsub(text, "[\\%[%];,]", formspec_escapes)
|
||||
end
|
||||
|
||||
|
||||
function core.wrap_text(text, max_length, as_table)
|
||||
local result = {}
|
||||
local line = {}
|
||||
if #text <= max_length then
|
||||
return as_table and {text} or text
|
||||
end
|
||||
|
||||
local line_length = 0
|
||||
for word in text:gmatch("%S+") do
|
||||
if line_length > 0 and line_length + #word + 1 >= max_length then
|
||||
-- word wouldn't fit on current line, move to next line
|
||||
table.insert(result, table.concat(line, " "))
|
||||
line = {word}
|
||||
line_length = #word
|
||||
else
|
||||
table.insert(line, word)
|
||||
line_length = line_length + 1 + #word
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(result, table.concat(line, " "))
|
||||
return as_table and result or table.concat(result, "\n")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
if INIT == "game" then
|
||||
local dirs1 = {9, 18, 7, 12}
|
||||
local dirs2 = {20, 23, 22, 21}
|
||||
|
||||
function core.rotate_and_place(itemstack, placer, pointed_thing,
|
||||
infinitestacks, orient_flags, prevent_after_place)
|
||||
orient_flags = orient_flags or {}
|
||||
|
||||
local unode = core.get_node_or_nil(pointed_thing.under)
|
||||
if not unode then
|
||||
return
|
||||
end
|
||||
local undef = core.registered_nodes[unode.name]
|
||||
local sneaking = placer and placer:get_player_control().sneak
|
||||
if undef and undef.on_rightclick and not sneaking then
|
||||
return undef.on_rightclick(pointed_thing.under, unode, placer,
|
||||
itemstack, pointed_thing)
|
||||
end
|
||||
local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0
|
||||
|
||||
local above = pointed_thing.above
|
||||
local under = pointed_thing.under
|
||||
local iswall = (above.y == under.y)
|
||||
local isceiling = not iswall and (above.y < under.y)
|
||||
|
||||
if undef and undef.buildable_to then
|
||||
iswall = false
|
||||
end
|
||||
|
||||
if orient_flags.force_floor then
|
||||
iswall = false
|
||||
isceiling = false
|
||||
elseif orient_flags.force_ceiling then
|
||||
iswall = false
|
||||
isceiling = true
|
||||
elseif orient_flags.force_wall then
|
||||
iswall = true
|
||||
isceiling = false
|
||||
elseif orient_flags.invert_wall then
|
||||
iswall = not iswall
|
||||
end
|
||||
|
||||
local param2 = fdir
|
||||
if iswall then
|
||||
param2 = dirs1[fdir + 1]
|
||||
elseif isceiling then
|
||||
if orient_flags.force_facedir then
|
||||
param2 = 20
|
||||
else
|
||||
param2 = dirs2[fdir + 1]
|
||||
end
|
||||
else -- place right side up
|
||||
if orient_flags.force_facedir then
|
||||
param2 = 0
|
||||
end
|
||||
end
|
||||
|
||||
local old_itemstack = ItemStack(itemstack)
|
||||
local new_itemstack = core.item_place_node(itemstack, placer,
|
||||
pointed_thing, param2, prevent_after_place)
|
||||
return infinitestacks and old_itemstack or new_itemstack
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--Wrapper for rotate_and_place() to check for sneak and assume Creative mode
|
||||
--implies infinite stacks when performing a 6d rotation.
|
||||
--------------------------------------------------------------------------------
|
||||
core.rotate_node = function(itemstack, placer, pointed_thing)
|
||||
local name = placer and placer:get_player_name() or ""
|
||||
local invert_wall = placer and placer:get_player_control().sneak or false
|
||||
return core.rotate_and_place(itemstack, placer, pointed_thing,
|
||||
core.is_creative_enabled(name),
|
||||
{invert_wall = invert_wall}, true)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_table_event(evt)
|
||||
if evt ~= nil then
|
||||
local parts = evt:split(":")
|
||||
if #parts == 3 then
|
||||
local t = parts[1]:trim()
|
||||
local r = tonumber(parts[2]:trim())
|
||||
local c = tonumber(parts[3]:trim())
|
||||
if type(r) == "number" and type(c) == "number"
|
||||
and t ~= "INV" then
|
||||
return {type=t, row=r, column=c}
|
||||
end
|
||||
end
|
||||
end
|
||||
return {type="INV", row=0, column=0}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_textlist_event(evt)
|
||||
if evt ~= nil then
|
||||
local parts = evt:split(":")
|
||||
if #parts == 2 then
|
||||
local t = parts[1]:trim()
|
||||
local r = tonumber(parts[2]:trim())
|
||||
if type(r) == "number" and t ~= "INV" then
|
||||
return {type=t, index=r}
|
||||
end
|
||||
end
|
||||
end
|
||||
return {type="INV", index=0}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.explode_scrollbar_event(evt)
|
||||
local retval = core.explode_textlist_event(evt)
|
||||
|
||||
retval.value = retval.index
|
||||
retval.index = nil
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.rgba(r, g, b, a)
|
||||
return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or
|
||||
string.format("#%02X%02X%02X", r, g, b)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.pos_to_string(pos, decimal_places)
|
||||
local x = pos.x
|
||||
local y = pos.y
|
||||
local z = pos.z
|
||||
if decimal_places ~= nil then
|
||||
x = string.format("%." .. decimal_places .. "f", x)
|
||||
y = string.format("%." .. decimal_places .. "f", y)
|
||||
z = string.format("%." .. decimal_places .. "f", z)
|
||||
end
|
||||
return "(" .. x .. "," .. y .. "," .. z .. ")"
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function core.string_to_pos(value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
value = value:match("^%((.-)%)$") or value -- strip parentheses
|
||||
|
||||
local x, y, z = value:trim():match("^([%d.-]+)[,%s]%s*([%d.-]+)[,%s]%s*([%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
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
do
|
||||
local rel_num_cap = "(~?-?%d*%.?%d*)" -- may be overly permissive as this will be tonumber'ed anyways
|
||||
local num_delim = "[,%s]%s*"
|
||||
local pattern = "^" .. table.concat({rel_num_cap, rel_num_cap, rel_num_cap}, num_delim) .. "$"
|
||||
|
||||
local function parse_area_string(pos, relative_to)
|
||||
local pp = {}
|
||||
pp.x, pp.y, pp.z = pos:trim():match(pattern)
|
||||
return core.parse_coordinates(pp.x, pp.y, pp.z, relative_to)
|
||||
end
|
||||
|
||||
function core.string_to_area(value, relative_to)
|
||||
local p1, p2 = value:match("^%((.-)%)%s*%((.-)%)$")
|
||||
if not p1 then
|
||||
return
|
||||
end
|
||||
|
||||
p1 = parse_area_string(p1, relative_to)
|
||||
p2 = parse_area_string(p2, relative_to)
|
||||
|
||||
if p1 == nil or p2 == nil then
|
||||
return
|
||||
end
|
||||
|
||||
return p1, p2
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function table.copy(t, seen)
|
||||
local n = {}
|
||||
seen = seen or {}
|
||||
seen[t] = n
|
||||
for k, v in pairs(t) do
|
||||
n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] =
|
||||
(type(v) == "table" and (seen[v] or table.copy(v, seen))) or v
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
function table.insert_all(t, other)
|
||||
for i=1, #other do
|
||||
t[#t + 1] = other[i]
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
function table.key_value_swap(t)
|
||||
local ti = {}
|
||||
for k,v in pairs(t) do
|
||||
ti[v] = k
|
||||
end
|
||||
return ti
|
||||
end
|
||||
|
||||
|
||||
function table.shuffle(t, from, to, random)
|
||||
from = from or 1
|
||||
to = to or #t
|
||||
random = random or math.random
|
||||
local n = to - from + 1
|
||||
while n > 1 do
|
||||
local r = from + n-1
|
||||
local l = from + random(0, n-1)
|
||||
t[l], t[r] = t[r], t[l]
|
||||
n = n-1
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- mainmenu only functions
|
||||
--------------------------------------------------------------------------------
|
||||
if INIT == "mainmenu" then
|
||||
function core.get_game(index)
|
||||
local games = core.get_games()
|
||||
|
||||
if index > 0 and index <= #games then
|
||||
return games[index]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if core.gettext then -- for client and mainmenu
|
||||
function fgettext_ne(text, ...)
|
||||
text = core.gettext(text)
|
||||
local arg = {n=select('#', ...), ...}
|
||||
if arg.n >= 1 then
|
||||
-- Insert positional parameters ($1, $2, ...)
|
||||
local result = ''
|
||||
local pos = 1
|
||||
while pos <= text:len() do
|
||||
local newpos = text:find('[$]', pos)
|
||||
if newpos == nil then
|
||||
result = result .. text:sub(pos)
|
||||
pos = text:len() + 1
|
||||
else
|
||||
local paramindex =
|
||||
tonumber(text:sub(newpos+1, newpos+1))
|
||||
result = result .. text:sub(pos, newpos-1)
|
||||
.. tostring(arg[paramindex])
|
||||
pos = newpos + 2
|
||||
end
|
||||
end
|
||||
text = result
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
function fgettext(text, ...)
|
||||
return core.formspec_escape(fgettext_ne(text, ...))
|
||||
end
|
||||
end
|
||||
|
||||
local ESCAPE_CHAR = string.char(0x1b)
|
||||
|
||||
function core.get_color_escape_sequence(color)
|
||||
return ESCAPE_CHAR .. "(c@" .. color .. ")"
|
||||
end
|
||||
|
||||
function core.get_background_escape_sequence(color)
|
||||
return ESCAPE_CHAR .. "(b@" .. color .. ")"
|
||||
end
|
||||
|
||||
function core.colorize(color, message)
|
||||
local lines = tostring(message):split("\n", true)
|
||||
local color_code = core.get_color_escape_sequence(color)
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
lines[i] = color_code .. line
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff")
|
||||
end
|
||||
|
||||
|
||||
function core.strip_foreground_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.strip_background_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.strip_colors(str)
|
||||
return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
|
||||
end
|
||||
|
||||
function core.translate(textdomain, str, ...)
|
||||
local start_seq
|
||||
if textdomain == "" then
|
||||
start_seq = ESCAPE_CHAR .. "T"
|
||||
else
|
||||
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
|
||||
end
|
||||
local arg = {n=select('#', ...), ...}
|
||||
local end_seq = ESCAPE_CHAR .. "E"
|
||||
local arg_index = 1
|
||||
local translated = str:gsub("@(.)", function(matched)
|
||||
local c = string.byte(matched)
|
||||
if string.byte("1") <= c and c <= string.byte("9") then
|
||||
local a = c - string.byte("0")
|
||||
if a ~= arg_index then
|
||||
error("Escape sequences in string given to core.translate " ..
|
||||
"are not in the correct order: got @" .. matched ..
|
||||
"but expected @" .. tostring(arg_index))
|
||||
end
|
||||
if a > arg.n then
|
||||
error("Not enough arguments provided to core.translate")
|
||||
end
|
||||
arg_index = arg_index + 1
|
||||
return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E"
|
||||
elseif matched == "n" then
|
||||
return "\n"
|
||||
else
|
||||
return matched
|
||||
end
|
||||
end)
|
||||
if arg_index < arg.n + 1 then
|
||||
error("Too many arguments provided to core.translate")
|
||||
end
|
||||
return start_seq .. translated .. end_seq
|
||||
end
|
||||
|
||||
function core.get_translator(textdomain)
|
||||
return function(str, ...) return core.translate(textdomain or "", str, ...) end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Returns the exact coordinate of a pointed surface
|
||||
--------------------------------------------------------------------------------
|
||||
function core.pointed_thing_to_face_pos(placer, pointed_thing)
|
||||
-- Avoid crash in some situations when player is inside a node, causing
|
||||
-- 'above' to equal 'under'.
|
||||
if vector.equals(pointed_thing.above, pointed_thing.under) then
|
||||
return pointed_thing.under
|
||||
end
|
||||
|
||||
local eye_height = placer:get_properties().eye_height
|
||||
local eye_offset_first = placer:get_eye_offset()
|
||||
local node_pos = pointed_thing.under
|
||||
local camera_pos = placer:get_pos()
|
||||
local pos_off = vector.multiply(
|
||||
vector.subtract(pointed_thing.above, node_pos), 0.5)
|
||||
local look_dir = placer:get_look_dir()
|
||||
local offset, nc
|
||||
local oc = {}
|
||||
|
||||
for c, v in pairs(pos_off) do
|
||||
if nc or v == 0 then
|
||||
oc[#oc + 1] = c
|
||||
else
|
||||
offset = v
|
||||
nc = c
|
||||
end
|
||||
end
|
||||
|
||||
local fine_pos = {[nc] = node_pos[nc] + offset}
|
||||
camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10
|
||||
local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc]
|
||||
|
||||
for i = 1, #oc do
|
||||
fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f
|
||||
end
|
||||
return fine_pos
|
||||
end
|
||||
|
||||
function core.string_to_privs(str, delim)
|
||||
assert(type(str) == "string")
|
||||
delim = delim or ','
|
||||
local privs = {}
|
||||
for _, priv in pairs(string.split(str, delim)) do
|
||||
privs[priv:trim()] = true
|
||||
end
|
||||
return privs
|
||||
end
|
||||
|
||||
function core.privs_to_string(privs, delim)
|
||||
assert(type(privs) == "table")
|
||||
delim = delim or ','
|
||||
local list = {}
|
||||
for priv, bool in pairs(privs) do
|
||||
if bool then
|
||||
list[#list + 1] = priv
|
||||
end
|
||||
end
|
||||
return table.concat(list, delim)
|
||||
end
|
||||
|
||||
function core.is_nan(number)
|
||||
return number ~= number
|
||||
end
|
||||
|
||||
--[[ Helper function for parsing an optionally relative number
|
||||
of a chat command parameter, using the chat command tilde notation.
|
||||
|
||||
Parameters:
|
||||
* arg: String snippet containing the number; possible values:
|
||||
* "<number>": return as number
|
||||
* "~<number>": return relative_to + <number>
|
||||
* "~": return relative_to
|
||||
* Anything else will return `nil`
|
||||
* relative_to: Number to which the `arg` number might be relative to
|
||||
|
||||
Returns:
|
||||
A number or `nil`, depending on `arg.
|
||||
|
||||
Examples:
|
||||
* `core.parse_relative_number("5", 10)` returns 5
|
||||
* `core.parse_relative_number("~5", 10)` returns 15
|
||||
* `core.parse_relative_number("~", 10)` returns 10
|
||||
]]
|
||||
function core.parse_relative_number(arg, relative_to)
|
||||
if not arg then
|
||||
return nil
|
||||
elseif arg == "~" then
|
||||
return relative_to
|
||||
elseif string.sub(arg, 1, 1) == "~" then
|
||||
local number = tonumber(string.sub(arg, 2))
|
||||
if not number then
|
||||
return nil
|
||||
end
|
||||
if core.is_nan(number) or number == math.huge or number == -math.huge then
|
||||
return nil
|
||||
end
|
||||
return relative_to + number
|
||||
else
|
||||
local number = tonumber(arg)
|
||||
if core.is_nan(number) or number == math.huge or number == -math.huge then
|
||||
return nil
|
||||
end
|
||||
return number
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Helper function to parse coordinates that might be relative
|
||||
to another position; supports chat command tilde notation.
|
||||
Intended to be used in chat command parameter parsing.
|
||||
|
||||
Parameters:
|
||||
* x, y, z: Parsed x, y, and z coordinates as strings
|
||||
* relative_to: Position to which to compare the position
|
||||
|
||||
Syntax of x, y and z:
|
||||
* "<number>": return as number
|
||||
* "~<number>": return <number> + player position on this axis
|
||||
* "~": return player position on this axis
|
||||
|
||||
Returns: a vector or nil for invalid input or if player does not exist
|
||||
]]
|
||||
function core.parse_coordinates(x, y, z, relative_to)
|
||||
if not relative_to then
|
||||
x, y, z = tonumber(x), tonumber(y), tonumber(z)
|
||||
return x and y and z and { x = x, y = y, z = z }
|
||||
end
|
||||
local rx = core.parse_relative_number(x, relative_to.x)
|
||||
local ry = core.parse_relative_number(y, relative_to.y)
|
||||
local rz = core.parse_relative_number(z, relative_to.z)
|
||||
return rx and ry and rz and { x = rx, y = ry, z = rz }
|
||||
end
|
||||
19
builtin/common/mod_storage.lua
Normal file
19
builtin/common/mod_storage.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Modify core.get_mod_storage to return the storage for the current mod.
|
||||
|
||||
local get_current_modname = core.get_current_modname
|
||||
|
||||
local old_get_mod_storage = core.get_mod_storage
|
||||
|
||||
local storages = setmetatable({}, {
|
||||
__mode = "v", -- values are weak references (can be garbage-collected)
|
||||
__index = function(self, modname)
|
||||
local storage = old_get_mod_storage(modname)
|
||||
self[modname] = storage
|
||||
return storage
|
||||
end,
|
||||
})
|
||||
|
||||
function core.get_mod_storage()
|
||||
local modname = get_current_modname()
|
||||
return modname and storages[modname]
|
||||
end
|
||||
238
builtin/common/serialize.lua
Normal file
238
builtin/common/serialize.lua
Normal file
@@ -0,0 +1,238 @@
|
||||
--- Lua module to serialize values as Lua code.
|
||||
-- From: https://github.com/appgurueu/modlib/blob/master/luon.lua
|
||||
-- License: MIT
|
||||
|
||||
local next, rawget, pairs, pcall, error, type, setfenv, loadstring
|
||||
= next, rawget, pairs, pcall, error, type, setfenv, loadstring
|
||||
|
||||
local table_concat, string_dump, string_format, string_match, math_huge
|
||||
= table.concat, string.dump, string.format, string.match, math.huge
|
||||
|
||||
-- Recursively counts occurences of objects (non-primitives including strings) in a table.
|
||||
local function count_objects(value)
|
||||
local counts = {}
|
||||
if value == nil then
|
||||
-- Early return for nil; tables can't contain nil
|
||||
return counts
|
||||
end
|
||||
local function count_values(val)
|
||||
local type_ = type(val)
|
||||
if type_ == "boolean" or type_ == "number" then
|
||||
return
|
||||
end
|
||||
local count = counts[val]
|
||||
counts[val] = (count or 0) + 1
|
||||
if type_ == "table" then
|
||||
if not count then
|
||||
for k, v in pairs(val) do
|
||||
count_values(k)
|
||||
count_values(v)
|
||||
end
|
||||
end
|
||||
elseif type_ ~= "string" and type_ ~= "function" then
|
||||
error("unsupported type: " .. type_)
|
||||
end
|
||||
end
|
||||
count_values(value)
|
||||
return counts
|
||||
end
|
||||
|
||||
-- Build a "set" of Lua keywords. These can't be used as short key names.
|
||||
-- See https://www.lua.org/manual/5.1/manual.html#2.1
|
||||
local keywords = {}
|
||||
for _, keyword in pairs({
|
||||
"and", "break", "do", "else", "elseif",
|
||||
"end", "false", "for", "function", "if",
|
||||
"in", "local", "nil", "not", "or",
|
||||
"repeat", "return", "then", "true", "until", "while",
|
||||
"goto" -- LuaJIT, Lua 5.2+
|
||||
}) do
|
||||
keywords[keyword] = true
|
||||
end
|
||||
|
||||
local function quote(string)
|
||||
return string_format("%q", string)
|
||||
end
|
||||
|
||||
local function dump_func(func)
|
||||
return string_format("loadstring(%q)", string_dump(func))
|
||||
end
|
||||
|
||||
-- Serializes Lua nil, booleans, numbers, strings, tables and even functions
|
||||
-- Tables are referenced by reference, strings are referenced by value. Supports circular tables.
|
||||
local function serialize(value, write)
|
||||
local reference, refnum = "1", 1
|
||||
-- [object] = reference
|
||||
local references = {}
|
||||
-- Circular tables that must be filled using `table[key] = value` statements
|
||||
local to_fill = {}
|
||||
for object, count in pairs(count_objects(value)) do
|
||||
local type_ = type(object)
|
||||
-- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
|
||||
if count >= 2 and (type_ ~= "string" or #reference + 5 < #object) then
|
||||
if refnum == 1 then
|
||||
write"local _={};" -- initialize reference table
|
||||
end
|
||||
write"_["
|
||||
write(reference)
|
||||
write("]=")
|
||||
if type_ == "table" then
|
||||
write("{}")
|
||||
elseif type_ == "function" then
|
||||
write(dump_func(object))
|
||||
elseif type_ == "string" then
|
||||
write(quote(object))
|
||||
end
|
||||
write(";")
|
||||
references[object] = reference
|
||||
if type_ == "table" then
|
||||
to_fill[object] = reference
|
||||
end
|
||||
refnum = refnum + 1
|
||||
reference = ("%d"):format(refnum)
|
||||
end
|
||||
end
|
||||
-- Used to decide whether we should do "key=..."
|
||||
local function use_short_key(key)
|
||||
return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$")
|
||||
end
|
||||
local function dump(value)
|
||||
-- Primitive types
|
||||
if value == nil then
|
||||
return write("nil")
|
||||
end
|
||||
if value == true then
|
||||
return write("true")
|
||||
end
|
||||
if value == false then
|
||||
return write("false")
|
||||
end
|
||||
local type_ = type(value)
|
||||
if type_ == "number" then
|
||||
if value ~= value then -- nan
|
||||
return write"0/0"
|
||||
elseif value == math_huge then
|
||||
return write"1/0"
|
||||
elseif value == -math_huge then
|
||||
return write"-1/0"
|
||||
else
|
||||
return write(string_format("%.17g", value))
|
||||
end
|
||||
end
|
||||
-- Reference types: table, function and string
|
||||
local ref = references[value]
|
||||
if ref then
|
||||
write"_["
|
||||
write(ref)
|
||||
return write"]"
|
||||
end
|
||||
if type_ == "string" then
|
||||
return write(quote(value))
|
||||
end
|
||||
if type_ == "function" then
|
||||
return write(dump_func(value))
|
||||
end
|
||||
if type_ == "table" then
|
||||
write("{")
|
||||
-- First write list keys:
|
||||
-- Don't use the table length #value here as it may horribly fail
|
||||
-- for tables which use large integers as keys in the hash part;
|
||||
-- stop at the first "hole" (nil value) instead
|
||||
local len = 0
|
||||
local first = true -- whether this is the first entry, which may not have a leading comma
|
||||
while true do
|
||||
local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
|
||||
if v == nil then break end
|
||||
if first then first = false else write(",") end
|
||||
dump(v)
|
||||
len = len + 1
|
||||
end
|
||||
-- Now write map keys ([key] = value)
|
||||
for k, v in next, value do
|
||||
-- We have written all non-float keys in [1, len] already
|
||||
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
|
||||
if first then first = false else write(",") end
|
||||
if use_short_key(k) then
|
||||
write(k)
|
||||
else
|
||||
write("[")
|
||||
dump(k)
|
||||
write("]")
|
||||
end
|
||||
write("=")
|
||||
dump(v)
|
||||
end
|
||||
end
|
||||
write("}")
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Write the statements to fill circular tables
|
||||
for table, ref in pairs(to_fill) do
|
||||
for k, v in pairs(table) do
|
||||
write("_[")
|
||||
write(ref)
|
||||
write("]")
|
||||
if use_short_key(k) then
|
||||
write(".")
|
||||
write(k)
|
||||
else
|
||||
write("[")
|
||||
dump(k)
|
||||
write("]")
|
||||
end
|
||||
write("=")
|
||||
dump(v)
|
||||
write(";")
|
||||
end
|
||||
end
|
||||
write("return ")
|
||||
dump(value)
|
||||
end
|
||||
|
||||
function core.serialize(value)
|
||||
local rope = {}
|
||||
serialize(value, function(text)
|
||||
-- Faster than table.insert(rope, text) on PUC Lua 5.1
|
||||
rope[#rope + 1] = text
|
||||
end)
|
||||
return table_concat(rope)
|
||||
end
|
||||
|
||||
local function dummy_func() end
|
||||
|
||||
function core.deserialize(str, safe)
|
||||
-- Backwards compatibility
|
||||
if str == nil then
|
||||
core.log("deprecated", "minetest.deserialize called with nil (expected string).")
|
||||
return nil, "Invalid type: Expected a string, got nil"
|
||||
end
|
||||
local t = type(str)
|
||||
if t ~= "string" then
|
||||
error(("minetest.deserialize called with %s (expected string)."):format(t))
|
||||
end
|
||||
|
||||
local func, err = loadstring(str)
|
||||
if not func then return nil, err end
|
||||
|
||||
-- math.huge was serialized to inf and NaNs to nan by Lua in Minetest 5.6, so we have to support this here
|
||||
local env = {inf = math_huge, nan = 0/0}
|
||||
if safe then
|
||||
env.loadstring = dummy_func
|
||||
else
|
||||
env.loadstring = function(str, ...)
|
||||
local func, err = loadstring(str, ...)
|
||||
if func then
|
||||
setfenv(func, env)
|
||||
return func
|
||||
end
|
||||
return nil, err
|
||||
end
|
||||
end
|
||||
setfenv(func, env)
|
||||
local success, value_or_err = pcall(func)
|
||||
if success then
|
||||
return value_or_err
|
||||
end
|
||||
return nil, value_or_err
|
||||
end
|
||||
46
builtin/common/strict.lua
Normal file
46
builtin/common/strict.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
local getinfo, rawget, rawset = debug.getinfo, rawget, rawset
|
||||
|
||||
function core.global_exists(name)
|
||||
if type(name) ~= "string" then
|
||||
error("core.global_exists: " .. tostring(name) .. " is not a string")
|
||||
end
|
||||
return rawget(_G, name) ~= nil
|
||||
end
|
||||
|
||||
|
||||
local meta = {}
|
||||
local declared = {}
|
||||
-- Key is source file, line, and variable name; seperated by NULs
|
||||
local warned = {}
|
||||
|
||||
function meta:__newindex(name, value)
|
||||
if declared[name] then
|
||||
return
|
||||
end
|
||||
local info = getinfo(2, "Sl")
|
||||
local desc = ("%s:%d"):format(info.short_src, info.currentline)
|
||||
local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
|
||||
if not warned[warn_key] and info.what ~= "main" and info.what ~= "C" then
|
||||
core.log("warning", ("Assignment to undeclared global %q inside a function at %s.")
|
||||
:format(name, desc))
|
||||
warned[warn_key] = true
|
||||
end
|
||||
rawset(self, name, value)
|
||||
declared[name] = true
|
||||
end
|
||||
|
||||
|
||||
function meta:__index(name)
|
||||
if declared[name] then
|
||||
return
|
||||
end
|
||||
local info = getinfo(2, "Sl")
|
||||
local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name)
|
||||
if not warned[warn_key] and info.what ~= "C" then
|
||||
core.log("warning", ("Undeclared global variable %q accessed at %s:%s")
|
||||
:format(name, info.short_src, info.currentline))
|
||||
warned[warn_key] = true
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(_G, meta)
|
||||
173
builtin/common/tests/misc_helpers_spec.lua
Normal file
173
builtin/common/tests/misc_helpers_spec.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
_G.core = {}
|
||||
_G.vector = {metatable = {}}
|
||||
dofile("builtin/common/vector.lua")
|
||||
dofile("builtin/common/misc_helpers.lua")
|
||||
|
||||
describe("string", function()
|
||||
it("trim()", function()
|
||||
assert.equal("foo bar", string.trim("\n \t\tfoo bar\t "))
|
||||
end)
|
||||
|
||||
describe("split()", function()
|
||||
it("removes empty", function()
|
||||
assert.same({ "hello" }, string.split("hello"))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world"))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world,,,"))
|
||||
assert.same({ "hello", "world" }, string.split(",,,hello,world"))
|
||||
assert.same({ "hello", "world", "2" }, string.split("hello,,,world,2"))
|
||||
assert.same({ "hello ", " world" }, string.split("hello :| world", ":|"))
|
||||
end)
|
||||
|
||||
it("keeps empty", function()
|
||||
assert.same({ "hello" }, string.split("hello", ",", true))
|
||||
assert.same({ "hello", "world" }, string.split("hello,world", ",", true))
|
||||
assert.same({ "hello", "world", "" }, string.split("hello,world,", ",", true))
|
||||
assert.same({ "hello", "", "", "world", "2" }, string.split("hello,,,world,2", ",", true))
|
||||
assert.same({ "", "", "hello", "world", "2" }, string.split(",,hello,world,2", ",", true))
|
||||
assert.same({ "hello ", " world | :" }, string.split("hello :| world | :", ":|"))
|
||||
end)
|
||||
|
||||
it("max_splits", function()
|
||||
assert.same({ "one" }, string.split("one", ",", true, 2))
|
||||
assert.same({ "one,two,three,four" }, string.split("one,two,three,four", ",", true, 0))
|
||||
assert.same({ "one", "two", "three,four" }, string.split("one,two,three,four", ",", true, 2))
|
||||
assert.same({ "one", "", "two,three,four" }, string.split("one,,two,three,four", ",", true, 2))
|
||||
assert.same({ "one", "two", "three,four" }, string.split("one,,,,,,two,three,four", ",", false, 2))
|
||||
end)
|
||||
|
||||
it("pattern", function()
|
||||
assert.same({ "one", "two" }, string.split("one,two", ",", false, -1, true))
|
||||
assert.same({ "one", "two", "three" }, string.split("one2two3three", "%d", false, -1, true))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("privs", function()
|
||||
it("from string", function()
|
||||
assert.same({ a = true, b = true }, core.string_to_privs("a,b"))
|
||||
end)
|
||||
|
||||
it("to string", function()
|
||||
assert.equal("one", core.privs_to_string({ one=true }))
|
||||
|
||||
local ret = core.privs_to_string({ a=true, b=true })
|
||||
assert(ret == "a,b" or ret == "b,a")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("pos", function()
|
||||
it("from string", function()
|
||||
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)
|
||||
|
||||
it("to string", function()
|
||||
assert.equal("(10.1,5.2,-2.3)", core.pos_to_string({ x = 10.1, y = 5.2, z = -2.3}))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("area parsing", function()
|
||||
describe("valid inputs", function()
|
||||
it("accepts absolute numbers", function()
|
||||
local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2 4 -12.53)")
|
||||
assert(p1.x == 10 and p1.y == 5 and p1.z == -2)
|
||||
assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53)
|
||||
end)
|
||||
|
||||
it("accepts relative numbers", function()
|
||||
local p1, p2 = core.string_to_area("(1,2,3) (~5,~-5,~)", {x=10,y=10,z=10})
|
||||
assert(type(p1) == "table" and type(p2) == "table")
|
||||
assert(p1.x == 1 and p1.y == 2 and p1.z == 3)
|
||||
assert(p2.x == 15 and p2.y == 5 and p2.z == 10)
|
||||
|
||||
p1, p2 = core.string_to_area("(1 2 3) (~5 ~-5 ~)", {x=10,y=10,z=10})
|
||||
assert(type(p1) == "table" and type(p2) == "table")
|
||||
assert(p1.x == 1 and p1.y == 2 and p1.z == 3)
|
||||
assert(p2.x == 15 and p2.y == 5 and p2.z == 10)
|
||||
end)
|
||||
end)
|
||||
describe("invalid inputs", function()
|
||||
it("rejects too few numbers", function()
|
||||
local p1, p2 = core.string_to_area("(1,1) (1,1,1,1)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end)
|
||||
|
||||
it("rejects too many numbers", function()
|
||||
local p1, p2 = core.string_to_area("(1,1,1,1) (1,1,1,1)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end)
|
||||
|
||||
it("rejects nan & inf", function()
|
||||
local p1, p2 = core.string_to_area("(1,1,1) (1,1,nan)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(1,1,1) (1,1,~nan)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(1,1,1) (1,~nan,1)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(1,1,1) (1,1,inf)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(1,1,1) (1,1,~inf)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(1,1,1) (1,~inf,1)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end)
|
||||
|
||||
it("rejects words", function()
|
||||
local p1, p2 = core.string_to_area("bananas", {x=1,y=1,z=1})
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("bananas", "foobar")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("bananas")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(bananas,bananas,bananas)")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(bananas,bananas,bananas) (bananas,bananas,bananas)")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end)
|
||||
|
||||
it("requires parenthesis & valid numbers", function()
|
||||
local p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
|
||||
p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53")
|
||||
assert(p1 == nil and p2 == nil)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("table", function()
|
||||
it("indexof()", function()
|
||||
assert.equal(1, table.indexof({"foo", "bar"}, "foo"))
|
||||
assert.equal(-1, table.indexof({"foo", "bar"}, "baz"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("formspec_escape", function()
|
||||
it("escapes", function()
|
||||
assert.equal(nil, core.formspec_escape(nil))
|
||||
assert.equal("", core.formspec_escape(""))
|
||||
assert.equal("\\[Hello\\\\\\[", core.formspec_escape("[Hello\\["))
|
||||
end)
|
||||
end)
|
||||
189
builtin/common/tests/serialize_spec.lua
Normal file
189
builtin/common/tests/serialize_spec.lua
Normal file
@@ -0,0 +1,189 @@
|
||||
_G.core = {}
|
||||
_G.vector = {metatable = {}}
|
||||
|
||||
_G.setfenv = require 'busted.compatibility'.setfenv
|
||||
|
||||
dofile("builtin/common/serialize.lua")
|
||||
dofile("builtin/common/vector.lua")
|
||||
|
||||
-- Supports circular tables; does not support table keys
|
||||
-- Correctly checks whether a mapping of references ("same") exists
|
||||
-- Is significantly more efficient than assert.same
|
||||
local function assert_same(a, b, same)
|
||||
same = same or {}
|
||||
if same[a] or same[b] then
|
||||
assert(same[a] == b and same[b] == a)
|
||||
return
|
||||
end
|
||||
if a == b then
|
||||
return
|
||||
end
|
||||
if type(a) ~= "table" or type(b) ~= "table" then
|
||||
assert(a == b)
|
||||
return
|
||||
end
|
||||
same[a] = b
|
||||
same[b] = a
|
||||
local count = 0
|
||||
for k, v in pairs(a) do
|
||||
count = count + 1
|
||||
assert(type(k) ~= "table")
|
||||
assert_same(v, b[k], same)
|
||||
end
|
||||
for _ in pairs(b) do
|
||||
count = count - 1
|
||||
end
|
||||
assert(count == 0)
|
||||
end
|
||||
|
||||
local x, y = {}, {}
|
||||
local t1, t2 = {x, x, y, y}, {x, y, x, y}
|
||||
assert.same(t1, t2) -- will succeed because it only checks whether the depths match
|
||||
assert(not pcall(assert_same, t1, t2)) -- will correctly fail because it checks whether the refs match
|
||||
|
||||
describe("serialize", function()
|
||||
local function assert_preserves(value)
|
||||
local preserved_value = core.deserialize(core.serialize(value))
|
||||
assert_same(value, preserved_value)
|
||||
end
|
||||
it("works", function()
|
||||
assert_preserves({cat={sound="nyan", speed=400}, dog={sound="woof"}})
|
||||
end)
|
||||
|
||||
it("handles characters", function()
|
||||
assert_preserves({escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"})
|
||||
end)
|
||||
|
||||
it("handles NaN & infinities", function()
|
||||
local nan = core.deserialize(core.serialize(0/0))
|
||||
assert(nan ~= nan)
|
||||
assert_preserves(math.huge)
|
||||
assert_preserves(-math.huge)
|
||||
end)
|
||||
|
||||
it("handles precise numbers", function()
|
||||
assert_preserves(0.2695949158945771)
|
||||
end)
|
||||
|
||||
it("handles big integers", function()
|
||||
assert_preserves(269594915894577)
|
||||
end)
|
||||
|
||||
it("handles recursive structures", function()
|
||||
local test_in = { hello = "world" }
|
||||
test_in.foo = test_in
|
||||
assert_preserves(test_in)
|
||||
end)
|
||||
|
||||
it("handles cross-referencing structures", function()
|
||||
local test_in = {
|
||||
foo = {
|
||||
baz = {
|
||||
{}
|
||||
},
|
||||
},
|
||||
bar = {
|
||||
baz = {},
|
||||
},
|
||||
}
|
||||
|
||||
test_in.foo.baz[1].foo = test_in.foo
|
||||
test_in.foo.baz[1].bar = test_in.bar
|
||||
test_in.bar.baz[1] = test_in.foo.baz[1]
|
||||
|
||||
assert_preserves(test_in)
|
||||
end)
|
||||
|
||||
it("strips functions in safe mode", function()
|
||||
local test_in = {
|
||||
func = function(a, b)
|
||||
error("test")
|
||||
end,
|
||||
foo = "bar"
|
||||
}
|
||||
setfenv(test_in.func, _G)
|
||||
|
||||
local str = core.serialize(test_in)
|
||||
assert.not_nil(str:find("loadstring"))
|
||||
|
||||
local test_out = core.deserialize(str, true)
|
||||
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_preserves({v})
|
||||
assert_preserves(v)
|
||||
|
||||
-- abuse
|
||||
v = vector.new(1, 2, 3)
|
||||
v.a = "bla"
|
||||
assert_preserves(v)
|
||||
end)
|
||||
|
||||
it("handles keywords as keys", function()
|
||||
assert_preserves({["and"] = "keyword", ["for"] = "keyword"})
|
||||
end)
|
||||
|
||||
describe("fuzzing", function()
|
||||
local atomics = {true, false, math.huge, -math.huge} -- no NaN or nil
|
||||
local function atomic()
|
||||
return atomics[math.random(1, #atomics)]
|
||||
end
|
||||
local function num()
|
||||
local sign = math.random() < 0.5 and -1 or 1
|
||||
-- HACK math.random(a, b) requires a, b & b - a to fit within a 32-bit int
|
||||
-- Use two random calls to generate a random number from 0 - 2^50 as lower & upper 25 bits
|
||||
local val = math.random(0, 2^25) * 2^25 + math.random(0, 2^25 - 1)
|
||||
local exp = math.random() < 0.5 and 1 or 2^(math.random(-120, 120))
|
||||
return sign * val * exp
|
||||
end
|
||||
local function charcodes(count)
|
||||
if count == 0 then return end
|
||||
return math.random(0, 0xFF), charcodes(count - 1)
|
||||
end
|
||||
local function str()
|
||||
return string.char(charcodes(math.random(0, 100)))
|
||||
end
|
||||
local primitives = {atomic, num, str}
|
||||
local function primitive()
|
||||
return primitives[math.random(1, #primitives)]()
|
||||
end
|
||||
local function tab(max_actions)
|
||||
local root = {}
|
||||
local tables = {root}
|
||||
local function random_table()
|
||||
return tables[math.random(1, #tables)]
|
||||
end
|
||||
for _ = 1, math.random(1, max_actions) do
|
||||
local tab = random_table()
|
||||
local value
|
||||
if math.random() < 0.5 then
|
||||
if math.random() < 0.5 then
|
||||
value = random_table()
|
||||
else
|
||||
value = {}
|
||||
table.insert(tables, value)
|
||||
end
|
||||
else
|
||||
value = primitive()
|
||||
end
|
||||
tab[math.random() < 0.5 and (#tab + 1) or primitive()] = value
|
||||
end
|
||||
return root
|
||||
end
|
||||
it("primitives work", function()
|
||||
for _ = 1, 1e3 do
|
||||
assert_preserves(primitive())
|
||||
end
|
||||
end)
|
||||
it("tables work", function()
|
||||
for _ = 1, 100 do
|
||||
local fuzzed_table = tab(1e3)
|
||||
assert_same(fuzzed_table, table.copy(fuzzed_table))
|
||||
assert_preserves(fuzzed_table)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
465
builtin/common/tests/vector_spec.lua
Normal file
465
builtin/common/tests/vector_spec.lua
Normal file
@@ -0,0 +1,465 @@
|
||||
_G.vector = {metatable = {}}
|
||||
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.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.equal(input, output)
|
||||
assert.is_false(rawequal(input, output))
|
||||
assert.equal(input, input:new())
|
||||
end)
|
||||
|
||||
it("throws on invalid input", function()
|
||||
assert.has.errors(function()
|
||||
vector.new({ x = 3 })
|
||||
end)
|
||||
|
||||
assert.has.errors(function()
|
||||
vector.new({ d = 3 })
|
||||
end)
|
||||
end)
|
||||
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)
|
||||
|
||||
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("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("combine()", function()
|
||||
local a = vector.new(1, 2, 3)
|
||||
local b = vector.new(3, 2, 1)
|
||||
assert.equal(vector.add(a, b), vector.combine(a, b, function(x, y) return x + y end))
|
||||
assert.equal(vector.new(3, 2, 3), vector.combine(a, b, math.max))
|
||||
assert.equal(vector.new(1, 2, 1), vector.combine(a, b, math.min))
|
||||
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.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.
|
||||
local function almost_equal(a, b)
|
||||
if type(a) == "number" then
|
||||
return math.abs(a - b) < 0.00000000001
|
||||
end
|
||||
return vector.distance(a, b) < 0.000000000001
|
||||
end
|
||||
|
||||
describe("rotate_around_axis()", function()
|
||||
it("rotates", function()
|
||||
assert.True(almost_equal({x = -1, y = 0, z = 0},
|
||||
vector.rotate_around_axis({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0}, math.pi)))
|
||||
assert.True(almost_equal({x = 0, y = 1, z = 0},
|
||||
vector.rotate_around_axis({x = 0, y = 0, z = 1}, {x = 1, y = 0, z = 0}, math.pi / 2)))
|
||||
assert.True(almost_equal({x = 4, y = 1, z = 1},
|
||||
vector.rotate_around_axis({x = 4, y = 1, z = 1}, {x = 4, y = 1, z = 1}, math.pi / 6)))
|
||||
end)
|
||||
it("keeps distance to axis", function()
|
||||
local rotate1 = {x = 1, y = 3, z = 1}
|
||||
local axis1 = {x = 1, y = 3, z = 2}
|
||||
local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13)
|
||||
assert.True(almost_equal(vector.distance(axis1, rotate1), vector.distance(axis1, rotated1)))
|
||||
local rotate2 = {x = 1, y = 1, z = 3}
|
||||
local axis2 = {x = 2, y = 6, z = 100}
|
||||
local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23)
|
||||
assert.True(almost_equal(vector.distance(axis2, rotate2), vector.distance(axis2, rotated2)))
|
||||
local rotate3 = {x = 1, y = -1, z = 3}
|
||||
local axis3 = {x = 2, y = 6, z = 100}
|
||||
local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2)
|
||||
assert.True(almost_equal(vector.distance(axis3, rotate3), vector.distance(axis3, rotated3)))
|
||||
end)
|
||||
it("rotates back", function()
|
||||
local rotate1 = {x = 1, y = 3, z = 1}
|
||||
local axis1 = {x = 1, y = 3, z = 2}
|
||||
local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13)
|
||||
rotated1 = vector.rotate_around_axis(rotated1, axis1, -math.pi / 13)
|
||||
assert.True(almost_equal(rotate1, rotated1))
|
||||
local rotate2 = {x = 1, y = 1, z = 3}
|
||||
local axis2 = {x = 2, y = 6, z = 100}
|
||||
local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23)
|
||||
rotated2 = vector.rotate_around_axis(rotated2, axis2, -math.pi / 23)
|
||||
assert.True(almost_equal(rotate2, rotated2))
|
||||
local rotate3 = {x = 1, y = -1, z = 3}
|
||||
local axis3 = {x = 2, y = 6, z = 100}
|
||||
local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2)
|
||||
rotated3 = vector.rotate_around_axis(rotated3, axis3, -math.pi / 2)
|
||||
assert.True(almost_equal(rotate3, rotated3))
|
||||
end)
|
||||
it("is right handed", function()
|
||||
local v_before1 = {x = 0, y = 1, z = -1}
|
||||
local v_after1 = vector.rotate_around_axis(v_before1, {x = 1, y = 0, z = 0}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before2 = {x = 0, y = 3, z = 4}
|
||||
local v_after2 = vector.rotate_around_axis(v_before2, {x = 1, y = 0, z = 0}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before3 = {x = 1, y = 0, z = -1}
|
||||
local v_after3 = vector.rotate_around_axis(v_before3, {x = 0, y = 1, z = 0}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before4 = {x = 3, y = 0, z = 4}
|
||||
local v_after4 = vector.rotate_around_axis(v_before4, {x = 0, y = 1, z = 0}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before5 = {x = 1, y = -1, z = 0}
|
||||
local v_after5 = vector.rotate_around_axis(v_before5, {x = 0, y = 0, z = 1}, math.pi / 4)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1}))
|
||||
|
||||
local v_before6 = {x = 3, y = 4, z = 0}
|
||||
local v_after6 = vector.rotate_around_axis(v_before6, {x = 0, y = 0, z = 1}, 2 * math.pi / 5)
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1}))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("rotate()", function()
|
||||
it("rotates", function()
|
||||
assert.True(almost_equal({x = -1, y = 0, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = math.pi, z = 0})))
|
||||
assert.True(almost_equal({x = 0, y = -1, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = 0, z = math.pi / 2})))
|
||||
assert.True(almost_equal({x = 1, y = 0, z = 0},
|
||||
vector.rotate({x = 1, y = 0, z = 0}, {x = math.pi / 123, y = 0, z = 0})))
|
||||
end)
|
||||
it("is counterclockwise", function()
|
||||
local v_before1 = {x = 0, y = 1, z = -1}
|
||||
local v_after1 = vector.rotate(v_before1, {x = math.pi / 4, y = 0, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before2 = {x = 0, y = 3, z = 4}
|
||||
local v_after2 = vector.rotate(v_before2, {x = 2 * math.pi / 5, y = 0, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0}))
|
||||
|
||||
local v_before3 = {x = 1, y = 0, z = -1}
|
||||
local v_after3 = vector.rotate(v_before3, {x = 0, y = math.pi / 4, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before4 = {x = 3, y = 0, z = 4}
|
||||
local v_after4 = vector.rotate(v_before4, {x = 0, y = 2 * math.pi / 5, z = 0})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0}))
|
||||
|
||||
local v_before5 = {x = 1, y = -1, z = 0}
|
||||
local v_after5 = vector.rotate(v_before5, {x = 0, y = 0, z = math.pi / 4})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1}))
|
||||
|
||||
local v_before6 = {x = 3, y = 4, z = 0}
|
||||
local v_after6 = vector.rotate(v_before6, {x = 0, y = 0, z = 2 * math.pi / 5})
|
||||
assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1}))
|
||||
end)
|
||||
end)
|
||||
|
||||
it("dir_to_rotation()", function()
|
||||
-- Comparing rotations (pitch, yaw, roll) is hard because of certain ambiguities,
|
||||
-- e.g. (pi, 0, pi) looks exactly the same as (0, pi, 0)
|
||||
-- So instead we convert the rotation back to vectors and compare these.
|
||||
local function forward_at_rot(rot)
|
||||
return vector.rotate(vector.new(0, 0, 1), rot)
|
||||
end
|
||||
local function up_at_rot(rot)
|
||||
return vector.rotate(vector.new(0, 1, 0), rot)
|
||||
end
|
||||
local rot1 = vector.dir_to_rotation({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0})
|
||||
assert.True(almost_equal({x = 1, y = 0, z = 0}, forward_at_rot(rot1)))
|
||||
assert.True(almost_equal({x = 0, y = 1, z = 0}, up_at_rot(rot1)))
|
||||
local rot2 = vector.dir_to_rotation({x = 1, y = 1, z = 0}, {x = 0, y = 0, z = 1})
|
||||
assert.True(almost_equal({x = 1/math.sqrt(2), y = 1/math.sqrt(2), z = 0}, forward_at_rot(rot2)))
|
||||
assert.True(almost_equal({x = 0, y = 0, z = 1}, up_at_rot(rot2)))
|
||||
for i = 1, 1000 do
|
||||
local rand_vec = vector.new(math.random(), math.random(), math.random())
|
||||
if vector.length(rand_vec) ~= 0 then
|
||||
local rot_1 = vector.dir_to_rotation(rand_vec)
|
||||
local rot_2 = {
|
||||
x = math.atan2(rand_vec.y, math.sqrt(rand_vec.z * rand_vec.z + rand_vec.x * rand_vec.x)),
|
||||
y = -math.atan2(rand_vec.x, rand_vec.z),
|
||||
z = 0
|
||||
}
|
||||
assert.True(almost_equal(rot_1, rot_2))
|
||||
end
|
||||
end
|
||||
|
||||
end)
|
||||
end)
|
||||
368
builtin/common/vector.lua
Normal file
368
builtin/common/vector.lua
Normal file
@@ -0,0 +1,368 @@
|
||||
--[[
|
||||
Vector helpers
|
||||
Note: The vector.*-functions must be able to accept old vectors that had no metatables
|
||||
]]
|
||||
|
||||
-- localize functions
|
||||
local setmetatable = setmetatable
|
||||
|
||||
-- vector.metatable is set by C++.
|
||||
local metatable = vector.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
|
||||
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 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.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 fast_new(0, 0, 0)
|
||||
else
|
||||
return vector.divide(v, len)
|
||||
end
|
||||
end
|
||||
|
||||
function vector.floor(v)
|
||||
return vector.apply(v, math.floor)
|
||||
end
|
||||
|
||||
function vector.round(v)
|
||||
return fast_new(
|
||||
math.round(v.x),
|
||||
math.round(v.y),
|
||||
math.round(v.z)
|
||||
)
|
||||
end
|
||||
|
||||
function vector.apply(v, func)
|
||||
return fast_new(
|
||||
func(v.x),
|
||||
func(v.y),
|
||||
func(v.z)
|
||||
)
|
||||
end
|
||||
|
||||
function vector.combine(a, b, func)
|
||||
return fast_new(
|
||||
func(a.x, b.x),
|
||||
func(a.y, b.y),
|
||||
func(a.z, b.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.sqrt(x * x + y * y + z * z)
|
||||
end
|
||||
|
||||
function vector.direction(pos1, pos2)
|
||||
return vector.subtract(pos2, pos1):normalize()
|
||||
end
|
||||
|
||||
function vector.angle(a, b)
|
||||
local dotp = vector.dot(a, b)
|
||||
local cp = vector.cross(a, b)
|
||||
local crossplen = vector.length(cp)
|
||||
return math.atan2(crossplen, dotp)
|
||||
end
|
||||
|
||||
function vector.dot(a, b)
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
end
|
||||
|
||||
function vector.cross(a, b)
|
||||
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 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.__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 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.__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 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 fast_new(
|
||||
a * b.x,
|
||||
a * b.y,
|
||||
a * b.z
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function vector.divide(a, b)
|
||||
if type(b) == "table" then
|
||||
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.__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 fast_new(
|
||||
v.x + x,
|
||||
v.y + y,
|
||||
v.z + z
|
||||
)
|
||||
end
|
||||
|
||||
function vector.sort(a, b)
|
||||
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)
|
||||
if x % math.pi == 0 then
|
||||
return 0
|
||||
else
|
||||
return math.sin(x)
|
||||
end
|
||||
end
|
||||
|
||||
local function cos(x)
|
||||
if x % math.pi == math.pi / 2 then
|
||||
return 0
|
||||
else
|
||||
return math.cos(x)
|
||||
end
|
||||
end
|
||||
|
||||
function vector.rotate_around_axis(v, axis, angle)
|
||||
local cosangle = cos(angle)
|
||||
local sinangle = sin(angle)
|
||||
axis = vector.normalize(axis)
|
||||
-- https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
|
||||
local dot_axis = vector.multiply(axis, vector.dot(axis, v))
|
||||
local cross = vector.cross(v, axis)
|
||||
return vector.new(
|
||||
cross.x * sinangle + (v.x - dot_axis.x) * cosangle + dot_axis.x,
|
||||
cross.y * sinangle + (v.y - dot_axis.y) * cosangle + dot_axis.y,
|
||||
cross.z * sinangle + (v.z - dot_axis.z) * cosangle + dot_axis.z
|
||||
)
|
||||
end
|
||||
|
||||
function vector.rotate(v, rot)
|
||||
local sinpitch = sin(-rot.x)
|
||||
local sinyaw = sin(-rot.y)
|
||||
local sinroll = sin(-rot.z)
|
||||
local cospitch = cos(rot.x)
|
||||
local cosyaw = cos(rot.y)
|
||||
local cosroll = math.cos(rot.z)
|
||||
-- Rotation matrix that applies yaw, pitch and roll
|
||||
local matrix = {
|
||||
{
|
||||
sinyaw * sinpitch * sinroll + cosyaw * cosroll,
|
||||
sinyaw * sinpitch * cosroll - cosyaw * sinroll,
|
||||
sinyaw * cospitch,
|
||||
},
|
||||
{
|
||||
cospitch * sinroll,
|
||||
cospitch * cosroll,
|
||||
-sinpitch,
|
||||
},
|
||||
{
|
||||
cosyaw * sinpitch * sinroll - sinyaw * cosroll,
|
||||
cosyaw * sinpitch * cosroll + sinyaw * sinroll,
|
||||
cosyaw * cospitch,
|
||||
},
|
||||
}
|
||||
-- Compute matrix multiplication: `matrix` * `v`
|
||||
return vector.new(
|
||||
matrix[1][1] * v.x + matrix[1][2] * v.y + matrix[1][3] * v.z,
|
||||
matrix[2][1] * v.x + matrix[2][2] * v.y + matrix[2][3] * v.z,
|
||||
matrix[3][1] * v.x + matrix[3][2] * v.y + matrix[3][3] * v.z
|
||||
)
|
||||
end
|
||||
|
||||
function vector.dir_to_rotation(forward, up)
|
||||
forward = vector.normalize(forward)
|
||||
local rot = vector.new(math.asin(forward.y), -math.atan2(forward.x, forward.z), 0)
|
||||
if not up then
|
||||
return rot
|
||||
end
|
||||
assert(vector.dot(forward, up) < 0.000001,
|
||||
"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(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)
|
||||
|
||||
-- Since vector.angle never returns a negative value or a value greater
|
||||
-- than math.pi, rot.z has to be inverted sometimes.
|
||||
-- To determine wether this is the case, we rotate the up vector back around
|
||||
-- the forward vector and check if it worked out.
|
||||
local back = vector.rotate_around_axis(up, forward, -rot.z)
|
||||
|
||||
-- We don't use vector.equals for this because of floating point imprecision.
|
||||
if (back.x - forwup.x) * (back.x - forwup.x) +
|
||||
(back.y - forwup.y) * (back.y - forwup.y) +
|
||||
(back.z - forwup.z) * (back.z - forwup.z) > 0.0000001 then
|
||||
rot.z = -rot.z
|
||||
end
|
||||
return rot
|
||||
end
|
||||
215
builtin/fstk/buttonbar.lua
Normal file
215
builtin/fstk/buttonbar.lua
Normal file
@@ -0,0 +1,215 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
|
||||
local function buttonbar_formspec(self)
|
||||
|
||||
if self.hidden then
|
||||
return ""
|
||||
end
|
||||
|
||||
local formspec = string.format("box[%f,%f;%f,%f;%s]",
|
||||
self.pos.x,self.pos.y ,self.size.x,self.size.y,self.bgcolor)
|
||||
|
||||
for i=self.startbutton,#self.buttons,1 do
|
||||
local btn_name = self.buttons[i].name
|
||||
local btn_pos = {}
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
btn_pos.x = self.pos.x + --base pos
|
||||
(i - self.startbutton) * self.btn_size + --button offset
|
||||
self.btn_initial_offset
|
||||
else
|
||||
btn_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
end
|
||||
|
||||
if self.orientation == "vertical" then
|
||||
btn_pos.y = self.pos.y + --base pos
|
||||
(i - self.startbutton) * self.btn_size + --button offset
|
||||
self.btn_initial_offset
|
||||
else
|
||||
btn_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
end
|
||||
|
||||
if (self.orientation == "vertical" and
|
||||
(btn_pos.y + self.btn_size <= self.pos.y + self.size.y)) or
|
||||
(self.orientation == "horizontal" and
|
||||
(btn_pos.x + self.btn_size <= self.pos.x + self.size.x)) then
|
||||
|
||||
local borders="true"
|
||||
|
||||
if self.buttons[i].image ~= nil then
|
||||
borders="false"
|
||||
end
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;%s]tooltip[%s;%s]",
|
||||
btn_pos.x, btn_pos.y, self.btn_size, self.btn_size,
|
||||
self.buttons[i].image, btn_name, self.buttons[i].caption,
|
||||
borders, btn_name, self.buttons[i].tooltip)
|
||||
else
|
||||
--print("end of displayable buttons: orientation: " .. self.orientation)
|
||||
--print( "button_end: " .. (btn_pos.y + self.btn_size - (self.btn_size * 0.05)))
|
||||
--print( "bar_end: " .. (self.pos.x + self.size.x))
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if (self.have_move_buttons) then
|
||||
local btn_dec_pos = {}
|
||||
btn_dec_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
btn_dec_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
local btn_inc_pos = {}
|
||||
local btn_size = {}
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
btn_size.x = 0.5
|
||||
btn_size.y = self.btn_size
|
||||
btn_inc_pos.x = self.pos.x + self.size.x - 0.5
|
||||
btn_inc_pos.y = self.pos.y + (self.btn_size * 0.05)
|
||||
else
|
||||
btn_size.x = self.btn_size
|
||||
btn_size.y = 0.5
|
||||
btn_inc_pos.x = self.pos.x + (self.btn_size * 0.05)
|
||||
btn_inc_pos.y = self.pos.y + self.size.y - 0.5
|
||||
end
|
||||
|
||||
local text_dec = "<"
|
||||
local text_inc = ">"
|
||||
if self.orientation == "vertical" then
|
||||
text_dec = "^"
|
||||
text_inc = "v"
|
||||
end
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]",
|
||||
btn_dec_pos.x, btn_dec_pos.y, btn_size.x, btn_size.y,
|
||||
self.name, text_dec)
|
||||
|
||||
formspec = formspec ..
|
||||
string.format("image_button[%f,%f;%f,%f;;btnbar_inc_%s;%s;true;true]",
|
||||
btn_inc_pos.x, btn_inc_pos.y, btn_size.x, btn_size.y,
|
||||
self.name, text_inc)
|
||||
end
|
||||
|
||||
return formspec
|
||||
end
|
||||
|
||||
local function buttonbar_buttonhandler(self, fields)
|
||||
|
||||
if fields["btnbar_inc_" .. self.name] ~= nil and
|
||||
self.startbutton < #self.buttons then
|
||||
|
||||
self.startbutton = self.startbutton + 1
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btnbar_dec_" .. self.name] ~= nil and self.startbutton > 1 then
|
||||
self.startbutton = self.startbutton - 1
|
||||
return true
|
||||
end
|
||||
|
||||
for i=1,#self.buttons,1 do
|
||||
if fields[self.buttons[i].name] ~= nil then
|
||||
return self.userbuttonhandler(fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local buttonbar_metatable = {
|
||||
handle_buttons = buttonbar_buttonhandler,
|
||||
handle_events = function(self, event) end,
|
||||
get_formspec = buttonbar_formspec,
|
||||
|
||||
hide = function(self) self.hidden = true end,
|
||||
show = function(self) self.hidden = false end,
|
||||
|
||||
delete = function(self) ui.delete(self) end,
|
||||
|
||||
add_button = function(self, name, caption, image, tooltip)
|
||||
if caption == nil then caption = "" end
|
||||
if image == nil then image = "" end
|
||||
if tooltip == nil then tooltip = "" end
|
||||
|
||||
self.buttons[#self.buttons + 1] = {
|
||||
name = name,
|
||||
caption = caption,
|
||||
image = image,
|
||||
tooltip = tooltip
|
||||
}
|
||||
if self.orientation == "horizontal" then
|
||||
if ( (self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2)
|
||||
> self.size.x ) then
|
||||
|
||||
self.btn_initial_offset = self.btn_size * 0.05 + 0.5
|
||||
self.have_move_buttons = true
|
||||
end
|
||||
else
|
||||
if ((self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2)
|
||||
> self.size.y ) then
|
||||
|
||||
self.btn_initial_offset = self.btn_size * 0.05 + 0.5
|
||||
self.have_move_buttons = true
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
set_bgparams = function(self, bgcolor)
|
||||
if (type(bgcolor) == "string") then
|
||||
self.bgcolor = bgcolor
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
buttonbar_metatable.__index = buttonbar_metatable
|
||||
|
||||
function buttonbar_create(name, cbf_buttonhandler, pos, orientation, size)
|
||||
assert(name ~= nil)
|
||||
assert(cbf_buttonhandler ~= nil)
|
||||
assert(orientation == "vertical" or orientation == "horizontal")
|
||||
assert(pos ~= nil and type(pos) == "table")
|
||||
assert(size ~= nil and type(size) == "table")
|
||||
|
||||
local self = {}
|
||||
self.name = name
|
||||
self.type = "addon"
|
||||
self.bgcolor = "#000000"
|
||||
self.pos = pos
|
||||
self.size = size
|
||||
self.orientation = orientation
|
||||
self.startbutton = 1
|
||||
self.have_move_buttons = false
|
||||
self.hidden = false
|
||||
|
||||
if self.orientation == "horizontal" then
|
||||
self.btn_size = self.size.y
|
||||
else
|
||||
self.btn_size = self.size.x
|
||||
end
|
||||
|
||||
if (self.btn_initial_offset == nil) then
|
||||
self.btn_initial_offset = self.btn_size * 0.05
|
||||
end
|
||||
|
||||
self.userbuttonhandler = cbf_buttonhandler
|
||||
self.buttons = {}
|
||||
|
||||
setmetatable(self,buttonbar_metatable)
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
||||
88
builtin/fstk/dialog.lua
Normal file
88
builtin/fstk/dialog.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--this program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local function dialog_event_handler(self,event)
|
||||
if self.user_eventhandler == nil or
|
||||
self.user_eventhandler(event) == false then
|
||||
|
||||
--close dialog on esc
|
||||
if event == "MenuQuit" then
|
||||
self:delete()
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dialog_metatable = {
|
||||
eventhandler = dialog_event_handler,
|
||||
get_formspec = function(self)
|
||||
if not self.hidden then return self.formspec(self.data) end
|
||||
end,
|
||||
handle_buttons = function(self,fields)
|
||||
if not self.hidden then return self.buttonhandler(self,fields) end
|
||||
end,
|
||||
handle_events = function(self,event)
|
||||
if not self.hidden then return self.eventhandler(self,event) end
|
||||
end,
|
||||
hide = function(self) self.hidden = true end,
|
||||
show = function(self) self.hidden = false end,
|
||||
delete = function(self)
|
||||
if self.parent ~= nil then
|
||||
self.parent:show()
|
||||
end
|
||||
ui.delete(self)
|
||||
end,
|
||||
set_parent = function(self,parent) self.parent = parent end
|
||||
}
|
||||
dialog_metatable.__index = dialog_metatable
|
||||
|
||||
function dialog_create(name,get_formspec,buttonhandler,eventhandler)
|
||||
local self = {}
|
||||
|
||||
self.name = name
|
||||
self.type = "toplevel"
|
||||
self.hidden = true
|
||||
self.data = {}
|
||||
|
||||
self.formspec = get_formspec
|
||||
self.buttonhandler = buttonhandler
|
||||
self.user_eventhandler = eventhandler
|
||||
|
||||
setmetatable(self,dialog_metatable)
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
||||
|
||||
function messagebox(name, message)
|
||||
return dialog_create(name,
|
||||
function()
|
||||
return ([[
|
||||
formspec_version[3]
|
||||
size[8,3]
|
||||
textarea[0.375,0.375;7.25,1.2;;;%s]
|
||||
button[3,1.825;2,0.8;ok;%s]
|
||||
]]):format(message, fgettext("OK"))
|
||||
end,
|
||||
function(this, fields)
|
||||
if fields.ok then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
end,
|
||||
nil)
|
||||
end
|
||||
257
builtin/fstk/tabview.lua
Normal file
257
builtin/fstk/tabview.lua
Normal file
@@ -0,0 +1,257 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- A tabview implementation --
|
||||
-- Usage: --
|
||||
-- tabview.create: returns initialized tabview raw element --
|
||||
-- element.add(tab): add a tab declaration --
|
||||
-- element.handle_buttons() --
|
||||
-- element.handle_events() --
|
||||
-- element.getFormspec() returns formspec of tabview --
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function add_tab(self,tab)
|
||||
assert(tab.size == nil or (type(tab.size) == table and
|
||||
tab.size.x ~= nil and tab.size.y ~= nil))
|
||||
assert(tab.cbf_formspec ~= nil and type(tab.cbf_formspec) == "function")
|
||||
assert(tab.cbf_button_handler == nil or
|
||||
type(tab.cbf_button_handler) == "function")
|
||||
assert(tab.cbf_events == nil or type(tab.cbf_events) == "function")
|
||||
|
||||
local newtab = {
|
||||
name = tab.name,
|
||||
caption = tab.caption,
|
||||
button_handler = tab.cbf_button_handler,
|
||||
event_handler = tab.cbf_events,
|
||||
get_formspec = tab.cbf_formspec,
|
||||
tabsize = tab.tabsize,
|
||||
on_change = tab.on_change,
|
||||
tabdata = {},
|
||||
}
|
||||
|
||||
self.tablist[#self.tablist + 1] = newtab
|
||||
|
||||
if self.last_tab_index == #self.tablist then
|
||||
self.current_tab = tab.name
|
||||
if tab.on_activate ~= nil then
|
||||
tab.on_activate(nil,tab.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_formspec(self)
|
||||
if self.hidden or (self.parent ~= nil and self.parent.hidden) then
|
||||
return ""
|
||||
end
|
||||
local tab = self.tablist[self.last_tab_index]
|
||||
|
||||
local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.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
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_buttons(self,fields)
|
||||
|
||||
if self.hidden then
|
||||
return false
|
||||
end
|
||||
|
||||
if self:handle_tab_buttons(fields) then
|
||||
return true
|
||||
end
|
||||
|
||||
if self.glb_btn_handler ~= nil and
|
||||
self.glb_btn_handler(self,fields) then
|
||||
return true
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_events(self,event)
|
||||
|
||||
if self.hidden then
|
||||
return false
|
||||
end
|
||||
|
||||
if self.glb_evt_handler ~= nil and
|
||||
self.glb_evt_handler(self,event) then
|
||||
return true
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function tab_header(self)
|
||||
|
||||
local toadd = ""
|
||||
|
||||
for i=1,#self.tablist,1 do
|
||||
|
||||
if toadd ~= "" then
|
||||
toadd = toadd .. ","
|
||||
end
|
||||
|
||||
toadd = toadd .. self.tablist[i].caption
|
||||
end
|
||||
return string.format("tabheader[%f,%f;%s;%s;%i;true;false]",
|
||||
self.header_x, self.header_y, self.name, toadd, self.last_tab_index);
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function switch_to_tab(self, index)
|
||||
--first call on_change for tab to leave
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("LEAVE",
|
||||
self.current_tab, self.tablist[index].name)
|
||||
end
|
||||
|
||||
--update tabview data
|
||||
self.last_tab_index = index
|
||||
local old_tab = self.current_tab
|
||||
self.current_tab = self.tablist[index].name
|
||||
|
||||
if (self.autosave_tab) then
|
||||
core.settings:set(self.name .. "_LAST",self.current_tab)
|
||||
end
|
||||
|
||||
-- call for tab to enter
|
||||
if self.tablist[index].on_change ~= nil then
|
||||
self.tablist[index].on_change("ENTER",
|
||||
old_tab,self.current_tab)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_tab_buttons(self,fields)
|
||||
--save tab selection to config file
|
||||
if fields[self.name] then
|
||||
local index = tonumber(fields[self.name])
|
||||
switch_to_tab(self, index)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function set_tab_by_name(self, name)
|
||||
for i=1,#self.tablist,1 do
|
||||
if self.tablist[i].name == name then
|
||||
switch_to_tab(self, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function hide_tabview(self)
|
||||
self.hidden=true
|
||||
|
||||
--call on_change as we're not gonna show self tab any longer
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("LEAVE",
|
||||
self.current_tab, nil)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function show_tabview(self)
|
||||
self.hidden=false
|
||||
|
||||
-- call for tab to enter
|
||||
if self.tablist[self.last_tab_index].on_change ~= nil then
|
||||
self.tablist[self.last_tab_index].on_change("ENTER",
|
||||
nil,self.current_tab)
|
||||
end
|
||||
end
|
||||
|
||||
local tabview_metatable = {
|
||||
add = add_tab,
|
||||
handle_buttons = handle_buttons,
|
||||
handle_events = handle_events,
|
||||
get_formspec = get_formspec,
|
||||
show = show_tabview,
|
||||
hide = hide_tabview,
|
||||
delete = function(self) ui.delete(self) end,
|
||||
set_parent = function(self,parent) self.parent = parent end,
|
||||
set_autosave_tab =
|
||||
function(self,value) self.autosave_tab = value end,
|
||||
set_tab = set_tab_by_name,
|
||||
set_global_button_handler =
|
||||
function(self,handler) self.glb_btn_handler = handler end,
|
||||
set_global_event_handler =
|
||||
function(self,handler) self.glb_evt_handler = handler end,
|
||||
set_fixed_size =
|
||||
function(self,state) self.fixed_size = state end,
|
||||
tab_header = tab_header,
|
||||
handle_tab_buttons = handle_tab_buttons
|
||||
}
|
||||
|
||||
tabview_metatable.__index = tabview_metatable
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function tabview_create(name, size, tabheaderpos)
|
||||
local self = {}
|
||||
|
||||
self.name = name
|
||||
self.type = "toplevel"
|
||||
self.width = size.x
|
||||
self.height = size.y
|
||||
self.header_x = tabheaderpos.x
|
||||
self.header_y = tabheaderpos.y
|
||||
|
||||
setmetatable(self, tabview_metatable)
|
||||
|
||||
self.fixed_size = true
|
||||
self.hidden = true
|
||||
self.current_tab = nil
|
||||
self.last_tab_index = 1
|
||||
self.tablist = {}
|
||||
|
||||
self.autosave_tab = false
|
||||
|
||||
ui.add(self)
|
||||
return self
|
||||
end
|
||||
212
builtin/fstk/ui.lua
Normal file
212
builtin/fstk/ui.lua
Normal file
@@ -0,0 +1,212 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
ui = {}
|
||||
ui.childlist = {}
|
||||
ui.default = nil
|
||||
-- Whether fstk is currently showing its own formspec instead of active ui elements.
|
||||
ui.overridden = false
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.add(child)
|
||||
--TODO check child
|
||||
ui.childlist[child.name] = child
|
||||
|
||||
return child.name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.delete(child)
|
||||
|
||||
if ui.childlist[child.name] == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
ui.childlist[child.name] = nil
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.set_default(name)
|
||||
ui.default = name
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.find_by_name(name)
|
||||
return ui.childlist[name]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- Internal functions not to be called from user
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function ui.update()
|
||||
ui.overridden = false
|
||||
local formspec = {}
|
||||
|
||||
-- handle errors
|
||||
if gamedata ~= nil and gamedata.reconnect_requested then
|
||||
local error_message = core.formspec_escape(
|
||||
gamedata.errormessage or fgettext("<none available>"))
|
||||
formspec = {
|
||||
"size[14,8]",
|
||||
"real_coordinates[true]",
|
||||
"set_focus[btn_reconnect_yes;true]",
|
||||
"box[0.5,1.2;13,5;#000]",
|
||||
("textarea[0.5,1.2;13,5;;%s;%s]"):format(
|
||||
fgettext("The server has requested a reconnect:"), error_message),
|
||||
"button[2,6.6;4,1;btn_reconnect_yes;" .. fgettext("Reconnect") .. "]",
|
||||
"button[8,6.6;4,1;btn_reconnect_no;" .. fgettext("Main menu") .. "]"
|
||||
}
|
||||
ui.overridden = true
|
||||
elseif gamedata ~= nil and gamedata.errormessage ~= nil then
|
||||
local error_message = core.formspec_escape(gamedata.errormessage)
|
||||
|
||||
local error_title
|
||||
if string.find(gamedata.errormessage, "ModError") then
|
||||
error_title = fgettext("An error occurred in a Lua script:")
|
||||
else
|
||||
error_title = fgettext("An error occurred:")
|
||||
end
|
||||
formspec = {
|
||||
"size[14,8]",
|
||||
"real_coordinates[true]",
|
||||
"set_focus[btn_error_confirm;true]",
|
||||
"box[0.5,1.2;13,5;#000]",
|
||||
("textarea[0.5,1.2;13,5;;%s;%s]"):format(
|
||||
error_title, error_message),
|
||||
"button[5,6.6;4,1;btn_error_confirm;" .. fgettext("OK") .. "]"
|
||||
}
|
||||
ui.overridden = true
|
||||
else
|
||||
local active_toplevel_ui_elements = 0
|
||||
for key,value in pairs(ui.childlist) do
|
||||
if (value.type == "toplevel") then
|
||||
local retval = value:get_formspec()
|
||||
|
||||
if retval ~= nil and retval ~= "" then
|
||||
active_toplevel_ui_elements = active_toplevel_ui_elements + 1
|
||||
table.insert(formspec, retval)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- no need to show addons if there ain't a toplevel element
|
||||
if (active_toplevel_ui_elements > 0) then
|
||||
for key,value in pairs(ui.childlist) do
|
||||
if (value.type == "addon") then
|
||||
local retval = value:get_formspec()
|
||||
|
||||
if retval ~= nil and retval ~= "" then
|
||||
table.insert(formspec, retval)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (active_toplevel_ui_elements > 1) then
|
||||
core.log("warning", "more than one active ui "..
|
||||
"element, self most likely isn't intended")
|
||||
end
|
||||
|
||||
if (active_toplevel_ui_elements == 0) then
|
||||
core.log("warning", "no toplevel ui element "..
|
||||
"active; switching to default")
|
||||
ui.childlist[ui.default]:show()
|
||||
formspec = {ui.childlist[ui.default]:get_formspec()}
|
||||
end
|
||||
end
|
||||
core.update_formspec(table.concat(formspec))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.handle_buttons(fields)
|
||||
for key,value in pairs(ui.childlist) do
|
||||
|
||||
local retval = value:handle_buttons(fields)
|
||||
|
||||
if retval then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function ui.handle_events(event)
|
||||
|
||||
for key,value in pairs(ui.childlist) do
|
||||
|
||||
if value.handle_events ~= nil then
|
||||
local retval = value:handle_events(event)
|
||||
|
||||
if retval then
|
||||
return retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
-- initialize callbacks
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
core.button_handler = function(fields)
|
||||
if fields["btn_reconnect_yes"] then
|
||||
gamedata.reconnect_requested = false
|
||||
gamedata.errormessage = nil
|
||||
gamedata.do_reconnect = true
|
||||
core.start()
|
||||
return
|
||||
elseif fields["btn_reconnect_no"] or fields["btn_error_confirm"] then
|
||||
gamedata.errormessage = nil
|
||||
gamedata.reconnect_requested = false
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
|
||||
if ui.handle_buttons(fields) then
|
||||
ui.update()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
core.event_handler = function(event)
|
||||
-- Handle error messages
|
||||
if ui.overridden then
|
||||
if event == "MenuQuit" then
|
||||
gamedata.errormessage = nil
|
||||
gamedata.reconnect_requested = false
|
||||
ui.update()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if ui.handle_events(event) then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
|
||||
if event == "Refresh" then
|
||||
ui.update()
|
||||
return
|
||||
end
|
||||
end
|
||||
22
builtin/game/async.lua
Normal file
22
builtin/game/async.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
core.async_jobs = {}
|
||||
|
||||
function core.async_event_handler(jobid, retval)
|
||||
local callback = core.async_jobs[jobid]
|
||||
assert(type(callback) == "function")
|
||||
callback(unpack(retval, 1, retval.n))
|
||||
core.async_jobs[jobid] = nil
|
||||
end
|
||||
|
||||
function core.handle_async(func, callback, ...)
|
||||
assert(type(func) == "function" and type(callback) == "function",
|
||||
"Invalid minetest.handle_async invocation")
|
||||
local args = {n = select("#", ...), ...}
|
||||
local mod_origin = core.get_last_run_mod()
|
||||
|
||||
local jobid = core.do_async_callback(func, args, mod_origin)
|
||||
core.async_jobs[jobid] = callback
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
185
builtin/game/auth.lua
Normal file
185
builtin/game/auth.lua
Normal file
@@ -0,0 +1,185 @@
|
||||
-- Minetest: builtin/auth.lua
|
||||
|
||||
--
|
||||
-- Builtin authentication handler
|
||||
--
|
||||
|
||||
-- Make the auth object private, deny access to mods
|
||||
local core_auth = core.auth
|
||||
core.auth = nil
|
||||
|
||||
core.builtin_auth_handler = {
|
||||
get_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
-- If no such auth found, return nil
|
||||
if not auth_entry then
|
||||
return nil
|
||||
end
|
||||
-- Figure out what privileges the player should have.
|
||||
-- Take a copy of the privilege table
|
||||
local privileges = {}
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
privileges[priv] = true
|
||||
end
|
||||
-- If singleplayer, give all privileges except those marked as give_to_singleplayer = false
|
||||
if core.is_singleplayer() then
|
||||
for priv, def in pairs(core.registered_privileges) do
|
||||
if def.give_to_singleplayer then
|
||||
privileges[priv] = true
|
||||
end
|
||||
end
|
||||
-- For the admin, give everything
|
||||
elseif name == core.settings:get("name") then
|
||||
for priv, def in pairs(core.registered_privileges) do
|
||||
if def.give_to_admin then
|
||||
privileges[priv] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- All done
|
||||
return {
|
||||
password = auth_entry.password,
|
||||
privileges = privileges,
|
||||
last_login = auth_entry.last_login,
|
||||
}
|
||||
end,
|
||||
create_auth = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
core.log('info', "Built-in authentication handler adding player '"..name.."'")
|
||||
return core_auth.create({
|
||||
name = name,
|
||||
password = password,
|
||||
privileges = core.string_to_privs(core.settings:get("default_privs")),
|
||||
last_login = -1, -- Defer login time calculation until record_login (called by on_joinplayer)
|
||||
})
|
||||
end,
|
||||
delete_auth = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
return false
|
||||
end
|
||||
core.log('info', "Built-in authentication handler deleting player '"..name.."'")
|
||||
return core_auth.delete(name)
|
||||
end,
|
||||
set_password = function(name, password)
|
||||
assert(type(name) == "string")
|
||||
assert(type(password) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
core.builtin_auth_handler.create_auth(name, password)
|
||||
else
|
||||
core.log('info', "Built-in authentication handler setting password of player '"..name.."'")
|
||||
auth_entry.password = password
|
||||
core_auth.save(auth_entry)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
set_privileges = function(name, privileges)
|
||||
assert(type(name) == "string")
|
||||
assert(type(privileges) == "table")
|
||||
local auth_entry = core_auth.read(name)
|
||||
if not auth_entry then
|
||||
auth_entry = core.builtin_auth_handler.create_auth(name,
|
||||
core.get_password_hash(name,
|
||||
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
|
||||
core.run_priv_callbacks(name, priv, nil, "grant")
|
||||
end
|
||||
end
|
||||
|
||||
-- Run revoke callbacks
|
||||
for priv, _ in pairs(auth_entry.privileges) do
|
||||
if not privileges[priv] then
|
||||
core.run_priv_callbacks(name, priv, nil, "revoke")
|
||||
end
|
||||
end
|
||||
core.notify_authentication_modified(name)
|
||||
end,
|
||||
reload = function()
|
||||
core_auth.reload()
|
||||
return true
|
||||
end,
|
||||
record_login = function(name)
|
||||
assert(type(name) == "string")
|
||||
local auth_entry = core_auth.read(name)
|
||||
assert(auth_entry)
|
||||
auth_entry.last_login = os.time()
|
||||
core_auth.save(auth_entry)
|
||||
end,
|
||||
iterate = function()
|
||||
local names = {}
|
||||
local nameslist = core_auth.list_names()
|
||||
for k,v in pairs(nameslist) do
|
||||
names[v] = true
|
||||
end
|
||||
return pairs(names)
|
||||
end,
|
||||
}
|
||||
|
||||
core.register_on_prejoinplayer(function(name, ip)
|
||||
if core.registered_auth_handler ~= nil then
|
||||
return -- Don't do anything if custom auth handler registered
|
||||
end
|
||||
local auth_entry = core_auth.read(name)
|
||||
if auth_entry ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
local name_lower = name:lower()
|
||||
for k in core.builtin_auth_handler.iterate() do
|
||||
if k:lower() == name_lower then
|
||||
return string.format("\nCannot create new player called '%s'. "..
|
||||
"Another account called '%s' is already registered. "..
|
||||
"Please check the spelling if it's your account "..
|
||||
"or use a different nickname.", name, k)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--
|
||||
-- Authentication API
|
||||
--
|
||||
|
||||
function core.register_authentication_handler(handler)
|
||||
if core.registered_auth_handler then
|
||||
error("Add-on authentication handler already registered by "..core.registered_auth_handler_modname)
|
||||
end
|
||||
core.registered_auth_handler = handler
|
||||
core.registered_auth_handler_modname = core.get_current_modname()
|
||||
handler.mod_origin = core.registered_auth_handler_modname
|
||||
end
|
||||
|
||||
function core.get_auth_handler()
|
||||
return core.registered_auth_handler or core.builtin_auth_handler
|
||||
end
|
||||
|
||||
local function auth_pass(name)
|
||||
return function(...)
|
||||
local auth_handler = core.get_auth_handler()
|
||||
if auth_handler[name] then
|
||||
return auth_handler[name](...)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
core.set_player_password = auth_pass("set_password")
|
||||
core.set_player_privs = auth_pass("set_privileges")
|
||||
core.remove_player_auth = auth_pass("delete_auth")
|
||||
core.auth_reload = auth_pass("reload")
|
||||
|
||||
local record_login = auth_pass("record_login")
|
||||
core.register_on_joinplayer(function(player)
|
||||
record_login(player:get_player_name())
|
||||
end)
|
||||
1359
builtin/game/chat.lua
Normal file
1359
builtin/game/chat.lua
Normal file
File diff suppressed because it is too large
Load Diff
31
builtin/game/constants.lua
Normal file
31
builtin/game/constants.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- Minetest: builtin/constants.lua
|
||||
|
||||
--
|
||||
-- Constants values for use with the Lua API
|
||||
--
|
||||
|
||||
-- mapnode.h
|
||||
-- Built-in Content IDs (for use with VoxelManip API)
|
||||
core.CONTENT_UNKNOWN = 125
|
||||
core.CONTENT_AIR = 126
|
||||
core.CONTENT_IGNORE = 127
|
||||
|
||||
-- emerge.h
|
||||
-- Block emerge status constants (for use with core.emerge_area)
|
||||
core.EMERGE_CANCELLED = 0
|
||||
core.EMERGE_ERRORED = 1
|
||||
core.EMERGE_FROM_MEMORY = 2
|
||||
core.EMERGE_FROM_DISK = 3
|
||||
core.EMERGE_GENERATED = 4
|
||||
|
||||
-- constants.h
|
||||
-- Size of mapblocks in nodes
|
||||
core.MAP_BLOCKSIZE = 16
|
||||
-- Default maximal HP of a player
|
||||
core.PLAYER_MAX_HP_DEFAULT = 20
|
||||
-- Default maximal breath of a player
|
||||
core.PLAYER_MAX_BREATH_DEFAULT = 10
|
||||
|
||||
-- light.h
|
||||
-- Maximum value for node 'light_source' parameter
|
||||
core.LIGHT_MAX = 14
|
||||
65
builtin/game/deprecated.lua
Normal file
65
builtin/game/deprecated.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Minetest: builtin/deprecated.lua
|
||||
|
||||
--
|
||||
-- EnvRef
|
||||
--
|
||||
core.env = {}
|
||||
local envref_deprecation_message_printed = false
|
||||
setmetatable(core.env, {
|
||||
__index = function(table, key)
|
||||
if not envref_deprecation_message_printed then
|
||||
core.log("deprecated", "core.env:[...] is deprecated and should be replaced with core.[...]")
|
||||
envref_deprecation_message_printed = true
|
||||
end
|
||||
local func = core[key]
|
||||
if type(func) == "function" then
|
||||
rawset(table, key, function(self, ...)
|
||||
return func(...)
|
||||
end)
|
||||
else
|
||||
rawset(table, key, nil)
|
||||
end
|
||||
return rawget(table, key)
|
||||
end
|
||||
})
|
||||
|
||||
function core.rollback_get_last_node_actor(pos, range, seconds)
|
||||
return core.rollback_get_node_actions(pos, range, seconds, 1)[1]
|
||||
end
|
||||
|
||||
--
|
||||
-- core.setting_*
|
||||
--
|
||||
|
||||
local settings = core.settings
|
||||
|
||||
local function setting_proxy(name)
|
||||
return function(...)
|
||||
core.log("deprecated", "WARNING: minetest.setting_* "..
|
||||
"functions are deprecated. "..
|
||||
"Use methods on the minetest.settings object.")
|
||||
return settings[name](settings, ...)
|
||||
end
|
||||
end
|
||||
|
||||
core.setting_set = setting_proxy("set")
|
||||
core.setting_get = setting_proxy("get")
|
||||
core.setting_setbool = setting_proxy("set_bool")
|
||||
core.setting_getbool = setting_proxy("get_bool")
|
||||
core.setting_save = setting_proxy("write")
|
||||
|
||||
--
|
||||
-- core.register_on_auth_fail
|
||||
--
|
||||
|
||||
function core.register_on_auth_fail(func)
|
||||
core.log("deprecated", "core.register_on_auth_fail " ..
|
||||
"is deprecated and should be replaced by " ..
|
||||
"core.register_on_authplayer instead.")
|
||||
|
||||
core.register_on_authplayer(function (player_name, ip, is_success)
|
||||
if not is_success then
|
||||
func(player_name, ip)
|
||||
end
|
||||
end)
|
||||
end
|
||||
24
builtin/game/detached_inventory.lua
Normal file
24
builtin/game/detached_inventory.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Minetest: builtin/detached_inventory.lua
|
||||
|
||||
core.detached_inventories = {}
|
||||
|
||||
function core.create_detached_inventory(name, callbacks, player_name)
|
||||
local stuff = {}
|
||||
stuff.name = name
|
||||
if callbacks then
|
||||
stuff.allow_move = callbacks.allow_move
|
||||
stuff.allow_put = callbacks.allow_put
|
||||
stuff.allow_take = callbacks.allow_take
|
||||
stuff.on_move = callbacks.on_move
|
||||
stuff.on_put = callbacks.on_put
|
||||
stuff.on_take = callbacks.on_take
|
||||
end
|
||||
stuff.mod_origin = core.get_current_modname() or "??"
|
||||
core.detached_inventories[name] = stuff
|
||||
return core.create_detached_inventory_raw(name, player_name)
|
||||
end
|
||||
|
||||
function core.remove_detached_inventory(name)
|
||||
core.detached_inventories[name] = nil
|
||||
return core.remove_detached_inventory_raw(name)
|
||||
end
|
||||
608
builtin/game/falling.lua
Normal file
608
builtin/game/falling.lua
Normal file
@@ -0,0 +1,608 @@
|
||||
-- Minetest: builtin/item.lua
|
||||
|
||||
local builtin_shared = ...
|
||||
local SCALE = 0.667
|
||||
|
||||
local facedir_to_euler = {
|
||||
{y = 0, x = 0, z = 0},
|
||||
{y = -math.pi/2, x = 0, z = 0},
|
||||
{y = math.pi, x = 0, z = 0},
|
||||
{y = math.pi/2, x = 0, z = 0},
|
||||
{y = math.pi/2, x = -math.pi/2, z = math.pi/2},
|
||||
{y = math.pi/2, x = math.pi, z = math.pi/2},
|
||||
{y = math.pi/2, x = math.pi/2, z = math.pi/2},
|
||||
{y = math.pi/2, x = 0, z = math.pi/2},
|
||||
{y = -math.pi/2, x = math.pi/2, z = math.pi/2},
|
||||
{y = -math.pi/2, x = 0, z = math.pi/2},
|
||||
{y = -math.pi/2, x = -math.pi/2, z = math.pi/2},
|
||||
{y = -math.pi/2, x = math.pi, z = math.pi/2},
|
||||
{y = 0, x = 0, z = math.pi/2},
|
||||
{y = 0, x = -math.pi/2, z = math.pi/2},
|
||||
{y = 0, x = math.pi, z = math.pi/2},
|
||||
{y = 0, x = math.pi/2, z = math.pi/2},
|
||||
{y = math.pi, x = math.pi, z = math.pi/2},
|
||||
{y = math.pi, x = math.pi/2, z = math.pi/2},
|
||||
{y = math.pi, x = 0, z = math.pi/2},
|
||||
{y = math.pi, x = -math.pi/2, z = math.pi/2},
|
||||
{y = math.pi, x = math.pi, z = 0},
|
||||
{y = -math.pi/2, x = math.pi, z = 0},
|
||||
{y = 0, x = math.pi, z = 0},
|
||||
{y = math.pi/2, x = math.pi, z = 0}
|
||||
}
|
||||
|
||||
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
|
||||
|
||||
--
|
||||
-- Falling stuff
|
||||
--
|
||||
|
||||
core.register_entity(":__builtin:falling_node", {
|
||||
initial_properties = {
|
||||
visual = "item",
|
||||
visual_size = vector.new(SCALE, SCALE, SCALE),
|
||||
textures = {},
|
||||
physical = true,
|
||||
is_visible = false,
|
||||
collide_with_objects = true,
|
||||
collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
|
||||
},
|
||||
|
||||
node = {},
|
||||
meta = {},
|
||||
floats = false,
|
||||
|
||||
set_node = function(self, node, meta)
|
||||
node.param2 = node.param2 or 0
|
||||
self.node = node
|
||||
meta = meta or {}
|
||||
if type(meta.to_table) == "function" then
|
||||
meta = meta:to_table()
|
||||
end
|
||||
for _, list in pairs(meta.inventory or {}) do
|
||||
for i, stack in pairs(list) do
|
||||
if type(stack) == "userdata" then
|
||||
list[i] = stack:to_string()
|
||||
end
|
||||
end
|
||||
end
|
||||
local def = core.registered_nodes[node.name]
|
||||
if not def then
|
||||
-- Don't allow unknown nodes to fall
|
||||
core.log("info",
|
||||
"Unknown falling node removed at "..
|
||||
core.pos_to_string(self.object:get_pos()))
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
self.meta = meta
|
||||
|
||||
-- Cache whether we're supposed to float on water
|
||||
self.floats = core.get_item_group(node.name, "float") ~= 0
|
||||
|
||||
-- Set entity visuals
|
||||
if def.drawtype == "torchlike" or def.drawtype == "signlike" then
|
||||
local textures
|
||||
if def.tiles and def.tiles[1] then
|
||||
local tile = def.tiles[1]
|
||||
if type(tile) == "table" then
|
||||
tile = tile.name
|
||||
end
|
||||
if def.drawtype == "torchlike" then
|
||||
textures = { "("..tile..")^[transformFX", tile }
|
||||
else
|
||||
textures = { tile, "("..tile..")^[transformFX" }
|
||||
end
|
||||
end
|
||||
local vsize
|
||||
if def.visual_scale then
|
||||
local s = def.visual_scale
|
||||
vsize = vector.new(s, s, s)
|
||||
end
|
||||
self.object:set_properties({
|
||||
is_visible = true,
|
||||
visual = "upright_sprite",
|
||||
visual_size = vsize,
|
||||
textures = textures,
|
||||
glow = def.light_source,
|
||||
})
|
||||
elseif def.drawtype ~= "airlike" then
|
||||
local itemstring = node.name
|
||||
if core.is_colored_paramtype(def.paramtype2) then
|
||||
itemstring = core.itemstring_with_palette(itemstring, node.param2)
|
||||
end
|
||||
-- FIXME: solution needed for paramtype2 == "leveled"
|
||||
-- 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 = s,
|
||||
glow = def.light_source,
|
||||
})
|
||||
end
|
||||
|
||||
-- Set collision box (certain nodeboxes only for now)
|
||||
local nb_types = {fixed=true, leveled=true, connected=true}
|
||||
if def.drawtype == "nodebox" and def.node_box and
|
||||
nb_types[def.node_box.type] and def.node_box.fixed then
|
||||
local box = table.copy(def.node_box.fixed)
|
||||
if type(box[1]) == "table" then
|
||||
box = #box == 1 and box[1] or nil -- We can only use a single box
|
||||
end
|
||||
if box then
|
||||
if def.paramtype2 == "leveled" and (self.node.level or 0) > 0 then
|
||||
box[5] = -0.5 + self.node.level / 64
|
||||
end
|
||||
self.object:set_properties({
|
||||
collisionbox = box
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Rotate entity
|
||||
if def.drawtype == "torchlike" then
|
||||
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"
|
||||
or def.drawtype == "mesh"
|
||||
or def.drawtype == "normal"
|
||||
or def.drawtype == "nodebox" then
|
||||
if (def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir") then
|
||||
local fdir = node.param2 % 32 % 24
|
||||
-- Get rotation from a precalculated lookup table
|
||||
local euler = facedir_to_euler[fdir + 1]
|
||||
self.object:set_rotation(euler)
|
||||
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
|
||||
pitch, yaw = math.pi/2, 0
|
||||
elseif rot == 1 then
|
||||
pitch, yaw = -math.pi/2, math.pi
|
||||
elseif rot == 2 then
|
||||
pitch, yaw = 0, math.pi/2
|
||||
elseif rot == 3 then
|
||||
pitch, yaw = 0, -math.pi/2
|
||||
elseif rot == 4 then
|
||||
pitch, yaw = 0, math.pi
|
||||
end
|
||||
else
|
||||
if rot == 1 then
|
||||
pitch, yaw = math.pi, math.pi
|
||||
elseif rot == 2 then
|
||||
pitch, yaw = math.pi/2, math.pi/2
|
||||
elseif rot == 3 then
|
||||
pitch, yaw = math.pi/2, -math.pi/2
|
||||
elseif rot == 4 then
|
||||
pitch, yaw = math.pi/2, math.pi
|
||||
elseif rot == 5 then
|
||||
pitch, yaw = math.pi/2, 0
|
||||
end
|
||||
end
|
||||
if def.drawtype == "signlike" then
|
||||
pitch = pitch - math.pi/2
|
||||
if rot == 0 then
|
||||
yaw = yaw + math.pi/2
|
||||
elseif rot == 1 then
|
||||
yaw = yaw - math.pi/2
|
||||
end
|
||||
elseif def.drawtype == "mesh" or def.drawtype == "normal" or def.drawtype == "nodebox" then
|
||||
if rot >= 0 and rot <= 1 then
|
||||
roll = roll + math.pi
|
||||
else
|
||||
yaw = yaw + math.pi
|
||||
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,
|
||||
|
||||
get_staticdata = function(self)
|
||||
local ds = {
|
||||
node = self.node,
|
||||
meta = self.meta,
|
||||
}
|
||||
return core.serialize(ds)
|
||||
end,
|
||||
|
||||
on_activate = function(self, staticdata)
|
||||
self.object:set_armor_groups({immortal = 1})
|
||||
self.object:set_acceleration(vector.new(0, -gravity, 0))
|
||||
|
||||
local ds = core.deserialize(staticdata)
|
||||
if ds and ds.node then
|
||||
self:set_node(ds.node, ds.meta)
|
||||
elseif ds then
|
||||
self:set_node(ds)
|
||||
elseif staticdata ~= "" then
|
||||
self:set_node({name = staticdata})
|
||||
end
|
||||
end,
|
||||
|
||||
try_place = function(self, bcp, bcn)
|
||||
local bcd = core.registered_nodes[bcn.name]
|
||||
-- Add levels if dropped on same leveled node
|
||||
if bcd and bcd.paramtype2 == "leveled" and
|
||||
bcn.name == self.node.name then
|
||||
local addlevel = self.node.level
|
||||
if (addlevel or 0) <= 0 then
|
||||
addlevel = bcd.leveled
|
||||
end
|
||||
if core.add_node_level(bcp, addlevel) < addlevel then
|
||||
return true
|
||||
elseif bcd.buildable_to then
|
||||
-- Node level has already reached max, don't place anything
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Decide if we're replacing the node or placing on top
|
||||
local np = vector.copy(bcp)
|
||||
if bcd and bcd.buildable_to and
|
||||
(not self.floats or bcd.liquidtype == "none") then
|
||||
core.remove_node(bcp)
|
||||
else
|
||||
np.y = np.y + 1
|
||||
end
|
||||
|
||||
-- Check what's here
|
||||
local n2 = core.get_node(np)
|
||||
local nd = core.registered_nodes[n2.name]
|
||||
-- If it's not air or liquid, remove node and replace it with
|
||||
-- it's drops
|
||||
if n2.name ~= "air" and (not nd or nd.liquidtype == "none") then
|
||||
if nd and nd.buildable_to == false then
|
||||
nd.on_dig(np, n2, nil)
|
||||
-- If it's still there, it might be protected
|
||||
if core.get_node(np).name == n2.name then
|
||||
return false
|
||||
end
|
||||
else
|
||||
core.remove_node(np)
|
||||
end
|
||||
end
|
||||
|
||||
-- Create node
|
||||
local def = core.registered_nodes[self.node.name]
|
||||
if def then
|
||||
core.add_node(np, self.node)
|
||||
if self.meta then
|
||||
core.get_meta(np):from_table(self.meta)
|
||||
end
|
||||
if def.sounds and def.sounds.place then
|
||||
core.sound_play(def.sounds.place, {pos = np}, true)
|
||||
end
|
||||
end
|
||||
core.check_for_falling(np)
|
||||
return true
|
||||
end,
|
||||
|
||||
on_step = function(self, dtime, moveresult)
|
||||
-- Fallback code since collision detection can't tell us
|
||||
-- about liquids (which do not collide)
|
||||
if self.floats then
|
||||
local pos = self.object:get_pos()
|
||||
|
||||
local bcp = pos:offset(0, -0.7, 0):round()
|
||||
local bcn = core.get_node(bcp)
|
||||
|
||||
local bcd = core.registered_nodes[bcn.name]
|
||||
if bcd and bcd.liquidtype ~= "none" then
|
||||
if self:try_place(bcp, bcn) then
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
assert(moveresult)
|
||||
if not moveresult.collides then
|
||||
return -- Nothing to do :)
|
||||
end
|
||||
|
||||
local bcp, bcn
|
||||
local player_collision
|
||||
if moveresult.touching_ground then
|
||||
for _, info in ipairs(moveresult.collisions) do
|
||||
if info.type == "object" then
|
||||
if info.axis == "y" and info.object:is_player() then
|
||||
player_collision = info
|
||||
end
|
||||
elseif info.axis == "y" then
|
||||
bcp = info.node_pos
|
||||
bcn = core.get_node(bcp)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not bcp then
|
||||
-- We're colliding with something, but not the ground. Irrelevant to us.
|
||||
if player_collision then
|
||||
-- Continue falling through players by moving a little into
|
||||
-- their collision box
|
||||
-- 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(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
|
||||
-- Delete on contact with ignore at world edges
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
local failure = false
|
||||
|
||||
local pos = self.object:get_pos()
|
||||
local distance = vector.apply(vector.subtract(pos, bcp), math.abs)
|
||||
if distance.x >= 1 or distance.z >= 1 then
|
||||
-- We're colliding with some part of a node that's sticking out
|
||||
-- Since we don't want to visually teleport, drop as item
|
||||
failure = true
|
||||
elseif distance.y >= 2 then
|
||||
-- Doors consist of a hidden top node and a bottom node that is
|
||||
-- the actual door. Despite the top node being solid, the moveresult
|
||||
-- almost always indicates collision with the bottom node.
|
||||
-- Compensate for this by checking the top node
|
||||
bcp.y = bcp.y + 1
|
||||
bcn = core.get_node(bcp)
|
||||
local def = core.registered_nodes[bcn.name]
|
||||
if not (def and def.walkable) then
|
||||
failure = true -- This is unexpected, fail
|
||||
end
|
||||
end
|
||||
|
||||
-- Try to actually place ourselves
|
||||
if not failure then
|
||||
failure = not self:try_place(bcp, bcn)
|
||||
end
|
||||
|
||||
if failure then
|
||||
local drops = core.get_node_drops(self.node, "")
|
||||
for _, item in pairs(drops) do
|
||||
core.add_item(pos, item)
|
||||
end
|
||||
end
|
||||
self.object:remove()
|
||||
end
|
||||
})
|
||||
|
||||
local function convert_to_falling_node(pos, node)
|
||||
local obj = core.add_entity(pos, "__builtin:falling_node")
|
||||
if not obj then
|
||||
return false
|
||||
end
|
||||
-- remember node level, the entities' set_node() uses this
|
||||
node.level = core.get_node_level(pos)
|
||||
local meta = core.get_meta(pos)
|
||||
local metatable = meta and meta:to_table() or {}
|
||||
|
||||
local def = core.registered_nodes[node.name]
|
||||
if def and def.sounds and def.sounds.fall then
|
||||
core.sound_play(def.sounds.fall, {pos = pos}, true)
|
||||
end
|
||||
|
||||
obj:get_luaentity():set_node(node, metatable)
|
||||
core.remove_node(pos)
|
||||
return true, obj
|
||||
end
|
||||
|
||||
function core.spawn_falling_node(pos)
|
||||
local node = core.get_node(pos)
|
||||
if node.name == "air" or node.name == "ignore" then
|
||||
return false
|
||||
end
|
||||
return convert_to_falling_node(pos, node)
|
||||
end
|
||||
|
||||
local function drop_attached_node(p)
|
||||
local n = core.get_node(p)
|
||||
local drops = core.get_node_drops(n, "")
|
||||
local def = core.registered_items[n.name]
|
||||
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 = vector.copy(p)
|
||||
local node_copy = {name=n.name, param1=n.param1, param2=n.param2}
|
||||
local drop_stacks = {}
|
||||
for k, v in pairs(drops) do
|
||||
drop_stacks[k] = ItemStack(v)
|
||||
end
|
||||
drops = drop_stacks
|
||||
def.preserve_metadata(pos_copy, node_copy, oldmeta, drops)
|
||||
end
|
||||
if def and def.sounds and def.sounds.fall then
|
||||
core.sound_play(def.sounds.fall, {pos = p}, true)
|
||||
end
|
||||
core.remove_node(p)
|
||||
for _, item in pairs(drops) do
|
||||
local pos = {
|
||||
x = p.x + math.random()/2 - 0.25,
|
||||
y = p.y + math.random()/2 - 0.25,
|
||||
z = p.z + math.random()/2 - 0.25,
|
||||
}
|
||||
core.add_item(pos, item)
|
||||
end
|
||||
end
|
||||
|
||||
function builtin_shared.check_attached_node(p, n)
|
||||
local def = core.registered_nodes[n.name]
|
||||
local d = vector.zero()
|
||||
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 vector.new(0, 1, 0)
|
||||
else
|
||||
d.y = -1
|
||||
end
|
||||
local p2 = vector.add(p, d)
|
||||
local nn = core.get_node(p2).name
|
||||
local def2 = core.registered_nodes[nn]
|
||||
if def2 and not def2.walkable then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
-- Some common functions
|
||||
--
|
||||
|
||||
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 = 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]
|
||||
if d_bottom then
|
||||
local same = n.name == n_bottom.name
|
||||
-- Let leveled nodes fall if it can merge with the bottom node
|
||||
if same and d_bottom.paramtype2 == "leveled" and
|
||||
core.get_node_level(p_bottom) <
|
||||
core.get_node_max_level(p_bottom) then
|
||||
convert_to_falling_node(p, n)
|
||||
return true
|
||||
end
|
||||
-- Otherwise only if the bottom node is considered "fall through"
|
||||
if not same and
|
||||
(not d_bottom.walkable or d_bottom.buildable_to) and
|
||||
(core.get_item_group(n.name, "float") == 0 or
|
||||
d_bottom.liquidtype == "none") then
|
||||
convert_to_falling_node(p, n)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if core.get_item_group(n.name, "attached_node") ~= 0 then
|
||||
if not builtin_shared.check_attached_node(p, n) then
|
||||
drop_attached_node(p)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- This table is specifically ordered.
|
||||
-- We don't walk diagonals, only our direct neighbors, and self.
|
||||
-- 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 = {
|
||||
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)
|
||||
-- Round p to prevent falling entities to get stuck.
|
||||
p = vector.round(p)
|
||||
|
||||
-- We make a stack, and manually maintain size for performance.
|
||||
-- Stored in the stack, we will maintain tables with pos, and
|
||||
-- last neighbor visited. This way, when we get back to each
|
||||
-- node, we know which directions we have already walked, and
|
||||
-- which direction is the next to walk.
|
||||
local s = {}
|
||||
local n = 0
|
||||
-- The neighbor order we will visit from our table.
|
||||
local v = 1
|
||||
|
||||
while true do
|
||||
-- Push current pos onto the stack.
|
||||
n = n + 1
|
||||
s[n] = {p = p, v = v}
|
||||
-- Select next node from neighbor list.
|
||||
p = vector.add(p, check_for_falling_neighbors[v])
|
||||
-- Now we check out the node. If it is in need of an update,
|
||||
-- it will let us know in the return value (true = updated).
|
||||
if not core.check_single_for_falling(p) then
|
||||
-- If we don't need to "recurse" (walk) to it then pop
|
||||
-- our previous pos off the stack and continue from there,
|
||||
-- with the v value we were at when we last were at that
|
||||
-- node
|
||||
repeat
|
||||
local pop = s[n]
|
||||
p = pop.p
|
||||
v = pop.v
|
||||
s[n] = nil
|
||||
n = n - 1
|
||||
-- If there's nothing left on the stack, and no
|
||||
-- more sides to walk to, we're done and can exit
|
||||
if n == 0 and v == 11 then
|
||||
return
|
||||
end
|
||||
until v < 11
|
||||
-- The next round walk the next neighbor in list.
|
||||
v = v + 1
|
||||
else
|
||||
-- If we did need to walk the neighbor, then
|
||||
-- start walking it from the walk order start (1),
|
||||
-- and not the order we just pushed up the stack.
|
||||
v = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Global callbacks
|
||||
--
|
||||
|
||||
local function on_placenode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_placenode(on_placenode)
|
||||
|
||||
local function on_dignode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_dignode(on_dignode)
|
||||
|
||||
local function on_punchnode(p, node)
|
||||
core.check_for_falling(p)
|
||||
end
|
||||
core.register_on_punchnode(on_punchnode)
|
||||
46
builtin/game/features.lua
Normal file
46
builtin/game/features.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Minetest: builtin/features.lua
|
||||
|
||||
core.features = {
|
||||
glasslike_framed = true,
|
||||
nodebox_as_selectionbox = true,
|
||||
get_all_craft_recipes_works = true,
|
||||
use_texture_alpha = true,
|
||||
no_legacy_abms = true,
|
||||
texture_names_parens = true,
|
||||
area_store_custom_ids = true,
|
||||
add_entity_with_staticdata = true,
|
||||
no_chat_message_prediction = true,
|
||||
object_use_texture_alpha = true,
|
||||
object_independent_selectionbox = true,
|
||||
httpfetch_binary_data = true,
|
||||
formspec_version_element = true,
|
||||
area_store_persistent_ids = true,
|
||||
pathfinder_works = true,
|
||||
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,
|
||||
particlespawner_tweenable = true,
|
||||
dynamic_add_media_table = true,
|
||||
get_sky_as_table = true,
|
||||
}
|
||||
|
||||
function core.has_feature(arg)
|
||||
if type(arg) == "table" then
|
||||
local missing_features = {}
|
||||
local result = true
|
||||
for ftr in pairs(arg) do
|
||||
if not core.features[ftr] then
|
||||
missing_features[ftr] = true
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result, missing_features
|
||||
elseif type(arg) == "string" then
|
||||
if not core.features[arg] then
|
||||
return false, {[arg]=true}
|
||||
end
|
||||
return true, {}
|
||||
end
|
||||
end
|
||||
126
builtin/game/forceloading.lua
Normal file
126
builtin/game/forceloading.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
-- Prevent anyone else accessing those functions
|
||||
local forceload_block = core.forceload_block
|
||||
local forceload_free_block = core.forceload_free_block
|
||||
core.forceload_block = nil
|
||||
core.forceload_free_block = nil
|
||||
|
||||
local blocks_forceloaded
|
||||
local blocks_temploaded = {}
|
||||
local total_forceloaded = 0
|
||||
|
||||
-- true, if the forceloaded blocks got changed (flag for persistence on-disk)
|
||||
local forceload_blocks_changed = false
|
||||
|
||||
local BLOCKSIZE = core.MAP_BLOCKSIZE
|
||||
local function get_blockpos(pos)
|
||||
return {
|
||||
x = math.floor(pos.x/BLOCKSIZE),
|
||||
y = math.floor(pos.y/BLOCKSIZE),
|
||||
z = math.floor(pos.z/BLOCKSIZE)}
|
||||
end
|
||||
|
||||
-- When we create/free a forceload, it's either transient or persistent. We want
|
||||
-- to add to/remove from the table that corresponds to the type of forceload, but
|
||||
-- we also need the other table because whether we forceload a block depends on
|
||||
-- both tables.
|
||||
-- This function returns the "primary" table we are adding to/removing from, and
|
||||
-- the other table.
|
||||
local function get_relevant_tables(transient)
|
||||
if transient then
|
||||
return blocks_temploaded, blocks_forceloaded
|
||||
else
|
||||
return blocks_forceloaded, blocks_temploaded
|
||||
end
|
||||
end
|
||||
|
||||
function core.forceload_block(pos, transient)
|
||||
-- set changed flag
|
||||
forceload_blocks_changed = true
|
||||
|
||||
local blockpos = get_blockpos(pos)
|
||||
local hash = core.hash_node_position(blockpos)
|
||||
local relevant_table, other_table = get_relevant_tables(transient)
|
||||
if relevant_table[hash] ~= nil then
|
||||
relevant_table[hash] = relevant_table[hash] + 1
|
||||
return true
|
||||
elseif other_table[hash] ~= nil then
|
||||
relevant_table[hash] = 1
|
||||
else
|
||||
if total_forceloaded >= (tonumber(core.settings:get("max_forceloaded_blocks")) or 16) then
|
||||
return false
|
||||
end
|
||||
total_forceloaded = total_forceloaded+1
|
||||
relevant_table[hash] = 1
|
||||
forceload_block(blockpos)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function core.forceload_free_block(pos, transient)
|
||||
-- set changed flag
|
||||
forceload_blocks_changed = true
|
||||
|
||||
local blockpos = get_blockpos(pos)
|
||||
local hash = core.hash_node_position(blockpos)
|
||||
local relevant_table, other_table = get_relevant_tables(transient)
|
||||
if relevant_table[hash] == nil then return end
|
||||
if relevant_table[hash] > 1 then
|
||||
relevant_table[hash] = relevant_table[hash] - 1
|
||||
elseif other_table[hash] ~= nil then
|
||||
relevant_table[hash] = nil
|
||||
else
|
||||
total_forceloaded = total_forceloaded-1
|
||||
relevant_table[hash] = nil
|
||||
forceload_free_block(blockpos)
|
||||
end
|
||||
end
|
||||
|
||||
-- Keep the forceloaded areas after restart
|
||||
local wpath = core.get_worldpath()
|
||||
local function read_file(filename)
|
||||
local f = io.open(filename, "r")
|
||||
if f==nil then return {} end
|
||||
local t = f:read("*all")
|
||||
f:close()
|
||||
if t=="" or t==nil then return {} end
|
||||
return core.deserialize(t) or {}
|
||||
end
|
||||
|
||||
blocks_forceloaded = read_file(wpath.."/force_loaded.txt")
|
||||
for _, __ in pairs(blocks_forceloaded) do
|
||||
total_forceloaded = total_forceloaded + 1
|
||||
end
|
||||
|
||||
core.after(5, function()
|
||||
for hash, _ in pairs(blocks_forceloaded) do
|
||||
local blockpos = core.get_position_from_hash(hash)
|
||||
forceload_block(blockpos)
|
||||
end
|
||||
end)
|
||||
|
||||
-- persists the currently forceloaded blocks to disk
|
||||
local function persist_forceloaded_blocks()
|
||||
local data = core.serialize(blocks_forceloaded)
|
||||
core.safe_file_write(wpath.."/force_loaded.txt", data)
|
||||
end
|
||||
|
||||
-- periodical forceload persistence
|
||||
local function periodically_persist_forceloaded_blocks()
|
||||
|
||||
-- only persist if the blocks actually changed
|
||||
if forceload_blocks_changed then
|
||||
persist_forceloaded_blocks()
|
||||
|
||||
-- reset changed flag
|
||||
forceload_blocks_changed = false
|
||||
end
|
||||
|
||||
-- recheck after some time
|
||||
core.after(10, periodically_persist_forceloaded_blocks)
|
||||
end
|
||||
|
||||
-- persist periodically
|
||||
core.after(5, periodically_persist_forceloaded_blocks)
|
||||
|
||||
-- persist on shutdown
|
||||
core.register_on_shutdown(persist_forceloaded_blocks)
|
||||
40
builtin/game/init.lua
Normal file
40
builtin/game/init.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
local scriptpath = core.get_builtin_path()
|
||||
local commonpath = scriptpath .. "common" .. DIR_DELIM
|
||||
local gamepath = scriptpath .. "game".. DIR_DELIM
|
||||
|
||||
-- Shared between builtin files, but
|
||||
-- not exposed to outer context
|
||||
local builtin_shared = {}
|
||||
|
||||
dofile(gamepath .. "constants.lua")
|
||||
dofile(gamepath .. "item_s.lua")
|
||||
assert(loadfile(gamepath .. "item.lua"))(builtin_shared)
|
||||
dofile(gamepath .. "register.lua")
|
||||
|
||||
if core.settings:get_bool("profiler.load") then
|
||||
profiler = dofile(scriptpath .. "profiler" .. DIR_DELIM .. "init.lua")
|
||||
end
|
||||
|
||||
dofile(commonpath .. "after.lua")
|
||||
dofile(commonpath .. "mod_storage.lua")
|
||||
dofile(gamepath .. "item_entity.lua")
|
||||
dofile(gamepath .. "deprecated.lua")
|
||||
dofile(gamepath .. "misc_s.lua")
|
||||
dofile(gamepath .. "misc.lua")
|
||||
dofile(gamepath .. "privileges.lua")
|
||||
dofile(gamepath .. "auth.lua")
|
||||
dofile(commonpath .. "chatcommands.lua")
|
||||
dofile(gamepath .. "chat.lua")
|
||||
dofile(commonpath .. "information_formspecs.lua")
|
||||
dofile(gamepath .. "static_spawn.lua")
|
||||
dofile(gamepath .. "detached_inventory.lua")
|
||||
assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
|
||||
dofile(gamepath .. "features.lua")
|
||||
dofile(gamepath .. "voxelarea.lua")
|
||||
dofile(gamepath .. "forceloading.lua")
|
||||
dofile(gamepath .. "statbars.lua")
|
||||
dofile(gamepath .. "knockback.lua")
|
||||
dofile(gamepath .. "async.lua")
|
||||
|
||||
profiler = nil
|
||||
680
builtin/game/item.lua
Normal file
680
builtin/game/item.lua
Normal file
@@ -0,0 +1,680 @@
|
||||
-- Minetest: builtin/item.lua
|
||||
|
||||
local builtin_shared = ...
|
||||
|
||||
local function copy_pointed_thing(pointed_thing)
|
||||
return {
|
||||
type = pointed_thing.type,
|
||||
above = pointed_thing.above and vector.copy(pointed_thing.above),
|
||||
under = pointed_thing.under and vector.copy(pointed_thing.under),
|
||||
ref = pointed_thing.ref,
|
||||
}
|
||||
end
|
||||
|
||||
--
|
||||
-- Item definition helpers
|
||||
--
|
||||
|
||||
function core.get_pointed_thing_position(pointed_thing, above)
|
||||
if pointed_thing.type == "node" then
|
||||
if above then
|
||||
-- The position where a node would be placed
|
||||
return pointed_thing.above
|
||||
end
|
||||
-- The position where a node would be dug
|
||||
return pointed_thing.under
|
||||
elseif pointed_thing.type == "object" then
|
||||
return pointed_thing.ref and pointed_thing.ref:get_pos()
|
||||
end
|
||||
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
|
||||
local param2 = 0
|
||||
-- New format, if node is table
|
||||
if (type(node) == "table") then
|
||||
nodename = node.name
|
||||
param2 = node.param2
|
||||
end
|
||||
local def = core.registered_nodes[nodename]
|
||||
local drop = def and def.drop
|
||||
local ptype = def and def.paramtype2
|
||||
-- get color, if there is color (otherwise nil)
|
||||
local palette_index = core.strip_param2_color(param2, ptype)
|
||||
if drop == nil then
|
||||
-- default drop
|
||||
if palette_index then
|
||||
local stack = ItemStack(nodename)
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
return {stack:to_string()}
|
||||
end
|
||||
return {nodename}
|
||||
elseif type(drop) == "string" then
|
||||
-- itemstring drop
|
||||
return drop ~= "" and {drop} or {}
|
||||
elseif drop.items == nil then
|
||||
-- drop = {} to disable default drop
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Extended drop table
|
||||
local got_items = {}
|
||||
local got_count = 0
|
||||
for _, item in ipairs(drop.items) do
|
||||
local good_rarity = true
|
||||
local good_tool = true
|
||||
if item.rarity ~= nil then
|
||||
good_rarity = item.rarity < 1 or math.random(item.rarity) == 1
|
||||
end
|
||||
if item.tools ~= nil or item.tool_groups ~= nil then
|
||||
good_tool = false
|
||||
end
|
||||
if item.tools ~= nil and toolname then
|
||||
for _, tool in ipairs(item.tools) do
|
||||
if tool:sub(1, 1) == '~' then
|
||||
good_tool = toolname:find(tool:sub(2)) ~= nil
|
||||
else
|
||||
good_tool = toolname == tool
|
||||
end
|
||||
if good_tool then
|
||||
break
|
||||
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
|
||||
-- add color, if necessary
|
||||
if item.inherit_color and palette_index then
|
||||
local stack = ItemStack(add_item)
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
add_item = stack:to_string()
|
||||
end
|
||||
got_items[#got_items+1] = add_item
|
||||
end
|
||||
if drop.max_items ~= nil and got_count == drop.max_items then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return got_items
|
||||
end
|
||||
|
||||
local function user_name(user)
|
||||
return user and user:get_player_name() or ""
|
||||
end
|
||||
|
||||
-- Returns a logging function. For empty names, does not log.
|
||||
local function make_log(name)
|
||||
return name ~= "" and core.log or function() end
|
||||
end
|
||||
|
||||
function core.item_place_node(itemstack, placer, pointed_thing, param2,
|
||||
prevent_after_place)
|
||||
local def = itemstack:get_definition()
|
||||
if def.type ~= "node" or pointed_thing.type ~= "node" then
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local under = pointed_thing.under
|
||||
local oldnode_under = core.get_node_or_nil(under)
|
||||
local above = pointed_thing.above
|
||||
local oldnode_above = core.get_node_or_nil(above)
|
||||
local playername = user_name(placer)
|
||||
local log = make_log(playername)
|
||||
|
||||
if not oldnode_under or not oldnode_above then
|
||||
log("info", playername .. " tried to place"
|
||||
.. " node in unloaded position " .. core.pos_to_string(above))
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local olddef_under = core.registered_nodes[oldnode_under.name]
|
||||
olddef_under = olddef_under or core.nodedef_default
|
||||
local olddef_above = core.registered_nodes[oldnode_above.name]
|
||||
olddef_above = olddef_above or core.nodedef_default
|
||||
|
||||
if not olddef_above.buildable_to and not olddef_under.buildable_to then
|
||||
log("info", playername .. " tried to place"
|
||||
.. " node in invalid position " .. core.pos_to_string(above)
|
||||
.. ", replacing " .. oldnode_above.name)
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
-- Place above pointed node
|
||||
local place_to = vector.copy(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 = vector.copy(under)
|
||||
end
|
||||
|
||||
if core.is_protected(place_to, playername) then
|
||||
log("action", playername
|
||||
.. " tried to place " .. def.name
|
||||
.. " at protected position "
|
||||
.. core.pos_to_string(place_to))
|
||||
core.record_protection_violation(place_to, playername)
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
local oldnode = core.get_node(place_to)
|
||||
local newnode = {name = def.name, param1 = 0, param2 = param2 or 0}
|
||||
|
||||
-- Calculate direction for wall mounted stuff like torches and signs
|
||||
if def.place_param2 ~= nil then
|
||||
newnode.param2 = def.place_param2
|
||||
elseif (def.paramtype2 == "wallmounted" or
|
||||
def.paramtype2 == "colorwallmounted") and not param2 then
|
||||
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 = vector.subtract(above, placer_pos)
|
||||
newnode.param2 = core.dir_to_facedir(dir)
|
||||
log("info", "facedir: " .. newnode.param2)
|
||||
end
|
||||
end
|
||||
|
||||
local metatable = itemstack:get_meta():to_table().fields
|
||||
|
||||
-- Transfer color information
|
||||
if metatable.palette_index and not def.place_param2 then
|
||||
local color_divisor = nil
|
||||
if def.paramtype2 == "color" then
|
||||
color_divisor = 1
|
||||
elseif def.paramtype2 == "colorwallmounted" then
|
||||
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)
|
||||
local other = newnode.param2 % color_divisor
|
||||
newnode.param2 = color * color_divisor + other
|
||||
end
|
||||
end
|
||||
|
||||
-- Check if the node is attached and if it can be placed there
|
||||
if core.get_item_group(def.name, "attached_node") ~= 0 and
|
||||
not builtin_shared.check_attached_node(place_to, newnode) then
|
||||
log("action", "attached node " .. def.name ..
|
||||
" can not be placed at " .. core.pos_to_string(place_to))
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
log("action", playername .. " places node "
|
||||
.. def.name .. " at " .. core.pos_to_string(place_to))
|
||||
|
||||
-- Add node and update
|
||||
core.add_node(place_to, newnode)
|
||||
|
||||
-- Play sound if it was done by a player
|
||||
if playername ~= "" and def.sounds and def.sounds.place then
|
||||
core.sound_play(def.sounds.place, {
|
||||
pos = place_to,
|
||||
exclude_player = playername,
|
||||
}, true)
|
||||
end
|
||||
|
||||
local take_item = true
|
||||
|
||||
-- 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 = vector.copy(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
|
||||
take_item = false
|
||||
end
|
||||
end
|
||||
|
||||
-- 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 = vector.copy(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)
|
||||
if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then
|
||||
take_item = false
|
||||
end
|
||||
end
|
||||
|
||||
if take_item then
|
||||
itemstack:take_item()
|
||||
end
|
||||
return itemstack, place_to
|
||||
end
|
||||
|
||||
-- deprecated, item_place does not call this
|
||||
function core.item_place_object(itemstack, placer, pointed_thing)
|
||||
local pos = core.get_pointed_thing_position(pointed_thing, true)
|
||||
if pos ~= nil then
|
||||
local item = itemstack:take_item()
|
||||
core.add_item(pos, item)
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
function core.item_place(itemstack, placer, pointed_thing, param2)
|
||||
-- Call on_rightclick if the pointed node defines it
|
||||
if pointed_thing.type == "node" and placer and
|
||||
not placer:get_player_control().sneak then
|
||||
local n = core.get_node(pointed_thing.under)
|
||||
local nn = n.name
|
||||
if core.registered_nodes[nn] and core.registered_nodes[nn].on_rightclick then
|
||||
return core.registered_nodes[nn].on_rightclick(pointed_thing.under, n,
|
||||
placer, itemstack, pointed_thing) or itemstack, nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Place if node, otherwise do nothing
|
||||
if itemstack:get_definition().type == "node" then
|
||||
return core.item_place_node(itemstack, placer, pointed_thing, param2)
|
||||
end
|
||||
return itemstack, nil
|
||||
end
|
||||
|
||||
function core.item_secondary_use(itemstack, placer)
|
||||
return itemstack
|
||||
end
|
||||
|
||||
function core.item_drop(itemstack, dropper, pos)
|
||||
local dropper_is_player = dropper and dropper:is_player()
|
||||
local p = table.copy(pos)
|
||||
local cnt = itemstack:get_count()
|
||||
if dropper_is_player then
|
||||
p.y = p.y + 1.2
|
||||
end
|
||||
local item = itemstack:take_item(cnt)
|
||||
local obj = core.add_item(p, item)
|
||||
if obj then
|
||||
if dropper_is_player then
|
||||
local dir = dropper:get_look_dir()
|
||||
dir.x = dir.x * 2.9
|
||||
dir.y = dir.y * 2.9 + 2
|
||||
dir.z = dir.z * 2.9
|
||||
obj:set_velocity(dir)
|
||||
obj:get_luaentity().dropped_by = dropper:get_player_name()
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
-- If we reach this, adding the object to the
|
||||
-- environment failed
|
||||
end
|
||||
|
||||
function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
||||
for _, callback in pairs(core.registered_on_item_eats) do
|
||||
local result = callback(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
-- read definition before potentially emptying the stack
|
||||
local def = itemstack:get_definition()
|
||||
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
|
||||
|
||||
-- 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 pos = user:get_pos()
|
||||
pos.y = math.floor(pos.y + 0.5)
|
||||
core.add_item(pos, replace_with_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
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)
|
||||
return function(itemstack, user, pointed_thing) -- closure
|
||||
if user then
|
||||
return core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function core.node_punch(pos, node, puncher, pointed_thing)
|
||||
-- Run script hook
|
||||
for _, callback in ipairs(core.registered_on_punchnodes) do
|
||||
-- Copy pos and node because callback can modify them
|
||||
local pos_copy = vector.copy(pos)
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
local pointed_thing_copy = pointed_thing and copy_pointed_thing(pointed_thing) or nil
|
||||
callback(pos_copy, node_copy, puncher, pointed_thing_copy)
|
||||
end
|
||||
end
|
||||
|
||||
function core.handle_node_drops(pos, drops, digger)
|
||||
-- Add dropped items to object's inventory
|
||||
local inv = digger and digger:get_inventory()
|
||||
local give_item
|
||||
if inv then
|
||||
give_item = function(item)
|
||||
return inv:add_item("main", item)
|
||||
end
|
||||
else
|
||||
give_item = function(item)
|
||||
-- itemstring to ItemStack for left:is_empty()
|
||||
return ItemStack(item)
|
||||
end
|
||||
end
|
||||
|
||||
for _, dropped_item in pairs(drops) do
|
||||
local left = give_item(dropped_item)
|
||||
if not left:is_empty() then
|
||||
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
|
||||
end
|
||||
|
||||
function core.node_dig(pos, node, digger)
|
||||
local diggername = user_name(digger)
|
||||
local log = make_log(diggername)
|
||||
local def = core.registered_nodes[node.name]
|
||||
-- Copy pos because the callback could modify it
|
||||
if def and (not def.diggable or
|
||||
(def.can_dig and not def.can_dig(vector.copy(pos), digger))) then
|
||||
log("info", diggername .. " tried to dig "
|
||||
.. node.name .. " which is not diggable "
|
||||
.. core.pos_to_string(pos))
|
||||
return false
|
||||
end
|
||||
|
||||
if core.is_protected(pos, diggername) then
|
||||
log("action", diggername
|
||||
.. " tried to dig " .. node.name
|
||||
.. " at protected position "
|
||||
.. core.pos_to_string(pos))
|
||||
core.record_protection_violation(pos, diggername)
|
||||
return false
|
||||
end
|
||||
|
||||
log('action', diggername .. " digs "
|
||||
.. node.name .. " at " .. core.pos_to_string(pos))
|
||||
|
||||
local wielded = digger and digger:get_wielded_item()
|
||||
local drops = core.get_node_drops(node, wielded and wielded:get_name())
|
||||
|
||||
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, wielded:get_wear())
|
||||
if wdef and wdef.after_use then
|
||||
wielded = wdef.after_use(wielded, digger, node, dp) or wielded
|
||||
else
|
||||
-- Wear out tool
|
||||
if not core.is_creative_enabled(diggername) then
|
||||
wielded:add_wear(dp.wear)
|
||||
if wielded:get_count() == 0 and wdef.sound and wdef.sound.breaks then
|
||||
core.sound_play(wdef.sound.breaks, {
|
||||
pos = pos,
|
||||
gain = 0.5
|
||||
}, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
digger:set_wielded_item(wielded)
|
||||
end
|
||||
|
||||
-- Check to see if metadata should be preserved.
|
||||
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 = vector.copy(pos)
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
local drop_stacks = {}
|
||||
for k, v in pairs(drops) do
|
||||
drop_stacks[k] = ItemStack(v)
|
||||
end
|
||||
drops = drop_stacks
|
||||
def.preserve_metadata(pos_copy, node_copy, oldmeta, drops)
|
||||
end
|
||||
|
||||
-- Handle drops
|
||||
core.handle_node_drops(pos, drops, digger)
|
||||
|
||||
local oldmetadata = nil
|
||||
if def and def.after_dig_node then
|
||||
oldmetadata = core.get_meta(pos):to_table()
|
||||
end
|
||||
|
||||
-- Remove node and update
|
||||
core.remove_node(pos)
|
||||
|
||||
-- Play sound if it was done by a player
|
||||
if diggername ~= "" and def and def.sounds and def.sounds.dug then
|
||||
core.sound_play(def.sounds.dug, {
|
||||
pos = pos,
|
||||
exclude_player = diggername,
|
||||
}, true)
|
||||
end
|
||||
|
||||
-- Run callback
|
||||
if def and def.after_dig_node then
|
||||
-- Copy pos and node because callback can modify them
|
||||
local pos_copy = vector.copy(pos)
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
def.after_dig_node(pos_copy, node_copy, oldmetadata, digger)
|
||||
end
|
||||
|
||||
-- Run script hook
|
||||
for _, callback in ipairs(core.registered_on_dignodes) do
|
||||
local origin = core.callback_origins[callback]
|
||||
core.set_last_run_mod(origin.mod)
|
||||
|
||||
-- Copy pos and node because callback can modify them
|
||||
local pos_copy = vector.copy(pos)
|
||||
local node_copy = {name=node.name, param1=node.param1, param2=node.param2}
|
||||
callback(pos_copy, node_copy, digger)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function core.itemstring_with_palette(item, palette_index)
|
||||
local stack = ItemStack(item) -- convert to ItemStack
|
||||
stack:get_meta():set_int("palette_index", palette_index)
|
||||
return stack:to_string()
|
||||
end
|
||||
|
||||
function core.itemstring_with_color(item, colorstring)
|
||||
local stack = ItemStack(item) -- convert to ItemStack
|
||||
stack:get_meta():set_string("color", colorstring)
|
||||
return stack:to_string()
|
||||
end
|
||||
|
||||
-- This is used to allow mods to redefine core.item_place and so on
|
||||
-- NOTE: This is not the preferred way. Preferred way is to provide enough
|
||||
-- callbacks to not require redefining global functions. -celeron55
|
||||
local function redef_wrapper(table, name)
|
||||
return function(...)
|
||||
return table[name](...)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Item definition defaults
|
||||
--
|
||||
|
||||
local default_stack_max = tonumber(core.settings:get("default_stack_max")) or 99
|
||||
|
||||
core.nodedef_default = {
|
||||
-- Item properties
|
||||
type="node",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = vector.new(1, 1, 1),
|
||||
stack_max = default_stack_max,
|
||||
usable = false,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
node_placement_prediction = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_use = nil,
|
||||
can_dig = nil,
|
||||
|
||||
on_punch = redef_wrapper(core, 'node_punch'), -- core.node_punch
|
||||
on_rightclick = nil,
|
||||
on_dig = redef_wrapper(core, 'node_dig'), -- core.node_dig
|
||||
|
||||
on_receive_fields = nil,
|
||||
|
||||
-- Node properties
|
||||
drawtype = "normal",
|
||||
visual_scale = 1.0,
|
||||
tiles = nil,
|
||||
special_tiles = nil,
|
||||
post_effect_color = {a=0, r=0, g=0, b=0},
|
||||
paramtype = "none",
|
||||
paramtype2 = "none",
|
||||
is_ground_content = true,
|
||||
sunlight_propagates = false,
|
||||
walkable = true,
|
||||
pointable = true,
|
||||
diggable = true,
|
||||
climbable = false,
|
||||
buildable_to = false,
|
||||
floodable = false,
|
||||
liquidtype = "none",
|
||||
liquid_alternative_flowing = "",
|
||||
liquid_alternative_source = "",
|
||||
liquid_viscosity = 0,
|
||||
drowning = 0,
|
||||
light_source = 0,
|
||||
damage_per_second = 0,
|
||||
selection_box = {type="regular"},
|
||||
legacy_facedir_simple = false,
|
||||
legacy_wallmounted = false,
|
||||
}
|
||||
|
||||
core.craftitemdef_default = {
|
||||
type="craft",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = vector.new(1, 1, 1),
|
||||
stack_max = default_stack_max,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_use = nil,
|
||||
}
|
||||
|
||||
core.tooldef_default = {
|
||||
type="tool",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = vector.new(1, 1, 1),
|
||||
stack_max = 1,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'), -- core.item_place
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop
|
||||
on_use = nil,
|
||||
}
|
||||
|
||||
core.noneitemdef_default = { -- This is used for the hand and unknown items
|
||||
type="none",
|
||||
-- name intentionally not defined here
|
||||
description = "",
|
||||
groups = {},
|
||||
inventory_image = "",
|
||||
wield_image = "",
|
||||
wield_scale = vector.new(1, 1, 1),
|
||||
stack_max = default_stack_max,
|
||||
liquids_pointable = false,
|
||||
tool_capabilities = nil,
|
||||
|
||||
-- Interaction callbacks
|
||||
on_place = redef_wrapper(core, 'item_place'),
|
||||
on_secondary_use = redef_wrapper(core, 'item_secondary_use'),
|
||||
on_drop = nil,
|
||||
on_use = nil,
|
||||
}
|
||||
333
builtin/game/item_entity.lua
Normal file
333
builtin/game/item_entity.lua
Normal file
@@ -0,0 +1,333 @@
|
||||
-- Minetest: builtin/item_entity.lua
|
||||
|
||||
function core.spawn_item(pos, item)
|
||||
-- Take item in any format
|
||||
local stack = ItemStack(item)
|
||||
local obj = core.add_entity(pos, "__builtin:item")
|
||||
-- Don't use obj if it couldn't be added to the map.
|
||||
if obj then
|
||||
obj:get_luaentity():set_item(stack:to_string())
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
-- If item_entity_ttl is not set, enity will have default life time
|
||||
-- Setting it to -1 disables the feature
|
||||
|
||||
local time_to_live = tonumber(core.settings:get("item_entity_ttl")) or 900
|
||||
local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
|
||||
|
||||
|
||||
core.register_entity(":__builtin:item", {
|
||||
initial_properties = {
|
||||
hp_max = 1,
|
||||
physical = true,
|
||||
collide_with_objects = false,
|
||||
collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3},
|
||||
visual = "wielditem",
|
||||
visual_size = {x = 0.4, y = 0.4},
|
||||
textures = {""},
|
||||
is_visible = false,
|
||||
},
|
||||
|
||||
itemstring = "",
|
||||
moving_state = true,
|
||||
physical_state = true,
|
||||
-- Item expiry
|
||||
age = 0,
|
||||
-- Pushing item out of solid nodes
|
||||
force_out = nil,
|
||||
force_out_start = nil,
|
||||
|
||||
set_item = function(self, item)
|
||||
local stack = ItemStack(item or self.itemstring)
|
||||
self.itemstring = stack:to_string()
|
||||
if self.itemstring == "" then
|
||||
-- item not yet known
|
||||
return
|
||||
end
|
||||
|
||||
-- Backwards compatibility: old clients use the texture
|
||||
-- to get the type of the item
|
||||
local itemname = stack:is_known() and stack:get_name() or "unknown"
|
||||
|
||||
local max_count = stack:get_stack_max()
|
||||
local count = math.min(stack:get_count(), max_count)
|
||||
local size = 0.2 + 0.1 * (count / max_count) ^ (1 / 3)
|
||||
local def = core.registered_items[itemname]
|
||||
local glow = def and def.light_source and
|
||||
math.floor(def.light_source / 2 + 0.5)
|
||||
|
||||
local size_bias = 1e-3 * math.random() -- small random bias to counter Z-fighting
|
||||
local c = {-size, -size, -size, size, size, size}
|
||||
self.object:set_properties({
|
||||
is_visible = true,
|
||||
visual = "wielditem",
|
||||
textures = {itemname},
|
||||
visual_size = {x = size + size_bias, y = size + size_bias},
|
||||
collisionbox = c,
|
||||
automatic_rotate = math.pi * 0.5 * 0.2 / size,
|
||||
wield_item = self.itemstring,
|
||||
glow = glow,
|
||||
})
|
||||
|
||||
-- cache for usage in on_step
|
||||
self._collisionbox = c
|
||||
end,
|
||||
|
||||
get_staticdata = function(self)
|
||||
return core.serialize({
|
||||
itemstring = self.itemstring,
|
||||
age = self.age,
|
||||
dropped_by = self.dropped_by
|
||||
})
|
||||
end,
|
||||
|
||||
on_activate = function(self, staticdata, dtime_s)
|
||||
if string.sub(staticdata, 1, string.len("return")) == "return" then
|
||||
local data = core.deserialize(staticdata)
|
||||
if data and type(data) == "table" then
|
||||
self.itemstring = data.itemstring
|
||||
self.age = (data.age or 0) + dtime_s
|
||||
self.dropped_by = data.dropped_by
|
||||
end
|
||||
else
|
||||
self.itemstring = staticdata
|
||||
end
|
||||
self.object:set_armor_groups({immortal = 1})
|
||||
self.object:set_velocity({x = 0, y = 2, z = 0})
|
||||
self.object:set_acceleration({x = 0, y = -gravity, z = 0})
|
||||
self._collisionbox = self.initial_properties.collisionbox
|
||||
self:set_item()
|
||||
end,
|
||||
|
||||
try_merge_with = function(self, own_stack, object, entity)
|
||||
if self.age == entity.age then
|
||||
-- Can not merge with itself
|
||||
return false
|
||||
end
|
||||
|
||||
local stack = ItemStack(entity.itemstring)
|
||||
local name = stack:get_name()
|
||||
if own_stack:get_name() ~= name or
|
||||
own_stack:get_meta() ~= stack:get_meta() or
|
||||
own_stack:get_wear() ~= stack:get_wear() or
|
||||
own_stack:get_free_space() == 0 then
|
||||
-- Can not merge different or full stack
|
||||
return false
|
||||
end
|
||||
|
||||
local count = own_stack:get_count()
|
||||
local total_count = stack:get_count() + count
|
||||
local max_count = stack:get_stack_max()
|
||||
|
||||
if total_count > max_count then
|
||||
return false
|
||||
end
|
||||
-- Merge the remote stack into this one
|
||||
|
||||
local pos = object:get_pos()
|
||||
pos.y = pos.y + ((total_count - count) / max_count) * 0.15
|
||||
self.object:move_to(pos)
|
||||
|
||||
self.age = 0 -- Handle as new entity
|
||||
own_stack:set_count(total_count)
|
||||
self:set_item(own_stack)
|
||||
|
||||
entity.itemstring = ""
|
||||
object:remove()
|
||||
return true
|
||||
end,
|
||||
|
||||
enable_physics = function(self)
|
||||
if not self.physical_state then
|
||||
self.physical_state = true
|
||||
self.object:set_properties({physical = true})
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
self.object:set_acceleration({x=0, y=-gravity, z=0})
|
||||
end
|
||||
end,
|
||||
|
||||
disable_physics = function(self)
|
||||
if self.physical_state then
|
||||
self.physical_state = false
|
||||
self.object:set_properties({physical = false})
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
self.object:set_acceleration({x=0, y=0, z=0})
|
||||
end
|
||||
end,
|
||||
|
||||
on_step = function(self, dtime, moveresult)
|
||||
self.age = self.age + dtime
|
||||
if time_to_live > 0 and self.age > time_to_live then
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
local pos = self.object:get_pos()
|
||||
local node = core.get_node_or_nil({
|
||||
x = pos.x,
|
||||
y = pos.y + self._collisionbox[2] - 0.05,
|
||||
z = pos.z
|
||||
})
|
||||
-- Delete in 'ignore' nodes
|
||||
if node and node.name == "ignore" then
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
|
||||
if self.force_out then
|
||||
-- This code runs after the entity got a push from the is_stuck code.
|
||||
-- It makes sure the entity is entirely outside the solid node
|
||||
local c = self._collisionbox
|
||||
local s = self.force_out_start
|
||||
local f = self.force_out
|
||||
local ok = (f.x > 0 and pos.x + c[1] > s.x + 0.5) or
|
||||
(f.y > 0 and pos.y + c[2] > s.y + 0.5) or
|
||||
(f.z > 0 and pos.z + c[3] > s.z + 0.5) or
|
||||
(f.x < 0 and pos.x + c[4] < s.x - 0.5) or
|
||||
(f.z < 0 and pos.z + c[6] < s.z - 0.5)
|
||||
if ok then
|
||||
-- Item was successfully forced out
|
||||
self.force_out = nil
|
||||
self:enable_physics()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not self.physical_state then
|
||||
return -- Don't do anything
|
||||
end
|
||||
|
||||
assert(moveresult,
|
||||
"Collision info missing, this is caused by an out-of-date/buggy mod or game")
|
||||
|
||||
if not moveresult.collides then
|
||||
-- future TODO: items should probably decelerate in air
|
||||
return
|
||||
end
|
||||
|
||||
-- Push item out when stuck inside solid node
|
||||
local is_stuck = false
|
||||
local snode = core.get_node_or_nil(pos)
|
||||
if snode then
|
||||
local sdef = core.registered_nodes[snode.name] or {}
|
||||
is_stuck = (sdef.walkable == nil or sdef.walkable == true)
|
||||
and (sdef.collision_box == nil or sdef.collision_box.type == "regular")
|
||||
and (sdef.node_box == nil or sdef.node_box.type == "regular")
|
||||
end
|
||||
|
||||
if is_stuck then
|
||||
local shootdir
|
||||
local order = {
|
||||
{x=1, y=0, z=0}, {x=-1, y=0, z= 0},
|
||||
{x=0, y=0, z=1}, {x= 0, y=0, z=-1},
|
||||
}
|
||||
|
||||
-- Check which one of the 4 sides is free
|
||||
for o = 1, #order do
|
||||
local cnode = core.get_node(vector.add(pos, order[o])).name
|
||||
local cdef = core.registered_nodes[cnode] or {}
|
||||
if cnode ~= "ignore" and cdef.walkable == false then
|
||||
shootdir = order[o]
|
||||
break
|
||||
end
|
||||
end
|
||||
-- If none of the 4 sides is free, check upwards
|
||||
if not shootdir then
|
||||
shootdir = {x=0, y=1, z=0}
|
||||
local cnode = core.get_node(vector.add(pos, shootdir)).name
|
||||
if cnode == "ignore" then
|
||||
shootdir = nil -- Do not push into ignore
|
||||
end
|
||||
end
|
||||
|
||||
if shootdir then
|
||||
-- Set new item moving speed accordingly
|
||||
local newv = vector.multiply(shootdir, 3)
|
||||
self:disable_physics()
|
||||
self.object:set_velocity(newv)
|
||||
|
||||
self.force_out = newv
|
||||
self.force_out_start = vector.round(pos)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
node = nil -- ground node we're colliding with
|
||||
if moveresult.touching_ground then
|
||||
for _, info in ipairs(moveresult.collisions) do
|
||||
if info.axis == "y" then
|
||||
node = core.get_node(info.node_pos)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Slide on slippery nodes
|
||||
local def = node and core.registered_nodes[node.name]
|
||||
local keep_movement = false
|
||||
|
||||
if def then
|
||||
local slippery = core.get_item_group(node.name, "slippery")
|
||||
local vel = self.object:get_velocity()
|
||||
if slippery ~= 0 and (math.abs(vel.x) > 0.1 or math.abs(vel.z) > 0.1) then
|
||||
-- Horizontal deceleration
|
||||
local factor = math.min(4 / (slippery + 4) * dtime, 1)
|
||||
self.object:set_velocity({
|
||||
x = vel.x * (1 - factor),
|
||||
y = 0,
|
||||
z = vel.z * (1 - factor)
|
||||
})
|
||||
keep_movement = true
|
||||
end
|
||||
end
|
||||
|
||||
if not keep_movement then
|
||||
self.object:set_velocity({x=0, y=0, z=0})
|
||||
end
|
||||
|
||||
if self.moving_state == keep_movement then
|
||||
-- Do not update anything until the moving state changes
|
||||
return
|
||||
end
|
||||
self.moving_state = keep_movement
|
||||
|
||||
-- Only collect items if not moving
|
||||
if self.moving_state then
|
||||
return
|
||||
end
|
||||
-- Collect the items around to merge with
|
||||
local own_stack = ItemStack(self.itemstring)
|
||||
if own_stack:get_free_space() == 0 then
|
||||
return
|
||||
end
|
||||
local objects = core.get_objects_inside_radius(pos, 1.0)
|
||||
for k, obj in pairs(objects) do
|
||||
local entity = obj:get_luaentity()
|
||||
if entity and entity.name == "__builtin:item" then
|
||||
if self:try_merge_with(own_stack, obj, entity) then
|
||||
own_stack = ItemStack(self.itemstring)
|
||||
if own_stack:get_free_space() == 0 then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
on_punch = function(self, hitter)
|
||||
local inv = hitter:get_inventory()
|
||||
if inv and self.itemstring ~= "" then
|
||||
local left = inv:add_item("main", self.itemstring)
|
||||
if left and not left:is_empty() then
|
||||
self:set_item(left)
|
||||
return
|
||||
end
|
||||
end
|
||||
self.itemstring = ""
|
||||
self.object:remove()
|
||||
end,
|
||||
})
|
||||
156
builtin/game/item_s.lua
Normal file
156
builtin/game/item_s.lua
Normal file
@@ -0,0 +1,156 @@
|
||||
-- Minetest: builtin/item_s.lua
|
||||
-- The distinction of what goes here is a bit tricky, basically it's everything
|
||||
-- that does not (directly or indirectly) need access to ServerEnvironment,
|
||||
-- Server or writable access to IGameDef on the engine side.
|
||||
-- (The '_s' stands for standalone.)
|
||||
|
||||
--
|
||||
-- Item definition helpers
|
||||
--
|
||||
|
||||
function core.inventorycube(img1, img2, img3)
|
||||
img2 = img2 or img1
|
||||
img3 = img3 or img1
|
||||
return "[inventorycube"
|
||||
.. "{" .. img1:gsub("%^", "&")
|
||||
.. "{" .. img2:gsub("%^", "&")
|
||||
.. "{" .. img3:gsub("%^", "&")
|
||||
end
|
||||
|
||||
function core.dir_to_facedir(dir, is6d)
|
||||
--account for y if requested
|
||||
if is6d and math.abs(dir.y) > math.abs(dir.x) and math.abs(dir.y) > math.abs(dir.z) then
|
||||
|
||||
--from above
|
||||
if dir.y < 0 then
|
||||
if math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 19
|
||||
else
|
||||
return 13
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 10
|
||||
else
|
||||
return 4
|
||||
end
|
||||
end
|
||||
|
||||
--from below
|
||||
else
|
||||
if math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 15
|
||||
else
|
||||
return 17
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 6
|
||||
else
|
||||
return 8
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--otherwise, place horizontally
|
||||
elseif math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 3
|
||||
else
|
||||
return 1
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 2
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Table of possible dirs
|
||||
local facedir_to_dir = {
|
||||
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 = {
|
||||
[0]=1, 2, 3, 4,
|
||||
5, 2, 6, 4,
|
||||
6, 2, 5, 4,
|
||||
1, 5, 3, 6,
|
||||
1, 6, 3, 5,
|
||||
1, 4, 3, 2,
|
||||
}
|
||||
function core.facedir_to_dir(facedir)
|
||||
return facedir_to_dir[facedir_to_dir_map[facedir % 32]]
|
||||
end
|
||||
|
||||
function core.dir_to_wallmounted(dir)
|
||||
if math.abs(dir.y) > math.max(math.abs(dir.x), math.abs(dir.z)) then
|
||||
if dir.y < 0 then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
end
|
||||
elseif math.abs(dir.x) > math.abs(dir.z) then
|
||||
if dir.x < 0 then
|
||||
return 3
|
||||
else
|
||||
return 2
|
||||
end
|
||||
else
|
||||
if dir.z < 0 then
|
||||
return 5
|
||||
else
|
||||
return 4
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- table of dirs in wallmounted order
|
||||
local wallmounted_to_dir = {
|
||||
[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]
|
||||
end
|
||||
|
||||
function core.dir_to_yaw(dir)
|
||||
return -math.atan2(dir.x, dir.z)
|
||||
end
|
||||
|
||||
function core.yaw_to_dir(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") or (ptype == "colordegrotate")
|
||||
end
|
||||
|
||||
function core.strip_param2_color(param2, paramtype2)
|
||||
if not core.is_colored_paramtype(paramtype2) then
|
||||
return nil
|
||||
end
|
||||
if paramtype2 == "colorfacedir" then
|
||||
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
|
||||
46
builtin/game/knockback.lua
Normal file
46
builtin/game/knockback.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
-- can be overriden by mods
|
||||
function core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, distance, damage)
|
||||
if damage == 0 or player:get_armor_groups().immortal then
|
||||
return 0.0
|
||||
end
|
||||
|
||||
local m = 8
|
||||
-- solve m - m*e^(k*4) = 4 for k
|
||||
local k = -0.17328
|
||||
local res = m - m * math.exp(k * damage)
|
||||
|
||||
if distance < 2.0 then
|
||||
res = res * 1.1 -- more knockback when closer
|
||||
elseif distance > 4.0 then
|
||||
res = res * 0.9 -- less when far away
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function vector_absmax(v)
|
||||
local max, abs = math.max, math.abs
|
||||
return max(max(abs(v.x), abs(v.y)), abs(v.z))
|
||||
end
|
||||
|
||||
core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, unused_dir, damage)
|
||||
if player:get_hp() == 0 then
|
||||
return -- RIP
|
||||
end
|
||||
|
||||
-- Server::handleCommand_Interact() adds eye offset to one but not the other
|
||||
-- so the direction is slightly off, calculate it ourselves
|
||||
local dir = vector.subtract(player:get_pos(), hitter:get_pos())
|
||||
local d = vector.length(dir)
|
||||
if d ~= 0.0 then
|
||||
dir = vector.divide(dir, d)
|
||||
end
|
||||
|
||||
local k = core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, d, damage)
|
||||
|
||||
local kdir = vector.multiply(dir, k)
|
||||
if vector_absmax(kdir) < 1.0 then
|
||||
return -- barely noticeable, so don't even send
|
||||
end
|
||||
|
||||
player:add_velocity(kdir)
|
||||
end)
|
||||
266
builtin/game/misc.lua
Normal file
266
builtin/game/misc.lua
Normal file
@@ -0,0 +1,266 @@
|
||||
-- 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()
|
||||
elseif type(name) ~= "string" then
|
||||
error("core.check_player_privs expects a player or playername as " ..
|
||||
"argument.", 2)
|
||||
end
|
||||
|
||||
local requested_privs = {...}
|
||||
local player_privs = core.get_player_privs(name)
|
||||
local missing_privileges = {}
|
||||
|
||||
if type(requested_privs[1]) == "table" then
|
||||
-- We were provided with a table like { privA = true, privB = true }.
|
||||
for priv, value in pairs(requested_privs[1]) do
|
||||
if value and not player_privs[priv] then
|
||||
missing_privileges[#missing_privileges + 1] = priv
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Only a list, we can process it directly.
|
||||
for key, priv in pairs(requested_privs) do
|
||||
if not player_privs[priv] then
|
||||
missing_privileges[#missing_privileges + 1] = priv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #missing_privileges > 0 then
|
||||
return false, missing_privileges
|
||||
end
|
||||
|
||||
return true, ""
|
||||
end
|
||||
|
||||
|
||||
function core.send_join_message(player_name)
|
||||
if not core.is_singleplayer() then
|
||||
core.chat_send_all("*** " .. S("@1 joined the game.", player_name))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function core.send_leave_message(player_name, timed_out)
|
||||
local announcement = "*** " .. S("@1 left the game.", player_name)
|
||||
if timed_out then
|
||||
announcement = "*** " .. S("@1 left the game (timed out).", player_name)
|
||||
end
|
||||
core.chat_send_all(announcement)
|
||||
end
|
||||
|
||||
|
||||
core.register_on_joinplayer(function(player)
|
||||
local player_name = player:get_player_name()
|
||||
if not core.is_singleplayer() then
|
||||
local status = core.get_server_status(player_name, true)
|
||||
if status and status ~= "" then
|
||||
core.chat_send_player(player_name, status)
|
||||
end
|
||||
end
|
||||
core.send_join_message(player_name)
|
||||
end)
|
||||
|
||||
|
||||
core.register_on_leaveplayer(function(player, timed_out)
|
||||
local player_name = player:get_player_name()
|
||||
core.send_leave_message(player_name, timed_out)
|
||||
end)
|
||||
|
||||
|
||||
function core.is_player(player)
|
||||
-- a table being a player is also supported because it quacks sufficiently
|
||||
-- like a player if it has the is_player function
|
||||
local t = type(player)
|
||||
return (t == "userdata" or t == "table") and
|
||||
type(player.is_player) == "function" and player:is_player()
|
||||
end
|
||||
|
||||
|
||||
function core.player_exists(name)
|
||||
return core.get_auth_handler().get_auth(name) ~= nil
|
||||
end
|
||||
|
||||
|
||||
-- Returns two position vectors representing a box of `radius` in each
|
||||
-- direction centered around the player corresponding to `player_name`
|
||||
|
||||
function core.get_player_radius_area(player_name, radius)
|
||||
local player = core.get_player_by_name(player_name)
|
||||
if player == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local p1 = player:get_pos()
|
||||
local p2 = p1
|
||||
|
||||
if radius then
|
||||
p1 = vector.subtract(p1, radius)
|
||||
p2 = vector.add(p2, radius)
|
||||
end
|
||||
|
||||
return p1, p2
|
||||
end
|
||||
|
||||
|
||||
-- To be overriden by protection mods
|
||||
|
||||
function core.is_protected(pos, name)
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function core.record_protection_violation(pos, name)
|
||||
for _, func in pairs(core.registered_on_protection_violation) do
|
||||
func(pos, name)
|
||||
end
|
||||
end
|
||||
|
||||
-- To be overridden by Creative mods
|
||||
|
||||
local creative_mode_cache = core.settings:get_bool("creative_mode")
|
||||
function core.is_creative_enabled(name)
|
||||
return creative_mode_cache
|
||||
end
|
||||
|
||||
-- Checks if specified volume intersects a protected volume
|
||||
|
||||
function core.is_area_protected(minp, maxp, player_name, interval)
|
||||
-- 'interval' is the largest allowed interval for the 3D lattice of checks.
|
||||
|
||||
-- Compute the optimal float step 'd' for each axis so that all corners and
|
||||
-- borders are checked. 'd' will be smaller or equal to 'interval'.
|
||||
-- Subtracting 1e-4 ensures that the max co-ordinate will be reached by the
|
||||
-- for loop (which might otherwise not be the case due to rounding errors).
|
||||
|
||||
-- Default to 4
|
||||
interval = interval or 4
|
||||
local d = {}
|
||||
|
||||
for _, c in pairs({"x", "y", "z"}) do
|
||||
if minp[c] > maxp[c] then
|
||||
-- Repair positions: 'minp' > 'maxp'
|
||||
local tmp = maxp[c]
|
||||
maxp[c] = minp[c]
|
||||
minp[c] = tmp
|
||||
end
|
||||
|
||||
if maxp[c] > minp[c] then
|
||||
d[c] = (maxp[c] - minp[c]) /
|
||||
math.ceil((maxp[c] - minp[c]) / interval) - 1e-4
|
||||
else
|
||||
d[c] = 1 -- Any value larger than 0 to avoid division by zero
|
||||
end
|
||||
end
|
||||
|
||||
for zf = minp.z, maxp.z, d.z do
|
||||
local z = math.floor(zf + 0.5)
|
||||
for yf = minp.y, maxp.y, d.y do
|
||||
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 = vector.new(x, y, z)
|
||||
if core.is_protected(pos, player_name) then
|
||||
return pos
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local raillike_ids = {}
|
||||
local raillike_cur_id = 0
|
||||
function core.raillike_group(name)
|
||||
local id = raillike_ids[name]
|
||||
if not id then
|
||||
raillike_cur_id = raillike_cur_id + 1
|
||||
raillike_ids[name] = raillike_cur_id
|
||||
id = raillike_cur_id
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
|
||||
-- HTTP callback interface
|
||||
|
||||
core.set_http_api_lua(function(httpenv)
|
||||
httpenv.fetch = function(req, callback)
|
||||
local handle = httpenv.fetch_async(req)
|
||||
|
||||
local function update_http_status()
|
||||
local res = httpenv.fetch_async_get(handle)
|
||||
if res.completed then
|
||||
callback(res)
|
||||
else
|
||||
core.after(0, update_http_status)
|
||||
end
|
||||
end
|
||||
core.after(0, update_http_status)
|
||||
end
|
||||
|
||||
return httpenv
|
||||
end)
|
||||
core.set_http_api_lua = nil
|
||||
|
||||
|
||||
function core.close_formspec(player_name, formname)
|
||||
return core.show_formspec(player_name, formname, "")
|
||||
end
|
||||
|
||||
|
||||
function core.cancel_shutdown_requests()
|
||||
core.request_shutdown("", false, -1)
|
||||
end
|
||||
|
||||
|
||||
-- Used for callback handling with dynamic_add_media
|
||||
core.dynamic_media_callbacks = {}
|
||||
|
||||
|
||||
-- Transfer of certain globals into async environment
|
||||
-- see builtin/async/game.lua for the other side
|
||||
|
||||
local function copy_filtering(t, seen)
|
||||
if type(t) == "userdata" or type(t) == "function" then
|
||||
return true -- don't use nil so presence can still be detected
|
||||
elseif type(t) ~= "table" then
|
||||
return t
|
||||
end
|
||||
local n = {}
|
||||
seen = seen or {}
|
||||
seen[t] = n
|
||||
for k, v in pairs(t) do
|
||||
local k_ = seen[k] or copy_filtering(k, seen)
|
||||
local v_ = seen[v] or copy_filtering(v, seen)
|
||||
n[k_] = v_
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
function core.get_globals_to_transfer()
|
||||
local all = {
|
||||
registered_items = copy_filtering(core.registered_items),
|
||||
registered_aliases = core.registered_aliases,
|
||||
}
|
||||
return all
|
||||
end
|
||||
93
builtin/game/misc_s.lua
Normal file
93
builtin/game/misc_s.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
-- Minetest: builtin/misc_s.lua
|
||||
-- The distinction of what goes here is a bit tricky, basically it's everything
|
||||
-- that does not (directly or indirectly) need access to ServerEnvironment,
|
||||
-- Server or writable access to IGameDef on the engine side.
|
||||
-- (The '_s' stands for standalone.)
|
||||
|
||||
--
|
||||
-- Misc. API functions
|
||||
--
|
||||
|
||||
function core.hash_node_position(pos)
|
||||
return (pos.z + 32768) * 65536 * 65536
|
||||
+ (pos.y + 32768) * 65536
|
||||
+ pos.x + 32768
|
||||
end
|
||||
|
||||
|
||||
function core.get_position_from_hash(hash)
|
||||
local x = (hash % 65536) - 32768
|
||||
hash = math.floor(hash / 65536)
|
||||
local y = (hash % 65536) - 32768
|
||||
hash = math.floor(hash / 65536)
|
||||
local z = (hash % 65536) - 32768
|
||||
return vector.new(x, y, z)
|
||||
end
|
||||
|
||||
|
||||
function core.get_item_group(name, group)
|
||||
if not core.registered_items[name] or not
|
||||
core.registered_items[name].groups[group] then
|
||||
return 0
|
||||
end
|
||||
return core.registered_items[name].groups[group]
|
||||
end
|
||||
|
||||
|
||||
function core.get_node_group(name, group)
|
||||
core.log("deprecated", "Deprecated usage of get_node_group, use get_item_group instead")
|
||||
return core.get_item_group(name, group)
|
||||
end
|
||||
|
||||
|
||||
function core.setting_get_pos(name)
|
||||
local value = core.settings:get(name)
|
||||
if not value then
|
||||
return nil
|
||||
end
|
||||
return core.string_to_pos(value)
|
||||
end
|
||||
|
||||
|
||||
-- See l_env.cpp for the other functions
|
||||
function core.get_artificial_light(param1)
|
||||
return math.floor(param1 / 16)
|
||||
end
|
||||
|
||||
-- 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 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 o_encode_png(width, height, data, compression or 6)
|
||||
end
|
||||
108
builtin/game/privileges.lua
Normal file
108
builtin/game/privileges.lua
Normal file
@@ -0,0 +1,108 @@
|
||||
-- Minetest: builtin/privileges.lua
|
||||
|
||||
local S = core.get_translator("__builtin")
|
||||
|
||||
--
|
||||
-- Privileges
|
||||
--
|
||||
|
||||
core.registered_privileges = {}
|
||||
|
||||
function core.register_privilege(name, param)
|
||||
local function fill_defaults(def)
|
||||
if def.give_to_singleplayer == nil then
|
||||
def.give_to_singleplayer = true
|
||||
end
|
||||
if def.give_to_admin == nil then
|
||||
def.give_to_admin = def.give_to_singleplayer
|
||||
end
|
||||
if def.description == nil then
|
||||
def.description = S("(no description)")
|
||||
end
|
||||
end
|
||||
local def
|
||||
if type(param) == "table" then
|
||||
def = param
|
||||
else
|
||||
def = {description = param}
|
||||
end
|
||||
fill_defaults(def)
|
||||
core.registered_privileges[name] = def
|
||||
end
|
||||
|
||||
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 = S("Can teleport self"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("bring", {
|
||||
description = S("Can teleport other players"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("settime", {
|
||||
description = S("Can set the time of day using /time"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("server", {
|
||||
description = S("Can do server maintenance stuff"),
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("protection_bypass", {
|
||||
description = S("Can bypass node protection in the world"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("ban", {
|
||||
description = S("Can ban and unban players"),
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("kick", {
|
||||
description = S("Can kick players"),
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("give", {
|
||||
description = S("Can use /give and /giveme"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("password", {
|
||||
description = S("Can use /setpassword and /clearpassword"),
|
||||
give_to_singleplayer = false,
|
||||
give_to_admin = true,
|
||||
})
|
||||
core.register_privilege("fly", {
|
||||
description = S("Can use fly mode"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("fast", {
|
||||
description = S("Can use fast mode"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("noclip", {
|
||||
description = S("Can fly through solid nodes using noclip mode"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("rollback", {
|
||||
description = S("Can use the rollback functionality"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
core.register_privilege("debug", {
|
||||
description = S("Can enable wireframe"),
|
||||
give_to_singleplayer = false,
|
||||
})
|
||||
|
||||
core.register_can_bypass_userlimit(function(name, ip)
|
||||
local privs = core.get_player_privs(name)
|
||||
return privs["server"] or privs["ban"] or privs["privs"] or privs["password"]
|
||||
end)
|
||||
625
builtin/game/register.lua
Normal file
625
builtin/game/register.lua
Normal file
@@ -0,0 +1,625 @@
|
||||
-- Minetest: builtin/register.lua
|
||||
|
||||
local S = core.get_translator("__builtin")
|
||||
|
||||
--
|
||||
-- Make raw registration functions inaccessible to anyone except this file
|
||||
--
|
||||
|
||||
local register_item_raw = core.register_item_raw
|
||||
core.register_item_raw = nil
|
||||
|
||||
local unregister_item_raw = core.unregister_item_raw
|
||||
core.unregister_item_raw = nil
|
||||
|
||||
local register_alias_raw = core.register_alias_raw
|
||||
core.register_alias_raw = nil
|
||||
|
||||
--
|
||||
-- Item / entity / ABM / LBM registration functions
|
||||
--
|
||||
|
||||
core.registered_abms = {}
|
||||
core.registered_lbms = {}
|
||||
core.registered_entities = {}
|
||||
core.registered_items = {}
|
||||
core.registered_nodes = {}
|
||||
core.registered_craftitems = {}
|
||||
core.registered_tools = {}
|
||||
core.registered_aliases = {}
|
||||
|
||||
-- For tables that are indexed by item name:
|
||||
-- If table[X] does not exist, default to table[core.registered_aliases[X]]
|
||||
local alias_metatable = {
|
||||
__index = function(t, name)
|
||||
return rawget(t, core.registered_aliases[name])
|
||||
end
|
||||
}
|
||||
setmetatable(core.registered_items, alias_metatable)
|
||||
setmetatable(core.registered_nodes, alias_metatable)
|
||||
setmetatable(core.registered_craftitems, alias_metatable)
|
||||
setmetatable(core.registered_tools, alias_metatable)
|
||||
|
||||
-- These item names may not be used because they would interfere
|
||||
-- with legacy itemstrings
|
||||
local forbidden_item_names = {
|
||||
MaterialItem = true,
|
||||
MaterialItem2 = true,
|
||||
MaterialItem3 = true,
|
||||
NodeItem = true,
|
||||
node = true,
|
||||
CraftItem = true,
|
||||
craft = true,
|
||||
MBOItem = true,
|
||||
ToolItem = true,
|
||||
tool = true,
|
||||
}
|
||||
|
||||
local function check_modname_prefix(name)
|
||||
if name:sub(1,1) == ":" then
|
||||
-- If the name starts with a colon, we can skip the modname prefix
|
||||
-- mechanism.
|
||||
return name:sub(2)
|
||||
else
|
||||
-- Enforce that the name starts with the correct mod name.
|
||||
local expected_prefix = core.get_current_modname() .. ":"
|
||||
if name:sub(1, #expected_prefix) ~= expected_prefix then
|
||||
error("Name " .. name .. " does not follow naming conventions: " ..
|
||||
"\"" .. expected_prefix .. "\" or \":\" prefix required")
|
||||
end
|
||||
|
||||
-- Enforce that the name only contains letters, numbers and underscores.
|
||||
local subname = name:sub(#expected_prefix+1)
|
||||
if subname:find("[^%w_]") then
|
||||
error("Name " .. name .. " does not follow naming conventions: " ..
|
||||
"contains unallowed characters")
|
||||
end
|
||||
|
||||
return name
|
||||
end
|
||||
end
|
||||
|
||||
function core.register_abm(spec)
|
||||
-- Add to core.registered_abms
|
||||
assert(type(spec.action) == "function", "Required field 'action' of type function")
|
||||
core.registered_abms[#core.registered_abms + 1] = spec
|
||||
spec.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_lbm(spec)
|
||||
-- Add to core.registered_lbms
|
||||
check_modname_prefix(spec.name)
|
||||
assert(type(spec.action) == "function", "Required field 'action' of type function")
|
||||
core.registered_lbms[#core.registered_lbms + 1] = spec
|
||||
spec.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_entity(name, prototype)
|
||||
-- Check name
|
||||
if name == nil then
|
||||
error("Unable to register entity: Name is nil")
|
||||
end
|
||||
name = check_modname_prefix(tostring(name))
|
||||
|
||||
prototype.name = name
|
||||
prototype.__index = prototype -- so that it can be used as a metatable
|
||||
|
||||
-- Add to core.registered_entities
|
||||
core.registered_entities[name] = prototype
|
||||
prototype.mod_origin = core.get_current_modname() or "??"
|
||||
end
|
||||
|
||||
function core.register_item(name, itemdef)
|
||||
-- Check name
|
||||
if name == nil then
|
||||
error("Unable to register item: Name is nil")
|
||||
end
|
||||
name = check_modname_prefix(tostring(name))
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register item: Name is forbidden: " .. name)
|
||||
end
|
||||
itemdef.name = name
|
||||
|
||||
-- Apply defaults and add to registered_* table
|
||||
if itemdef.type == "node" then
|
||||
-- Use the nodebox as selection box if it's not set manually
|
||||
if itemdef.drawtype == "nodebox" and not itemdef.selection_box then
|
||||
itemdef.selection_box = itemdef.node_box
|
||||
elseif itemdef.drawtype == "fencelike" and not itemdef.selection_box then
|
||||
itemdef.selection_box = {
|
||||
type = "fixed",
|
||||
fixed = {-1/8, -1/2, -1/8, 1/8, 1/2, 1/8},
|
||||
}
|
||||
end
|
||||
if itemdef.light_source and itemdef.light_source > core.LIGHT_MAX then
|
||||
itemdef.light_source = core.LIGHT_MAX
|
||||
core.log("warning", "Node 'light_source' value exceeds maximum," ..
|
||||
" limiting to maximum: " ..name)
|
||||
end
|
||||
setmetatable(itemdef, {__index = core.nodedef_default})
|
||||
core.registered_nodes[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "craft" then
|
||||
setmetatable(itemdef, {__index = core.craftitemdef_default})
|
||||
core.registered_craftitems[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "tool" then
|
||||
setmetatable(itemdef, {__index = core.tooldef_default})
|
||||
core.registered_tools[itemdef.name] = itemdef
|
||||
elseif itemdef.type == "none" then
|
||||
setmetatable(itemdef, {__index = core.noneitemdef_default})
|
||||
else
|
||||
error("Unable to register item: Type is invalid: " .. dump(itemdef))
|
||||
end
|
||||
|
||||
-- Flowing liquid uses param2
|
||||
if itemdef.type == "node" and itemdef.liquidtype == "flowing" then
|
||||
itemdef.paramtype2 = "flowingliquid"
|
||||
end
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if itemdef.cookresult_itemstring ~= nil and itemdef.cookresult_itemstring ~= "" then
|
||||
core.register_craft({
|
||||
type="cooking",
|
||||
output=itemdef.cookresult_itemstring,
|
||||
recipe=itemdef.name,
|
||||
cooktime=itemdef.furnace_cooktime
|
||||
})
|
||||
end
|
||||
if itemdef.furnace_burntime ~= nil and itemdef.furnace_burntime >= 0 then
|
||||
core.register_craft({
|
||||
type="fuel",
|
||||
recipe=itemdef.name,
|
||||
burntime=itemdef.furnace_burntime
|
||||
})
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
itemdef.mod_origin = core.get_current_modname() or "??"
|
||||
|
||||
-- Disable all further modifications
|
||||
getmetatable(itemdef).__newindex = {}
|
||||
|
||||
--core.log("Registering item: " .. itemdef.name)
|
||||
core.registered_items[itemdef.name] = itemdef
|
||||
core.registered_aliases[itemdef.name] = nil
|
||||
register_item_raw(itemdef)
|
||||
end
|
||||
|
||||
function core.unregister_item(name)
|
||||
if not core.registered_items[name] then
|
||||
core.log("warning", "Not unregistering item " ..name..
|
||||
" because it doesn't exist.")
|
||||
return
|
||||
end
|
||||
-- Erase from registered_* table
|
||||
local type = core.registered_items[name].type
|
||||
if type == "node" then
|
||||
core.registered_nodes[name] = nil
|
||||
elseif type == "craft" then
|
||||
core.registered_craftitems[name] = nil
|
||||
elseif type == "tool" then
|
||||
core.registered_tools[name] = nil
|
||||
end
|
||||
core.registered_items[name] = nil
|
||||
|
||||
|
||||
unregister_item_raw(name)
|
||||
end
|
||||
|
||||
function core.register_node(name, nodedef)
|
||||
nodedef.type = "node"
|
||||
core.register_item(name, nodedef)
|
||||
end
|
||||
|
||||
function core.register_craftitem(name, craftitemdef)
|
||||
craftitemdef.type = "craft"
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if craftitemdef.inventory_image == nil and craftitemdef.image ~= nil then
|
||||
craftitemdef.inventory_image = craftitemdef.image
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
core.register_item(name, craftitemdef)
|
||||
end
|
||||
|
||||
function core.register_tool(name, tooldef)
|
||||
tooldef.type = "tool"
|
||||
tooldef.stack_max = 1
|
||||
|
||||
-- BEGIN Legacy stuff
|
||||
if tooldef.inventory_image == nil and tooldef.image ~= nil then
|
||||
tooldef.inventory_image = tooldef.image
|
||||
end
|
||||
if tooldef.tool_capabilities == nil and
|
||||
(tooldef.full_punch_interval ~= nil or
|
||||
tooldef.basetime ~= nil or
|
||||
tooldef.dt_weight ~= nil or
|
||||
tooldef.dt_crackiness ~= nil or
|
||||
tooldef.dt_crumbliness ~= nil or
|
||||
tooldef.dt_cuttability ~= nil or
|
||||
tooldef.basedurability ~= nil or
|
||||
tooldef.dd_weight ~= nil or
|
||||
tooldef.dd_crackiness ~= nil or
|
||||
tooldef.dd_crumbliness ~= nil or
|
||||
tooldef.dd_cuttability ~= nil) then
|
||||
tooldef.tool_capabilities = {
|
||||
full_punch_interval = tooldef.full_punch_interval,
|
||||
basetime = tooldef.basetime,
|
||||
dt_weight = tooldef.dt_weight,
|
||||
dt_crackiness = tooldef.dt_crackiness,
|
||||
dt_crumbliness = tooldef.dt_crumbliness,
|
||||
dt_cuttability = tooldef.dt_cuttability,
|
||||
basedurability = tooldef.basedurability,
|
||||
dd_weight = tooldef.dd_weight,
|
||||
dd_crackiness = tooldef.dd_crackiness,
|
||||
dd_crumbliness = tooldef.dd_crumbliness,
|
||||
dd_cuttability = tooldef.dd_cuttability,
|
||||
}
|
||||
end
|
||||
-- END Legacy stuff
|
||||
|
||||
-- This isn't just legacy, but more of a convenience feature
|
||||
local toolcaps = tooldef.tool_capabilities
|
||||
if toolcaps and toolcaps.punch_attack_uses == nil then
|
||||
for _, cap in pairs(toolcaps.groupcaps or {}) do
|
||||
local level = (cap.maxlevel or 0) - 1
|
||||
if (cap.uses or 0) ~= 0 and level >= 0 then
|
||||
toolcaps.punch_attack_uses = cap.uses * (3 ^ level)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
core.register_item(name, tooldef)
|
||||
end
|
||||
|
||||
function core.register_alias(name, convert_to)
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register alias: Name is forbidden: " .. name)
|
||||
end
|
||||
if core.registered_items[name] ~= nil then
|
||||
core.log("warning", "Not registering alias, item with same name" ..
|
||||
" is already defined: " .. name .. " -> " .. convert_to)
|
||||
else
|
||||
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
|
||||
core.registered_aliases[name] = convert_to
|
||||
register_alias_raw(name, convert_to)
|
||||
end
|
||||
end
|
||||
|
||||
function core.register_alias_force(name, convert_to)
|
||||
if forbidden_item_names[name] then
|
||||
error("Unable to register alias: Name is forbidden: " .. name)
|
||||
end
|
||||
if core.registered_items[name] ~= nil then
|
||||
core.unregister_item(name)
|
||||
core.log("info", "Removed item " ..name..
|
||||
" while attempting to force add an alias")
|
||||
end
|
||||
--core.log("Registering alias: " .. name .. " -> " .. convert_to)
|
||||
core.registered_aliases[name] = convert_to
|
||||
register_alias_raw(name, convert_to)
|
||||
end
|
||||
|
||||
function core.on_craft(itemstack, player, old_craft_list, craft_inv)
|
||||
for _, func in ipairs(core.registered_on_crafts) do
|
||||
-- cast to ItemStack since func() could return a string
|
||||
itemstack = ItemStack(func(itemstack, player, old_craft_list, craft_inv) or itemstack)
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
function core.craft_predict(itemstack, player, old_craft_list, craft_inv)
|
||||
for _, func in ipairs(core.registered_craft_predicts) do
|
||||
-- cast to ItemStack since func() could return a string
|
||||
itemstack = ItemStack(func(itemstack, player, old_craft_list, craft_inv) or itemstack)
|
||||
end
|
||||
return itemstack
|
||||
end
|
||||
|
||||
-- Alias the forbidden item names to "" so they can't be
|
||||
-- created via itemstrings (e.g. /give)
|
||||
for name in pairs(forbidden_item_names) do
|
||||
core.registered_aliases[name] = ""
|
||||
register_alias_raw(name, "")
|
||||
end
|
||||
|
||||
--
|
||||
-- Built-in node definitions. Also defined in C.
|
||||
--
|
||||
|
||||
core.register_item(":unknown", {
|
||||
type = "none",
|
||||
description = S("Unknown Item"),
|
||||
inventory_image = "unknown_item.png",
|
||||
on_place = core.item_place,
|
||||
on_secondary_use = core.item_secondary_use,
|
||||
on_drop = core.item_drop,
|
||||
groups = {not_in_creative_inventory=1},
|
||||
diggable = true,
|
||||
})
|
||||
|
||||
core.register_node(":air", {
|
||||
description = S("Air"),
|
||||
inventory_image = "air.png",
|
||||
wield_image = "air.png",
|
||||
drawtype = "airlike",
|
||||
paramtype = "light",
|
||||
sunlight_propagates = true,
|
||||
walkable = false,
|
||||
pointable = false,
|
||||
diggable = false,
|
||||
buildable_to = true,
|
||||
floodable = true,
|
||||
air_equivalent = true,
|
||||
drop = "",
|
||||
groups = {not_in_creative_inventory=1},
|
||||
})
|
||||
|
||||
core.register_node(":ignore", {
|
||||
description = S("Ignore"),
|
||||
inventory_image = "ignore.png",
|
||||
wield_image = "ignore.png",
|
||||
drawtype = "airlike",
|
||||
paramtype = "none",
|
||||
sunlight_propagates = false,
|
||||
walkable = false,
|
||||
pointable = false,
|
||||
diggable = false,
|
||||
buildable_to = true, -- A way to remove accidentally placed ignores
|
||||
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",
|
||||
S("You can't place 'ignore' nodes!")))
|
||||
return ""
|
||||
end,
|
||||
})
|
||||
|
||||
-- The hand (bare definition)
|
||||
core.register_item(":", {
|
||||
type = "none",
|
||||
wield_image = "wieldhand.png",
|
||||
groups = {not_in_creative_inventory=1},
|
||||
})
|
||||
|
||||
|
||||
function core.override_item(name, redefinition)
|
||||
if redefinition.name ~= nil then
|
||||
error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2)
|
||||
end
|
||||
if redefinition.type ~= nil then
|
||||
error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2)
|
||||
end
|
||||
local item = core.registered_items[name]
|
||||
if not item then
|
||||
error("Attempt to override non-existent item "..name, 2)
|
||||
end
|
||||
for k, v in pairs(redefinition) do
|
||||
rawset(item, k, v)
|
||||
end
|
||||
register_item_raw(item)
|
||||
end
|
||||
|
||||
do
|
||||
local default = {mod = "??", name = "??"}
|
||||
core.callback_origins = setmetatable({}, {
|
||||
__index = function()
|
||||
return default
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
function core.run_callbacks(callbacks, mode, ...)
|
||||
assert(type(callbacks) == "table")
|
||||
local cb_len = #callbacks
|
||||
if cb_len == 0 then
|
||||
if mode == 2 or mode == 3 then
|
||||
return true
|
||||
elseif mode == 4 or mode == 5 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local ret = nil
|
||||
for i = 1, cb_len do
|
||||
local origin = core.callback_origins[callbacks[i]]
|
||||
core.set_last_run_mod(origin.mod)
|
||||
local cb_ret = callbacks[i](...)
|
||||
|
||||
if mode == 0 and i == 1 then
|
||||
ret = cb_ret
|
||||
elseif mode == 1 and i == cb_len then
|
||||
ret = cb_ret
|
||||
elseif mode == 2 then
|
||||
if not cb_ret or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 3 then
|
||||
if cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
ret = cb_ret
|
||||
elseif mode == 4 then
|
||||
if (cb_ret and not ret) or i == 1 then
|
||||
ret = cb_ret
|
||||
end
|
||||
elseif mode == 5 and cb_ret then
|
||||
return cb_ret
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function core.run_priv_callbacks(name, priv, caller, method)
|
||||
local def = core.registered_privileges[priv]
|
||||
if not def or not def["on_" .. method] or
|
||||
not def["on_" .. method](name, caller) then
|
||||
for _, func in ipairs(core["registered_on_priv_" .. method]) do
|
||||
if not func(name, caller, priv) then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Callback registration
|
||||
--
|
||||
|
||||
local function make_registration()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
t[#t + 1] = func
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
local function make_registration_reverse()
|
||||
local t = {}
|
||||
local registerfunc = function(func)
|
||||
table.insert(t, 1, func)
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
--local origin = core.callback_origins[func]
|
||||
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
|
||||
end
|
||||
return t, registerfunc
|
||||
end
|
||||
|
||||
local function make_registration_wrap(reg_fn_name, clear_fn_name)
|
||||
local list = {}
|
||||
|
||||
local orig_reg_fn = core[reg_fn_name]
|
||||
core[reg_fn_name] = function(def)
|
||||
local retval = orig_reg_fn(def)
|
||||
if retval ~= nil then
|
||||
if def.name ~= nil then
|
||||
list[def.name] = def
|
||||
else
|
||||
list[retval] = def
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
local orig_clear_fn = core[clear_fn_name]
|
||||
core[clear_fn_name] = function()
|
||||
for k in pairs(list) do
|
||||
list[k] = nil
|
||||
end
|
||||
return orig_clear_fn()
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
local function make_wrap_deregistration(reg_fn, clear_fn, list)
|
||||
local unregister = function (key)
|
||||
if type(key) ~= "string" then
|
||||
error("key is not a string", 2)
|
||||
end
|
||||
if not list[key] then
|
||||
error("Attempt to unregister non-existent element - '" .. key .. "'", 2)
|
||||
end
|
||||
local temporary_list = table.copy(list)
|
||||
clear_fn()
|
||||
for k,v in pairs(temporary_list) do
|
||||
if key ~= k then
|
||||
reg_fn(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
return unregister
|
||||
end
|
||||
|
||||
core.registered_on_player_hpchanges = { modifiers = { }, loggers = { } }
|
||||
|
||||
function core.registered_on_player_hpchange(player, hp_change, reason)
|
||||
local last
|
||||
for i = #core.registered_on_player_hpchanges.modifiers, 1, -1 do
|
||||
local func = core.registered_on_player_hpchanges.modifiers[i]
|
||||
hp_change, last = func(player, hp_change, reason)
|
||||
if type(hp_change) ~= "number" then
|
||||
local debuginfo = debug.getinfo(func)
|
||||
error("The register_on_hp_changes function has to return a number at " ..
|
||||
debuginfo.short_src .. " line " .. debuginfo.linedefined)
|
||||
end
|
||||
if last then
|
||||
break
|
||||
end
|
||||
end
|
||||
for i, func in ipairs(core.registered_on_player_hpchanges.loggers) do
|
||||
func(player, hp_change, reason)
|
||||
end
|
||||
return hp_change
|
||||
end
|
||||
|
||||
function core.register_on_player_hpchange(func, modifier)
|
||||
if modifier then
|
||||
core.registered_on_player_hpchanges.modifiers[#core.registered_on_player_hpchanges.modifiers + 1] = func
|
||||
else
|
||||
core.registered_on_player_hpchanges.loggers[#core.registered_on_player_hpchanges.loggers + 1] = func
|
||||
end
|
||||
core.callback_origins[func] = {
|
||||
mod = core.get_current_modname() or "??",
|
||||
name = debug.getinfo(1, "n").name or "??"
|
||||
}
|
||||
end
|
||||
|
||||
core.registered_biomes = make_registration_wrap("register_biome", "clear_registered_biomes")
|
||||
core.registered_ores = make_registration_wrap("register_ore", "clear_registered_ores")
|
||||
core.registered_decorations = make_registration_wrap("register_decoration", "clear_registered_decorations")
|
||||
|
||||
core.unregister_biome = make_wrap_deregistration(core.register_biome,
|
||||
core.clear_registered_biomes, core.registered_biomes)
|
||||
|
||||
core.registered_on_chat_messages, core.register_on_chat_message = make_registration()
|
||||
core.registered_on_chatcommands, core.register_on_chatcommand = make_registration()
|
||||
core.registered_globalsteps, core.register_globalstep = make_registration()
|
||||
core.registered_playerevents, core.register_playerevent = make_registration()
|
||||
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()
|
||||
core.registered_on_shutdown, core.register_on_shutdown = make_registration()
|
||||
core.registered_on_punchnodes, core.register_on_punchnode = make_registration()
|
||||
core.registered_on_placenodes, core.register_on_placenode = make_registration()
|
||||
core.registered_on_dignodes, core.register_on_dignode = make_registration()
|
||||
core.registered_on_generateds, core.register_on_generated = make_registration()
|
||||
core.registered_on_newplayers, core.register_on_newplayer = make_registration()
|
||||
core.registered_on_dieplayers, core.register_on_dieplayer = make_registration()
|
||||
core.registered_on_respawnplayers, core.register_on_respawnplayer = make_registration()
|
||||
core.registered_on_prejoinplayers, core.register_on_prejoinplayer = make_registration()
|
||||
core.registered_on_joinplayers, core.register_on_joinplayer = make_registration()
|
||||
core.registered_on_leaveplayers, core.register_on_leaveplayer = make_registration()
|
||||
core.registered_on_player_receive_fields, core.register_on_player_receive_fields = make_registration_reverse()
|
||||
core.registered_on_cheats, core.register_on_cheat = make_registration()
|
||||
core.registered_on_crafts, core.register_on_craft = make_registration()
|
||||
core.registered_craft_predicts, core.register_craft_predict = make_registration()
|
||||
core.registered_on_protection_violation, core.register_on_protection_violation = make_registration()
|
||||
core.registered_on_item_eats, core.register_on_item_eat = make_registration()
|
||||
core.registered_on_punchplayers, core.register_on_punchplayer = make_registration()
|
||||
core.registered_on_priv_grant, core.register_on_priv_grant = make_registration()
|
||||
core.registered_on_priv_revoke, core.register_on_priv_revoke = make_registration()
|
||||
core.registered_on_authplayers, core.register_on_authplayer = make_registration()
|
||||
core.registered_can_bypass_userlimit, core.register_can_bypass_userlimit = make_registration()
|
||||
core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration()
|
||||
core.registered_on_player_inventory_actions, core.register_on_player_inventory_action = make_registration()
|
||||
core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration()
|
||||
core.registered_on_rightclickplayers, core.register_on_rightclickplayer = make_registration()
|
||||
core.registered_on_liquid_transformed, core.register_on_liquid_transformed = make_registration()
|
||||
|
||||
--
|
||||
-- Compatibility for on_mapgen_init()
|
||||
--
|
||||
|
||||
core.register_on_mapgen_init = function(func) func(core.get_mapgen_params()) end
|
||||
187
builtin/game/statbars.lua
Normal file
187
builtin/game/statbars.lua
Normal file
@@ -0,0 +1,187 @@
|
||||
-- cache setting
|
||||
local enable_damage = core.settings:get_bool("enable_damage")
|
||||
|
||||
local bar_definitions = {
|
||||
hp = {
|
||||
hud_elem_type = "statbar",
|
||||
position = {x = 0.5, y = 1},
|
||||
text = "heart.png",
|
||||
text2 = "heart_gone.png",
|
||||
number = core.PLAYER_MAX_HP_DEFAULT,
|
||||
item = core.PLAYER_MAX_HP_DEFAULT,
|
||||
direction = 0,
|
||||
size = {x = 24, y = 24},
|
||||
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
|
||||
},
|
||||
breath = {
|
||||
hud_elem_type = "statbar",
|
||||
position = {x = 0.5, y = 1},
|
||||
text = "bubble.png",
|
||||
text2 = "bubble_gone.png",
|
||||
number = core.PLAYER_MAX_BREATH_DEFAULT * 2,
|
||||
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
|
||||
direction = 0,
|
||||
size = {x = 24, y = 24},
|
||||
offset = {x = 25, y= -(48 + 24 + 16)},
|
||||
},
|
||||
}
|
||||
|
||||
local hud_ids = {}
|
||||
|
||||
local function scaleToHudMax(player, field)
|
||||
-- Scale "hp" or "breath" to the hud maximum dimensions
|
||||
local current = player["get_" .. field](player)
|
||||
local nominal = bar_definitions[field].item
|
||||
local max_display = math.max(player:get_properties()[field .. "_max"], current)
|
||||
return math.ceil(current / max_display * nominal)
|
||||
end
|
||||
|
||||
local function update_builtin_statbars(player)
|
||||
local name = player:get_player_name()
|
||||
|
||||
if name == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local flags = player:hud_get_flags()
|
||||
if not hud_ids[name] then
|
||||
hud_ids[name] = {}
|
||||
-- flags are not transmitted to client on connect, we need to make sure
|
||||
-- our current flags are transmitted by sending them actively
|
||||
player:hud_set_flags(flags)
|
||||
end
|
||||
local hud = hud_ids[name]
|
||||
|
||||
local immortal = player:get_armor_groups().immortal == 1
|
||||
|
||||
if flags.healthbar and enable_damage and not immortal then
|
||||
local number = scaleToHudMax(player, "hp")
|
||||
if hud.id_healthbar == nil then
|
||||
local hud_def = table.copy(bar_definitions.hp)
|
||||
hud_def.number = number
|
||||
hud.id_healthbar = player:hud_add(hud_def)
|
||||
else
|
||||
player:hud_change(hud.id_healthbar, "number", number)
|
||||
end
|
||||
elseif hud.id_healthbar then
|
||||
player:hud_remove(hud.id_healthbar)
|
||||
hud.id_healthbar = nil
|
||||
end
|
||||
|
||||
local show_breathbar = flags.breathbar and enable_damage and not immortal
|
||||
|
||||
local breath = player:get_breath()
|
||||
local breath_max = player:get_properties().breath_max
|
||||
if show_breathbar and breath <= breath_max then
|
||||
local number = scaleToHudMax(player, "breath")
|
||||
if not hud.id_breathbar and breath < breath_max then
|
||||
local hud_def = table.copy(bar_definitions.breath)
|
||||
hud_def.number = number
|
||||
hud.id_breathbar = player:hud_add(hud_def)
|
||||
elseif hud.id_breathbar then
|
||||
player:hud_change(hud.id_breathbar, "number", number)
|
||||
end
|
||||
end
|
||||
|
||||
if hud.id_breathbar and (not show_breathbar or breath == breath_max) then
|
||||
core.after(1, function(player_name, breath_bar)
|
||||
local player = core.get_player_by_name(player_name)
|
||||
if player then
|
||||
player:hud_remove(breath_bar)
|
||||
end
|
||||
end, name, hud.id_breathbar)
|
||||
hud.id_breathbar = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function cleanup_builtin_statbars(player)
|
||||
local name = player:get_player_name()
|
||||
|
||||
if name == "" then
|
||||
return
|
||||
end
|
||||
|
||||
hud_ids[name] = nil
|
||||
end
|
||||
|
||||
local function player_event_handler(player,eventname)
|
||||
assert(player:is_player())
|
||||
|
||||
local name = player:get_player_name()
|
||||
|
||||
if name == "" or not hud_ids[name] then
|
||||
return
|
||||
end
|
||||
|
||||
if eventname == "health_changed" then
|
||||
update_builtin_statbars(player)
|
||||
|
||||
if hud_ids[name].id_healthbar then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if eventname == "breath_changed" then
|
||||
update_builtin_statbars(player)
|
||||
|
||||
if hud_ids[name].id_breathbar then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if eventname == "hud_changed" or eventname == "properties_changed" then
|
||||
update_builtin_statbars(player)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function core.hud_replace_builtin(hud_name, definition)
|
||||
if type(definition) ~= "table" or
|
||||
definition.hud_elem_type ~= "statbar" then
|
||||
return false
|
||||
end
|
||||
|
||||
definition = table.copy(definition)
|
||||
|
||||
if hud_name == "health" then
|
||||
definition.item = definition.item or definition.number or core.PLAYER_MAX_HP_DEFAULT
|
||||
bar_definitions.hp = definition
|
||||
|
||||
for name, ids in pairs(hud_ids) do
|
||||
local player = core.get_player_by_name(name)
|
||||
if player and ids.id_healthbar then
|
||||
player:hud_remove(ids.id_healthbar)
|
||||
ids.id_healthbar = nil
|
||||
update_builtin_statbars(player)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if hud_name == "breath" then
|
||||
definition.item = definition.item or definition.number or core.PLAYER_MAX_BREATH_DEFAULT
|
||||
bar_definitions.breath = definition
|
||||
|
||||
for name, ids in pairs(hud_ids) do
|
||||
local player = core.get_player_by_name(name)
|
||||
if player and ids.id_breathbar then
|
||||
player:hud_remove(ids.id_breathbar)
|
||||
ids.id_breathbar = nil
|
||||
update_builtin_statbars(player)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Append "update_builtin_statbars" as late as possible
|
||||
-- This ensures that the HUD is hidden when the flags are updated in this callback
|
||||
core.register_on_mods_loaded(function()
|
||||
core.register_on_joinplayer(update_builtin_statbars)
|
||||
end)
|
||||
core.register_on_leaveplayer(cleanup_builtin_statbars)
|
||||
core.register_playerevent(player_event_handler)
|
||||
23
builtin/game/static_spawn.lua
Normal file
23
builtin/game/static_spawn.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Minetest: builtin/static_spawn.lua
|
||||
|
||||
local static_spawnpoint_string = core.settings:get("static_spawnpoint")
|
||||
if static_spawnpoint_string and
|
||||
static_spawnpoint_string ~= "" and
|
||||
not core.setting_get_pos("static_spawnpoint") then
|
||||
error('The static_spawnpoint setting is invalid: "' ..
|
||||
static_spawnpoint_string .. '"')
|
||||
end
|
||||
|
||||
local function put_player_in_spawn(player_obj)
|
||||
local static_spawnpoint = core.setting_get_pos("static_spawnpoint")
|
||||
if not static_spawnpoint then
|
||||
return false
|
||||
end
|
||||
core.log("action", "Moving " .. player_obj:get_player_name() ..
|
||||
" to static spawnpoint at " .. core.pos_to_string(static_spawnpoint))
|
||||
player_obj:set_pos(static_spawnpoint)
|
||||
return true
|
||||
end
|
||||
|
||||
core.register_on_newplayer(put_player_in_spawn)
|
||||
core.register_on_respawnplayer(put_player_in_spawn)
|
||||
134
builtin/game/voxelarea.lua
Normal file
134
builtin/game/voxelarea.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
local math_floor = math.floor
|
||||
local vector_new = vector.new
|
||||
|
||||
VoxelArea = {
|
||||
MinEdge = vector_new(1, 1, 1),
|
||||
MaxEdge = vector_new(0, 0, 0),
|
||||
ystride = 0,
|
||||
zstride = 0,
|
||||
}
|
||||
|
||||
function VoxelArea:new(o)
|
||||
o = o or {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
|
||||
local e = o:getExtent()
|
||||
o.ystride = e.x
|
||||
o.zstride = e.x * e.y
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
function VoxelArea:getExtent()
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return vector_new(
|
||||
MaxEdge.x - MinEdge.x + 1,
|
||||
MaxEdge.y - MinEdge.y + 1,
|
||||
MaxEdge.z - MinEdge.z + 1
|
||||
)
|
||||
end
|
||||
|
||||
function VoxelArea:getVolume()
|
||||
local e = self:getExtent()
|
||||
return e.x * e.y * e.z
|
||||
end
|
||||
|
||||
function VoxelArea:index(x, y, z)
|
||||
local MinEdge = self.MinEdge
|
||||
local i = (z - MinEdge.z) * self.zstride +
|
||||
(y - MinEdge.y) * self.ystride +
|
||||
(x - MinEdge.x) + 1
|
||||
return math_floor(i)
|
||||
end
|
||||
|
||||
function VoxelArea:indexp(p)
|
||||
local MinEdge = self.MinEdge
|
||||
local i = (p.z - MinEdge.z) * self.zstride +
|
||||
(p.y - MinEdge.y) * self.ystride +
|
||||
(p.x - MinEdge.x) + 1
|
||||
return math_floor(i)
|
||||
end
|
||||
|
||||
function VoxelArea:position(i)
|
||||
local MinEdge = self.MinEdge
|
||||
|
||||
i = i - 1
|
||||
|
||||
local z = math_floor(i / self.zstride) + MinEdge.z
|
||||
i = i % self.zstride
|
||||
|
||||
local y = math_floor(i / self.ystride) + MinEdge.y
|
||||
i = i % self.ystride
|
||||
|
||||
local x = math_floor(i) + MinEdge.x
|
||||
|
||||
return vector_new(x, y, z)
|
||||
end
|
||||
|
||||
function VoxelArea:contains(x, y, z)
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return (x >= MinEdge.x) and (x <= MaxEdge.x) and
|
||||
(y >= MinEdge.y) and (y <= MaxEdge.y) and
|
||||
(z >= MinEdge.z) and (z <= MaxEdge.z)
|
||||
end
|
||||
|
||||
function VoxelArea:containsp(p)
|
||||
local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge
|
||||
return (p.x >= MinEdge.x) and (p.x <= MaxEdge.x) and
|
||||
(p.y >= MinEdge.y) and (p.y <= MaxEdge.y) and
|
||||
(p.z >= MinEdge.z) and (p.z <= MaxEdge.z)
|
||||
end
|
||||
|
||||
function VoxelArea:containsi(i)
|
||||
return (i >= 1) and (i <= self:getVolume())
|
||||
end
|
||||
|
||||
function VoxelArea:iter(minx, miny, minz, maxx, maxy, maxz)
|
||||
local i = self:index(minx, miny, minz) - 1
|
||||
local xrange = maxx - minx + 1
|
||||
local nextaction = i + 1 + xrange
|
||||
|
||||
local y = 0
|
||||
local yrange = maxy - miny + 1
|
||||
local yreqstride = self.ystride - xrange
|
||||
|
||||
local z = 0
|
||||
local zrange = maxz - minz + 1
|
||||
local multistride = self.zstride - ((yrange - 1) * self.ystride + xrange)
|
||||
|
||||
return function()
|
||||
-- continue i until it needs to jump
|
||||
i = i + 1
|
||||
if i ~= nextaction then
|
||||
return i
|
||||
end
|
||||
|
||||
-- continue y until maxy is exceeded
|
||||
y = y + 1
|
||||
if y ~= yrange then
|
||||
-- set i to index(minx, miny + y, minz + z) - 1
|
||||
i = i + yreqstride
|
||||
nextaction = i + xrange
|
||||
return i
|
||||
end
|
||||
|
||||
-- continue z until maxz is exceeded
|
||||
z = z + 1
|
||||
if z == zrange then
|
||||
-- cuboid finished, return nil
|
||||
return
|
||||
end
|
||||
|
||||
-- set i to index(minx, miny, minz + z) - 1
|
||||
i = i + multistride
|
||||
|
||||
y = 0
|
||||
nextaction = i + xrange
|
||||
return i
|
||||
end
|
||||
end
|
||||
|
||||
function VoxelArea:iterp(minp, maxp)
|
||||
return self:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z)
|
||||
end
|
||||
67
builtin/init.lua
Normal file
67
builtin/init.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
--
|
||||
-- This file contains built-in stuff in Minetest implemented in Lua.
|
||||
--
|
||||
-- It is always loaded and executed after registration of the C API,
|
||||
-- before loading and running any mods.
|
||||
--
|
||||
|
||||
-- Initialize some very basic things
|
||||
function core.debug(...) core.log(table.concat({...}, "\t")) end
|
||||
if core.print then
|
||||
local core_print = core.print
|
||||
-- Override native print and use
|
||||
-- terminal if that's turned on
|
||||
function print(...)
|
||||
local n, t = select("#", ...), {...}
|
||||
for i = 1, n do
|
||||
t[i] = tostring(t[i])
|
||||
end
|
||||
core_print(table.concat(t, "\t"))
|
||||
end
|
||||
core.print = nil -- don't pollute our namespace
|
||||
end
|
||||
math.randomseed(os.time())
|
||||
minetest = core
|
||||
|
||||
-- Load other files
|
||||
local scriptdir = core.get_builtin_path()
|
||||
local gamepath = scriptdir .. "game" .. DIR_DELIM
|
||||
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")
|
||||
|
||||
if INIT == "game" then
|
||||
dofile(gamepath .. "init.lua")
|
||||
assert(not core.get_http_api)
|
||||
elseif INIT == "mainmenu" then
|
||||
local mm_script = core.settings:get("main_menu_script")
|
||||
local custom_loaded = false
|
||||
if mm_script and mm_script ~= "" then
|
||||
local testfile = io.open(mm_script, "r")
|
||||
if testfile then
|
||||
testfile:close()
|
||||
dofile(mm_script)
|
||||
custom_loaded = true
|
||||
core.log("info", "Loaded custom main menu script: "..mm_script)
|
||||
else
|
||||
core.log("error", "Failed to load custom main menu script: "..mm_script)
|
||||
core.log("info", "Falling back to default main menu script")
|
||||
end
|
||||
end
|
||||
if not custom_loaded then
|
||||
dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua")
|
||||
end
|
||||
elseif INIT == "async" then
|
||||
dofile(asyncpath .. "mainmenu.lua")
|
||||
elseif INIT == "async_game" then
|
||||
dofile(asyncpath .. "game.lua")
|
||||
elseif INIT == "client" then
|
||||
dofile(clientpath .. "init.lua")
|
||||
else
|
||||
error(("Unrecognized builtin initialization type %s!"):format(tostring(INIT)))
|
||||
end
|
||||
246
builtin/locale/__builtin.de.tr
Normal file
246
builtin/locale/__builtin.de.tr
Normal file
@@ -0,0 +1,246 @@
|
||||
# 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. Cannot modify secure settings. Edit the settings file manually.=Fehlgeschlagen. Sicherheitseinstellungen können nicht modifiziert werden. Bearbeiten Sie die Einstellungsdatei manuell.
|
||||
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
|
||||
You cannot ban players in singleplayer!=Im Einzelspielermodus können Sie keine Spieler verbannen!
|
||||
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)
|
||||
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
|
||||
Available privileges:=Verfügbare Privilegien:
|
||||
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 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
|
||||
259
builtin/locale/__builtin.it.tr
Normal file
259
builtin/locale/__builtin.it.tr
Normal file
@@ -0,0 +1,259 @@
|
||||
# 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. Cannot modify secure settings. Edit the settings file manually.=
|
||||
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
|
||||
You cannot ban players in singleplayer!=
|
||||
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)=
|
||||
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
|
||||
Available privileges:=Privilegi disponibili:
|
||||
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 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'
|
||||
246
builtin/locale/template.txt
Normal file
246
builtin/locale/template.txt
Normal file
@@ -0,0 +1,246 @@
|
||||
# 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. Cannot modify secure settings. Edit the settings file manually.=
|
||||
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=
|
||||
You cannot ban players in singleplayer!=
|
||||
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)=
|
||||
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=
|
||||
Available privileges:=
|
||||
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 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=
|
||||
32
builtin/mainmenu/async_event.lua
Normal file
32
builtin/mainmenu/async_event.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
core.async_jobs = {}
|
||||
|
||||
local function handle_job(jobid, serialized_retval)
|
||||
local retval = core.deserialize(serialized_retval)
|
||||
assert(type(core.async_jobs[jobid]) == "function")
|
||||
core.async_jobs[jobid](retval)
|
||||
core.async_jobs[jobid] = nil
|
||||
end
|
||||
|
||||
core.async_event_handler = handle_job
|
||||
|
||||
function core.handle_async(func, parameter, callback)
|
||||
-- Serialize function
|
||||
local serialized_func = string.dump(func)
|
||||
|
||||
assert(serialized_func ~= nil)
|
||||
|
||||
-- Serialize parameters
|
||||
local serialized_param = core.serialize(parameter)
|
||||
|
||||
if serialized_param == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local jobid = core.do_async_callback(serialized_func, serialized_param)
|
||||
|
||||
core.async_jobs[jobid] = callback
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
246
builtin/mainmenu/common.lua
Normal file
246
builtin/mainmenu/common.lua
Normal file
@@ -0,0 +1,246 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
-- Global menu data
|
||||
menudata = {}
|
||||
|
||||
-- Local cached values
|
||||
local min_supp_proto, max_supp_proto
|
||||
|
||||
function common_update_cached_supp_proto()
|
||||
min_supp_proto = core.get_min_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 > 999 then return '99+'
|
||||
elseif n >= 0 then return tostring(n)
|
||||
else return '?' end
|
||||
end
|
||||
|
||||
local function configure_selected_world_params(idx)
|
||||
local worldconfig = pkgmgr.get_worldconfig(menudata.worldlist:get_list()[idx].path)
|
||||
if worldconfig.creative_mode then
|
||||
core.settings:set("creative_mode", worldconfig.creative_mode)
|
||||
end
|
||||
if worldconfig.enable_damage then
|
||||
core.settings:set("enable_damage", worldconfig.enable_damage)
|
||||
end
|
||||
end
|
||||
|
||||
function render_serverlist_row(spec)
|
||||
local text = ""
|
||||
if spec.name then
|
||||
text = text .. core.formspec_escape(spec.name:trim())
|
||||
elseif spec.address then
|
||||
text = text .. core.formspec_escape(spec.address:trim())
|
||||
if spec.port then
|
||||
text = text .. ":" .. spec.port
|
||||
end
|
||||
end
|
||||
|
||||
local grey_out = not spec.is_compatible
|
||||
|
||||
local details = {}
|
||||
|
||||
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
|
||||
table.insert(details, "4")
|
||||
end
|
||||
else
|
||||
table.insert(details, "0")
|
||||
end
|
||||
|
||||
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
|
||||
-- (relatively to clients_max)
|
||||
local clients_color
|
||||
if grey_out then clients_color = '#aaaaaa'
|
||||
elseif spec.clients == 0 then clients_color = '' -- 0 players: default/white
|
||||
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
|
||||
end
|
||||
|
||||
table.insert(details, clients_color)
|
||||
table.insert(details, render_client_count(spec.clients) .. " / " ..
|
||||
render_client_count(spec.clients_max))
|
||||
else
|
||||
table.insert(details, color)
|
||||
table.insert(details, "?")
|
||||
end
|
||||
|
||||
if spec.creative then
|
||||
table.insert(details, "1") -- creative icon
|
||||
else
|
||||
table.insert(details, "0")
|
||||
end
|
||||
|
||||
if spec.pvp then
|
||||
table.insert(details, "2") -- pvp icon
|
||||
elseif spec.damage then
|
||||
table.insert(details, "1") -- heart icon
|
||||
else
|
||||
table.insert(details, "0")
|
||||
end
|
||||
|
||||
table.insert(details, color)
|
||||
table.insert(details, text)
|
||||
|
||||
return table.concat(details, ",")
|
||||
end
|
||||
---------------------------------------------------------------------------------
|
||||
os.tmpname = function()
|
||||
error('do not use') -- instead use core.get_temp_path()
|
||||
end
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function menu_render_worldlist()
|
||||
local retval = {}
|
||||
local current_worldlist = menudata.worldlist:get_list()
|
||||
|
||||
for i, v in ipairs(current_worldlist) do
|
||||
retval[#retval+1] = core.formspec_escape(v.name)
|
||||
end
|
||||
|
||||
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
|
||||
if fields.key_up and oldidx and oldidx > 1 then
|
||||
newidx = oldidx - 1
|
||||
elseif fields.key_down and oldidx and
|
||||
oldidx < menudata.worldlist:size() then
|
||||
newidx = oldidx + 1
|
||||
end
|
||||
core.settings:set(settingname, menudata.worldlist:get_raw_index(newidx))
|
||||
configure_selected_world_params(newidx)
|
||||
return true
|
||||
end
|
||||
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 ..
|
||||
"," .. height .. ";" .. tl_name .. ";"
|
||||
|
||||
for i = 1, #textlines do
|
||||
textlines[i] = textlines[i]:gsub("\r", "")
|
||||
retval = retval .. core.formspec_escape(textlines[i]) .. ","
|
||||
end
|
||||
|
||||
retval = retval .. ";0;"
|
||||
if transparency then retval = retval .. "true" end
|
||||
retval = retval .. "]"
|
||||
|
||||
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.
|
||||
return true
|
||||
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
|
||||
local s_p_min = server_proto_min
|
||||
local s_p_max = server_proto_max
|
||||
|
||||
if s_p_min ~= s_p_max then
|
||||
server_prot_ver_info = fgettext_ne("Server supports protocol versions between $1 and $2. ",
|
||||
s_p_min, s_p_max)
|
||||
else
|
||||
server_prot_ver_info = fgettext_ne("Server enforces protocol version $1. ",
|
||||
s_p_min)
|
||||
end
|
||||
if min_supp_proto ~= max_supp_proto then
|
||||
client_prot_ver_info= fgettext_ne("We support protocol versions between version $1 and $2.",
|
||||
min_supp_proto, max_supp_proto)
|
||||
else
|
||||
client_prot_ver_info = fgettext_ne("We only support protocol version $1.", min_supp_proto)
|
||||
end
|
||||
gamedata.errormessage = fgettext_ne("Protocol version mismatch. ")
|
||||
.. server_prot_ver_info
|
||||
.. client_prot_ver_info
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function menu_worldmt(selected, setting, value)
|
||||
local world = menudata.worldlist:get_list()[selected]
|
||||
if world then
|
||||
local filename = world.path .. DIR_DELIM .. "world.mt"
|
||||
local world_conf = Settings(filename)
|
||||
|
||||
if value then
|
||||
if not world_conf:write() then
|
||||
core.log("error", "Failed to write world config file")
|
||||
end
|
||||
world_conf:set(setting, value)
|
||||
world_conf:write()
|
||||
else
|
||||
return world_conf:get(setting)
|
||||
end
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function menu_worldmt_legacy(selected)
|
||||
local modes_names = {"creative_mode", "enable_damage", "server_announce"}
|
||||
for _, mode_name in pairs(modes_names) do
|
||||
local mode_val = menu_worldmt(selected, mode_name)
|
||||
if mode_val then
|
||||
core.settings:set(mode_name, mode_val)
|
||||
else
|
||||
menu_worldmt(selected, mode_name, core.settings:get(mode_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function confirmation_formspec(message, confirm_id, confirm_label, cancel_id, cancel_label)
|
||||
return "size[10,2.5,true]" ..
|
||||
"label[0.5,0.5;" .. message .. "]" ..
|
||||
"style[" .. confirm_id .. ";bgcolor=red]" ..
|
||||
"button[0.5,1.5;2.5,0.5;" .. confirm_id .. ";" .. confirm_label .. "]" ..
|
||||
"button[7.0,1.5;2.5,0.5;" .. cancel_id .. ";" .. cancel_label .. "]"
|
||||
end
|
||||
409
builtin/mainmenu/dlg_config_world.lua
Normal file
409
builtin/mainmenu/dlg_config_world.lua
Normal file
@@ -0,0 +1,409 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local enabled_all = false
|
||||
|
||||
local function modname_valid(name)
|
||||
return not name:find("[^a-z0-9_]")
|
||||
end
|
||||
|
||||
local function init_data(data)
|
||||
data.list = filterlist.create(
|
||||
pkgmgr.preparemodlist,
|
||||
pkgmgr.comparemod,
|
||||
function(element, uid)
|
||||
if element.name == uid then
|
||||
return true
|
||||
end
|
||||
end,
|
||||
function(element, criteria)
|
||||
if criteria.hide_game and
|
||||
element.is_game_content then
|
||||
return false
|
||||
end
|
||||
|
||||
if criteria.hide_modpackcontents and
|
||||
element.modpack ~= nil then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end,
|
||||
{
|
||||
worldpath = data.worldspec.path,
|
||||
gameid = data.worldspec.gameid
|
||||
})
|
||||
|
||||
if data.selected_mod > data.list:size() then
|
||||
data.selected_mod = 0
|
||||
end
|
||||
|
||||
data.list:set_filtercriteria({
|
||||
hide_game = data.hide_gamemods,
|
||||
hide_modpackcontents = data.hide_modpackcontents
|
||||
})
|
||||
data.list:add_sort_mechanism("alphabetic", sort_mod_list)
|
||||
data.list:set_sortmode("alphabetic")
|
||||
end
|
||||
|
||||
|
||||
-- Returns errors errors and a list of all enabled mods (inc. game and world mods)
|
||||
--
|
||||
-- `with_errors` is a table from mod virtual path to `{ type = "error" | "warning" }`.
|
||||
-- `enabled_mods_by_name` is a table from mod virtual path to `true`.
|
||||
--
|
||||
-- @param world_path Path to the world
|
||||
-- @param all_mods List of mods, with `enabled` property.
|
||||
-- @returns with_errors, enabled_mods_by_name
|
||||
local function check_mod_configuration(world_path, all_mods)
|
||||
-- Build up lookup tables for enabled mods and all mods by vpath
|
||||
local enabled_mod_paths = {}
|
||||
local all_mods_by_vpath = {}
|
||||
for _, mod in ipairs(all_mods) do
|
||||
if mod.type == "mod" then
|
||||
all_mods_by_vpath[mod.virtual_path] = mod
|
||||
end
|
||||
if mod.enabled then
|
||||
enabled_mod_paths[mod.virtual_path] = mod.path
|
||||
end
|
||||
end
|
||||
|
||||
-- Use the engine's mod configuration code to resolve dependencies and return any errors
|
||||
local config_status = core.check_mod_configuration(world_path, enabled_mod_paths)
|
||||
|
||||
-- Build the list of enabled mod virtual paths
|
||||
local enabled_mods_by_name = {}
|
||||
for _, mod in ipairs(config_status.satisfied_mods) do
|
||||
assert(mod.virtual_path ~= "")
|
||||
enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod
|
||||
end
|
||||
for _, mod in ipairs(config_status.unsatisfied_mods) do
|
||||
assert(mod.virtual_path ~= "")
|
||||
enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod
|
||||
end
|
||||
|
||||
-- Build the table of errors
|
||||
local with_error = {}
|
||||
for _, mod in ipairs(config_status.unsatisfied_mods) do
|
||||
local error = { type = "warning" }
|
||||
with_error[mod.virtual_path] = error
|
||||
|
||||
for _, depname in ipairs(mod.unsatisfied_depends) do
|
||||
if not enabled_mods_by_name[depname] then
|
||||
error.type = "error"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return with_error, enabled_mods_by_name
|
||||
end
|
||||
|
||||
local function get_formspec(data)
|
||||
if not data.list then
|
||||
init_data(data)
|
||||
end
|
||||
|
||||
local all_mods = data.list:get_list()
|
||||
local with_error, enabled_mods_by_name = check_mod_configuration(data.worldspec.path, all_mods)
|
||||
|
||||
local mod = all_mods[data.selected_mod] or {name = ""}
|
||||
|
||||
local retval =
|
||||
"size[11.5,7.5,true]" ..
|
||||
"label[0.5,0;" .. fgettext("World:") .. "]" ..
|
||||
"label[1.75,0;" .. data.worldspec.name .. "]"
|
||||
|
||||
if mod.is_modpack or mod.type == "game" then
|
||||
local info = core.formspec_escape(
|
||||
core.get_content_info(mod.path).description)
|
||||
if info == "" then
|
||||
if mod.is_modpack then
|
||||
info = fgettext("No modpack description provided.")
|
||||
else
|
||||
info = fgettext("No game description provided.")
|
||||
end
|
||||
end
|
||||
retval = retval ..
|
||||
"textarea[0.25,0.7;5.75,7.2;;" .. info .. ";]"
|
||||
else
|
||||
local hard_deps, soft_deps = pkgmgr.get_dependencies(mod.path)
|
||||
|
||||
-- Add error messages to dep lists
|
||||
if mod.enabled or mod.is_game_content then
|
||||
for i, dep_name in ipairs(hard_deps) do
|
||||
local dep = enabled_mods_by_name[dep_name]
|
||||
if not dep then
|
||||
hard_deps[i] = mt_color_red .. dep_name .. " " .. fgettext("(Unsatisfied)")
|
||||
elseif with_error[dep.virtual_path] then
|
||||
hard_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)")
|
||||
else
|
||||
hard_deps[i] = mt_color_green .. dep_name
|
||||
end
|
||||
end
|
||||
for i, dep_name in ipairs(soft_deps) do
|
||||
local dep = enabled_mods_by_name[dep_name]
|
||||
if dep and with_error[dep.virtual_path] then
|
||||
soft_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)")
|
||||
elseif dep then
|
||||
soft_deps[i] = mt_color_green .. dep_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hard_deps_str = table.concat(hard_deps, ",")
|
||||
local soft_deps_str = table.concat(soft_deps, ",")
|
||||
|
||||
retval = retval ..
|
||||
"label[0,0.7;" .. fgettext("Mod:") .. "]" ..
|
||||
"label[0.75,0.7;" .. mod.name .. "]"
|
||||
|
||||
if hard_deps_str == "" then
|
||||
if soft_deps_str == "" then
|
||||
retval = retval ..
|
||||
"label[0,1.25;" ..
|
||||
fgettext("No (optional) dependencies") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("No hard dependencies") ..
|
||||
"]" ..
|
||||
"label[0,1.75;" .. fgettext("Optional dependencies:") ..
|
||||
"]" ..
|
||||
"textlist[0,2.25;5,4;world_config_optdepends;" ..
|
||||
soft_deps_str .. ";0]"
|
||||
end
|
||||
else
|
||||
if soft_deps_str == "" then
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("Dependencies:") .. "]" ..
|
||||
"textlist[0,1.75;5,4;world_config_depends;" ..
|
||||
hard_deps_str .. ";0]" ..
|
||||
"label[0,6;" .. fgettext("No optional dependencies") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"label[0,1.25;" .. fgettext("Dependencies:") .. "]" ..
|
||||
"textlist[0,1.75;5,2.125;world_config_depends;" ..
|
||||
hard_deps_str .. ";0]" ..
|
||||
"label[0,3.9;" .. fgettext("Optional dependencies:") ..
|
||||
"]" ..
|
||||
"textlist[0,4.375;5,1.8;world_config_optdepends;" ..
|
||||
soft_deps_str .. ";0]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"button[3.25,7;2.5,0.5;btn_config_world_save;" ..
|
||||
fgettext("Save") .. "]" ..
|
||||
"button[5.75,7;2.5,0.5;btn_config_world_cancel;" ..
|
||||
fgettext("Cancel") .. "]" ..
|
||||
"button[9,7;2.5,0.5;btn_config_world_cdb;" ..
|
||||
fgettext("Find More Mods") .. "]"
|
||||
|
||||
if mod.name ~= "" and not mod.is_game_content then
|
||||
if mod.is_modpack then
|
||||
if pkgmgr.is_modpack_entirely_enabled(data, mod.name) then
|
||||
retval = retval ..
|
||||
"button[5.5,0.125;3,0.5;btn_mp_disable;" ..
|
||||
fgettext("Disable modpack") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[5.5,0.125;3,0.5;btn_mp_enable;" ..
|
||||
fgettext("Enable modpack") .. "]"
|
||||
end
|
||||
else
|
||||
retval = retval ..
|
||||
"checkbox[5.5,-0.125;cb_mod_enable;" .. fgettext("enabled") ..
|
||||
";" .. tostring(mod.enabled) .. "]"
|
||||
end
|
||||
end
|
||||
if enabled_all then
|
||||
retval = retval ..
|
||||
"button[8.95,0.125;2.5,0.5;btn_disable_all_mods;" ..
|
||||
fgettext("Disable all") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.95,0.125;2.5,0.5;btn_enable_all_mods;" ..
|
||||
fgettext("Enable all") .. "]"
|
||||
end
|
||||
|
||||
local use_technical_names = core.settings:get_bool("show_technical_names")
|
||||
|
||||
return retval ..
|
||||
"tablecolumns[color;tree;image,align=inline,width=1.5,0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") ..
|
||||
",1=" .. core.formspec_escape(defaulttexturedir .. "checkbox_16_white.png") ..
|
||||
",2=" .. core.formspec_escape(defaulttexturedir .. "error_icon_orange.png") ..
|
||||
",3=" .. core.formspec_escape(defaulttexturedir .. "error_icon_red.png") .. ";text]" ..
|
||||
"table[5.5,0.75;5.75,6;world_config_modlist;" ..
|
||||
pkgmgr.render_packagelist(data.list, use_technical_names, with_error) .. ";" .. data.selected_mod .."]"
|
||||
end
|
||||
|
||||
local function handle_buttons(this, fields)
|
||||
if fields.world_config_modlist then
|
||||
local event = core.explode_table_event(fields.world_config_modlist)
|
||||
this.data.selected_mod = event.row
|
||||
core.settings:set("world_config_selected_mod", event.row)
|
||||
|
||||
if event.type == "DCL" then
|
||||
pkgmgr.enable_mod(this)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.key_enter then
|
||||
pkgmgr.enable_mod(this)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.cb_mod_enable ~= nil then
|
||||
pkgmgr.enable_mod(this, core.is_yes(fields.cb_mod_enable))
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_enable ~= nil or
|
||||
fields.btn_mp_disable then
|
||||
pkgmgr.enable_mod(this, fields.btn_mp_enable ~= nil)
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_save then
|
||||
local filename = this.data.worldspec.path .. DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
local mods = worldfile:to_table()
|
||||
|
||||
local rawlist = this.data.list:get_raw_list()
|
||||
local was_set = {}
|
||||
|
||||
for i = 1, #rawlist do
|
||||
local mod = rawlist[i]
|
||||
if not mod.is_modpack and
|
||||
not mod.is_game_content then
|
||||
if modname_valid(mod.name) then
|
||||
if mod.enabled then
|
||||
worldfile:set("load_mod_" .. mod.name, mod.virtual_path)
|
||||
was_set[mod.name] = true
|
||||
elseif not was_set[mod.name] then
|
||||
worldfile:set("load_mod_" .. mod.name, "false")
|
||||
end
|
||||
elseif mod.enabled then
|
||||
gamedata.errormessage = fgettext_ne("Failed to enable mo" ..
|
||||
"d \"$1\" as it contains disallowed characters. " ..
|
||||
"Only characters [a-z0-9_] are allowed.",
|
||||
mod.name)
|
||||
end
|
||||
mods["load_mod_" .. mod.name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove mods that are not present anymore
|
||||
for key in pairs(mods) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
worldfile:remove(key)
|
||||
end
|
||||
end
|
||||
|
||||
if not worldfile:write() then
|
||||
core.log("error", "Failed to write world config file")
|
||||
end
|
||||
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_cancel then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_config_world_cdb then
|
||||
this.data.list = nil
|
||||
|
||||
local dlg = create_store_dlg("mod")
|
||||
dlg:set_parent(this)
|
||||
this:hide()
|
||||
dlg:show()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_enable_all_mods then
|
||||
local list = this.data.list:get_raw_list()
|
||||
|
||||
-- When multiple copies of a mod are installed, we need to avoid enabling multiple of them
|
||||
-- at a time. So lets first collect all the enabled mods, and then use this to exclude
|
||||
-- multiple enables.
|
||||
|
||||
local was_enabled = {}
|
||||
for i = 1, #list do
|
||||
if not list[i].is_game_content
|
||||
and not list[i].is_modpack and list[i].enabled then
|
||||
was_enabled[list[i].name] = true
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #list do
|
||||
if not list[i].is_game_content and not list[i].is_modpack and
|
||||
not was_enabled[list[i].name] then
|
||||
list[i].enabled = true
|
||||
was_enabled[list[i].name] = true
|
||||
end
|
||||
end
|
||||
|
||||
enabled_all = true
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_disable_all_mods then
|
||||
local list = this.data.list:get_raw_list()
|
||||
|
||||
for i = 1, #list do
|
||||
if not list[i].is_game_content
|
||||
and not list[i].is_modpack then
|
||||
list[i].enabled = false
|
||||
end
|
||||
end
|
||||
enabled_all = false
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function create_configure_world_dlg(worldidx)
|
||||
local dlg = dialog_create("sp_config_world", get_formspec, handle_buttons)
|
||||
|
||||
dlg.data.selected_mod = tonumber(
|
||||
core.settings:get("world_config_selected_mod")) or 0
|
||||
|
||||
dlg.data.worldspec = core.get_worlds()[worldidx]
|
||||
if not dlg.data.worldspec then
|
||||
dlg:delete()
|
||||
return
|
||||
end
|
||||
|
||||
dlg.data.worldconfig = pkgmgr.get_worldconfig(dlg.data.worldspec.path)
|
||||
|
||||
if not dlg.data.worldconfig or not dlg.data.worldconfig.id or
|
||||
dlg.data.worldconfig.id == "" then
|
||||
dlg:delete()
|
||||
return
|
||||
end
|
||||
|
||||
return dlg
|
||||
end
|
||||
1062
builtin/mainmenu/dlg_contentstore.lua
Normal file
1062
builtin/mainmenu/dlg_contentstore.lua
Normal file
File diff suppressed because it is too large
Load Diff
488
builtin/mainmenu/dlg_create_world.lua
Normal file
488
builtin/mainmenu/dlg_create_world.lua
Normal file
@@ -0,0 +1,488 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local function table_to_flags(ftable)
|
||||
-- Convert e.g. { jungles = true, caves = false } to "jungles,nocaves"
|
||||
local str = {}
|
||||
for flag, is_set in pairs(ftable) do
|
||||
str[#str + 1] = is_set and flag or ("no" .. flag)
|
||||
end
|
||||
return table.concat(str, ",")
|
||||
end
|
||||
|
||||
-- Same as check_flag but returns a string
|
||||
local function strflag(flags, flag)
|
||||
return (flags[flag] == true) and "true" or "false"
|
||||
end
|
||||
|
||||
local cb_caverns = { "caverns", fgettext("Caverns"),
|
||||
fgettext("Very large caverns deep in the underground") }
|
||||
|
||||
local flag_checkboxes = {
|
||||
v5 = {
|
||||
cb_caverns,
|
||||
},
|
||||
v7 = {
|
||||
cb_caverns,
|
||||
{ "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"), fgettext("Sea level rivers") },
|
||||
},
|
||||
valleys = {
|
||||
{ "altitude_chill", fgettext("Altitude chill"),
|
||||
fgettext("Reduces heat with altitude") },
|
||||
{ "altitude_dry", fgettext("Altitude dry"),
|
||||
fgettext("Reduces humidity with altitude") },
|
||||
{ "humid_rivers", fgettext("Humid rivers"),
|
||||
fgettext("Increases humidity around rivers") },
|
||||
{ "vary_river_depth", fgettext("Vary river depth"),
|
||||
fgettext("Low humidity and high heat causes shallow or dry rivers") },
|
||||
},
|
||||
flat = {
|
||||
cb_caverns,
|
||||
{ "hills", fgettext("Hills") },
|
||||
{ "lakes", fgettext("Lakes") },
|
||||
},
|
||||
fractal = {
|
||||
{ "terrain", fgettext("Additional terrain"),
|
||||
fgettext("Generate non-fractal terrain: Oceans and underground") },
|
||||
},
|
||||
v6 = {
|
||||
{ "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
|
||||
},
|
||||
}
|
||||
|
||||
local mgv6_biomes = {
|
||||
{
|
||||
fgettext("Temperate, Desert, Jungle, Tundra, Taiga"),
|
||||
{jungles = true, snowbiomes = true}
|
||||
},
|
||||
{
|
||||
fgettext("Temperate, Desert, Jungle"),
|
||||
{jungles = true, snowbiomes = false}
|
||||
},
|
||||
{
|
||||
fgettext("Temperate, Desert"),
|
||||
{jungles = false, snowbiomes = false}
|
||||
},
|
||||
}
|
||||
|
||||
local function create_world_formspec(dialogdata)
|
||||
|
||||
-- Point the player to ContentDB when no games are found
|
||||
if #pkgmgr.games == 0 then
|
||||
return "size[8,2.5,true]" ..
|
||||
"style[label_button;border=false]" ..
|
||||
"button[0.5,0.5;7,0.5;label_button;" ..
|
||||
fgettext("You have no games installed.") .. "]" ..
|
||||
"button[0.5,1.5;2.5,0.5;world_create_open_cdb;" .. fgettext("Install a game") .. "]" ..
|
||||
"button[5.0,1.5;2.5,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
|
||||
end
|
||||
|
||||
local current_mg = dialogdata.mg
|
||||
local mapgens = core.get_mapgen_names()
|
||||
|
||||
local gameid = core.settings:get("menu_last_game")
|
||||
|
||||
local flags = dialogdata.flags
|
||||
|
||||
local game = pkgmgr.find_by_gameid(gameid)
|
||||
if game == nil then
|
||||
-- should never happen but just pick the first game
|
||||
game = pkgmgr.get_game(1)
|
||||
core.settings:set("menu_last_game", game.id)
|
||||
end
|
||||
|
||||
local disallowed_mapgen_settings = {}
|
||||
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
|
||||
allowed_mapgens[key] = value:trim()
|
||||
end
|
||||
|
||||
local disallowed_mapgens = (gameconfig:get("disallowed_mapgens") or ""):split()
|
||||
for key, value in pairs(disallowed_mapgens) do
|
||||
disallowed_mapgens[key] = value:trim()
|
||||
end
|
||||
|
||||
if #allowed_mapgens > 0 then
|
||||
for i = #mapgens, 1, -1 do
|
||||
if table.indexof(allowed_mapgens, mapgens[i]) == -1 then
|
||||
table.remove(mapgens, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #disallowed_mapgens > 0 then
|
||||
for i = #mapgens, 1, -1 do
|
||||
if table.indexof(disallowed_mapgens, mapgens[i]) > 0 then
|
||||
table.remove(mapgens, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ds = (gameconfig:get("disallowed_mapgen_settings") or ""):split()
|
||||
for _, value in pairs(ds) do
|
||||
disallowed_mapgen_settings[value:trim()] = true
|
||||
end
|
||||
end
|
||||
|
||||
local mglist = ""
|
||||
local selindex
|
||||
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 not selindex then
|
||||
selindex = 1
|
||||
current_mg = first_mg
|
||||
end
|
||||
mglist = mglist:sub(1, -2)
|
||||
end
|
||||
|
||||
-- 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
|
||||
return "", y
|
||||
end
|
||||
if disallowed_mapgen_settings["mg_flags"] then
|
||||
return "", y
|
||||
end
|
||||
|
||||
local form = "checkbox[0," .. y .. ";flag_main_caves;" ..
|
||||
fgettext("Caves") .. ";"..strflag(flags.main, "caves").."]"
|
||||
y = y + 0.5
|
||||
|
||||
form = form .. "checkbox[0,"..y..";flag_main_dungeons;" ..
|
||||
fgettext("Dungeons") .. ";"..strflag(flags.main, "dungeons").."]"
|
||||
y = y + 0.5
|
||||
|
||||
local d_name = fgettext("Decorations")
|
||||
local d_tt
|
||||
if mapgen == "v6" then
|
||||
d_tt = fgettext("Structures appearing on the terrain (no effect on trees and jungle grass created by v6)")
|
||||
else
|
||||
d_tt = fgettext("Structures appearing on the terrain, typically trees and plants")
|
||||
end
|
||||
form = form .. "checkbox[0,"..y..";flag_main_decorations;" ..
|
||||
d_name .. ";" ..
|
||||
strflag(flags.main, "decorations").."]" ..
|
||||
"tooltip[flag_mg_decorations;" ..
|
||||
d_tt ..
|
||||
"]"
|
||||
y = y + 0.5
|
||||
|
||||
form = form .. "tooltip[flag_main_caves;" ..
|
||||
fgettext("Network of tunnels and caves")
|
||||
.. "]"
|
||||
return form, y
|
||||
end
|
||||
|
||||
local mg_specific_flags = function(mapgen, y)
|
||||
if not flag_checkboxes[mapgen] then
|
||||
return "", y
|
||||
end
|
||||
if disallowed_mapgen_settings["mg"..mapgen.."_spflags"] then
|
||||
return "", y
|
||||
end
|
||||
local form = ""
|
||||
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[1]))
|
||||
|
||||
if tab[3] then
|
||||
form = form .. "tooltip["..id..";"..tab[3].."]"
|
||||
end
|
||||
y = y + 0.5
|
||||
end
|
||||
|
||||
if mapgen ~= "v6" then
|
||||
-- No special treatment
|
||||
return form, y
|
||||
end
|
||||
-- Special treatment for v6 (add biome widgets)
|
||||
|
||||
-- Biome type (jungles, snowbiomes)
|
||||
local biometype
|
||||
if flags.v6.snowbiomes == true then
|
||||
biometype = 1
|
||||
elseif flags.v6.jungles == true then
|
||||
biometype = 2
|
||||
else
|
||||
biometype = 3
|
||||
end
|
||||
y = y + 0.3
|
||||
|
||||
form = form .. "label[0,"..(y+0.1)..";" .. fgettext("Biomes") .. "]"
|
||||
y = y + 0.6
|
||||
|
||||
form = form .. "dropdown[0,"..y..";6.3;mgv6_biomes;"
|
||||
for b=1, #mgv6_biomes do
|
||||
form = form .. mgv6_biomes[b][1]
|
||||
if b < #mgv6_biomes then
|
||||
form = form .. ","
|
||||
end
|
||||
end
|
||||
form = form .. ";" .. biometype.. "]"
|
||||
|
||||
-- biomeblend
|
||||
y = y + 0.55
|
||||
form = form .. "checkbox[0,"..y..";flag_v6_biomeblend;" ..
|
||||
fgettext("Biome blending") .. ";"..strflag(flags.v6, "biomeblend").."]" ..
|
||||
"tooltip[flag_v6_biomeblend;" ..
|
||||
fgettext("Smooth transition between biomes") .. "]"
|
||||
|
||||
return form, y
|
||||
end
|
||||
|
||||
local y_start = 0.0
|
||||
local y = y_start
|
||||
local str_flags, str_spflags
|
||||
local label_flags, label_spflags = "", ""
|
||||
y = y + 0.3
|
||||
str_flags, y = mg_main_flags(current_mg, y)
|
||||
if str_flags ~= "" then
|
||||
label_flags = "label[0,"..y_start..";" .. fgettext("Mapgen flags") .. "]"
|
||||
y_start = y + 0.4
|
||||
else
|
||||
y_start = 0.0
|
||||
end
|
||||
y = y_start + 0.3
|
||||
str_spflags = mg_specific_flags(current_mg, y)
|
||||
if str_spflags ~= "" then
|
||||
label_spflags = "label[0,"..y_start..";" .. fgettext("Mapgen-specific flags") .. "]"
|
||||
end
|
||||
|
||||
local retval =
|
||||
"size[12.25,7,true]" ..
|
||||
|
||||
-- Left side
|
||||
"container[0,0]"..
|
||||
"field[0.3,0.6;6,0.5;te_world_name;" ..
|
||||
fgettext("World name") ..
|
||||
";" .. core.formspec_escape(dialogdata.worldname) .. "]" ..
|
||||
"set_focus[te_world_name;false]"
|
||||
|
||||
if not disallowed_mapgen_settings["seed"] then
|
||||
|
||||
retval = retval .. "field[0.3,1.7;6,0.5;te_seed;" ..
|
||||
fgettext("Seed") ..
|
||||
";".. core.formspec_escape(dialogdata.seed) .. "]"
|
||||
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"label[0,2;" .. fgettext("Mapgen") .. "]"..
|
||||
"dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]"
|
||||
|
||||
-- Warning if only devtest is installed
|
||||
if #pkgmgr.games == 1 and pkgmgr.games[1].id == "devtest" then
|
||||
retval = retval ..
|
||||
"container[0,3.5]" ..
|
||||
"box[0,0;5.8,1.7;#ff8800]" ..
|
||||
"textarea[0.4,0.1;6,1.8;;;"..
|
||||
fgettext("Development Test is meant for developers.") .. "]" ..
|
||||
"button[1,1;4,0.5;world_create_open_cdb;" .. fgettext("Install another game") .. "]" ..
|
||||
"container_end[]"
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"container_end[]" ..
|
||||
|
||||
-- Right side
|
||||
"container[6.2,0]"..
|
||||
label_flags .. str_flags ..
|
||||
label_spflags .. str_spflags ..
|
||||
"container_end[]"..
|
||||
|
||||
-- Menu buttons
|
||||
"button[3.25,6.5;3,0.5;world_create_confirm;" .. fgettext("Create") .. "]" ..
|
||||
"button[6.25,6.5;3,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]"
|
||||
|
||||
return retval
|
||||
|
||||
end
|
||||
|
||||
local function create_world_buttonhandler(this, fields)
|
||||
|
||||
if fields["world_create_open_cdb"] then
|
||||
local dlg = create_store_dlg("game")
|
||||
dlg:set_parent(this.parent)
|
||||
this:delete()
|
||||
this.parent:hide()
|
||||
dlg:show()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_create_confirm"] or
|
||||
fields["key_enter"] then
|
||||
|
||||
local worldname = fields["te_world_name"]
|
||||
local game, gameindex = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
|
||||
|
||||
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.
|
||||
if worldname == "" then
|
||||
local worldnum_max = 0
|
||||
for _, world in ipairs(menudata.worldlist:get_list()) do
|
||||
if world.name:match("^world%d+$") then
|
||||
local worldnum = tonumber(world.name:sub(6))
|
||||
worldnum_max = math.max(worldnum_max, worldnum)
|
||||
end
|
||||
end
|
||||
worldname = "world" .. worldnum_max + 1
|
||||
end
|
||||
|
||||
if menudata.worldlist:uid_exists_raw(worldname) then
|
||||
message = fgettext("A world named \"$1\" already exists", worldname)
|
||||
end
|
||||
end
|
||||
|
||||
if message == nil then
|
||||
this.data.seed = fields["te_seed"] or ""
|
||||
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)
|
||||
menudata.worldlist:set_filtercriteria(game.id)
|
||||
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
|
||||
|
||||
this.data.worldname = fields["te_world_name"]
|
||||
this.data.seed = fields["te_seed"] or ""
|
||||
|
||||
if fields["games"] then
|
||||
local gameindex = core.get_textlist_index("games")
|
||||
core.settings:set("menu_last_game", pkgmgr.games[gameindex].id)
|
||||
return true
|
||||
end
|
||||
|
||||
for k,v in pairs(fields) do
|
||||
local split = string.split(k, "_", nil, 3)
|
||||
if split and split[1] == "flag" then
|
||||
-- We replaced the underscore of flag names with a dash.
|
||||
local flag = string.gsub(split[3], "-", "_")
|
||||
local ftable = this.data.flags[split[2]]
|
||||
assert(ftable)
|
||||
ftable[flag] = v == "true"
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if fields["world_create_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["mgv6_biomes"] then
|
||||
local entry = core.formspec_escape(fields["mgv6_biomes"])
|
||||
for b=1, #mgv6_biomes do
|
||||
if entry == mgv6_biomes[b][1] then
|
||||
local ftable = this.data.flags.v6
|
||||
ftable.jungles = mgv6_biomes[b][2].jungles
|
||||
ftable.snowbiomes = mgv6_biomes[b][2].snowbiomes
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if fields["dd_mapgen"] then
|
||||
this.data.mg = fields["dd_mapgen"]
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function create_create_world_dlg()
|
||||
local retval = dialog_create("sp_create_world",
|
||||
create_world_formspec,
|
||||
create_world_buttonhandler,
|
||||
nil)
|
||||
retval.data = {
|
||||
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
|
||||
70
builtin/mainmenu/dlg_delete_content.lua
Normal file
70
builtin/mainmenu/dlg_delete_content.lua
Normal file
@@ -0,0 +1,70 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function delete_content_formspec(dialogdata)
|
||||
return confirmation_formspec(
|
||||
fgettext("Are you sure you want to delete \"$1\"?", dialogdata.content.name),
|
||||
'dlg_delete_content_confirm', fgettext("Delete"),
|
||||
'dlg_delete_content_cancel', fgettext("Cancel"))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function delete_content_buttonhandler(this, fields)
|
||||
if fields["dlg_delete_content_confirm"] ~= nil then
|
||||
|
||||
if this.data.content.path ~= nil and
|
||||
this.data.content.path ~= "" and
|
||||
this.data.content.path ~= core.get_modpath() and
|
||||
this.data.content.path ~= core.get_gamepath() and
|
||||
this.data.content.path ~= core.get_texturepath() then
|
||||
if not core.delete_dir(this.data.content.path) then
|
||||
gamedata.errormessage = fgettext("pkgmgr: failed to delete \"$1\"", this.data.content.path)
|
||||
end
|
||||
|
||||
if this.data.content.type == "game" then
|
||||
pkgmgr.update_gamelist()
|
||||
else
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
else
|
||||
gamedata.errormessage = fgettext("pkgmgr: invalid path \"$1\"", this.data.content.path)
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["dlg_delete_content_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function create_delete_content_dlg(content)
|
||||
assert(content.name)
|
||||
|
||||
local retval = dialog_create("dlg_delete_content",
|
||||
delete_content_formspec,
|
||||
delete_content_buttonhandler,
|
||||
nil)
|
||||
retval.data.content = content
|
||||
return retval
|
||||
end
|
||||
58
builtin/mainmenu/dlg_delete_world.lua
Normal file
58
builtin/mainmenu/dlg_delete_world.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
|
||||
local function delete_world_formspec(dialogdata)
|
||||
return confirmation_formspec(
|
||||
fgettext("Delete World \"$1\"?", dialogdata.delete_name),
|
||||
'world_delete_confirm', fgettext("Delete"),
|
||||
'world_delete_cancel', fgettext("Cancel"))
|
||||
end
|
||||
|
||||
local function delete_world_buttonhandler(this, fields)
|
||||
if fields["world_delete_confirm"] then
|
||||
if this.data.delete_index > 0 and
|
||||
this.data.delete_index <= #menudata.worldlist:get_raw_list() then
|
||||
core.delete_world(this.data.delete_index)
|
||||
menudata.worldlist:refresh()
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_delete_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function create_delete_world_dlg(name_to_del, index_to_del)
|
||||
assert(name_to_del ~= nil and type(name_to_del) == "string" and name_to_del ~= "")
|
||||
assert(index_to_del ~= nil and type(index_to_del) == "number")
|
||||
|
||||
local retval = dialog_create("delete_world",
|
||||
delete_world_formspec,
|
||||
delete_world_buttonhandler,
|
||||
nil)
|
||||
retval.data.delete_name = name_to_del
|
||||
retval.data.delete_index = index_to_del
|
||||
|
||||
return retval
|
||||
end
|
||||
123
builtin/mainmenu/dlg_register.lua
Normal file
123
builtin/mainmenu/dlg_register.lua
Normal file
@@ -0,0 +1,123 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2022 rubenwardy
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function register_formspec(dialogdata)
|
||||
local title = fgettext("Joining $1", dialogdata.server and dialogdata.server.name or dialogdata.address)
|
||||
local buttons_y = 4 + 1.3
|
||||
if dialogdata.error then
|
||||
buttons_y = buttons_y + 0.8
|
||||
end
|
||||
|
||||
local retval = {
|
||||
"formspec_version[4]",
|
||||
"size[8,", tostring(buttons_y + 1.175), "]",
|
||||
"set_focus[", (dialogdata.name ~= "" and "password" or "name"), "]",
|
||||
"label[0.375,0.8;", title, "]",
|
||||
"field[0.375,1.575;7.25,0.8;name;", core.formspec_escape(fgettext("Name")), ";",
|
||||
core.formspec_escape(dialogdata.name), "]",
|
||||
"pwdfield[0.375,2.875;7.25,0.8;password;", core.formspec_escape(fgettext("Password")), "]",
|
||||
"pwdfield[0.375,4.175;7.25,0.8;password_2;", core.formspec_escape(fgettext("Confirm Password")), "]"
|
||||
}
|
||||
|
||||
if dialogdata.error then
|
||||
table.insert_all(retval, {
|
||||
"box[0.375,", tostring(buttons_y - 0.9), ";7.25,0.6;darkred]",
|
||||
"label[0.625,", tostring(buttons_y - 0.6), ";", core.formspec_escape(dialogdata.error), "]",
|
||||
})
|
||||
end
|
||||
|
||||
table.insert_all(retval, {
|
||||
"container[0.375,", tostring(buttons_y), "]",
|
||||
"button[0,0;2.5,0.8;dlg_register_confirm;", fgettext("Register"), "]",
|
||||
"button[4.75,0;2.5,0.8;dlg_register_cancel;", fgettext("Cancel"), "]",
|
||||
"container_end[]",
|
||||
})
|
||||
|
||||
return table.concat(retval, "")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function register_buttonhandler(this, fields)
|
||||
this.data.name = fields.name
|
||||
this.data.error = nil
|
||||
|
||||
if fields.dlg_register_confirm or fields.key_enter then
|
||||
if fields.name == "" then
|
||||
this.data.error = fgettext("Missing name")
|
||||
return true
|
||||
end
|
||||
if fields.password ~= fields.password_2 then
|
||||
this.data.error = fgettext("Passwords do not match")
|
||||
return true
|
||||
end
|
||||
|
||||
gamedata.playername = fields.name
|
||||
gamedata.password = fields.password
|
||||
gamedata.address = this.data.address
|
||||
gamedata.port = this.data.port
|
||||
gamedata.allow_login_or_register = "register"
|
||||
gamedata.selected_world = 0
|
||||
|
||||
assert(gamedata.address and gamedata.port)
|
||||
|
||||
local server = this.data.server
|
||||
if server then
|
||||
serverlistmgr.add_favorite(server)
|
||||
gamedata.servername = server.name
|
||||
gamedata.serverdescription = server.description
|
||||
else
|
||||
gamedata.servername = ""
|
||||
gamedata.serverdescription = ""
|
||||
|
||||
serverlistmgr.add_favorite({
|
||||
address = gamedata.address,
|
||||
port = gamedata.port,
|
||||
})
|
||||
end
|
||||
|
||||
core.settings:set("name", fields.name)
|
||||
core.settings:set("address", gamedata.address)
|
||||
core.settings:set("remote_port", gamedata.port)
|
||||
|
||||
core.start()
|
||||
end
|
||||
|
||||
if fields["dlg_register_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function create_register_dialog(address, port, server)
|
||||
assert(address)
|
||||
assert(type(port) == "number")
|
||||
|
||||
local retval = dialog_create("dlg_register",
|
||||
register_formspec,
|
||||
register_buttonhandler,
|
||||
nil)
|
||||
retval.data.address = address
|
||||
retval.data.port = port
|
||||
retval.data.server = server
|
||||
retval.data.name = core.settings:get("name") or ""
|
||||
return retval
|
||||
end
|
||||
73
builtin/mainmenu/dlg_rename_modpack.lua
Normal file
73
builtin/mainmenu/dlg_rename_modpack.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function rename_modpack_formspec(dialogdata)
|
||||
local retval =
|
||||
"size[11.5,4.5,true]" ..
|
||||
"button[3.25,3.5;2.5,0.5;dlg_rename_modpack_confirm;"..
|
||||
fgettext("Accept") .. "]" ..
|
||||
"button[5.75,3.5;2.5,0.5;dlg_rename_modpack_cancel;"..
|
||||
fgettext("Cancel") .. "]"
|
||||
|
||||
local input_y = 2
|
||||
if dialogdata.mod.is_name_explicit then
|
||||
retval = retval .. "textarea[1,0.2;10,2;;;" ..
|
||||
fgettext("This modpack has an explicit name given in its modpack.conf " ..
|
||||
"which will override any renaming here.") .. "]"
|
||||
input_y = 2.5
|
||||
end
|
||||
retval = retval ..
|
||||
"field[2.5," .. input_y .. ";7,0.5;te_modpack_name;" ..
|
||||
fgettext("Rename Modpack:") .. ";" .. dialogdata.mod.dir_name .. "]"
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function rename_modpack_buttonhandler(this, fields)
|
||||
if fields["dlg_rename_modpack_confirm"] ~= nil then
|
||||
local oldpath = this.data.mod.path
|
||||
local targetpath = this.data.mod.parent_dir .. DIR_DELIM .. fields["te_modpack_name"]
|
||||
os.rename(oldpath, targetpath)
|
||||
pkgmgr.refresh_globals()
|
||||
pkgmgr.selected_mod = pkgmgr.global_mods:get_current_index(
|
||||
pkgmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"]))
|
||||
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["dlg_rename_modpack_cancel"] then
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function create_rename_modpack_dlg(modpack)
|
||||
|
||||
local retval = dialog_create("dlg_delete_mod",
|
||||
rename_modpack_formspec,
|
||||
rename_modpack_buttonhandler,
|
||||
nil)
|
||||
retval.data.mod = modpack
|
||||
return retval
|
||||
end
|
||||
1137
builtin/mainmenu/dlg_settings_advanced.lua
Normal file
1137
builtin/mainmenu/dlg_settings_advanced.lua
Normal file
File diff suppressed because it is too large
Load Diff
172
builtin/mainmenu/dlg_version_info.lua
Normal file
172
builtin/mainmenu/dlg_version_info.lua
Normal file
@@ -0,0 +1,172 @@
|
||||
--[[
|
||||
Minetest
|
||||
Copyright (C) 2018-2020 SmallJoker, 2022 rubenwardy
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
]]
|
||||
|
||||
if not core.get_http_api then
|
||||
function check_new_version()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local function version_info_formspec(data)
|
||||
local cur_ver = core.get_version()
|
||||
local title = fgettext("A new $1 version is available", cur_ver.project)
|
||||
local message =
|
||||
fgettext("Installed version: $1\nNew version: $2\n" ..
|
||||
"Visit $3 to find out how to get the newest version and stay up to date" ..
|
||||
" with features and bugfixes.",
|
||||
cur_ver.string, data.new_version or "", data.url or "")
|
||||
|
||||
local fs = {
|
||||
"formspec_version[3]",
|
||||
"size[12.8,7]",
|
||||
"style_type[label;textcolor=#0E0]",
|
||||
"label[0.5,0.8;", core.formspec_escape(title), "]",
|
||||
"textarea[0.4,1.6;12,3.4;;;",
|
||||
core.formspec_escape(message), "]",
|
||||
"container[0.4,5.8]",
|
||||
"button[0.0,0;4.0,0.8;version_check_visit;", fgettext("Visit website"), "]",
|
||||
"button[4.5,0;3.5,0.8;version_check_remind;", fgettext("Later"), "]",
|
||||
"button[8.5.5,0;3.5,0.8;version_check_never;", fgettext("Never"), "]",
|
||||
"container_end[]",
|
||||
}
|
||||
|
||||
return table.concat(fs, "")
|
||||
end
|
||||
|
||||
local function version_info_buttonhandler(this, fields)
|
||||
if fields.version_check_remind then
|
||||
-- Erase last known, user will be reminded again at next check
|
||||
core.settings:set("update_last_known", "")
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
if fields.version_check_never then
|
||||
core.settings:set("update_last_checked", "disabled")
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
if fields.version_check_visit then
|
||||
if type(this.data.url) == "string" then
|
||||
core.open_url(this.data.url)
|
||||
end
|
||||
this:delete()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function create_version_info_dlg(new_version, url)
|
||||
assert(type(new_version) == "string")
|
||||
assert(type(url) == "string")
|
||||
|
||||
local retval = dialog_create("version_info",
|
||||
version_info_formspec,
|
||||
version_info_buttonhandler,
|
||||
nil)
|
||||
|
||||
retval.data.new_version = new_version
|
||||
retval.data.url = url
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
local function get_current_version_code()
|
||||
-- Format: Major.Minor.Patch
|
||||
-- Convert to MMMNNNPPP
|
||||
local cur_string = core.get_version().string
|
||||
local cur_major, cur_minor, cur_patch = cur_string:match("^(%d+).(%d+).(%d+)")
|
||||
|
||||
if not cur_patch then
|
||||
core.log("error", "Failed to parse version numbers (invalid tag format?)")
|
||||
return
|
||||
end
|
||||
|
||||
return (cur_major * 1000 + cur_minor) * 1000 + cur_patch
|
||||
end
|
||||
|
||||
local function on_version_info_received(json)
|
||||
local maintab = ui.find_by_name("maintab")
|
||||
if maintab.hidden then
|
||||
-- Another dialog is open, abort.
|
||||
return
|
||||
end
|
||||
|
||||
local known_update = tonumber(core.settings:get("update_last_known")) or 0
|
||||
|
||||
-- Format: MMNNPPP (Major, Minor, Patch)
|
||||
local new_number = type(json.latest) == "table" and json.latest.version_code
|
||||
if type(new_number) ~= "number" then
|
||||
core.log("error", "Failed to read version number (invalid response?)")
|
||||
return
|
||||
end
|
||||
|
||||
local cur_number = get_current_version_code()
|
||||
if new_number <= known_update or new_number < cur_number then
|
||||
return
|
||||
end
|
||||
|
||||
-- Also consider updating from 1.2.3-dev to 1.2.3
|
||||
if new_number == cur_number and not core.get_version().is_dev then
|
||||
return
|
||||
end
|
||||
|
||||
core.settings:set("update_last_known", tostring(new_number))
|
||||
|
||||
-- Show version info dialog (once)
|
||||
maintab:hide()
|
||||
|
||||
local version_info_dlg = create_version_info_dlg(json.latest.version, json.latest.url)
|
||||
version_info_dlg:set_parent(maintab)
|
||||
version_info_dlg:show()
|
||||
|
||||
ui.update()
|
||||
end
|
||||
|
||||
function check_new_version()
|
||||
local url = core.settings:get("update_information_url")
|
||||
if core.settings:get("update_last_checked") == "disabled" or
|
||||
url == "" then
|
||||
-- Never show any updates
|
||||
return
|
||||
end
|
||||
|
||||
local time_now = os.time()
|
||||
local time_checked = tonumber(core.settings:get("update_last_checked")) or 0
|
||||
if time_now - time_checked < 2 * 24 * 3600 then
|
||||
-- Check interval of 2 entire days
|
||||
return
|
||||
end
|
||||
|
||||
core.settings:set("update_last_checked", tostring(time_now))
|
||||
|
||||
core.handle_async(function(params)
|
||||
local http = core.get_http_api()
|
||||
return http.fetch_sync(params)
|
||||
end, { url = url }, function(result)
|
||||
local json = result.succeeded and core.parse_json(result.data)
|
||||
if type(json) ~= "table" or not json.latest then
|
||||
core.log("error", "Failed to read JSON output from " .. url ..
|
||||
", status code = " .. result.code)
|
||||
return
|
||||
end
|
||||
|
||||
on_version_info_received(json)
|
||||
end)
|
||||
end
|
||||
203
builtin/mainmenu/game_theme.lua
Normal file
203
builtin/mainmenu/game_theme.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
|
||||
mm_game_theme = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.init()
|
||||
mm_game_theme.defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
|
||||
DIR_DELIM .. "pack" .. DIR_DELIM
|
||||
mm_game_theme.basetexturedir = mm_game_theme.defaulttexturedir
|
||||
|
||||
mm_game_theme.texturepack = core.settings:get("texture_path")
|
||||
|
||||
mm_game_theme.gameid = nil
|
||||
|
||||
mm_game_theme.music_handle = nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.update(tab,gamedetails)
|
||||
if tab ~= "singleplayer" then
|
||||
mm_game_theme.reset()
|
||||
return
|
||||
end
|
||||
|
||||
if gamedetails == nil then
|
||||
return
|
||||
end
|
||||
|
||||
mm_game_theme.update_game(gamedetails)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.reset()
|
||||
mm_game_theme.gameid = nil
|
||||
local have_bg = false
|
||||
local have_overlay = mm_game_theme.set_generic("overlay")
|
||||
|
||||
if not have_overlay then
|
||||
have_bg = mm_game_theme.set_generic("background")
|
||||
end
|
||||
|
||||
mm_game_theme.clear("header")
|
||||
mm_game_theme.clear("footer")
|
||||
core.set_clouds(false)
|
||||
|
||||
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_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_game_theme.update_game(gamedetails)
|
||||
if mm_game_theme.gameid == gamedetails.id then
|
||||
return
|
||||
end
|
||||
|
||||
local have_bg = false
|
||||
local have_overlay = mm_game_theme.set_game("overlay",gamedetails)
|
||||
|
||||
if not have_overlay then
|
||||
have_bg = mm_game_theme.set_game("background",gamedetails)
|
||||
end
|
||||
|
||||
mm_game_theme.clear("header")
|
||||
mm_game_theme.clear("footer")
|
||||
core.set_clouds(false)
|
||||
|
||||
if not have_bg then
|
||||
|
||||
if core.settings:get_bool("menu_clouds") then
|
||||
core.set_clouds(true)
|
||||
else
|
||||
mm_game_theme.set_dirt_bg()
|
||||
end
|
||||
end
|
||||
|
||||
mm_game_theme.set_game("footer",gamedetails)
|
||||
mm_game_theme.set_game("header",gamedetails)
|
||||
|
||||
mm_game_theme.gameid = gamedetails.id
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.clear(identifier)
|
||||
core.set_background(identifier,"")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.set_generic(identifier)
|
||||
--try texture pack first
|
||||
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_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
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function mm_game_theme.set_game(identifier, gamedetails)
|
||||
|
||||
if gamedetails == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
-- Find out how many randomized textures the game provides
|
||||
local n = 0
|
||||
local filename
|
||||
local menu_files = core.get_dir_list(gamedetails.path .. DIR_DELIM .. "menu", false)
|
||||
for i = 1, #menu_files do
|
||||
filename = identifier .. "." .. i .. ".png"
|
||||
if table.indexof(menu_files, filename) == -1 then
|
||||
n = i - 1
|
||||
break
|
||||
end
|
||||
end
|
||||
-- Select random texture, 0 means standard texture
|
||||
n = math.random(0, n)
|
||||
if n == 0 then
|
||||
filename = identifier .. ".png"
|
||||
else
|
||||
filename = identifier .. "." .. n .. ".png"
|
||||
end
|
||||
|
||||
local path = gamedetails.path .. DIR_DELIM .. "menu" ..
|
||||
DIR_DELIM .. filename
|
||||
if core.set_background(identifier, path) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
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
|
||||
end
|
||||
|
||||
-- Use universal fallback texture in textures/base/pack
|
||||
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
|
||||
136
builtin/mainmenu/generate_from_settingtypes.lua
Normal file
136
builtin/mainmenu/generate_from_settingtypes.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
local settings = ...
|
||||
|
||||
local concat = table.concat
|
||||
local insert = table.insert
|
||||
local sprintf = string.format
|
||||
local rep = string.rep
|
||||
|
||||
local minetest_example_header = [[
|
||||
# This file contains a list of all available settings and their default value for minetest.conf
|
||||
|
||||
# By default, all the settings are commented and not functional.
|
||||
# Uncomment settings by removing the preceding #.
|
||||
|
||||
# minetest.conf is read by default from:
|
||||
# ../minetest.conf
|
||||
# ../../minetest.conf
|
||||
# Any other path can be chosen by passing the path as a parameter
|
||||
# to the program, eg. "minetest.exe --config ../minetest.conf.example".
|
||||
|
||||
# Further documentation:
|
||||
# http://wiki.minetest.net/
|
||||
|
||||
]]
|
||||
|
||||
local group_format_template = [[
|
||||
# %s = {
|
||||
# offset = %s,
|
||||
# scale = %s,
|
||||
# spread = (%s, %s, %s),
|
||||
# seed = %s,
|
||||
# octaves = %s,
|
||||
# persistence = %s,
|
||||
# lacunarity = %s,
|
||||
# flags =%s
|
||||
# }
|
||||
|
||||
]]
|
||||
|
||||
local function create_minetest_conf_example()
|
||||
local result = { minetest_example_header }
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.type == "category" then
|
||||
if entry.level == 0 then
|
||||
insert(result, "#\n# " .. entry.name .. "\n#\n\n")
|
||||
else
|
||||
insert(result, rep("#", entry.level))
|
||||
insert(result, "# " .. entry.name .. "\n\n")
|
||||
end
|
||||
else
|
||||
local group_format = false
|
||||
if entry.noise_params and entry.values then
|
||||
if entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then
|
||||
group_format = true
|
||||
end
|
||||
end
|
||||
if entry.comment ~= "" then
|
||||
for _, comment_line in ipairs(entry.comment:split("\n", true)) do
|
||||
if comment_line == "" then
|
||||
insert(result, "#\n")
|
||||
else
|
||||
insert(result, "# " .. comment_line .. "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
insert(result, "# type: " .. entry.type)
|
||||
if entry.min then
|
||||
insert(result, " min: " .. entry.min)
|
||||
end
|
||||
if entry.max then
|
||||
insert(result, " max: " .. entry.max)
|
||||
end
|
||||
if entry.values and entry.noise_params == nil then
|
||||
insert(result, " values: " .. concat(entry.values, ", "))
|
||||
end
|
||||
if entry.possible then
|
||||
insert(result, " possible values: " .. concat(entry.possible, ", "))
|
||||
end
|
||||
insert(result, "\n")
|
||||
if group_format == true then
|
||||
local flags = entry.values[10]
|
||||
if flags ~= "" then
|
||||
flags = " "..flags
|
||||
end
|
||||
insert(result, sprintf(group_format_template, entry.name, entry.values[1],
|
||||
entry.values[2], entry.values[3], entry.values[4], entry.values[5],
|
||||
entry.values[6], entry.values[7], entry.values[8], entry.values[9],
|
||||
flags))
|
||||
else
|
||||
local append
|
||||
if entry.default ~= "" then
|
||||
append = " " .. entry.default
|
||||
end
|
||||
insert(result, sprintf("# %s =%s\n\n", entry.name, append or ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
return concat(result)
|
||||
end
|
||||
|
||||
local translation_file_header = [[
|
||||
// This file is automatically generated
|
||||
// It contains a bunch of fake gettext calls, to tell xgettext about the strings in config files
|
||||
// To update it, refer to the bottom of builtin/mainmenu/dlg_settings_advanced.lua
|
||||
|
||||
fake_function() {]]
|
||||
|
||||
local function create_translation_file()
|
||||
local result = { translation_file_header }
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.type == "category" then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.name))
|
||||
else
|
||||
if entry.readable_name then
|
||||
insert(result, sprintf("\tgettext(%q);", entry.readable_name))
|
||||
end
|
||||
if entry.comment ~= "" then
|
||||
local comment_escaped = entry.comment:gsub("\n", "\\n")
|
||||
comment_escaped = comment_escaped:gsub("\"", "\\\"")
|
||||
insert(result, "\tgettext(\"" .. comment_escaped .. "\");")
|
||||
end
|
||||
end
|
||||
end
|
||||
insert(result, "}\n")
|
||||
return concat(result, "\n")
|
||||
end
|
||||
|
||||
local file = assert(io.open("minetest.conf.example", "w"))
|
||||
file:write(create_minetest_conf_example())
|
||||
file:close()
|
||||
|
||||
file = assert(io.open("src/settings_translation_file.cpp", "w"))
|
||||
-- If 'minetest.conf.example' appears in the 'bin' folder, the line below may have to be
|
||||
-- used instead. The file will also appear in the 'bin' folder.
|
||||
--file = assert(io.open("settings_translation_file.cpp", "w"))
|
||||
file:write(create_translation_file())
|
||||
file:close()
|
||||
131
builtin/mainmenu/init.lua
Normal file
131
builtin/mainmenu/init.lua
Normal file
@@ -0,0 +1,131 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
mt_color_grey = "#AAAAAA"
|
||||
mt_color_blue = "#6389FF"
|
||||
mt_color_lightblue = "#99CCFF"
|
||||
mt_color_green = "#72FF63"
|
||||
mt_color_dark_green = "#25C191"
|
||||
mt_color_orange = "#FF8800"
|
||||
mt_color_red = "#FF3300"
|
||||
|
||||
local menupath = core.get_mainmenu_path()
|
||||
local basepath = core.get_builtin_path()
|
||||
defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
|
||||
DIR_DELIM .. "pack" .. DIR_DELIM
|
||||
|
||||
dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "tabview.lua")
|
||||
dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "async_event.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "common.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "pkgmgr.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "game_theme.lua")
|
||||
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_register.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_rename_modpack.lua")
|
||||
dofile(menupath .. DIR_DELIM .. "dlg_version_info.lua")
|
||||
|
||||
local tabs = {}
|
||||
|
||||
tabs.settings = dofile(menupath .. DIR_DELIM .. "tab_settings.lua")
|
||||
tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.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")
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function main_event_handler(tabview, event)
|
||||
if event == "MenuQuit" then
|
||||
core.close()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function init_globals()
|
||||
-- Init gamedata
|
||||
gamedata.worldindex = 0
|
||||
|
||||
menudata.worldlist = filterlist.create(
|
||||
core.get_worlds,
|
||||
compare_worlds,
|
||||
-- Unique id comparison function
|
||||
function(element, uid)
|
||||
return element.name == uid
|
||||
end,
|
||||
-- Filter function
|
||||
function(element, gameid)
|
||||
return element.gameid == gameid
|
||||
end
|
||||
)
|
||||
|
||||
menudata.worldlist:add_sort_mechanism("alphabetic", sort_worlds_alphabetic)
|
||||
menudata.worldlist:set_sortmode("alphabetic")
|
||||
|
||||
if not core.settings:get("menu_last_game") then
|
||||
local default_game = core.settings:get("default_game") or "minetest"
|
||||
core.settings:set("menu_last_game", default_game)
|
||||
end
|
||||
|
||||
mm_game_theme.init()
|
||||
|
||||
-- Create main tabview
|
||||
local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0})
|
||||
-- note: size would be 15.5,7.1 in real coordinates mode
|
||||
|
||||
tv_main:set_autosave_tab(true)
|
||||
tv_main:add(tabs.local_game)
|
||||
tv_main:add(tabs.play_online)
|
||||
|
||||
tv_main:add(tabs.content)
|
||||
tv_main:add(tabs.settings)
|
||||
tv_main:add(tabs.about)
|
||||
|
||||
tv_main:set_global_event_handler(main_event_handler)
|
||||
tv_main:set_fixed_size(false)
|
||||
|
||||
local last_tab = core.settings:get("maintab_LAST")
|
||||
if last_tab and tv_main.current_tab ~= last_tab then
|
||||
tv_main:set_tab(last_tab)
|
||||
end
|
||||
|
||||
-- In case the folder of the last selected game has been deleted,
|
||||
-- display "Minetest" as a header
|
||||
if tv_main.current_tab == "local" then
|
||||
local game = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
|
||||
if game == nil then
|
||||
mm_game_theme.reset()
|
||||
end
|
||||
end
|
||||
|
||||
ui.set_default("maintab")
|
||||
check_new_version()
|
||||
tv_main:show()
|
||||
ui.update()
|
||||
end
|
||||
|
||||
init_globals()
|
||||
929
builtin/mainmenu/pkgmgr.lua
Normal file
929
builtin/mainmenu/pkgmgr.lua
Normal file
@@ -0,0 +1,929 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_last_folder(text,count)
|
||||
local parts = text:split(DIR_DELIM)
|
||||
|
||||
if count == nil then
|
||||
return parts[#parts]
|
||||
end
|
||||
|
||||
local retval = ""
|
||||
for i=1,count,1 do
|
||||
retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
local function cleanup_path(temppath)
|
||||
|
||||
local parts = temppath:split("-")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. "_"
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split(".")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. "_"
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split("'")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath .. ""
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
parts = temppath:split(" ")
|
||||
temppath = ""
|
||||
for i=1,#parts,1 do
|
||||
if temppath ~= "" then
|
||||
temppath = temppath
|
||||
end
|
||||
temppath = temppath .. parts[i]
|
||||
end
|
||||
|
||||
return temppath
|
||||
end
|
||||
|
||||
local function load_texture_packs(txtpath, retval)
|
||||
local list = core.get_dir_list(txtpath, true)
|
||||
local current_texture_path = core.settings:get("texture_path")
|
||||
|
||||
for _, item in ipairs(list) do
|
||||
if item ~= "base" then
|
||||
local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM
|
||||
local conf = Settings(path .. "texture_pack.conf")
|
||||
local enabled = path == current_texture_path
|
||||
|
||||
local title = conf:get("title") or item
|
||||
|
||||
-- list_* is only used if non-nil, else the regular versions are used.
|
||||
retval[#retval + 1] = {
|
||||
name = item,
|
||||
title = title,
|
||||
list_name = enabled and fgettext("$1 (Enabled)", item) or nil,
|
||||
list_title = enabled and fgettext("$1 (Enabled)", title) or nil,
|
||||
author = conf:get("author"),
|
||||
release = tonumber(conf:get("release")) or 0,
|
||||
type = "txp",
|
||||
path = path,
|
||||
enabled = enabled,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function get_mods(path, virtual_path, retval, modpack)
|
||||
local mods = core.get_dir_list(path, true)
|
||||
|
||||
for _, name in ipairs(mods) do
|
||||
if name:sub(1, 1) ~= "." then
|
||||
local mod_path = path .. DIR_DELIM .. name
|
||||
local mod_virtual_path = virtual_path .. "/" .. name
|
||||
local toadd = {
|
||||
dir_name = name,
|
||||
parent_dir = path,
|
||||
}
|
||||
retval[#retval + 1] = toadd
|
||||
|
||||
-- Get config file
|
||||
local mod_conf
|
||||
local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf")
|
||||
if modpack_conf then
|
||||
toadd.is_modpack = true
|
||||
modpack_conf:close()
|
||||
|
||||
mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table()
|
||||
if mod_conf.name then
|
||||
name = mod_conf.name
|
||||
toadd.is_name_explicit = true
|
||||
end
|
||||
else
|
||||
mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table()
|
||||
if mod_conf.name then
|
||||
name = mod_conf.name
|
||||
toadd.is_name_explicit = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Read from config
|
||||
toadd.name = name
|
||||
toadd.title = mod_conf.title
|
||||
toadd.author = mod_conf.author
|
||||
toadd.release = tonumber(mod_conf.release) or 0
|
||||
toadd.path = mod_path
|
||||
toadd.virtual_path = mod_virtual_path
|
||||
toadd.type = "mod"
|
||||
|
||||
-- Check modpack.txt
|
||||
-- Note: modpack.conf is already checked above
|
||||
local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
|
||||
if modpackfile then
|
||||
modpackfile:close()
|
||||
toadd.is_modpack = true
|
||||
end
|
||||
|
||||
-- Deal with modpack contents
|
||||
if modpack and modpack ~= "" then
|
||||
toadd.modpack = modpack
|
||||
elseif toadd.is_modpack then
|
||||
toadd.type = "modpack"
|
||||
toadd.is_modpack = true
|
||||
get_mods(mod_path, mod_virtual_path, retval, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--modmanager implementation
|
||||
pkgmgr = {}
|
||||
|
||||
function pkgmgr.get_texture_packs()
|
||||
local txtpath = core.get_texturepath()
|
||||
local txtpath_system = core.get_texturepath_share()
|
||||
local retval = {}
|
||||
|
||||
load_texture_packs(txtpath, retval)
|
||||
-- on portable versions these two paths coincide. It avoids loading the path twice
|
||||
if txtpath ~= txtpath_system then
|
||||
load_texture_packs(txtpath_system, retval)
|
||||
end
|
||||
|
||||
table.sort(retval, function(a, b)
|
||||
return a.name > b.name
|
||||
end)
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_folder_type(path)
|
||||
local testfile = io.open(path .. DIR_DELIM .. "init.lua","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "mod", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "modpack", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "modpack", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "game.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "game", path = path }
|
||||
end
|
||||
|
||||
testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r")
|
||||
if testfile ~= nil then
|
||||
testfile:close()
|
||||
return { type = "txp", path = path }
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
function pkgmgr.get_base_folder(temppath)
|
||||
if temppath == nil then
|
||||
return { type = "invalid", path = "" }
|
||||
end
|
||||
|
||||
local ret = pkgmgr.get_folder_type(temppath)
|
||||
if ret then
|
||||
return ret
|
||||
end
|
||||
|
||||
local subdirs = core.get_dir_list(temppath, true)
|
||||
if #subdirs == 1 then
|
||||
ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1])
|
||||
if ret then
|
||||
return ret
|
||||
else
|
||||
return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] }
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.isValidModname(modpath)
|
||||
if modpath:find("-") ~= nil then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.parse_register_line(line)
|
||||
local pos1 = line:find("\"")
|
||||
local pos2 = nil
|
||||
if pos1 ~= nil then
|
||||
pos2 = line:find("\"",pos1+1)
|
||||
end
|
||||
|
||||
if pos1 ~= nil and pos2 ~= nil then
|
||||
local item = line:sub(pos1+1,pos2-1)
|
||||
|
||||
if item ~= nil and
|
||||
item ~= "" then
|
||||
local pos3 = item:find(":")
|
||||
|
||||
if pos3 ~= nil then
|
||||
local retval = item:sub(1,pos3-1)
|
||||
if retval ~= nil and
|
||||
retval ~= "" then
|
||||
return retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.parse_dofile_line(modpath,line)
|
||||
local pos1 = line:find("\"")
|
||||
local pos2 = nil
|
||||
if pos1 ~= nil then
|
||||
pos2 = line:find("\"",pos1+1)
|
||||
end
|
||||
|
||||
if pos1 ~= nil and pos2 ~= nil then
|
||||
local filename = line:sub(pos1+1,pos2-1)
|
||||
|
||||
if filename ~= nil and
|
||||
filename ~= "" and
|
||||
filename:find(".lua") then
|
||||
return pkgmgr.identify_modname(modpath,filename)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.identify_modname(modpath,filename)
|
||||
local testfile = io.open(modpath .. DIR_DELIM .. filename,"r")
|
||||
if testfile ~= nil then
|
||||
local line = testfile:read()
|
||||
|
||||
while line~= nil do
|
||||
local modname = nil
|
||||
|
||||
if line:find("minetest.register_tool") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
if line:find("minetest.register_craftitem") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
|
||||
if line:find("minetest.register_node") then
|
||||
modname = pkgmgr.parse_register_line(line)
|
||||
end
|
||||
|
||||
if line:find("dofile") then
|
||||
modname = pkgmgr.parse_dofile_line(modpath,line)
|
||||
end
|
||||
|
||||
if modname ~= nil then
|
||||
testfile:close()
|
||||
return modname
|
||||
end
|
||||
|
||||
line = testfile:read()
|
||||
end
|
||||
testfile:close()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.render_packagelist(render_list, use_technical_names, with_error)
|
||||
if not render_list then
|
||||
if not pkgmgr.global_mods then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
render_list = pkgmgr.global_mods
|
||||
end
|
||||
|
||||
local list = render_list:get_list()
|
||||
local retval = {}
|
||||
for i, v in ipairs(list) do
|
||||
local color = ""
|
||||
local icon = 0
|
||||
local error = with_error and with_error[v.virtual_path]
|
||||
local function update_error(val)
|
||||
if val and (not error or (error.type == "warning" and val.type == "error")) then
|
||||
error = val
|
||||
end
|
||||
end
|
||||
|
||||
if v.is_modpack then
|
||||
local rawlist = render_list:get_raw_list()
|
||||
color = mt_color_dark_green
|
||||
|
||||
for j = 1, #rawlist do
|
||||
if rawlist[j].modpack == list[i].name then
|
||||
if with_error then
|
||||
update_error(with_error[rawlist[j].virtual_path])
|
||||
end
|
||||
|
||||
if rawlist[j].enabled then
|
||||
icon = 1
|
||||
else
|
||||
-- Modpack not entirely enabled so showing as grey
|
||||
color = mt_color_grey
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif v.is_game_content or v.type == "game" then
|
||||
icon = 1
|
||||
color = mt_color_blue
|
||||
|
||||
local rawlist = render_list:get_raw_list()
|
||||
if v.type == "game" and with_error then
|
||||
for j = 1, #rawlist do
|
||||
if rawlist[j].is_game_content then
|
||||
update_error(with_error[rawlist[j].virtual_path])
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif v.enabled or v.type == "txp" then
|
||||
icon = 1
|
||||
color = mt_color_green
|
||||
end
|
||||
|
||||
if error then
|
||||
if error.type == "warning" then
|
||||
color = mt_color_orange
|
||||
icon = 2
|
||||
else
|
||||
color = mt_color_red
|
||||
icon = 3
|
||||
end
|
||||
end
|
||||
|
||||
retval[#retval + 1] = color
|
||||
if v.modpack ~= nil or v.loc == "game" then
|
||||
retval[#retval + 1] = "1"
|
||||
else
|
||||
retval[#retval + 1] = "0"
|
||||
end
|
||||
|
||||
if with_error then
|
||||
retval[#retval + 1] = icon
|
||||
end
|
||||
|
||||
if use_technical_names then
|
||||
retval[#retval + 1] = core.formspec_escape(v.list_name or v.name)
|
||||
else
|
||||
retval[#retval + 1] = core.formspec_escape(v.list_title or v.list_name or v.title or v.name)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(retval, ",")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_dependencies(path)
|
||||
if path == nil then
|
||||
return {}, {}
|
||||
end
|
||||
|
||||
local info = core.get_content_info(path)
|
||||
return info.depends or {}, info.optional_depends or {}
|
||||
end
|
||||
|
||||
----------- tests whether all of the mods in the modpack are enabled -----------
|
||||
function pkgmgr.is_modpack_entirely_enabled(data, name)
|
||||
local rawlist = data.list:get_raw_list()
|
||||
for j = 1, #rawlist do
|
||||
if rawlist[j].modpack == name and not rawlist[j].enabled then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function disable_all_by_name(list, name, except)
|
||||
for i=1, #list do
|
||||
if list[i].name == name and list[i] ~= except then
|
||||
list[i].enabled = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---------- toggles or en/disables a mod or modpack and its dependencies --------
|
||||
local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
|
||||
if not mod.is_modpack then
|
||||
-- Toggle or en/disable the mod
|
||||
if toset == nil then
|
||||
toset = not mod.enabled
|
||||
end
|
||||
if mod.enabled ~= toset then
|
||||
toggled_mods[#toggled_mods+1] = mod.name
|
||||
end
|
||||
if toset then
|
||||
-- Mark this mod for recursive dependency traversal
|
||||
enabled_mods[mod.name] = true
|
||||
|
||||
-- Disable other mods with the same name
|
||||
disable_all_by_name(list, mod.name, mod)
|
||||
end
|
||||
mod.enabled = toset
|
||||
else
|
||||
-- Toggle or en/disable every mod in the modpack,
|
||||
-- interleaved unsupported
|
||||
for i = 1, #list do
|
||||
if list[i].modpack == mod.name then
|
||||
toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, list[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function pkgmgr.enable_mod(this, toset)
|
||||
local list = this.data.list:get_list()
|
||||
local mod = list[this.data.selected_mod]
|
||||
|
||||
-- Game mods can't be enabled or disabled
|
||||
if mod.is_game_content then
|
||||
return
|
||||
end
|
||||
|
||||
local toggled_mods = {}
|
||||
local enabled_mods = {}
|
||||
toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod)
|
||||
|
||||
if next(enabled_mods) == nil then
|
||||
-- Mod(s) were disabled, so no dependencies need to be enabled
|
||||
table.sort(toggled_mods)
|
||||
core.log("info", "Following mods were disabled: " ..
|
||||
table.concat(toggled_mods, ", "))
|
||||
return
|
||||
end
|
||||
|
||||
-- Enable mods' depends after activation
|
||||
|
||||
-- Make a list of mod ids indexed by their names. Among mods with the
|
||||
-- same name, enabled mods take precedence, after which game mods take
|
||||
-- precedence, being last in the mod list.
|
||||
local mod_ids = {}
|
||||
for id, mod2 in pairs(list) do
|
||||
if mod2.type == "mod" and not mod2.is_modpack then
|
||||
local prev_id = mod_ids[mod2.name]
|
||||
if not prev_id or not list[prev_id].enabled then
|
||||
mod_ids[mod2.name] = id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- to_enable is used as a DFS stack with sp as stack pointer
|
||||
local to_enable = {}
|
||||
local sp = 0
|
||||
for name in pairs(enabled_mods) do
|
||||
local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path)
|
||||
for i = 1, #depends do
|
||||
local dependency_name = depends[i]
|
||||
if not enabled_mods[dependency_name] then
|
||||
sp = sp+1
|
||||
to_enable[sp] = dependency_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If sp is 0, every dependency is already activated
|
||||
while sp > 0 do
|
||||
local name = to_enable[sp]
|
||||
sp = sp-1
|
||||
|
||||
if not enabled_mods[name] then
|
||||
enabled_mods[name] = true
|
||||
local mod_to_enable = list[mod_ids[name]]
|
||||
if not mod_to_enable then
|
||||
core.log("warning", "Mod dependency \"" .. name ..
|
||||
"\" not found!")
|
||||
elseif not mod_to_enable.is_game_content then
|
||||
if not mod_to_enable.enabled then
|
||||
mod_to_enable.enabled = true
|
||||
toggled_mods[#toggled_mods+1] = mod_to_enable.name
|
||||
end
|
||||
-- Push the dependencies of the dependency onto the stack
|
||||
local depends = pkgmgr.get_dependencies(mod_to_enable.path)
|
||||
for i = 1, #depends do
|
||||
if not enabled_mods[depends[i]] then
|
||||
sp = sp+1
|
||||
to_enable[sp] = depends[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Log the list of enabled mods
|
||||
table.sort(toggled_mods)
|
||||
core.log("info", "Following mods were enabled: " ..
|
||||
table.concat(toggled_mods, ", "))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_worldconfig(worldpath)
|
||||
local filename = worldpath ..
|
||||
DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
|
||||
local worldconfig = {}
|
||||
worldconfig.global_mods = {}
|
||||
worldconfig.game_mods = {}
|
||||
|
||||
for key,value in pairs(worldfile:to_table()) do
|
||||
if key == "gameid" then
|
||||
worldconfig.id = value
|
||||
elseif key:sub(0, 9) == "load_mod_" then
|
||||
-- Compatibility: Check against "nil" which was erroneously used
|
||||
-- as value for fresh configured worlds
|
||||
worldconfig.global_mods[key] = value ~= "false" and value ~= "nil"
|
||||
and value
|
||||
else
|
||||
worldconfig[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
--read gamemods
|
||||
local gamespec = pkgmgr.find_by_gameid(worldconfig.id)
|
||||
pkgmgr.get_game_mods(gamespec, worldconfig.game_mods)
|
||||
|
||||
return worldconfig
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.install_dir(type, path, basename, targetpath)
|
||||
local basefolder = pkgmgr.get_base_folder(path)
|
||||
|
||||
-- There's no good way to detect a texture pack, so let's just assume
|
||||
-- it's correct for now.
|
||||
if type == "txp" then
|
||||
if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then
|
||||
return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type)
|
||||
end
|
||||
|
||||
local from = basefolder and basefolder.path or path
|
||||
if not targetpath then
|
||||
targetpath = core.get_texturepath() .. DIR_DELIM .. basename
|
||||
end
|
||||
core.delete_dir(targetpath)
|
||||
if not core.copy_dir(from, targetpath, false) then
|
||||
return nil,
|
||||
fgettext("Failed to install $1 to $2", basename, targetpath)
|
||||
end
|
||||
return targetpath, nil
|
||||
|
||||
elseif not basefolder then
|
||||
return nil, fgettext("Unable to find a valid mod or modpack")
|
||||
end
|
||||
|
||||
--
|
||||
-- Get destination
|
||||
--
|
||||
if basefolder.type == "modpack" then
|
||||
if type ~= "mod" then
|
||||
return nil, fgettext("Unable to install a modpack as a $1", type)
|
||||
end
|
||||
|
||||
-- Get destination name for modpack
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
else
|
||||
local clean_path = nil
|
||||
if basename ~= nil then
|
||||
clean_path = basename
|
||||
end
|
||||
if not clean_path then
|
||||
clean_path = get_last_folder(cleanup_path(basefolder.path))
|
||||
end
|
||||
if clean_path then
|
||||
targetpath = core.get_modpath() .. DIR_DELIM .. clean_path
|
||||
else
|
||||
return nil,
|
||||
fgettext("Install Mod: Unable to find suitable folder name for modpack $1",
|
||||
path)
|
||||
end
|
||||
end
|
||||
elseif basefolder.type == "mod" then
|
||||
if type ~= "mod" then
|
||||
return nil, fgettext("Unable to install a mod as a $1", type)
|
||||
end
|
||||
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
else
|
||||
local targetfolder = basename
|
||||
if targetfolder == nil then
|
||||
targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua")
|
||||
end
|
||||
|
||||
-- If heuristic failed try to use current foldername
|
||||
if targetfolder == nil then
|
||||
targetfolder = get_last_folder(basefolder.path)
|
||||
end
|
||||
|
||||
if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then
|
||||
targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder
|
||||
else
|
||||
return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path)
|
||||
end
|
||||
end
|
||||
|
||||
elseif basefolder.type == "game" then
|
||||
if type ~= "game" then
|
||||
return nil, fgettext("Unable to install a game as a $1", type)
|
||||
end
|
||||
|
||||
if targetpath then
|
||||
core.delete_dir(targetpath)
|
||||
else
|
||||
targetpath = core.get_gamepath() .. DIR_DELIM .. basename
|
||||
end
|
||||
else
|
||||
error("basefolder didn't return a recognised type, this shouldn't happen")
|
||||
end
|
||||
|
||||
-- Copy it
|
||||
core.delete_dir(targetpath)
|
||||
if not core.copy_dir(basefolder.path, targetpath, false) then
|
||||
return nil,
|
||||
fgettext("Failed to install $1 to $2", basename, targetpath)
|
||||
end
|
||||
|
||||
if basefolder.type == "game" then
|
||||
pkgmgr.update_gamelist()
|
||||
else
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
|
||||
return targetpath, nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.preparemodlist(data)
|
||||
local retval = {}
|
||||
|
||||
local global_mods = {}
|
||||
local game_mods = {}
|
||||
|
||||
--read global mods
|
||||
local modpaths = core.get_modpaths()
|
||||
for key, modpath in pairs(modpaths) do
|
||||
get_mods(modpath, key, global_mods)
|
||||
end
|
||||
|
||||
for i=1,#global_mods,1 do
|
||||
global_mods[i].type = "mod"
|
||||
global_mods[i].loc = "global"
|
||||
global_mods[i].enabled = false
|
||||
retval[#retval + 1] = global_mods[i]
|
||||
end
|
||||
|
||||
--read game mods
|
||||
local gamespec = pkgmgr.find_by_gameid(data.gameid)
|
||||
pkgmgr.get_game_mods(gamespec, game_mods)
|
||||
|
||||
if #game_mods > 0 then
|
||||
-- Add title
|
||||
retval[#retval + 1] = {
|
||||
type = "game",
|
||||
is_game_content = true,
|
||||
name = fgettext("$1 mods", gamespec.title),
|
||||
path = gamespec.path
|
||||
}
|
||||
end
|
||||
|
||||
for i=1,#game_mods,1 do
|
||||
game_mods[i].type = "mod"
|
||||
game_mods[i].loc = "game"
|
||||
game_mods[i].is_game_content = true
|
||||
retval[#retval + 1] = game_mods[i]
|
||||
end
|
||||
|
||||
if data.worldpath == nil then
|
||||
return retval
|
||||
end
|
||||
|
||||
--read world mod configuration
|
||||
local filename = data.worldpath ..
|
||||
DIR_DELIM .. "world.mt"
|
||||
|
||||
local worldfile = Settings(filename)
|
||||
for key, value in pairs(worldfile:to_table()) do
|
||||
if key:sub(1, 9) == "load_mod_" then
|
||||
key = key:sub(10)
|
||||
local mod_found = false
|
||||
|
||||
local fallback_found = false
|
||||
local fallback_mod = nil
|
||||
|
||||
for i=1, #retval do
|
||||
if retval[i].name == key and
|
||||
not retval[i].is_modpack then
|
||||
if core.is_yes(value) or retval[i].virtual_path == value then
|
||||
retval[i].enabled = true
|
||||
mod_found = true
|
||||
break
|
||||
elseif fallback_found then
|
||||
-- Only allow fallback if only one mod matches
|
||||
fallback_mod = nil
|
||||
else
|
||||
fallback_found = true
|
||||
fallback_mod = retval[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not mod_found then
|
||||
if fallback_mod and value:find("/") then
|
||||
fallback_mod.enabled = true
|
||||
else
|
||||
core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
function pkgmgr.compare_package(a, b)
|
||||
return a and b and a.name == b.name and a.path == b.path
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.comparemod(elem1,elem2)
|
||||
if elem1 == nil or elem2 == nil then
|
||||
return false
|
||||
end
|
||||
if elem1.name ~= elem2.name then
|
||||
return false
|
||||
end
|
||||
if elem1.is_modpack ~= elem2.is_modpack then
|
||||
return false
|
||||
end
|
||||
if elem1.type ~= elem2.type then
|
||||
return false
|
||||
end
|
||||
if elem1.modpack ~= elem2.modpack then
|
||||
return false
|
||||
end
|
||||
|
||||
if elem1.path ~= elem2.path then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.mod_exists(basename)
|
||||
|
||||
if pkgmgr.global_mods == nil then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
|
||||
if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_global_mod(idx)
|
||||
|
||||
if pkgmgr.global_mods == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
if idx == nil or idx < 1 or
|
||||
idx > pkgmgr.global_mods:size() then
|
||||
return nil
|
||||
end
|
||||
|
||||
return pkgmgr.global_mods:get_list()[idx]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.refresh_globals()
|
||||
local function is_equal(element,uid) --uid match
|
||||
if element.name == uid then
|
||||
return true
|
||||
end
|
||||
end
|
||||
pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist,
|
||||
pkgmgr.comparemod, is_equal, nil, {})
|
||||
pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list)
|
||||
pkgmgr.global_mods:set_sortmode("alphabetic")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.find_by_gameid(gameid)
|
||||
for i=1,#pkgmgr.games,1 do
|
||||
if pkgmgr.games[i].id == gameid then
|
||||
return pkgmgr.games[i], i
|
||||
end
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game_mods(gamespec, retval)
|
||||
if gamespec ~= nil and
|
||||
gamespec.gamemods_path ~= nil and
|
||||
gamespec.gamemods_path ~= "" then
|
||||
get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game_modlist(gamespec)
|
||||
local retval = ""
|
||||
local game_mods = {}
|
||||
pkgmgr.get_game_mods(gamespec, game_mods)
|
||||
for i=1,#game_mods,1 do
|
||||
if retval ~= "" then
|
||||
retval = retval..","
|
||||
end
|
||||
retval = retval .. game_mods[i].name
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.get_game(index)
|
||||
if index > 0 and index <= #pkgmgr.games then
|
||||
return pkgmgr.games[index]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.update_gamelist()
|
||||
pkgmgr.games = core.get_games()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function pkgmgr.gamelist()
|
||||
local retval = ""
|
||||
if #pkgmgr.games > 0 then
|
||||
retval = retval .. core.formspec_escape(pkgmgr.games[1].title)
|
||||
|
||||
for i=2,#pkgmgr.games,1 do
|
||||
retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].title)
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- read initial data
|
||||
--------------------------------------------------------------------------------
|
||||
pkgmgr.update_gamelist()
|
||||
252
builtin/mainmenu/serverlistmgr.lua
Normal file
252
builtin/mainmenu/serverlistmgr.lua
Normal file
@@ -0,0 +1,252 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2020 rubenwardy
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
serverlistmgr = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function order_server_list(list)
|
||||
local res = {}
|
||||
--orders the favorite list after support
|
||||
for i = 1, #list do
|
||||
local fav = list[i]
|
||||
if is_server_protocol_compat(fav.proto_min, fav.proto_max) then
|
||||
res[#res + 1] = fav
|
||||
end
|
||||
end
|
||||
for i = 1, #list do
|
||||
local fav = list[i]
|
||||
if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then
|
||||
res[#res + 1] = fav
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local public_downloading = false
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function serverlistmgr.sync()
|
||||
if not serverlistmgr.servers then
|
||||
serverlistmgr.servers = {{
|
||||
name = fgettext("Loading..."),
|
||||
description = fgettext_ne("Try reenabling public serverlist and check your internet connection.")
|
||||
}}
|
||||
end
|
||||
|
||||
local serverlist_url = core.settings:get("serverlist_url") or ""
|
||||
if not core.get_http_api or serverlist_url == "" then
|
||||
serverlistmgr.servers = {{
|
||||
name = fgettext("Public server list is disabled"),
|
||||
description = ""
|
||||
}}
|
||||
return
|
||||
end
|
||||
|
||||
if public_downloading then
|
||||
return
|
||||
end
|
||||
public_downloading = true
|
||||
|
||||
core.handle_async(
|
||||
function(param)
|
||||
local http = core.get_http_api()
|
||||
local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format(
|
||||
core.settings:get("serverlist_url"),
|
||||
core.get_min_supp_proto(),
|
||||
core.get_max_supp_proto())
|
||||
|
||||
local response = http.fetch_sync({ url = url })
|
||||
if not response.succeeded then
|
||||
return {}
|
||||
end
|
||||
|
||||
local retval = core.parse_json(response.data)
|
||||
return retval and retval.list or {}
|
||||
end,
|
||||
nil,
|
||||
function(result)
|
||||
public_downloading = nil
|
||||
local favs = order_server_list(result)
|
||||
if favs[1] then
|
||||
serverlistmgr.servers = favs
|
||||
end
|
||||
core.event_handler("Refresh")
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_favorites_path(folder)
|
||||
local base = core.get_user_path() .. DIR_DELIM .. "client" .. DIR_DELIM .. "serverlist" .. DIR_DELIM
|
||||
if folder then
|
||||
return base
|
||||
end
|
||||
return base .. core.settings:get("serverlist_file")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function save_favorites(favorites)
|
||||
local filename = core.settings:get("serverlist_file")
|
||||
-- If setting specifies legacy format change the filename to the new one
|
||||
if filename:sub(#filename - 3):lower() == ".txt" then
|
||||
core.settings:set("serverlist_file", filename:sub(1, #filename - 4) .. ".json")
|
||||
end
|
||||
|
||||
assert(core.create_dir(get_favorites_path(true)))
|
||||
core.safe_file_write(get_favorites_path(), core.write_json(favorites))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function serverlistmgr.read_legacy_favorites(path)
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
return nil
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in file:lines() do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
file:close()
|
||||
|
||||
local favorites = {}
|
||||
|
||||
local i = 1
|
||||
while i < #lines do
|
||||
local function pop()
|
||||
local line = lines[i]
|
||||
i = i + 1
|
||||
return line and line:trim()
|
||||
end
|
||||
|
||||
if pop():lower() == "[server]" then
|
||||
local name = pop()
|
||||
local address = pop()
|
||||
local port = tonumber(pop())
|
||||
local description = pop()
|
||||
|
||||
if name == "" then
|
||||
name = nil
|
||||
end
|
||||
|
||||
if description == "" then
|
||||
description = nil
|
||||
end
|
||||
|
||||
if not address or #address < 3 then
|
||||
core.log("warning", "Malformed favorites file, missing address at line " .. i)
|
||||
elseif not port or port < 1 or port > 65535 then
|
||||
core.log("warning", "Malformed favorites file, missing port at line " .. i)
|
||||
elseif (name and name:upper() == "[SERVER]") or
|
||||
(address and address:upper() == "[SERVER]") or
|
||||
(description and description:upper() == "[SERVER]") then
|
||||
core.log("warning", "Potentially malformed favorites file, overran at line " .. i)
|
||||
else
|
||||
favorites[#favorites + 1] = {
|
||||
name = name,
|
||||
address = address,
|
||||
port = port,
|
||||
description = description
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return favorites
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function read_favorites()
|
||||
local path = get_favorites_path()
|
||||
|
||||
-- If new format configured fall back to reading the legacy file
|
||||
if path:sub(#path - 4):lower() == ".json" then
|
||||
local file = io.open(path, "r")
|
||||
if file then
|
||||
local json = file:read("*all")
|
||||
file:close()
|
||||
return core.parse_json(json)
|
||||
end
|
||||
|
||||
path = path:sub(1, #path - 5) .. ".txt"
|
||||
end
|
||||
|
||||
local favs = serverlistmgr.read_legacy_favorites(path)
|
||||
if favs then
|
||||
save_favorites(favs)
|
||||
os.remove(path)
|
||||
end
|
||||
return favs
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function delete_favorite(favorites, del_favorite)
|
||||
for i=1, #favorites do
|
||||
local fav = favorites[i]
|
||||
|
||||
if fav.address == del_favorite.address and fav.port == del_favorite.port then
|
||||
table.remove(favorites, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function serverlistmgr.get_favorites()
|
||||
if serverlistmgr.favorites then
|
||||
return serverlistmgr.favorites
|
||||
end
|
||||
|
||||
serverlistmgr.favorites = {}
|
||||
|
||||
-- Add favorites, removing duplicates
|
||||
local seen = {}
|
||||
for _, fav in ipairs(read_favorites() or {}) do
|
||||
local key = ("%s:%d"):format(fav.address:lower(), fav.port)
|
||||
if not seen[key] then
|
||||
seen[key] = true
|
||||
serverlistmgr.favorites[#serverlistmgr.favorites + 1] = fav
|
||||
end
|
||||
end
|
||||
|
||||
return serverlistmgr.favorites
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function serverlistmgr.add_favorite(new_favorite)
|
||||
assert(type(new_favorite.port) == "number")
|
||||
|
||||
-- Whitelist favorite keys
|
||||
new_favorite = {
|
||||
name = new_favorite.name,
|
||||
address = new_favorite.address,
|
||||
port = new_favorite.port,
|
||||
description = new_favorite.description,
|
||||
}
|
||||
|
||||
local favorites = serverlistmgr.get_favorites()
|
||||
delete_favorite(favorites, new_favorite)
|
||||
table.insert(favorites, 1, new_favorite)
|
||||
save_favorites(favorites)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
function serverlistmgr.delete_favorite(del_favorite)
|
||||
local favorites = serverlistmgr.get_favorites()
|
||||
delete_favorite(favorites, del_favorite)
|
||||
save_favorites(favorites)
|
||||
end
|
||||
195
builtin/mainmenu/tab_about.lua
Normal file
195
builtin/mainmenu/tab_about.lua
Normal file
@@ -0,0 +1,195 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
-- https://github.com/orgs/minetest/teams/engine/members
|
||||
|
||||
local core_developers = {
|
||||
"Perttu Ahola (celeron55) <celeron55@gmail.com> [Project founder]",
|
||||
"sfan5 <sfan5@live.de>",
|
||||
"ShadowNinja <shadowninja@minetest.net>",
|
||||
"Nathanaëlle Courant (Nore/Ekdohibs) <nore@mesecons.net>",
|
||||
"Loic Blot (nerzhul/nrz) <loic.blot@unix-experience.fr>",
|
||||
"Andrew Ward (rubenwardy) <rw@rubenwardy.com>",
|
||||
"Krock/SmallJoker <mk939@ymail.com>",
|
||||
"Lars Hofhansl <larsh@apache.org>",
|
||||
"v-rob <robinsonvincent89@gmail.com>",
|
||||
"hecks",
|
||||
"Hugues Ross <hugues.ross@gmail.com>",
|
||||
"Dmitry Kostenko (x2048) <codeforsmile@gmail.com>",
|
||||
}
|
||||
|
||||
local core_team = {
|
||||
"Zughy [Issue triager]",
|
||||
}
|
||||
|
||||
-- For updating active/previous contributors, see the script in ./util/gather_git_credits.py
|
||||
|
||||
local active_contributors = {
|
||||
"Wuzzy [Features, translations, devtest]",
|
||||
"Lars Müller [Lua optimizations and fixes]",
|
||||
"Jude Melton-Houghton [Optimizations, bugfixes]",
|
||||
"paradust7 [Performance, fixes, Irrlicht refactoring]",
|
||||
"Desour [Fixes]",
|
||||
"ROllerozxa [Main menu]",
|
||||
"savilli [Bugfixes]",
|
||||
"Lexi Hale [Particlespawner animation]",
|
||||
"Liso [Shadow Mapping]",
|
||||
"JosiahWI [Fixes, build system]",
|
||||
"numzero [Graphics and rendering]",
|
||||
"HybridDog [Fixes]",
|
||||
"NeroBurner [Joystick]",
|
||||
"pecksin [Clickable web links]",
|
||||
"Daroc Alden [Fixes]",
|
||||
"Jean-Patrick Guerrero (kilbith) [Fixes]",
|
||||
}
|
||||
|
||||
local previous_core_developers = {
|
||||
"BlockMen",
|
||||
"Maciej Kasatkin (RealBadAngel) [RIP]",
|
||||
"Lisa Milne (darkrose) <lisa@ltmnet.com>",
|
||||
"proller",
|
||||
"Ilya Zhuravlev (xyz) <xyz@minetest.net>",
|
||||
"PilzAdam <pilzadam@minetest.net>",
|
||||
"est31 <MTest31@outlook.com>",
|
||||
"kahrl <kahrl@gmx.net>",
|
||||
"Ryan Kwolek (kwolekr) <kwolekr@minetest.net>",
|
||||
"sapier",
|
||||
"Zeno",
|
||||
"Auke Kok (sofar) <sofar@foo-projects.org>",
|
||||
"Aaron Suen <warr1024@gmail.com>",
|
||||
"paramat",
|
||||
"Pierre-Yves Rollo <dev@pyrollo.com>",
|
||||
}
|
||||
|
||||
local previous_contributors = {
|
||||
"Nils Dagsson Moskopp (erlehmann) <nils@dieweltistgarnichtso.net> [Minetest logo]",
|
||||
"red-001 <red-001@outlook.ie>",
|
||||
"Giuseppe Bilotta",
|
||||
"ClobberXD",
|
||||
"Dániel Juhász (juhdanad) <juhdanad@gmail.com>",
|
||||
"MirceaKitsune <mirceakitsune@gmail.com>",
|
||||
"MoNTE48",
|
||||
"Constantin Wenger (SpeedProg)",
|
||||
"Ciaran Gultnieks (CiaranG)",
|
||||
"Paul Ouellette (pauloue)",
|
||||
"stujones11",
|
||||
"srifqi",
|
||||
"Rogier <rogier777@gmail.com>",
|
||||
"Gregory Currie (gregorycu)",
|
||||
"JacobF",
|
||||
"Jeija <jeija@mesecons.net>",
|
||||
}
|
||||
|
||||
local function prepare_credits(dest, source)
|
||||
for _, s in ipairs(source) do
|
||||
-- if there's text inside brackets make it gray-ish
|
||||
s = s:gsub("%[.-%]", core.colorize("#aaa", "%1"))
|
||||
dest[#dest+1] = s
|
||||
end
|
||||
end
|
||||
|
||||
local function build_hacky_list(items, spacing)
|
||||
spacing = spacing or 0.5
|
||||
local y = spacing / 2
|
||||
local ret = {}
|
||||
for _, item in ipairs(items) do
|
||||
if item ~= "" then
|
||||
ret[#ret+1] = ("label[0,%f;%s]"):format(y, core.formspec_escape(item))
|
||||
end
|
||||
y = y + spacing
|
||||
end
|
||||
return table.concat(ret, ""), y
|
||||
end
|
||||
|
||||
return {
|
||||
name = "about",
|
||||
caption = fgettext("About"),
|
||||
cbf_formspec = function(tabview, name, tabdata)
|
||||
local logofile = defaulttexturedir .. "logo.png"
|
||||
local version = core.get_version()
|
||||
|
||||
local credit_list = {}
|
||||
table.insert_all(credit_list, {
|
||||
core.colorize("#ff0", fgettext("Core Developers"))
|
||||
})
|
||||
prepare_credits(credit_list, core_developers)
|
||||
table.insert_all(credit_list, {
|
||||
"",
|
||||
core.colorize("#ff0", fgettext("Core Team"))
|
||||
})
|
||||
prepare_credits(credit_list, core_team)
|
||||
table.insert_all(credit_list, {
|
||||
"",
|
||||
core.colorize("#ff0", fgettext("Active Contributors"))
|
||||
})
|
||||
prepare_credits(credit_list, active_contributors)
|
||||
table.insert_all(credit_list, {
|
||||
"",
|
||||
core.colorize("#ff0", fgettext("Previous Core Developers"))
|
||||
})
|
||||
prepare_credits(credit_list, previous_core_developers)
|
||||
table.insert_all(credit_list, {
|
||||
"",
|
||||
core.colorize("#ff0", fgettext("Previous Contributors"))
|
||||
})
|
||||
prepare_credits(credit_list, previous_contributors)
|
||||
local credit_fs, scroll_height = build_hacky_list(credit_list)
|
||||
-- account for the visible portion
|
||||
scroll_height = math.max(0, scroll_height - 6.9)
|
||||
|
||||
local fs = "image[1.5,0.6;2.5,2.5;" .. core.formspec_escape(logofile) .. "]" ..
|
||||
"style[label_button;border=false]" ..
|
||||
"button[0.1,3.4;5.3,0.5;label_button;" ..
|
||||
core.formspec_escape(version.project .. " " .. version.string) .. "]" ..
|
||||
"button[1.5,4.1;2.5,0.8;homepage;minetest.net]" ..
|
||||
"scroll_container[5.5,0.1;9.5,6.9;scroll_credits;vertical;" ..
|
||||
tostring(scroll_height / 1000) .. "]" .. credit_fs ..
|
||||
"scroll_container_end[]"..
|
||||
"scrollbar[15,0.1;0.4,6.9;vertical;scroll_credits;0]"
|
||||
|
||||
-- Render information
|
||||
fs = fs .. "style[label_button2;border=false]" ..
|
||||
"button[0.1,6;5.3,1;label_button2;" ..
|
||||
fgettext("Active renderer:") .. "\n" ..
|
||||
core.formspec_escape(core.get_screen_info().render_info) .. "]"
|
||||
|
||||
if PLATFORM == "Android" then
|
||||
fs = fs .. "button[0.5,5.1;4.5,0.8;share_debug;" .. fgettext("Share debug log") .. "]"
|
||||
else
|
||||
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.5,5.1;4.5,0.8;userdata;" .. fgettext("Open User Data Directory") .. "]"
|
||||
end
|
||||
|
||||
return fs, "size[15.5,7.1,false]real_coordinates[true]"
|
||||
end,
|
||||
cbf_button_handler = function(this, fields, name, tabdata)
|
||||
if fields.homepage then
|
||||
core.open_url("https://www.minetest.net")
|
||||
end
|
||||
|
||||
if fields.share_debug then
|
||||
local path = core.get_user_path() .. DIR_DELIM .. "debug.txt"
|
||||
core.share_file(path)
|
||||
end
|
||||
|
||||
if fields.userdata then
|
||||
core.open_dir(core.get_user_path())
|
||||
end
|
||||
end,
|
||||
}
|
||||
237
builtin/mainmenu/tab_content.lua
Normal file
237
builtin/mainmenu/tab_content.lua
Normal file
@@ -0,0 +1,237 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--Copyright (C) 2018 rubenwardy <rw@rubenwardy.com>
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local packages_raw
|
||||
local packages
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function get_formspec(tabview, name, tabdata)
|
||||
if pkgmgr.global_mods == nil then
|
||||
pkgmgr.refresh_globals()
|
||||
end
|
||||
if pkgmgr.games == nil then
|
||||
pkgmgr.update_gamelist()
|
||||
end
|
||||
|
||||
if packages == nil then
|
||||
packages_raw = {}
|
||||
table.insert_all(packages_raw, pkgmgr.games)
|
||||
table.insert_all(packages_raw, pkgmgr.get_texture_packs())
|
||||
table.insert_all(packages_raw, pkgmgr.global_mods:get_list())
|
||||
|
||||
local function get_data()
|
||||
return packages_raw
|
||||
end
|
||||
|
||||
local function is_equal(element, uid) --uid match
|
||||
return (element.type == "game" and element.id == uid) or
|
||||
element.name == uid
|
||||
end
|
||||
|
||||
packages = filterlist.create(get_data, pkgmgr.compare_package,
|
||||
is_equal, nil, {})
|
||||
end
|
||||
|
||||
if tabdata.selected_pkg == nil then
|
||||
tabdata.selected_pkg = 1
|
||||
end
|
||||
|
||||
local use_technical_names = core.settings:get_bool("show_technical_names")
|
||||
|
||||
|
||||
local retval =
|
||||
"label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" ..
|
||||
"tablecolumns[color;tree;text]" ..
|
||||
"table[0,0.25;5.1,4.3;pkglist;" ..
|
||||
pkgmgr.render_packagelist(packages, use_technical_names) ..
|
||||
";" .. tabdata.selected_pkg .. "]" ..
|
||||
"button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]"
|
||||
|
||||
|
||||
local selected_pkg
|
||||
if filterlist.size(packages) >= tabdata.selected_pkg then
|
||||
selected_pkg = packages:get_list()[tabdata.selected_pkg]
|
||||
end
|
||||
|
||||
if selected_pkg ~= nil then
|
||||
--check for screenshot beeing available
|
||||
local screenshotfilename = selected_pkg.path .. DIR_DELIM .. "screenshot.png"
|
||||
local screenshotfile, error = io.open(screenshotfilename, "r")
|
||||
|
||||
local modscreenshot
|
||||
if error == nil then
|
||||
screenshotfile:close()
|
||||
modscreenshot = screenshotfilename
|
||||
end
|
||||
|
||||
if modscreenshot == nil then
|
||||
modscreenshot = defaulttexturedir .. "no_screenshot.png"
|
||||
end
|
||||
|
||||
local info = core.get_content_info(selected_pkg.path)
|
||||
local desc = fgettext("No package description available")
|
||||
if info.description and info.description:trim() ~= "" then
|
||||
desc = info.description
|
||||
end
|
||||
|
||||
local title_and_name
|
||||
if selected_pkg.type == "game" then
|
||||
title_and_name = selected_pkg.name
|
||||
else
|
||||
title_and_name = (selected_pkg.title or selected_pkg.name) .. "\n" ..
|
||||
core.colorize("#BFBFBF", selected_pkg.name)
|
||||
end
|
||||
|
||||
retval = retval ..
|
||||
"image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" ..
|
||||
"label[8.25,0.6;" .. core.formspec_escape(title_and_name) .. "]" ..
|
||||
"box[5.5,2.2;6.15,2.35;#000]"
|
||||
|
||||
if selected_pkg.type == "mod" then
|
||||
if selected_pkg.is_modpack then
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" ..
|
||||
fgettext("Rename") .. "]"
|
||||
else
|
||||
--show dependencies
|
||||
desc = desc .. "\n\n"
|
||||
local toadd_hard = table.concat(info.depends or {}, "\n")
|
||||
local toadd_soft = table.concat(info.optional_depends or {}, "\n")
|
||||
if toadd_hard == "" and toadd_soft == "" then
|
||||
desc = desc .. fgettext("No dependencies.")
|
||||
else
|
||||
if toadd_hard ~= "" then
|
||||
desc = desc ..fgettext("Dependencies:") ..
|
||||
"\n" .. toadd_hard
|
||||
end
|
||||
if toadd_soft ~= "" then
|
||||
if toadd_hard ~= "" then
|
||||
desc = desc .. "\n\n"
|
||||
end
|
||||
desc = desc .. fgettext("Optional dependencies:") ..
|
||||
"\n" .. toadd_soft
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
if selected_pkg.type == "txp" then
|
||||
if selected_pkg.enabled then
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_disable_txp;" ..
|
||||
fgettext("Disable Texture Pack") .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"button[8.65,4.65;3.25,1;btn_mod_mgr_use_txp;" ..
|
||||
fgettext("Use Texture Pack") .. "]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval .. "textarea[5.85,2.2;6.35,2.9;;" ..
|
||||
fgettext("Information:") .. ";" .. desc .. "]"
|
||||
|
||||
if core.may_modify_path(selected_pkg.path) then
|
||||
retval = retval ..
|
||||
"button[5.5,4.65;3.25,1;btn_mod_mgr_delete_mod;" ..
|
||||
fgettext("Uninstall Package") .. "]"
|
||||
end
|
||||
end
|
||||
return retval
|
||||
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
|
||||
|
||||
mm_game_theme.init()
|
||||
mm_game_theme.reset()
|
||||
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
|
||||
|
||||
if fields["btn_contentdb"] ~= nil then
|
||||
local dlg = create_store_dlg()
|
||||
dlg:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btn_mod_mgr_rename_modpack"] ~= nil then
|
||||
local mod = packages:get_list()[tabdata.selected_pkg]
|
||||
local dlg_renamemp = create_rename_modpack_dlg(mod)
|
||||
dlg_renamemp:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg_renamemp:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["btn_mod_mgr_delete_mod"] ~= nil then
|
||||
local mod = packages:get_list()[tabdata.selected_pkg]
|
||||
local dlg_delmod = create_delete_content_dlg(mod)
|
||||
dlg_delmod:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg_delmod:show()
|
||||
packages = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mod_mgr_use_txp or fields.btn_mod_mgr_disable_txp then
|
||||
local txp_path = ""
|
||||
if fields.btn_mod_mgr_use_txp then
|
||||
txp_path = packages:get_list()[tabdata.selected_pkg].path
|
||||
end
|
||||
|
||||
core.settings:set("texture_path", txp_path)
|
||||
packages = nil
|
||||
|
||||
mm_game_theme.init()
|
||||
mm_game_theme.reset()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
return {
|
||||
name = "content",
|
||||
caption = fgettext("Content"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = handle_buttons,
|
||||
on_change = pkgmgr.update_gamelist
|
||||
}
|
||||
393
builtin/mainmenu/tab_local.lua
Normal file
393
builtin/mainmenu/tab_local.lua
Normal file
@@ -0,0 +1,393 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
|
||||
local current_game, singleplayer_refresh_gamebar
|
||||
local valid_disabled_settings = {
|
||||
["enable_damage"]=true,
|
||||
["creative_mode"]=true,
|
||||
["enable_server"]=true,
|
||||
}
|
||||
|
||||
-- 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)
|
||||
|
||||
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
|
||||
|
||||
local function game_buttonbar_button_handler(fields)
|
||||
if fields.game_open_cdb then
|
||||
local maintab = ui.find_by_name("maintab")
|
||||
local dlg = create_store_dlg("game")
|
||||
dlg:set_parent(maintab)
|
||||
maintab:hide()
|
||||
dlg:show()
|
||||
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
|
||||
|
||||
local btnbar = buttonbar_create("game_button_bar",
|
||||
game_buttonbar_button_handler,
|
||||
{x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15})
|
||||
|
||||
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(game.title)
|
||||
|
||||
if (game.menuicon_path or "") ~= "" then
|
||||
image = core.formspec_escape(game.menuicon_path)
|
||||
else
|
||||
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 ~= "" then
|
||||
text = text .. "\n" .. part3
|
||||
end
|
||||
end
|
||||
btnbar:add_button(btn_name, text, image, tooltip)
|
||||
end
|
||||
|
||||
local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png")
|
||||
btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB"))
|
||||
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")))
|
||||
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:") .. "]"..
|
||||
creative ..
|
||||
damage ..
|
||||
host ..
|
||||
"textlist[3.9,0.4;7.9,3.45;sp_worlds;" ..
|
||||
menu_render_worldlist() ..
|
||||
";" .. index .. "]"
|
||||
|
||||
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,"..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")) .. "]" ..
|
||||
"pwdfield[0.3,4.05;3.8,0.5;te_passwd;" .. fgettext("Password") .. "]"
|
||||
|
||||
local bind_addr = core.settings:get("bind_address")
|
||||
if bind_addr ~= nil and bind_addr ~= "" then
|
||||
retval = retval ..
|
||||
"field[0.3,5.25;2.5,0.5;te_serveraddr;" .. fgettext("Bind Address") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("bind_address")) .. "]" ..
|
||||
"field[2.85,5.25;1.25,0.5;te_serverport;" .. fgettext("Port") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("port")) .. "]"
|
||||
else
|
||||
retval = retval ..
|
||||
"field[0.3,5.25;3.8,0.5;te_serverport;" .. fgettext("Server Port") .. ";" ..
|
||||
core.formspec_escape(core.settings:get("port")) .. "]"
|
||||
end
|
||||
else
|
||||
retval = retval ..
|
||||
"button[7.9,4.75;4.1,1;play;" .. fgettext("Play Game") .. "]"
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
local function main_button_handler(this, fields, name, tabdata)
|
||||
|
||||
assert(name == "local")
|
||||
|
||||
local world_doubleclick = false
|
||||
|
||||
if fields["sp_worlds"] ~= nil then
|
||||
local event = core.explode_textlist_event(fields["sp_worlds"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
|
||||
menu_worldmt_legacy(selected)
|
||||
|
||||
if event.type == "DCL" then
|
||||
world_doubleclick = true
|
||||
end
|
||||
|
||||
if event.type == "CHG" and selected ~= nil then
|
||||
core.settings:set("mainmenu_last_selected_world",
|
||||
menudata.worldlist:get_raw_index(selected))
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if menu_handle_key_up_down(fields,"sp_worlds","mainmenu_last_selected_world") then
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_creative_mode"] then
|
||||
core.settings:set("creative_mode", fields["cb_creative_mode"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
menu_worldmt(selected, "creative_mode", fields["cb_creative_mode"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_enable_damage"] then
|
||||
core.settings:set("enable_damage", fields["cb_enable_damage"])
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
menu_worldmt(selected, "enable_damage", fields["cb_enable_damage"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_server"] then
|
||||
core.settings:set("enable_server", fields["cb_server"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["cb_server_announce"] then
|
||||
core.settings:set("server_announce", fields["cb_server_announce"])
|
||||
local selected = core.get_textlist_index("srv_worlds")
|
||||
menu_worldmt(selected, "server_announce", fields["cb_server_announce"])
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["play"] ~= nil or world_doubleclick or fields["key_enter"] then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
gamedata.selected_world = menudata.worldlist:get_raw_index(selected)
|
||||
|
||||
if selected == nil or gamedata.selected_world == 0 then
|
||||
gamedata.errormessage =
|
||||
fgettext("No world created or selected!")
|
||||
return true
|
||||
end
|
||||
|
||||
-- Update last game
|
||||
local world = menudata.worldlist:get_raw_element(gamedata.selected_world)
|
||||
local game_obj
|
||||
if world then
|
||||
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
|
||||
gamedata.playername = fields["te_playername"]
|
||||
gamedata.password = fields["te_passwd"]
|
||||
gamedata.port = fields["te_serverport"]
|
||||
gamedata.address = ""
|
||||
|
||||
core.settings:set("port",gamedata.port)
|
||||
if fields["te_serveraddr"] ~= nil then
|
||||
core.settings:set("bind_address",fields["te_serveraddr"])
|
||||
end
|
||||
else
|
||||
gamedata.singleplayer = true
|
||||
end
|
||||
|
||||
core.start()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_create"] ~= nil then
|
||||
local create_world_dlg = create_create_world_dlg()
|
||||
create_world_dlg:set_parent(this)
|
||||
this:hide()
|
||||
create_world_dlg:show()
|
||||
mm_game_theme.update("singleplayer", current_game())
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_delete"] ~= nil then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
if selected ~= nil and
|
||||
selected <= menudata.worldlist:size() then
|
||||
local world = menudata.worldlist:get_list()[selected]
|
||||
if world ~= nil and
|
||||
world.name ~= nil and
|
||||
world.name ~= "" then
|
||||
local index = menudata.worldlist:get_raw_index(selected)
|
||||
local delete_world_dlg = create_delete_world_dlg(world.name,index)
|
||||
delete_world_dlg:set_parent(this)
|
||||
this:hide()
|
||||
delete_world_dlg:show()
|
||||
mm_game_theme.update("singleplayer",current_game())
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields["world_configure"] ~= nil then
|
||||
local selected = core.get_textlist_index("sp_worlds")
|
||||
if selected ~= nil then
|
||||
local configdialog =
|
||||
create_configure_world_dlg(
|
||||
menudata.worldlist:get_raw_index(selected))
|
||||
|
||||
if (configdialog ~= nil) then
|
||||
configdialog:set_parent(this)
|
||||
this:hide()
|
||||
configdialog:show()
|
||||
mm_game_theme.update("singleplayer",current_game())
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function on_change(type, old_tab, new_tab)
|
||||
if (type == "ENTER") then
|
||||
local game = current_game()
|
||||
if game then
|
||||
apply_game(game)
|
||||
end
|
||||
|
||||
singleplayer_refresh_gamebar()
|
||||
ui.find_by_name("game_button_bar"):show()
|
||||
else
|
||||
menudata.worldlist:set_filtercriteria(nil)
|
||||
local gamebar = ui.find_by_name("game_button_bar")
|
||||
if gamebar then
|
||||
gamebar:hide()
|
||||
end
|
||||
core.set_topleft_text("")
|
||||
mm_game_theme.update(new_tab,nil)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
return {
|
||||
name = "local",
|
||||
caption = fgettext("Start Game"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = main_button_handler,
|
||||
on_change = on_change
|
||||
}
|
||||
427
builtin/mainmenu/tab_online.lua
Normal file
427
builtin/mainmenu/tab_online.lua
Normal file
@@ -0,0 +1,427 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2014 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
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()
|
||||
|
||||
if not tabdata.search_for then
|
||||
tabdata.search_for = ""
|
||||
end
|
||||
|
||||
local retval =
|
||||
-- Search
|
||||
"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[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[4.25,0.5;1.25,0.75;te_port;;" ..
|
||||
core.formspec_escape(core.settings:get("remote_port")) .. "]" ..
|
||||
|
||||
-- Description Background
|
||||
"label[0.25,1.6;" .. fgettext("Server Description") .. "]" ..
|
||||
"box[0.25,1.85;5.25,2.7;#999999]"..
|
||||
|
||||
-- Name / Password
|
||||
"container[0,4.8]" ..
|
||||
"label[0.25,0;" .. fgettext("Name") .. "]" ..
|
||||
"label[2.875,0;" .. fgettext("Password") .. "]" ..
|
||||
"field[0.25,0.2;2.625,0.75;te_name;;" .. core.formspec_escape(core.settings:get("name")) .. "]" ..
|
||||
"pwdfield[2.875,0.2;2.625,0.75;te_pwd;]" ..
|
||||
"container_end[]" ..
|
||||
|
||||
-- Connect
|
||||
"button[3,6;2.5,0.75;btn_mp_login;" .. fgettext("Login") .. "]"
|
||||
|
||||
if core.settings:get_bool("enable_split_login_register") then
|
||||
retval = retval .. "button[0.25,6;2.5,0.75;btn_mp_register;" .. fgettext("Register") .. "]"
|
||||
end
|
||||
|
||||
if tabdata.selected then
|
||||
if gamedata.fav then
|
||||
retval = retval .. "tooltip[btn_delete_favorite;" .. fgettext("Remove favorite") .. "]"
|
||||
retval = retval .. "style[btn_delete_favorite;padding=6]"
|
||||
retval = retval .. "image_button[5,1.3;0.5,0.5;" .. core.formspec_escape(defaulttexturedir ..
|
||||
"server_favorite_delete.png") .. ";btn_delete_favorite;]"
|
||||
end
|
||||
if gamedata.serverdescription then
|
||||
retval = retval .. "textarea[0.25,1.85;5.2,2.75;;;" ..
|
||||
core.formspec_escape(gamedata.serverdescription) .. "]"
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval .. "container_end[]"
|
||||
|
||||
-- Table
|
||||
retval = retval .. "tablecolumns[" ..
|
||||
"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,align=inline;"..
|
||||
"color,span=1;" ..
|
||||
"text,align=inline,width=4.25;" ..
|
||||
"image,tooltip=" .. fgettext("Creative mode") .. "," ..
|
||||
"0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," ..
|
||||
"1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_creative.png") .. "," ..
|
||||
"align=inline,padding=0.25,width=1.5;" ..
|
||||
--~ PvP = Player versus Player
|
||||
"image,tooltip=" .. fgettext("Damage / PvP") .. "," ..
|
||||
"0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," ..
|
||||
"1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_damage.png") .. "," ..
|
||||
"2=" .. core.formspec_escape(defaulttexturedir .. "server_flags_pvp.png") .. "," ..
|
||||
"align=inline,padding=0.25,width=1.5;" ..
|
||||
"color,align=inline,span=1;" ..
|
||||
"text,align=inline,padding=1]" ..
|
||||
"table[0.25,1;9.25,5.75;servers;"
|
||||
|
||||
local servers = get_sorted_servers()
|
||||
|
||||
local dividers = {
|
||||
fav = "5,#ffff00," .. fgettext("Favorites") .. ",,,0,0,,",
|
||||
public = "6,#4bdd42," .. fgettext("Public Servers") .. ",,,0,0,,",
|
||||
incompatible = "7,"..mt_color_grey.."," .. fgettext("Incompatible Servers") .. ",,,0,0,,"
|
||||
}
|
||||
local order = {"fav", "public", "incompatible"}
|
||||
|
||||
tabdata.lookup = {} -- maps row number to server
|
||||
local rows = {}
|
||||
for _, section in ipairs(order) do
|
||||
local section_servers = servers[section]
|
||||
if next(section_servers) ~= nil then
|
||||
rows[#rows + 1] = dividers[section]
|
||||
for _, server in ipairs(section_servers) do
|
||||
tabdata.lookup[#rows + 1] = server
|
||||
rows[#rows + 1] = render_serverlist_row(server)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
retval = retval .. table.concat(rows, ",")
|
||||
|
||||
if tabdata.selected then
|
||||
retval = retval .. ";" .. tabdata.selected .. "]"
|
||||
else
|
||||
retval = retval .. ";0]"
|
||||
end
|
||||
|
||||
return retval, "size[15.5,7,false]real_coordinates[true]"
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function search_server_list(input)
|
||||
menudata.search_result = nil
|
||||
if #serverlistmgr.servers < 2 then
|
||||
return
|
||||
end
|
||||
|
||||
-- setup the keyword list
|
||||
local keywords = {}
|
||||
for word in input:gmatch("%S+") do
|
||||
word = word:gsub("(%W)", "%%%1")
|
||||
table.insert(keywords, word)
|
||||
end
|
||||
|
||||
if #keywords == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
menudata.search_result = {}
|
||||
|
||||
-- Search the serverlist
|
||||
local search_result = {}
|
||||
for i = 1, #serverlistmgr.servers do
|
||||
local server = serverlistmgr.servers[i]
|
||||
local found = 0
|
||||
for k = 1, #keywords do
|
||||
local keyword = keywords[k]
|
||||
if server.name then
|
||||
local sername = server.name:lower()
|
||||
local _, count = sername:gsub(keyword, keyword)
|
||||
found = found + count * 4
|
||||
end
|
||||
|
||||
if 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.servers then
|
||||
local event = core.explode_table_event(fields.servers)
|
||||
local server = tabdata.lookup[event.row]
|
||||
|
||||
if server then
|
||||
if event.type == "DCL" then
|
||||
if not is_server_protocol_compat_or_error(
|
||||
server.proto_min, server.proto_max) then
|
||||
return true
|
||||
end
|
||||
|
||||
gamedata.address = server.address
|
||||
gamedata.port = server.port
|
||||
gamedata.playername = fields.te_name
|
||||
gamedata.selected_world = 0
|
||||
|
||||
if fields.te_pwd then
|
||||
gamedata.password = fields.te_pwd
|
||||
end
|
||||
|
||||
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
|
||||
if event.type == "CHG" then
|
||||
set_selected_server(tabdata, event.row, server)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if fields.btn_delete_favorite then
|
||||
local idx = core.get_table_index("servers")
|
||||
if not idx then return end
|
||||
local server = tabdata.lookup[idx]
|
||||
if not server then return end
|
||||
|
||||
serverlistmgr.delete_favorite(server)
|
||||
-- the server at [idx+1] will be at idx once list is refreshed
|
||||
set_selected_server(tabdata, idx, tabdata.lookup[idx+1])
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_clear then
|
||||
tabdata.search_for = ""
|
||||
menudata.search_result = nil
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_search or fields.key_enter_field == "te_search" then
|
||||
tabdata.search_for = fields.te_search
|
||||
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
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_refresh then
|
||||
serverlistmgr.sync()
|
||||
return true
|
||||
end
|
||||
|
||||
if (fields.btn_mp_login or fields.key_enter)
|
||||
and fields.te_address ~= "" and fields.te_port then
|
||||
gamedata.playername = fields.te_name
|
||||
gamedata.password = fields.te_pwd
|
||||
gamedata.address = fields.te_address
|
||||
gamedata.port = tonumber(fields.te_port)
|
||||
|
||||
local enable_split_login_register = core.settings:get_bool("enable_split_login_register")
|
||||
gamedata.allow_login_or_register = enable_split_login_register and "login" or "any"
|
||||
gamedata.selected_world = 0
|
||||
|
||||
local idx = core.get_table_index("servers")
|
||||
local server = idx and tabdata.lookup[idx]
|
||||
|
||||
set_selected_server(tabdata)
|
||||
|
||||
if server and server.address == gamedata.address and
|
||||
server.port == gamedata.port then
|
||||
|
||||
serverlistmgr.add_favorite(server)
|
||||
|
||||
gamedata.servername = server.name
|
||||
gamedata.serverdescription = server.description
|
||||
|
||||
if not is_server_protocol_compat_or_error(
|
||||
server.proto_min, server.proto_max) then
|
||||
return true
|
||||
end
|
||||
else
|
||||
gamedata.servername = ""
|
||||
gamedata.serverdescription = ""
|
||||
|
||||
serverlistmgr.add_favorite({
|
||||
address = gamedata.address,
|
||||
port = gamedata.port,
|
||||
})
|
||||
end
|
||||
|
||||
core.settings:set("address", gamedata.address)
|
||||
core.settings:set("remote_port", gamedata.port)
|
||||
|
||||
core.start()
|
||||
return true
|
||||
end
|
||||
|
||||
if fields.btn_mp_register and fields.te_address ~= "" and fields.te_port then
|
||||
local idx = core.get_table_index("servers")
|
||||
local server = idx and tabdata.lookup[idx]
|
||||
if server and (server.address ~= fields.te_address or server.port ~= tonumber(fields.te_port)) then
|
||||
server = nil
|
||||
end
|
||||
|
||||
if server and not is_server_protocol_compat_or_error(
|
||||
server.proto_min, server.proto_max) then
|
||||
return true
|
||||
end
|
||||
|
||||
local dlg = create_register_dialog(fields.te_address, tonumber(fields.te_port), server)
|
||||
dlg:set_parent(tabview)
|
||||
tabview:hide()
|
||||
dlg:show()
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function on_change(type, old_tab, new_tab)
|
||||
if type == "LEAVE" then return end
|
||||
serverlistmgr.sync()
|
||||
end
|
||||
|
||||
return {
|
||||
name = "online",
|
||||
caption = fgettext("Join Game"),
|
||||
cbf_formspec = get_formspec,
|
||||
cbf_button_handler = main_button_handler,
|
||||
on_change = on_change
|
||||
}
|
||||
403
builtin/mainmenu/tab_settings.lua
Normal file
403
builtin/mainmenu/tab_settings.lua
Normal file
@@ -0,0 +1,403 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2013 sapier
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local labels = {
|
||||
leaves = {
|
||||
fgettext("Opaque Leaves"),
|
||||
fgettext("Simple Leaves"),
|
||||
fgettext("Fancy Leaves")
|
||||
},
|
||||
node_highlighting = {
|
||||
fgettext("Node Outlining"),
|
||||
fgettext("Node Highlighting"),
|
||||
fgettext("None")
|
||||
},
|
||||
filters = {
|
||||
fgettext("No Filter"),
|
||||
fgettext("Bilinear Filter"),
|
||||
fgettext("Trilinear Filter")
|
||||
},
|
||||
mipmap = {
|
||||
fgettext("No Mipmap"),
|
||||
fgettext("Mipmap"),
|
||||
fgettext("Mipmap + Aniso. Filter")
|
||||
},
|
||||
antialiasing = {
|
||||
fgettext("None"),
|
||||
fgettext("2x"),
|
||||
fgettext("4x"),
|
||||
fgettext("8x")
|
||||
},
|
||||
shadow_levels = {
|
||||
fgettext("Disabled"),
|
||||
fgettext("Very Low"),
|
||||
fgettext("Low"),
|
||||
fgettext("Medium"),
|
||||
fgettext("High"),
|
||||
fgettext("Very High")
|
||||
}
|
||||
}
|
||||
|
||||
local dd_options = {
|
||||
leaves = {
|
||||
table.concat(labels.leaves, ","),
|
||||
{"opaque", "simple", "fancy"}
|
||||
},
|
||||
node_highlighting = {
|
||||
table.concat(labels.node_highlighting, ","),
|
||||
{"box", "halo", "none"}
|
||||
},
|
||||
filters = {
|
||||
table.concat(labels.filters, ","),
|
||||
{"", "bilinear_filter", "trilinear_filter"}
|
||||
},
|
||||
mipmap = {
|
||||
table.concat(labels.mipmap, ","),
|
||||
{"", "mip_map", "anisotropic_filter"}
|
||||
},
|
||||
antialiasing = {
|
||||
table.concat(labels.antialiasing, ","),
|
||||
{"0", "2", "4", "8"}
|
||||
},
|
||||
shadow_levels = {
|
||||
table.concat(labels.shadow_levels, ","),
|
||||
{ "0", "1", "2", "3", "4", "5" }
|
||||
}
|
||||
}
|
||||
|
||||
local getSettingIndex = {
|
||||
Leaves = function()
|
||||
local style = core.settings:get("leaves_style")
|
||||
for idx, name in pairs(dd_options.leaves[2]) do
|
||||
if style == name then return idx end
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
NodeHighlighting = function()
|
||||
local style = core.settings:get("node_highlighting")
|
||||
for idx, name in pairs(dd_options.node_highlighting[2]) do
|
||||
if style == name then return idx end
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Filter = function()
|
||||
if core.settings:get(dd_options.filters[2][3]) == "true" then
|
||||
return 3
|
||||
elseif core.settings:get(dd_options.filters[2][3]) == "false" and
|
||||
core.settings:get(dd_options.filters[2][2]) == "true" then
|
||||
return 2
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Mipmap = function()
|
||||
if core.settings:get(dd_options.mipmap[2][3]) == "true" then
|
||||
return 3
|
||||
elseif core.settings:get(dd_options.mipmap[2][3]) == "false" and
|
||||
core.settings:get(dd_options.mipmap[2][2]) == "true" then
|
||||
return 2
|
||||
end
|
||||
return 1
|
||||
end,
|
||||
Antialiasing = function()
|
||||
local antialiasing_setting = core.settings:get("fsaa")
|
||||
for i = 1, #dd_options.antialiasing[2] do
|
||||
if antialiasing_setting == dd_options.antialiasing[2][i] then
|
||||
return i
|
||||
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
|
||||
}
|
||||
|
||||
local function antialiasing_fname_to_name(fname)
|
||||
for i = 1, #labels.antialiasing do
|
||||
if fname == labels.antialiasing[i] then
|
||||
return dd_options.antialiasing[2][i]
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function formspec(tabview, name, tabdata)
|
||||
local tab_string =
|
||||
"box[0,0;3.75,4.5;#999999]" ..
|
||||
"checkbox[0.25,0;cb_smooth_lighting;" .. fgettext("Smooth Lighting") .. ";"
|
||||
.. dump(core.settings:get_bool("smooth_lighting")) .. "]" ..
|
||||
"checkbox[0.25,0.5;cb_particles;" .. fgettext("Particles") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_particles")) .. "]" ..
|
||||
"checkbox[0.25,1;cb_3d_clouds;" .. fgettext("3D Clouds") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_3d_clouds")) .. "]" ..
|
||||
"checkbox[0.25,1.5;cb_opaque_water;" .. fgettext("Opaque Water") .. ";"
|
||||
.. dump(core.settings:get_bool("opaque_water")) .. "]" ..
|
||||
"checkbox[0.25,2.0;cb_connected_glass;" .. fgettext("Connected Glass") .. ";"
|
||||
.. dump(core.settings:get_bool("connected_glass")) .. "]" ..
|
||||
"dropdown[0.25,2.8;3.5;dd_node_highlighting;" .. dd_options.node_highlighting[1] .. ";"
|
||||
.. getSettingIndex.NodeHighlighting() .. "]" ..
|
||||
"dropdown[0.25,3.6;3.5;dd_leaves_style;" .. dd_options.leaves[1] .. ";"
|
||||
.. getSettingIndex.Leaves() .. "]" ..
|
||||
"box[4,0;3.75,4.9;#999999]" ..
|
||||
"label[4.25,0.1;" .. fgettext("Texturing:") .. "]" ..
|
||||
"dropdown[4.25,0.55;3.5;dd_filters;" .. dd_options.filters[1] .. ";"
|
||||
.. getSettingIndex.Filter() .. "]" ..
|
||||
"dropdown[4.25,1.35;3.5;dd_mipmap;" .. dd_options.mipmap[1] .. ";"
|
||||
.. getSettingIndex.Mipmap() .. "]" ..
|
||||
"label[4.25,2.15;" .. fgettext("Antialiasing:") .. "]" ..
|
||||
"dropdown[4.25,2.6;3.5;dd_antialiasing;" .. dd_options.antialiasing[1] .. ";"
|
||||
.. getSettingIndex.Antialiasing() .. "]" ..
|
||||
"box[8,0;3.75,4.5;#999999]"
|
||||
|
||||
local video_driver = core.settings:get("video_driver")
|
||||
local shaders_enabled = core.settings:get_bool("enable_shaders")
|
||||
if video_driver == "opengl" then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders") .. ";"
|
||||
.. tostring(shaders_enabled) .. "]"
|
||||
elseif video_driver == "ogles2" then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders (experimental)") .. ";"
|
||||
.. tostring(shaders_enabled) .. "]"
|
||||
else
|
||||
core.settings:set_bool("enable_shaders", false)
|
||||
shaders_enabled = false
|
||||
tab_string = tab_string ..
|
||||
"label[8.38,0.2;" .. core.colorize("#888888",
|
||||
fgettext("Shaders (unavailable)")) .. "]"
|
||||
end
|
||||
|
||||
tab_string = tab_string ..
|
||||
"button[8,4.75;3.95,1;btn_change_keys;"
|
||||
.. fgettext("Change Keys") .. "]"
|
||||
|
||||
tab_string = tab_string ..
|
||||
"button[0,4.75;3.95,1;btn_advanced_settings;"
|
||||
.. fgettext("All Settings") .. "]"
|
||||
|
||||
|
||||
if core.settings:get("touchscreen_threshold") ~= nil then
|
||||
tab_string = tab_string ..
|
||||
"label[4.25,3.5;" .. fgettext("Touch threshold (px):") .. "]" ..
|
||||
"dropdown[4.25,3.95;3.5;dd_touchthreshold;0,10,20,30,40,50;" ..
|
||||
((tonumber(core.settings:get("touchscreen_threshold")) / 10) + 1) ..
|
||||
"]"
|
||||
else
|
||||
tab_string = tab_string ..
|
||||
"label[4.25,3.65;" .. fgettext("Screen:") .. "]" ..
|
||||
"checkbox[4.25,3.9;cb_autosave_screensize;" .. fgettext("Autosave Screen Size") .. ";"
|
||||
.. dump(core.settings:get_bool("autosave_screensize")) .. "]"
|
||||
end
|
||||
|
||||
if shaders_enabled then
|
||||
tab_string = tab_string ..
|
||||
"checkbox[8.25,0.5;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";"
|
||||
.. dump(core.settings:get_bool("tone_mapping")) .. "]" ..
|
||||
"checkbox[8.25,1;cb_waving_water;" .. fgettext("Waving Liquids") .. ";"
|
||||
.. dump(core.settings:get_bool("enable_waving_water")) .. "]" ..
|
||||
"checkbox[8.25,1.5;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";"
|
||||
.. 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")) .. "]"
|
||||
|
||||
if video_driver == "opengl" then
|
||||
tab_string = tab_string ..
|
||||
"label[8.25,2.8;" .. fgettext("Dynamic shadows:") .. "]" ..
|
||||
"label[8.25,3.2;" .. fgettext("(game support required)") .. "]" ..
|
||||
"dropdown[8.25,3.7;3.5;dd_shadows;" .. dd_options.shadow_levels[1] .. ";"
|
||||
.. getSettingIndex.ShadowMapping() .. "]"
|
||||
else
|
||||
tab_string = tab_string ..
|
||||
"label[8.38,2.7;" .. core.colorize("#888888",
|
||||
fgettext("Dynamic shadows")) .. "]"
|
||||
end
|
||||
else
|
||||
tab_string = tab_string ..
|
||||
"label[8.38,0.7;" .. core.colorize("#888888",
|
||||
fgettext("Tone Mapping")) .. "]" ..
|
||||
"label[8.38,1.2;" .. core.colorize("#888888",
|
||||
fgettext("Waving Liquids")) .. "]" ..
|
||||
"label[8.38,1.7;" .. core.colorize("#888888",
|
||||
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
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
local function handle_settings_buttons(this, fields, tabname, tabdata)
|
||||
|
||||
if fields["btn_advanced_settings"] ~= nil then
|
||||
local adv_settings_dlg = create_adv_settings_dlg()
|
||||
adv_settings_dlg:set_parent(this)
|
||||
this:hide()
|
||||
adv_settings_dlg:show()
|
||||
--mm_game_theme.update("singleplayer", current_game())
|
||||
return true
|
||||
end
|
||||
if fields["cb_smooth_lighting"] then
|
||||
core.settings:set("smooth_lighting", fields["cb_smooth_lighting"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_particles"] then
|
||||
core.settings:set("enable_particles", fields["cb_particles"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_3d_clouds"] then
|
||||
core.settings:set("enable_3d_clouds", fields["cb_3d_clouds"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_opaque_water"] then
|
||||
core.settings:set("opaque_water", fields["cb_opaque_water"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_connected_glass"] then
|
||||
core.settings:set("connected_glass", fields["cb_connected_glass"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_autosave_screensize"] then
|
||||
core.settings:set("autosave_screensize", fields["cb_autosave_screensize"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_shaders"] then
|
||||
core.settings:set("enable_shaders", fields["cb_shaders"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_tonemapping"] then
|
||||
core.settings:set("tone_mapping", fields["cb_tonemapping"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_waving_water"] then
|
||||
core.settings:set("enable_waving_water", fields["cb_waving_water"])
|
||||
return true
|
||||
end
|
||||
if fields["cb_waving_leaves"] then
|
||||
core.settings:set("enable_waving_leaves", fields["cb_waving_leaves"])
|
||||
end
|
||||
if fields["cb_waving_plants"] then
|
||||
core.settings:set("enable_waving_plants", fields["cb_waving_plants"])
|
||||
return true
|
||||
end
|
||||
if fields["btn_change_keys"] then
|
||||
core.show_keys_menu()
|
||||
return true
|
||||
end
|
||||
if fields["cb_touchscreen_target"] then
|
||||
core.settings:set("touchtarget", fields["cb_touchscreen_target"])
|
||||
return true
|
||||
end
|
||||
|
||||
--Note dropdowns have to be handled LAST!
|
||||
local ddhandled = false
|
||||
|
||||
for i = 1, #labels.leaves do
|
||||
if fields["dd_leaves_style"] == labels.leaves[i] then
|
||||
core.settings:set("leaves_style", dd_options.leaves[2][i])
|
||||
ddhandled = true
|
||||
end
|
||||
end
|
||||
for i = 1, #labels.node_highlighting do
|
||||
if fields["dd_node_highlighting"] == labels.node_highlighting[i] then
|
||||
core.settings:set("node_highlighting", dd_options.node_highlighting[2][i])
|
||||
ddhandled = true
|
||||
end
|
||||
end
|
||||
if fields["dd_filters"] == labels.filters[1] then
|
||||
core.settings:set("bilinear_filter", "false")
|
||||
core.settings:set("trilinear_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_filters"] == labels.filters[2] then
|
||||
core.settings:set("bilinear_filter", "true")
|
||||
core.settings:set("trilinear_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_filters"] == labels.filters[3] then
|
||||
core.settings:set("bilinear_filter", "false")
|
||||
core.settings:set("trilinear_filter", "true")
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_mipmap"] == labels.mipmap[1] then
|
||||
core.settings:set("mip_map", "false")
|
||||
core.settings:set("anisotropic_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_mipmap"] == labels.mipmap[2] then
|
||||
core.settings:set("mip_map", "true")
|
||||
core.settings:set("anisotropic_filter", "false")
|
||||
ddhandled = true
|
||||
elseif fields["dd_mipmap"] == labels.mipmap[3] then
|
||||
core.settings:set("mip_map", "true")
|
||||
core.settings:set("anisotropic_filter", "true")
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_antialiasing"] then
|
||||
core.settings:set("fsaa",
|
||||
antialiasing_fname_to_name(fields["dd_antialiasing"]))
|
||||
ddhandled = true
|
||||
end
|
||||
if fields["dd_touchthreshold"] then
|
||||
core.settings:set("touchscreen_threshold", fields["dd_touchthreshold"])
|
||||
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] = { 62, 512, "true", 0, "false" },
|
||||
[3] = { 93, 1024, "true", 0, "false" },
|
||||
[4] = { 140, 2048, "true", 1, "false" },
|
||||
[5] = { 210, 4096, "true", 2, "true" },
|
||||
[6] = { 300, 8192, "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
|
||||
|
||||
return {
|
||||
name = "settings",
|
||||
caption = fgettext("Settings"),
|
||||
cbf_formspec = formspec,
|
||||
cbf_button_handler = handle_settings_buttons
|
||||
}
|
||||
29
builtin/mainmenu/tests/favorites_wellformed.txt
Normal file
29
builtin/mainmenu/tests/favorites_wellformed.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
[server]
|
||||
|
||||
127.0.0.1
|
||||
30000
|
||||
|
||||
|
||||
[server]
|
||||
|
||||
localhost
|
||||
30000
|
||||
|
||||
|
||||
[server]
|
||||
|
||||
vps.rubenwardy.com
|
||||
30001
|
||||
|
||||
|
||||
[server]
|
||||
|
||||
gundul.ddnss.de
|
||||
39155
|
||||
|
||||
|
||||
[server]
|
||||
VanessaE's Dreambuilder creative Server
|
||||
daconcepts.com
|
||||
30000
|
||||
VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets.
|
||||
38
builtin/mainmenu/tests/serverlistmgr_spec.lua
Normal file
38
builtin/mainmenu/tests/serverlistmgr_spec.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
_G.core = {}
|
||||
_G.vector = {metatable = {}}
|
||||
_G.unpack = table.unpack
|
||||
_G.serverlistmgr = {}
|
||||
|
||||
dofile("builtin/common/vector.lua")
|
||||
dofile("builtin/common/misc_helpers.lua")
|
||||
dofile("builtin/mainmenu/serverlistmgr.lua")
|
||||
|
||||
local base = "builtin/mainmenu/tests/"
|
||||
|
||||
describe("legacy favorites", function()
|
||||
it("loads well-formed correctly", function()
|
||||
local favs = serverlistmgr.read_legacy_favorites(base .. "favorites_wellformed.txt")
|
||||
|
||||
local expected = {
|
||||
{
|
||||
address = "127.0.0.1",
|
||||
port = 30000,
|
||||
},
|
||||
|
||||
{ address = "localhost", port = 30000 },
|
||||
|
||||
{ address = "vps.rubenwardy.com", port = 30001 },
|
||||
|
||||
{ address = "gundul.ddnss.de", port = 39155 },
|
||||
|
||||
{
|
||||
address = "daconcepts.com",
|
||||
port = 30000,
|
||||
name = "VanessaE's Dreambuilder creative Server",
|
||||
description = "VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets."
|
||||
},
|
||||
}
|
||||
|
||||
assert.same(expected, favs)
|
||||
end)
|
||||
end)
|
||||
80
builtin/profiler/init.lua
Normal file
80
builtin/profiler/init.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local S = core.get_translator("__builtin")
|
||||
|
||||
local function get_bool_default(name, default)
|
||||
local val = core.settings:get_bool(name)
|
||||
if val == nil then
|
||||
return default
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
local profiler_path = core.get_builtin_path().."profiler"..DIR_DELIM
|
||||
local profiler = {}
|
||||
local sampler = assert(loadfile(profiler_path .. "sampling.lua"))(profiler)
|
||||
local instrumentation = assert(loadfile(profiler_path .. "instrumentation.lua"))(profiler, sampler, get_bool_default)
|
||||
local reporter = dofile(profiler_path .. "reporter.lua")
|
||||
profiler.instrument = instrumentation.instrument
|
||||
|
||||
---
|
||||
-- Delayed registration of the /profiler chat command
|
||||
-- Is called later, after `core.register_chatcommand` was set up.
|
||||
--
|
||||
function profiler.init_chatcommand()
|
||||
local instrument_profiler = get_bool_default("instrument.profiler", false)
|
||||
if instrument_profiler then
|
||||
instrumentation.init_chatcommand()
|
||||
end
|
||||
|
||||
local param_usage = S("print [<filter>] | dump [<filter>] | save [<format> [<filter>]] | reset")
|
||||
core.register_chatcommand("profiler", {
|
||||
description = S("Handle the profiler and profiling data"),
|
||||
params = param_usage,
|
||||
privs = { server=true },
|
||||
func = function(name, param)
|
||||
local command, arg0 = string.match(param, "([^ ]+) ?(.*)")
|
||||
local args = arg0 and string.split(arg0, " ")
|
||||
|
||||
if command == "dump" then
|
||||
core.log("action", reporter.print(sampler.profile, arg0))
|
||||
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, S("Statistics were reset.")
|
||||
end
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
if not instrument_profiler then
|
||||
instrumentation.init_chatcommand()
|
||||
end
|
||||
end
|
||||
|
||||
sampler.init()
|
||||
instrumentation.init()
|
||||
|
||||
return profiler
|
||||
235
builtin/profiler/instrumentation.lua
Normal file
235
builtin/profiler/instrumentation.lua
Normal file
@@ -0,0 +1,235 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
local format, pairs, type = string.format, pairs, type
|
||||
local core, get_current_modname = core, core.get_current_modname
|
||||
local profiler, sampler, get_bool_default = ...
|
||||
|
||||
local instrument_builtin = get_bool_default("instrument.builtin", false)
|
||||
|
||||
local register_functions = {
|
||||
register_globalstep = 0,
|
||||
register_playerevent = 0,
|
||||
register_on_placenode = 0,
|
||||
register_on_dignode = 0,
|
||||
register_on_punchnode = 0,
|
||||
register_on_generated = 0,
|
||||
register_on_newplayer = 0,
|
||||
register_on_dieplayer = 0,
|
||||
register_on_respawnplayer = 0,
|
||||
register_on_prejoinplayer = 0,
|
||||
register_on_joinplayer = 0,
|
||||
register_on_leaveplayer = 0,
|
||||
register_on_cheat = 0,
|
||||
register_on_chat_message = 0,
|
||||
register_on_player_receive_fields = 0,
|
||||
register_on_craft = 0,
|
||||
register_craft_predict = 0,
|
||||
register_on_protection_violation = 0,
|
||||
register_on_item_eat = 0,
|
||||
register_on_punchplayer = 0,
|
||||
register_on_player_hpchange = 0,
|
||||
}
|
||||
|
||||
---
|
||||
-- Create an unique instrument name.
|
||||
-- Generate a missing label with a running index number.
|
||||
--
|
||||
local counts = {}
|
||||
local function generate_name(def)
|
||||
local class, label, func_name = def.class, def.label, def.func_name
|
||||
if label then
|
||||
if class or func_name then
|
||||
return format("%s '%s' %s", class or "", label, func_name or ""):trim()
|
||||
end
|
||||
return format("%s", label):trim()
|
||||
elseif label == false then
|
||||
return format("%s", class or func_name):trim()
|
||||
end
|
||||
|
||||
local index_id = def.mod .. (class or func_name)
|
||||
local index = counts[index_id] or 1
|
||||
counts[index_id] = index + 1
|
||||
return format("%s[%d] %s", class or func_name, index, class and func_name or ""):trim()
|
||||
end
|
||||
|
||||
---
|
||||
-- Keep `measure` and the closure in `instrument` lean, as these, and their
|
||||
-- directly called functions are the overhead that is caused by instrumentation.
|
||||
--
|
||||
local time, log = core.get_us_time, sampler.log
|
||||
local function measure(modname, instrument_name, start, ...)
|
||||
log(modname, instrument_name, time() - start)
|
||||
return ...
|
||||
end
|
||||
--- Automatically instrument a function to measure and log to the sampler.
|
||||
-- def = {
|
||||
-- mod = "",
|
||||
-- class = "",
|
||||
-- func_name = "",
|
||||
-- -- if nil, will create a label based on registration order
|
||||
-- label = "" | false,
|
||||
-- }
|
||||
local function instrument(def)
|
||||
if not def or not def.func then
|
||||
return
|
||||
end
|
||||
def.mod = def.mod or get_current_modname() or "??"
|
||||
local modname = def.mod
|
||||
local instrument_name = generate_name(def)
|
||||
local func = def.func
|
||||
|
||||
if not instrument_builtin and modname == "*builtin*" then
|
||||
return func
|
||||
end
|
||||
|
||||
return function(...)
|
||||
-- This tail-call allows passing all return values of `func`
|
||||
-- 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 func(...)
|
||||
local start = time()
|
||||
return measure(modname, instrument_name, start, func(...))
|
||||
end
|
||||
end
|
||||
|
||||
local function can_be_called(func)
|
||||
-- It has to be a function or callable table
|
||||
return type(func) == "function" or
|
||||
((type(func) == "table" or type(func) == "userdata") and
|
||||
getmetatable(func) and getmetatable(func).__call)
|
||||
end
|
||||
|
||||
local function assert_can_be_called(func, func_name, level)
|
||||
if not can_be_called(func) then
|
||||
-- Then throw an *helpful* error, by pointing on our caller instead of us.
|
||||
error(format("Invalid argument to %s. Expected function-like type instead of '%s'.",
|
||||
func_name, type(func)), level + 1)
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Wraps a registration function `func` in such a way,
|
||||
-- that it will automatically instrument any callback function passed as first argument.
|
||||
--
|
||||
local function instrument_register(func, func_name)
|
||||
local register_name = func_name:gsub("^register_", "", 1)
|
||||
return function(callback, ...)
|
||||
assert_can_be_called(callback, func_name, 2)
|
||||
register_functions[func_name] = register_functions[func_name] + 1
|
||||
return func(instrument {
|
||||
func = callback,
|
||||
func_name = register_name
|
||||
}, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function init_chatcommand()
|
||||
if get_bool_default("instrument.chatcommand", true) then
|
||||
local orig_register_chatcommand = core.register_chatcommand
|
||||
core.register_chatcommand = function(cmd, def)
|
||||
def.func = instrument {
|
||||
func = def.func,
|
||||
label = "/" .. cmd,
|
||||
}
|
||||
orig_register_chatcommand(cmd, def)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Start instrumenting selected functions
|
||||
--
|
||||
local function init()
|
||||
if get_bool_default("instrument.entity", true) then
|
||||
-- Explicitly declare entity api-methods.
|
||||
-- Simple iteration would ignore lookup via __index.
|
||||
local entity_instrumentation = {
|
||||
"on_activate",
|
||||
"on_deactivate",
|
||||
"on_step",
|
||||
"on_punch",
|
||||
"on_rightclick",
|
||||
"get_staticdata",
|
||||
}
|
||||
-- Wrap register_entity() to instrument them on registration.
|
||||
local orig_register_entity = core.register_entity
|
||||
core.register_entity = function(name, prototype)
|
||||
local modname = get_current_modname()
|
||||
for _, func_name in pairs(entity_instrumentation) do
|
||||
prototype[func_name] = instrument {
|
||||
func = prototype[func_name],
|
||||
mod = modname,
|
||||
func_name = func_name,
|
||||
label = prototype.label,
|
||||
}
|
||||
end
|
||||
orig_register_entity(name,prototype)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.abm", true) then
|
||||
-- Wrap register_abm() to automatically instrument abms.
|
||||
local orig_register_abm = core.register_abm
|
||||
core.register_abm = function(spec)
|
||||
spec.action = instrument {
|
||||
func = spec.action,
|
||||
class = "ABM",
|
||||
label = spec.label,
|
||||
}
|
||||
orig_register_abm(spec)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.lbm", true) then
|
||||
-- Wrap register_lbm() to automatically instrument lbms.
|
||||
local orig_register_lbm = core.register_lbm
|
||||
core.register_lbm = function(spec)
|
||||
spec.action = instrument {
|
||||
func = spec.action,
|
||||
class = "LBM",
|
||||
label = spec.label or spec.name,
|
||||
}
|
||||
orig_register_lbm(spec)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.global_callback", true) then
|
||||
for func_name, _ in pairs(register_functions) do
|
||||
core[func_name] = instrument_register(core[func_name], func_name)
|
||||
end
|
||||
end
|
||||
|
||||
if get_bool_default("instrument.profiler", false) then
|
||||
-- Measure overhead of instrumentation, but keep it down for functions
|
||||
-- So keep the `return` for better optimization.
|
||||
profiler.empty_instrument = instrument {
|
||||
func = function() return end,
|
||||
mod = "*profiler*",
|
||||
class = "Instrumentation overhead",
|
||||
label = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
register_functions = register_functions,
|
||||
instrument = instrument,
|
||||
init = init,
|
||||
init_chatcommand = init_chatcommand,
|
||||
}
|
||||
280
builtin/profiler/reporter.lua
Normal file
280
builtin/profiler/reporter.lua
Normal file
@@ -0,0 +1,280 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
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
|
||||
local core, settings = core, core.settings
|
||||
local reporter = {}
|
||||
|
||||
---
|
||||
-- Shorten a string. End on an ellipsis if shortened.
|
||||
--
|
||||
local function shorten(str, length)
|
||||
if str and str:len() > length then
|
||||
return "..." .. str:sub(-(length-3))
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local function filter_matches(filter, text)
|
||||
return not filter or string.match(text, filter)
|
||||
end
|
||||
|
||||
local function format_number(number, fmt)
|
||||
number = tonumber(number)
|
||||
if not number then
|
||||
return "N/A"
|
||||
end
|
||||
return sprintf(fmt or "%d", number)
|
||||
end
|
||||
|
||||
local Formatter = {
|
||||
new = function(self, object)
|
||||
object = object or {}
|
||||
object.out = {} -- output buffer
|
||||
self.__index = self
|
||||
return setmetatable(object, self)
|
||||
end,
|
||||
__tostring = function (self)
|
||||
return table.concat(self.out, LINE_DELIM)
|
||||
end,
|
||||
print = function(self, text, ...)
|
||||
if (...) then
|
||||
text = sprintf(text, ...)
|
||||
end
|
||||
|
||||
if text then
|
||||
-- Avoid format unicode issues.
|
||||
text = text:gsub("Ms", "µs")
|
||||
end
|
||||
|
||||
table.insert(self.out, text or LINE_DELIM)
|
||||
end,
|
||||
flush = function(self)
|
||||
table.insert(self.out, LINE_DELIM)
|
||||
local text = table.concat(self.out, LINE_DELIM)
|
||||
self.out = {}
|
||||
return text
|
||||
end
|
||||
}
|
||||
|
||||
local widths = { 55, 9, 9, 9, 5, 5, 5 }
|
||||
local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths))
|
||||
|
||||
local HR = {}
|
||||
for i=1, #widths do
|
||||
HR[i]= rep("-", widths[i])
|
||||
end
|
||||
-- ' | ' should break less with github than '-+-', when people are pasting there
|
||||
HR = sprintf("-%s-", table.concat(HR, " | "))
|
||||
|
||||
local TxtFormatter = Formatter:new {
|
||||
format_row = function(self, modname, instrument_name, statistics)
|
||||
local label
|
||||
if instrument_name then
|
||||
label = shorten(instrument_name, widths[1] - 5)
|
||||
label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len()))
|
||||
else -- Print mod_stats
|
||||
label = shorten(modname, widths[1] - 2) .. ":"
|
||||
end
|
||||
|
||||
self:print(txt_row_format, label,
|
||||
format_number(statistics.time_min),
|
||||
format_number(statistics.time_max),
|
||||
format_number(statistics:get_time_avg()),
|
||||
format_number(statistics.part_min, "%.1f"),
|
||||
format_number(statistics.part_max, "%.1f"),
|
||||
format_number(statistics:get_part_avg(), "%.1f")
|
||||
)
|
||||
end,
|
||||
format = function(self, filter)
|
||||
local profile = self.profile
|
||||
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(S("The output is limited to '@1'.", filter))
|
||||
end
|
||||
|
||||
self:print()
|
||||
self:print(
|
||||
txt_row_format,
|
||||
"instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %"
|
||||
)
|
||||
self:print(HR)
|
||||
for modname,mod_stats in pairs(profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
self:format_row(modname, nil, mod_stats)
|
||||
|
||||
if mod_stats.instruments ~= nil then
|
||||
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
||||
self:format_row(nil, instrument_name, instrument_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:print(HR)
|
||||
if not filter then
|
||||
self:format_row("total", nil, profile.stats_total)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local CsvFormatter = Formatter:new {
|
||||
format_row = function(self, modname, instrument_name, statistics)
|
||||
self:print(
|
||||
"%q,%q,%d,%d,%d,%d,%d,%f,%f,%f",
|
||||
modname, instrument_name,
|
||||
statistics.samples,
|
||||
statistics.time_min,
|
||||
statistics.time_max,
|
||||
statistics:get_time_avg(),
|
||||
statistics.time_all,
|
||||
statistics.part_min,
|
||||
statistics.part_max,
|
||||
statistics:get_part_avg()
|
||||
)
|
||||
end,
|
||||
format = function(self, filter)
|
||||
self:print(
|
||||
"%q,%q,%q,%q,%q,%q,%q,%q,%q,%q",
|
||||
"modname", "instrumentation",
|
||||
"samples",
|
||||
"time min µs",
|
||||
"time max µs",
|
||||
"time avg µs",
|
||||
"time all µs",
|
||||
"part min %",
|
||||
"part max %",
|
||||
"part avg %"
|
||||
)
|
||||
for modname, mod_stats in pairs(self.profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
self:format_row(modname, "*", mod_stats)
|
||||
|
||||
if mod_stats.instruments ~= nil then
|
||||
for instrument_name, instrument_stats in pairs(mod_stats.instruments) do
|
||||
self:format_row(modname, instrument_name, instrument_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function format_statistics(profile, format, filter)
|
||||
local formatter
|
||||
if format == "csv" then
|
||||
formatter = CsvFormatter:new {
|
||||
profile = profile
|
||||
}
|
||||
else
|
||||
formatter = TxtFormatter:new {
|
||||
profile = profile
|
||||
}
|
||||
end
|
||||
formatter:format(filter)
|
||||
return formatter:flush()
|
||||
end
|
||||
|
||||
---
|
||||
-- Format the profile ready for display and
|
||||
-- @return string to be printed to the console
|
||||
--
|
||||
function reporter.print(profile, filter)
|
||||
if filter == "" then filter = nil end
|
||||
return format_statistics(profile, "txt", filter)
|
||||
end
|
||||
|
||||
---
|
||||
-- Serialize the profile data and
|
||||
-- @return serialized data to be saved to a file
|
||||
--
|
||||
local function serialize_profile(profile, format, filter)
|
||||
if format == "lua" or format == "json" or format == "json_pretty" then
|
||||
local stats = filter and {} or profile.stats
|
||||
if filter then
|
||||
for modname, mod_stats in pairs(profile.stats) do
|
||||
if filter_matches(filter, modname) then
|
||||
stats[modname] = mod_stats
|
||||
end
|
||||
end
|
||||
end
|
||||
if format == "lua" then
|
||||
return core.serialize(stats)
|
||||
elseif format == "json" then
|
||||
return core.write_json(stats)
|
||||
elseif format == "json_pretty" then
|
||||
return core.write_json(stats, true)
|
||||
end
|
||||
end
|
||||
-- Fall back to textual formats.
|
||||
return format_statistics(profile, format, filter)
|
||||
end
|
||||
|
||||
local worldpath = core.get_worldpath()
|
||||
local function get_save_path(format, filter)
|
||||
local report_path = settings:get("profiler.report_path") or ""
|
||||
if report_path ~= "" then
|
||||
core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path))
|
||||
end
|
||||
return (sprintf(
|
||||
"%s/%s/profile-%s%s.%s",
|
||||
worldpath,
|
||||
report_path,
|
||||
os.date("%Y%m%dT%H%M%S"),
|
||||
filter and ("-" .. filter) or "",
|
||||
format
|
||||
):gsub("[/\\]+", DIR_DELIM))-- Clean up delims
|
||||
end
|
||||
|
||||
---
|
||||
-- Save the profile to the world path.
|
||||
-- @return success, log message
|
||||
--
|
||||
function reporter.save(profile, format, filter)
|
||||
if not format or format == "" then
|
||||
format = settings:get("profiler.default_report_format") or "txt"
|
||||
end
|
||||
if filter == "" then
|
||||
filter = nil
|
||||
end
|
||||
|
||||
local path = get_save_path(format, filter)
|
||||
|
||||
local output, io_err = io.open(path, "w")
|
||||
if not output then
|
||||
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, S("Saving of profile failed: @1", err)
|
||||
end
|
||||
output:write(content)
|
||||
output:close()
|
||||
|
||||
core.log("action", "Profile saved to " .. path)
|
||||
return true, S("Profile saved to @1", path)
|
||||
end
|
||||
|
||||
return reporter
|
||||
206
builtin/profiler/sampling.lua
Normal file
206
builtin/profiler/sampling.lua
Normal file
@@ -0,0 +1,206 @@
|
||||
--Minetest
|
||||
--Copyright (C) 2016 T4im
|
||||
--
|
||||
--This program is free software; you can redistribute it and/or modify
|
||||
--it under the terms of the GNU Lesser General Public License as published by
|
||||
--the Free Software Foundation; either version 2.1 of the License, or
|
||||
--(at your option) any later version.
|
||||
--
|
||||
--This program is distributed in the hope that it will be useful,
|
||||
--but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
--GNU Lesser General Public License for more details.
|
||||
--
|
||||
--You should have received a copy of the GNU Lesser General Public License along
|
||||
--with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
local setmetatable = setmetatable
|
||||
local pairs, format = pairs, string.format
|
||||
local min, max, huge = math.min, math.max, math.huge
|
||||
local core = core
|
||||
|
||||
local profiler = ...
|
||||
-- Split sampler and profile up, to possibly allow for rotation later.
|
||||
local sampler = {}
|
||||
local profile
|
||||
local stats_total
|
||||
local logged_time, logged_data
|
||||
|
||||
local _stat_mt = {
|
||||
get_time_avg = function(self)
|
||||
return self.time_all/self.samples
|
||||
end,
|
||||
get_part_avg = function(self)
|
||||
if not self.part_all then
|
||||
return 100 -- Extra handling for "total"
|
||||
end
|
||||
return self.part_all/self.samples
|
||||
end,
|
||||
}
|
||||
_stat_mt.__index = _stat_mt
|
||||
|
||||
function sampler.reset()
|
||||
-- Accumulated logged time since last sample.
|
||||
-- This helps determining, the relative time a mod used up.
|
||||
logged_time = 0
|
||||
-- The measurements taken through instrumentation since last sample.
|
||||
logged_data = {}
|
||||
|
||||
profile = {
|
||||
-- Current mod statistics (max/min over the entire mod lifespan)
|
||||
-- Mod specific instrumentation statistics are nested within.
|
||||
stats = {},
|
||||
-- Current stats over all mods.
|
||||
stats_total = setmetatable({
|
||||
samples = 0,
|
||||
time_min = huge,
|
||||
time_max = 0,
|
||||
time_all = 0,
|
||||
part_min = 100,
|
||||
part_max = 100
|
||||
}, _stat_mt)
|
||||
}
|
||||
stats_total = profile.stats_total
|
||||
|
||||
-- Provide access to the most recent profile.
|
||||
sampler.profile = profile
|
||||
end
|
||||
|
||||
---
|
||||
-- Log a measurement for the sampler to pick up later.
|
||||
-- Keep `log` and its often called functions lean.
|
||||
-- It will directly add to the instrumentation overhead.
|
||||
--
|
||||
function sampler.log(modname, instrument_name, time_diff)
|
||||
if time_diff <= 0 then
|
||||
if time_diff < 0 then
|
||||
-- This **might** have happened on a semi-regular basis with huge mods,
|
||||
-- resulting in negative statistics (perhaps midnight time jumps or ntp corrections?).
|
||||
core.log("warning", format(
|
||||
"Time travel of %s::%s by %dµs.",
|
||||
modname, instrument_name, time_diff
|
||||
))
|
||||
end
|
||||
-- Throwing these away is better, than having them mess with the overall result.
|
||||
return
|
||||
end
|
||||
|
||||
local mod_data = logged_data[modname]
|
||||
if mod_data == nil then
|
||||
mod_data = {}
|
||||
logged_data[modname] = mod_data
|
||||
end
|
||||
|
||||
mod_data[instrument_name] = (mod_data[instrument_name] or 0) + time_diff
|
||||
-- Update logged time since last sample.
|
||||
logged_time = logged_time + time_diff
|
||||
end
|
||||
|
||||
---
|
||||
-- Return a requested statistic.
|
||||
-- Initialize if necessary.
|
||||
--
|
||||
local function get_statistic(stats_table, name)
|
||||
local statistic = stats_table[name]
|
||||
if statistic == nil then
|
||||
statistic = setmetatable({
|
||||
samples = 0,
|
||||
time_min = huge,
|
||||
time_max = 0,
|
||||
time_all = 0,
|
||||
part_min = 100,
|
||||
part_max = 0,
|
||||
part_all = 0,
|
||||
}, _stat_mt)
|
||||
stats_table[name] = statistic
|
||||
end
|
||||
return statistic
|
||||
end
|
||||
|
||||
---
|
||||
-- Update a statistic table
|
||||
--
|
||||
local function update_statistic(stats_table, time)
|
||||
stats_table.samples = stats_table.samples + 1
|
||||
|
||||
-- Update absolute time (µs) spend by the subject
|
||||
stats_table.time_min = min(stats_table.time_min, time)
|
||||
stats_table.time_max = max(stats_table.time_max, time)
|
||||
stats_table.time_all = stats_table.time_all + time
|
||||
|
||||
-- Update relative time (%) of this sample spend by the subject
|
||||
local current_part = (time/logged_time) * 100
|
||||
stats_table.part_min = min(stats_table.part_min, current_part)
|
||||
stats_table.part_max = max(stats_table.part_max, current_part)
|
||||
stats_table.part_all = stats_table.part_all + current_part
|
||||
end
|
||||
|
||||
---
|
||||
-- Sample all logged measurements each server step.
|
||||
-- Like any globalstep function, this should not be too heavy,
|
||||
-- but does not add to the instrumentation overhead.
|
||||
--
|
||||
local function sample(dtime)
|
||||
-- Rare, but happens and is currently of no informational value.
|
||||
if logged_time == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
for modname, instruments in pairs(logged_data) do
|
||||
local mod_stats = get_statistic(profile.stats, modname)
|
||||
if mod_stats.instruments == nil then
|
||||
-- Current statistics for each instrumentation component
|
||||
mod_stats.instruments = {}
|
||||
end
|
||||
|
||||
local mod_time = 0
|
||||
for instrument_name, time in pairs(instruments) do
|
||||
if time > 0 then
|
||||
mod_time = mod_time + time
|
||||
local instrument_stats = get_statistic(mod_stats.instruments, instrument_name)
|
||||
|
||||
-- Update time of this sample spend by the instrumented function.
|
||||
update_statistic(instrument_stats, time)
|
||||
-- Reset logged data for the next sample.
|
||||
instruments[instrument_name] = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Update time of this sample spend by this mod.
|
||||
update_statistic(mod_stats, mod_time)
|
||||
end
|
||||
|
||||
-- Update the total time spend over all mods.
|
||||
stats_total.time_min = min(stats_total.time_min, logged_time)
|
||||
stats_total.time_max = max(stats_total.time_max, logged_time)
|
||||
stats_total.time_all = stats_total.time_all + logged_time
|
||||
|
||||
stats_total.samples = stats_total.samples + 1
|
||||
logged_time = 0
|
||||
end
|
||||
|
||||
---
|
||||
-- Setup empty profile and register the sampling function
|
||||
--
|
||||
function sampler.init()
|
||||
sampler.reset()
|
||||
|
||||
if core.settings:get_bool("instrument.profiler") then
|
||||
core.register_globalstep(function()
|
||||
if logged_time == 0 then
|
||||
return
|
||||
end
|
||||
return profiler.empty_instrument()
|
||||
end)
|
||||
core.register_globalstep(profiler.instrument {
|
||||
func = sample,
|
||||
mod = "*profiler*",
|
||||
class = "Sampler (update stats)",
|
||||
label = false,
|
||||
})
|
||||
else
|
||||
core.register_globalstep(sample)
|
||||
end
|
||||
end
|
||||
|
||||
return sampler
|
||||
2353
builtin/settingtypes.txt
Normal file
2353
builtin/settingtypes.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user