Initial commit
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user