aboutsummaryrefslogtreecommitdiff
path: root/builtin/common
diff options
context:
space:
mode:
Diffstat (limited to 'builtin/common')
-rw-r--r--builtin/common/after.lua9
-rw-r--r--builtin/common/chatcommands.lua142
-rw-r--r--builtin/common/information_formspecs.lua60
-rw-r--r--builtin/common/misc_helpers.lua46
-rw-r--r--builtin/common/tests/misc_helpers_spec.lua5
-rw-r--r--builtin/common/tests/serialize_spec.lua13
-rw-r--r--builtin/common/tests/vector_spec.lua303
-rw-r--r--builtin/common/vector.lua250
8 files changed, 642 insertions, 186 deletions
diff --git a/builtin/common/after.lua b/builtin/common/after.lua
index e20f292f0..bce262537 100644
--- a/builtin/common/after.lua
+++ b/builtin/common/after.lua
@@ -37,7 +37,14 @@ function core.after(after, func, ...)
arg = {...},
mod_origin = core.get_last_run_mod(),
}
+
jobs[#jobs + 1] = new_job
time_next = math.min(time_next, expire)
- return { cancel = function() new_job.func = function() end end }
+
+ return {
+ cancel = function()
+ new_job.func = function() end
+ new_job.args = {}
+ end
+ }
end
diff --git a/builtin/common/chatcommands.lua b/builtin/common/chatcommands.lua
index 52edda659..7c3da0601 100644
--- a/builtin/common/chatcommands.lua
+++ b/builtin/common/chatcommands.lua
@@ -1,7 +1,47 @@
-- Minetest: builtin/common/chatcommands.lua
+-- For server-side translations (if INIT == "game")
+-- Otherwise, use core.gettext
+local S = core.get_translator("__builtin")
+
core.registered_chatcommands = {}
+-- Interpret the parameters of a command, separating options and arguments.
+-- Input: command, param
+-- command: name of command
+-- param: parameters of command
+-- Returns: opts, args
+-- opts is a string of option letters, or false on error
+-- args is an array with the non-option arguments in order, or an error message
+-- Example: for this command line:
+-- /command a b -cd e f -g
+-- the function would receive:
+-- a b -cd e f -g
+-- and it would return:
+-- "cdg", {"a", "b", "e", "f"}
+-- Negative numbers are taken as arguments. Long options (--option) are
+-- currently rejected as reserved.
+local function getopts(command, param)
+ local opts = ""
+ local args = {}
+ for match in param:gmatch("%S+") do
+ if match:byte(1) == 45 then -- 45 = '-'
+ local second = match:byte(2)
+ if second == 45 then
+ return false, S("Invalid parameters (see /help @1).", command)
+ elseif second and (second < 48 or second > 57) then -- 48 = '0', 57 = '9'
+ opts = opts .. match:sub(2)
+ else
+ -- numeric, add it to args
+ args[#args + 1] = match
+ end
+ else
+ args[#args + 1] = match
+ end
+ end
+ return opts, args
+end
+
function core.register_chatcommand(cmd, def)
def = def or {}
def.params = def.params or ""
@@ -29,35 +69,30 @@ function core.override_chatcommand(name, redefinition)
core.registered_chatcommands[name] = chatcommand
end
-local cmd_marker = "/"
-
-local function gettext(...)
- return ...
-end
-
-local function gettext_replace(text, replace)
- return text:gsub("$1", replace)
-end
-
-
-if INIT == "client" then
- cmd_marker = "."
- gettext = core.gettext
- gettext_replace = fgettext_ne
+local function format_help_line(cmd, def)
+ local cmd_marker = INIT == "client" and "." or "/"
+ local msg = core.colorize("#00ffff", cmd_marker .. cmd)
+ if def.params and def.params ~= "" then
+ msg = msg .. " " .. def.params
+ end
+ if def.description and def.description ~= "" then
+ msg = msg .. ": " .. def.description
+ end
+ return msg
end
local function do_help_cmd(name, param)
- local function format_help_line(cmd, def)
- local msg = core.colorize("#00ffff", cmd_marker .. cmd)
- if def.params and def.params ~= "" then
- msg = msg .. " " .. def.params
- end
- if def.description and def.description ~= "" then
- msg = msg .. ": " .. def.description
- end
- return msg
+ local opts, args = getopts("help", param)
+ if not opts then
+ return false, args
+ end
+ if #args > 1 then
+ return false, S("Too many arguments, try using just /help <command>")
end
- if param == "" then
+ local use_gui = INIT ~= "client" and core.get_player_by_name(name)
+ use_gui = use_gui and not opts:find("t")
+
+ if #args == 0 and not use_gui then
local cmds = {}
for cmd, def in pairs(core.registered_chatcommands) do
if INIT == "client" or core.check_player_privs(name, def.privs) then
@@ -65,10 +100,25 @@ local function do_help_cmd(name, param)
end
end
table.sort(cmds)
- return true, gettext("Available commands: ") .. table.concat(cmds, " ") .. "\n"
- .. gettext_replace("Use '$1help <cmd>' to get more information,"
- .. " or '$1help all' to list everything.", cmd_marker)
- elseif param == "all" then
+ local msg
+ if INIT == "game" then
+ msg = S("Available commands: @1",
+ table.concat(cmds, " ")) .. "\n"
+ .. S("Use '/help <cmd>' to get more "
+ .. "information, or '/help all' to list "
+ .. "everything.")
+ else
+ msg = core.gettext("Available commands: ")
+ .. table.concat(cmds, " ") .. "\n"
+ .. core.gettext("Use '.help <cmd>' to get more "
+ .. "information, or '.help all' to list "
+ .. "everything.")
+ end
+ return true, msg
+ elseif #args == 0 or (args[1] == "all" and use_gui) then
+ core.show_general_help_formspec(name)
+ return true
+ elseif args[1] == "all" then
local cmds = {}
for cmd, def in pairs(core.registered_chatcommands) do
if INIT == "client" or core.check_player_privs(name, def.privs) then
@@ -76,19 +126,35 @@ local function do_help_cmd(name, param)
end
end
table.sort(cmds)
- return true, gettext("Available commands:").."\n"..table.concat(cmds, "\n")
- elseif INIT == "game" and param == "privs" then
+ local msg
+ if INIT == "game" then
+ msg = S("Available commands:")
+ else
+ msg = core.gettext("Available commands:")
+ end
+ return true, msg.."\n"..table.concat(cmds, "\n")
+ elseif INIT == "game" and args[1] == "privs" then
+ if use_gui then
+ core.show_privs_help_formspec(name)
+ return true
+ end
local privs = {}
for priv, def in pairs(core.registered_privileges) do
privs[#privs + 1] = priv .. ": " .. def.description
end
table.sort(privs)
- return true, "Available privileges:\n"..table.concat(privs, "\n")
+ return true, S("Available privileges:").."\n"..table.concat(privs, "\n")
else
- local cmd = param
+ local cmd = args[1]
local def = core.registered_chatcommands[cmd]
if not def then
- return false, gettext("Command not available: ")..cmd
+ local msg
+ if INIT == "game" then
+ msg = S("Command not available: @1", cmd)
+ else
+ msg = core.gettext("Command not available: ") .. cmd
+ end
+ return false, msg
else
return true, format_help_line(cmd, def)
end
@@ -97,16 +163,16 @@ end
if INIT == "client" then
core.register_chatcommand("help", {
- params = gettext("[all | <cmd>]"),
- description = gettext("Get help for commands"),
+ params = core.gettext("[all | <cmd>]"),
+ description = core.gettext("Get help for commands"),
func = function(param)
return do_help_cmd(nil, param)
end,
})
else
core.register_chatcommand("help", {
- params = "[all | privs | <cmd>]",
- description = "Get help for commands or list privileges",
+ params = S("[all | privs | <cmd>] [-t]"),
+ description = S("Get help for commands or list privileges (-t: output in chat)"),
func = do_help_cmd,
})
end
diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua
index 3e2f1f079..3405263bf 100644
--- a/builtin/common/information_formspecs.lua
+++ b/builtin/common/information_formspecs.lua
@@ -20,7 +20,8 @@ local LIST_FORMSPEC_DESCRIPTION = [[
button_exit[5,7;3,1;quit;%s]
]]
-local formspec_escape = core.formspec_escape
+local F = core.formspec_escape
+local S = core.get_translator("__builtin")
local check_player_privs = core.check_player_privs
@@ -51,22 +52,23 @@ core.after(0, load_mod_command_tree)
local function build_chatcommands_formspec(name, sel, copy)
local rows = {}
- rows[1] = "#FFF,0,Command,Parameters"
+ rows[1] = "#FFF,0,"..F(S("Command"))..","..F(S("Parameters"))
- local description = "For more information, click on any entry in the list.\n" ..
- "Double-click to copy the entry to the chat history."
+ local description = S("For more information, click on "
+ .. "any entry in the list.").. "\n" ..
+ S("Double-click to copy the entry to the chat history.")
for i, data in ipairs(mod_cmds) do
- rows[#rows + 1] = COLOR_BLUE .. ",0," .. formspec_escape(data[1]) .. ","
+ rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. ","
for j, cmds in ipairs(data[2]) do
local has_priv = check_player_privs(name, cmds[2].privs)
rows[#rows + 1] = ("%s,1,%s,%s"):format(
has_priv and COLOR_GREEN or COLOR_GRAY,
- cmds[1], formspec_escape(cmds[2].params))
+ cmds[1], F(cmds[2].params))
if sel == #rows then
description = cmds[2].description
if copy then
- core.chat_send_player(name, ("Command: %s %s"):format(
+ core.chat_send_player(name, S("Command: @1 @2",
core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params))
end
end
@@ -74,9 +76,9 @@ local function build_chatcommands_formspec(name, sel, copy)
end
return LIST_FORMSPEC_DESCRIPTION:format(
- "Available commands: (see also: /help <cmd>)",
+ F(S("Available commands: (see also: /help <cmd>)")),
table.concat(rows, ","), sel or 0,
- description, "Close"
+ F(description), F(S("Close"))
)
end
@@ -91,19 +93,19 @@ local function build_privs_formspec(name)
table.sort(privs, function(a, b) return a[1] < b[1] end)
local rows = {}
- rows[1] = "#FFF,0,Privilege,Description"
+ rows[1] = "#FFF,0,"..F(S("Privilege"))..","..F(S("Description"))
local player_privs = core.get_player_privs(name)
for i, data in ipairs(privs) do
rows[#rows + 1] = ("%s,0,%s,%s"):format(
player_privs[data[1]] and COLOR_GREEN or COLOR_GRAY,
- data[1], formspec_escape(data[2].description))
+ data[1], F(data[2].description))
end
return LIST_FORMSPEC:format(
- "Available privileges:",
+ F(S("Available privileges:")),
table.concat(rows, ","),
- "Close"
+ F(S("Close"))
)
end
@@ -123,30 +125,12 @@ core.register_on_player_receive_fields(function(player, formname, fields)
end
end)
-
-local help_command = core.registered_chatcommands["help"]
-local old_help_func = help_command.func
-
-help_command.func = function(name, param)
- local admin = core.settings:get("name")
-
- -- If the admin ran help, put the output in the chat buffer as well to
- -- work with the server terminal
- if param == "privs" then
- core.show_formspec(name, "__builtin:help_privs",
- build_privs_formspec(name))
- if name ~= admin then
- return true
- end
- end
- if param == "" or param == "all" then
- core.show_formspec(name, "__builtin:help_cmds",
- build_chatcommands_formspec(name))
- if name ~= admin then
- return true
- end
- end
-
- return old_help_func(name, param)
+function core.show_general_help_formspec(name)
+ core.show_formspec(name, "__builtin:help_cmds",
+ build_chatcommands_formspec(name))
end
+function core.show_privs_help_formspec(name)
+ core.show_formspec(name, "__builtin:help_privs",
+ build_privs_formspec(name))
+end
diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua
index 0f3897f47..f5f89acd7 100644
--- a/builtin/common/misc_helpers.lua
+++ b/builtin/common/misc_helpers.lua
@@ -209,14 +209,7 @@ end
--------------------------------------------------------------------------------
function math.hypot(x, y)
- local t
- x = math.abs(x)
- y = math.abs(y)
- t = math.min(x, y)
- x = math.max(x, y)
- if x == 0 then return 0 end
- t = t / x
- return x * math.sqrt(1 + t * t)
+ return math.sqrt(x * x + y * y)
end
--------------------------------------------------------------------------------
@@ -244,6 +237,15 @@ function math.factorial(x)
return v
end
+
+function math.round(x)
+ if x >= 0 then
+ return math.floor(x + 0.5)
+ end
+ return math.ceil(x - 0.5)
+end
+
+
function core.formspec_escape(text)
if text ~= nil then
text = string.gsub(text,"\\","\\\\")
@@ -423,21 +425,19 @@ function core.string_to_pos(value)
return nil
end
- local p = {}
- p.x, p.y, p.z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
- if p.x and p.y and p.z then
- p.x = tonumber(p.x)
- p.y = tonumber(p.y)
- p.z = tonumber(p.z)
- return p
+ local x, y, z = string.match(value, "^([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+)$")
+ if x and y and z then
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ return vector.new(x, y, z)
end
- p = {}
- p.x, p.y, p.z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
- if p.x and p.y and p.z then
- p.x = tonumber(p.x)
- p.y = tonumber(p.y)
- p.z = tonumber(p.z)
- return p
+ x, y, z = string.match(value, "^%( *([%d.-]+)[, ] *([%d.-]+)[, ] *([%d.-]+) *%)$")
+ if x and y and z then
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ return vector.new(x, y, z)
end
return nil
end
@@ -532,7 +532,7 @@ if INIT == "mainmenu" then
end
end
-if INIT == "client" or INIT == "mainmenu" then
+if core.gettext then -- for client and mainmenu
function fgettext_ne(text, ...)
text = core.gettext(text)
local arg = {n=select('#', ...), ...}
diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua
index bb9d13e7f..b16987f0b 100644
--- a/builtin/common/tests/misc_helpers_spec.lua
+++ b/builtin/common/tests/misc_helpers_spec.lua
@@ -1,4 +1,5 @@
_G.core = {}
+dofile("builtin/common/vector.lua")
dofile("builtin/common/misc_helpers.lua")
describe("string", function()
@@ -55,8 +56,8 @@ end)
describe("pos", function()
it("from string", function()
- assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("10.0, 5.1, -2"))
- assert.same({ x = 10, y = 5.1, z = -2}, core.string_to_pos("( 10.0, 5.1, -2)"))
+ assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("10.0, 5.1, -2"))
+ assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("( 10.0, 5.1, -2)"))
assert.is_nil(core.string_to_pos("asd, 5, -2)"))
end)
diff --git a/builtin/common/tests/serialize_spec.lua b/builtin/common/tests/serialize_spec.lua
index 17c6a60f7..e46b7dcc5 100644
--- a/builtin/common/tests/serialize_spec.lua
+++ b/builtin/common/tests/serialize_spec.lua
@@ -3,6 +3,7 @@ _G.core = {}
_G.setfenv = require 'busted.compatibility'.setfenv
dofile("builtin/common/serialize.lua")
+dofile("builtin/common/vector.lua")
describe("serialize", function()
it("works", function()
@@ -53,4 +54,16 @@ describe("serialize", function()
assert.is_nil(test_out.func)
assert.equals(test_out.foo, "bar")
end)
+
+ it("vectors work", function()
+ local v = vector.new(1, 2, 3)
+ assert.same({{x = 1, y = 2, z = 3}}, core.deserialize(core.serialize({v})))
+ assert.same({x = 1, y = 2, z = 3}, core.deserialize(core.serialize(v)))
+
+ -- abuse
+ v = vector.new(1, 2, 3)
+ v.a = "bla"
+ assert.same({x = 1, y = 2, z = 3, a = "bla"},
+ core.deserialize(core.serialize(v)))
+ end)
end)
diff --git a/builtin/common/tests/vector_spec.lua b/builtin/common/tests/vector_spec.lua
index 0f287363a..2f72f3383 100644
--- a/builtin/common/tests/vector_spec.lua
+++ b/builtin/common/tests/vector_spec.lua
@@ -4,14 +4,20 @@ dofile("builtin/common/vector.lua")
describe("vector", function()
describe("new()", function()
it("constructs", function()
- assert.same({ x = 0, y = 0, z = 0 }, vector.new())
- assert.same({ x = 1, y = 2, z = 3 }, vector.new(1, 2, 3))
- assert.same({ x = 3, y = 2, z = 1 }, vector.new({ x = 3, y = 2, z = 1 }))
+ assert.same({x = 0, y = 0, z = 0}, vector.new())
+ assert.same({x = 1, y = 2, z = 3}, vector.new(1, 2, 3))
+ assert.same({x = 3, y = 2, z = 1}, vector.new({x = 3, y = 2, z = 1}))
+
+ assert.is_true(vector.check(vector.new()))
+ assert.is_true(vector.check(vector.new(1, 2, 3)))
+ assert.is_true(vector.check(vector.new({x = 3, y = 2, z = 1})))
local input = vector.new({ x = 3, y = 2, z = 1 })
local output = vector.new(input)
assert.same(input, output)
- assert.are_not.equal(input, output)
+ assert.equal(input, output)
+ assert.is_false(rawequal(input, output))
+ assert.equal(input, input:new())
end)
it("throws on invalid input", function()
@@ -25,27 +31,286 @@ describe("vector", function()
end)
end)
- it("equal()", function()
- local function assertE(a, b)
- assert.is_true(vector.equals(a, b))
- end
- local function assertNE(a, b)
- assert.is_false(vector.equals(a, b))
- end
+ it("zero()", function()
+ assert.same({x = 0, y = 0, z = 0}, vector.zero())
+ assert.same(vector.new(), vector.zero())
+ assert.equal(vector.new(), vector.zero())
+ assert.is_true(vector.check(vector.zero()))
+ end)
- assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
- assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1})
- local a = { x = 2, y = 4, z = -10 }
- assertE(a, a)
- assertNE({x = -1, y = 0, z = 1}, a)
+ it("copy()", function()
+ local v = vector.new(1, 2, 3)
+ assert.same(v, vector.copy(v))
+ assert.same(vector.new(v), vector.copy(v))
+ assert.equal(vector.new(v), vector.copy(v))
+ assert.is_true(vector.check(vector.copy(v)))
end)
- it("add()", function()
- assert.same({ x = 2, y = 4, z = 6 }, vector.add(vector.new(1, 2, 3), { x = 1, y = 2, z = 3 }))
+ it("indexes", function()
+ local some_vector = vector.new(24, 42, 13)
+ assert.equal(24, some_vector[1])
+ assert.equal(24, some_vector.x)
+ assert.equal(42, some_vector[2])
+ assert.equal(42, some_vector.y)
+ assert.equal(13, some_vector[3])
+ assert.equal(13, some_vector.z)
+
+ some_vector[1] = 100
+ assert.equal(100, some_vector.x)
+ some_vector.x = 101
+ assert.equal(101, some_vector[1])
+
+ some_vector[2] = 100
+ assert.equal(100, some_vector.y)
+ some_vector.y = 102
+ assert.equal(102, some_vector[2])
+
+ some_vector[3] = 100
+ assert.equal(100, some_vector.z)
+ some_vector.z = 103
+ assert.equal(103, some_vector[3])
+ end)
+
+ it("direction()", function()
+ local a = vector.new(1, 0, 0)
+ local b = vector.new(1, 42, 0)
+ assert.equal(vector.new(0, 1, 0), vector.direction(a, b))
+ assert.equal(vector.new(0, 1, 0), a:direction(b))
+ end)
+
+ it("distance()", function()
+ local a = vector.new(1, 0, 0)
+ local b = vector.new(3, 42, 9)
+ assert.is_true(math.abs(43 - vector.distance(a, b)) < 1.0e-12)
+ assert.is_true(math.abs(43 - a:distance(b)) < 1.0e-12)
+ assert.equal(0, vector.distance(a, a))
+ assert.equal(0, b:distance(b))
+ end)
+
+ it("length()", function()
+ local a = vector.new(0, 0, -23)
+ assert.equal(0, vector.length(vector.new()))
+ assert.equal(23, vector.length(a))
+ assert.equal(23, a:length())
+ end)
+
+ it("normalize()", function()
+ local a = vector.new(0, 0, -23)
+ assert.equal(vector.new(0, 0, -1), vector.normalize(a))
+ assert.equal(vector.new(0, 0, -1), a:normalize())
+ assert.equal(vector.new(), vector.normalize(vector.new()))
+ end)
+
+ it("floor()", function()
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(0, 0, -1), vector.floor(a))
+ assert.equal(vector.new(0, 0, -1), a:floor())
+ end)
+
+ it("round()", function()
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(0, 1, -1), vector.round(a))
+ assert.equal(vector.new(0, 1, -1), a:round())
+ end)
+
+ it("apply()", function()
+ local i = 0
+ local f = function(x)
+ i = i + 1
+ return x + i
+ end
+ local a = vector.new(0.1, 0.9, -0.5)
+ assert.equal(vector.new(1, 1, 0), vector.apply(a, math.ceil))
+ assert.equal(vector.new(1, 1, 0), a:apply(math.ceil))
+ assert.equal(vector.new(0.1, 0.9, 0.5), vector.apply(a, math.abs))
+ assert.equal(vector.new(0.1, 0.9, 0.5), a:apply(math.abs))
+ assert.equal(vector.new(1.1, 2.9, 2.5), vector.apply(a, f))
+ assert.equal(vector.new(4.1, 5.9, 5.5), a:apply(f))
+ end)
+
+ it("equals()", function()
+ local function assertE(a, b)
+ assert.is_true(vector.equals(a, b))
+ end
+ local function assertNE(a, b)
+ assert.is_false(vector.equals(a, b))
+ end
+
+ assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0})
+ assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1})
+ assertE({x = -1, y = 0, z = 1}, vector.new(-1, 0, 1))
+ local a = {x = 2, y = 4, z = -10}
+ assertE(a, a)
+ assertNE({x = -1, y = 0, z = 1}, a)
+
+ assert.equal(vector.new(1, 2, 3), vector.new(1, 2, 3))
+ assert.is_true(vector.new(1, 2, 3):equals(vector.new(1, 2, 3)))
+ assert.not_equal(vector.new(1, 2, 3), vector.new(1, 2, 4))
+ assert.is_true(vector.new(1, 2, 3) == vector.new(1, 2, 3))
+ assert.is_false(vector.new(1, 2, 3) == vector.new(1, 3, 3))
+ end)
+
+ it("metatable is same", function()
+ local a = vector.new()
+ local b = vector.new(1, 2, 3)
+
+ assert.equal(true, vector.check(a))
+ assert.equal(true, vector.check(b))
+
+ assert.equal(vector.metatable, getmetatable(a))
+ assert.equal(vector.metatable, getmetatable(b))
+ assert.equal(vector.metatable, a.metatable)
+ end)
+
+ it("sort()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(0.5, 232, -2)
+ local sorted = {vector.new(0.5, 2, -2), vector.new(1, 232, 3)}
+ assert.same(sorted, {vector.sort(a, b)})
+ assert.same(sorted, {a:sort(b)})
+ end)
+
+ it("angle()", function()
+ assert.equal(math.pi, vector.angle(vector.new(-1, -2, -3), vector.new(1, 2, 3)))
+ assert.equal(math.pi/2, vector.new(0, 1, 0):angle(vector.new(1, 0, 0)))
+ end)
+
+ it("dot()", function()
+ assert.equal(-14, vector.dot(vector.new(-1, -2, -3), vector.new(1, 2, 3)))
+ assert.equal(0, vector.new():dot(vector.new(1, 2, 3)))
+ end)
+
+ it("cross()", function()
+ local a = vector.new(-1, -2, 0)
+ local b = vector.new(1, 2, 3)
+ assert.equal(vector.new(-6, 3, 0), vector.cross(a, b))
+ assert.equal(vector.new(-6, 3, 0), a:cross(b))
end)
it("offset()", function()
- assert.same({ x = 41, y = 52, z = 63 }, vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.same({x = 41, y = 52, z = 63}, vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.equal(vector.new(41, 52, 63), vector.offset(vector.new(1, 2, 3), 40, 50, 60))
+ assert.equal(vector.new(41, 52, 63), vector.new(1, 2, 3):offset(40, 50, 60))
+ end)
+
+ it("is()", function()
+ local some_table1 = {foo = 13, [42] = 1, "bar", 2}
+ local some_table2 = {1, 2, 3}
+ local some_table3 = {x = 1, 2, 3}
+ local some_table4 = {1, 2, z = 3}
+ local old = {x = 1, y = 2, z = 3}
+ local real = vector.new(1, 2, 3)
+
+ assert.is_false(vector.check(nil))
+ assert.is_false(vector.check(1))
+ assert.is_false(vector.check(true))
+ assert.is_false(vector.check("foo"))
+ assert.is_false(vector.check(some_table1))
+ assert.is_false(vector.check(some_table2))
+ assert.is_false(vector.check(some_table3))
+ assert.is_false(vector.check(some_table4))
+ assert.is_false(vector.check(old))
+ assert.is_true(vector.check(real))
+ assert.is_true(real:check())
+ end)
+
+ it("global pairs", function()
+ local out = {}
+ local vec = vector.new(10, 20, 30)
+ for k, v in pairs(vec) do
+ out[k] = v
+ end
+ assert.same({x = 10, y = 20, z = 30}, out)
+ end)
+
+ it("abusing works", function()
+ local v = vector.new(1, 2, 3)
+ v.a = 1
+ assert.equal(1, v.a)
+
+ local a_is_there = false
+ for key, value in pairs(v) do
+ if key == "a" then
+ a_is_there = true
+ assert.equal(value, 1)
+ break
+ end
+ end
+ assert.is_true(a_is_there)
+ end)
+
+ it("add()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(1, 4, 3)
+ local c = vector.new(2, 6, 6)
+ assert.equal(c, vector.add(a, {x = 1, y = 4, z = 3}))
+ assert.equal(c, vector.add(a, b))
+ assert.equal(c, a:add(b))
+ assert.equal(c, a + b)
+ assert.equal(c, b + a)
+ end)
+
+ it("subtract()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(-1, -2, 0)
+ assert.equal(c, vector.subtract(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.subtract(a, b))
+ assert.equal(c, a:subtract(b))
+ assert.equal(c, a - b)
+ assert.equal(c, -b + a)
+ end)
+
+ it("multiply()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(2, 8, 9)
+ local s = 2
+ local d = vector.new(2, 4, 6)
+ assert.equal(c, vector.multiply(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.multiply(a, b))
+ assert.equal(d, vector.multiply(a, s))
+ assert.equal(d, a:multiply(s))
+ assert.equal(d, a * s)
+ assert.equal(d, s * a)
+ assert.equal(-a, -1 * a)
+ end)
+
+ it("divide()", function()
+ local a = vector.new(1, 2, 3)
+ local b = vector.new(2, 4, 3)
+ local c = vector.new(0.5, 0.5, 1)
+ local s = 2
+ local d = vector.new(0.5, 1, 1.5)
+ assert.equal(c, vector.divide(a, {x = 2, y = 4, z = 3}))
+ assert.equal(c, vector.divide(a, b))
+ assert.equal(d, vector.divide(a, s))
+ assert.equal(d, a:divide(s))
+ assert.equal(d, a / s)
+ assert.equal(d, 1/s * a)
+ assert.equal(-a, a / -1)
+ end)
+
+ it("to_string()", function()
+ local v = vector.new(1, 2, 3.14)
+ assert.same("(1, 2, 3.14)", vector.to_string(v))
+ assert.same("(1, 2, 3.14)", v:to_string())
+ assert.same("(1, 2, 3.14)", tostring(v))
+ end)
+
+ it("from_string()", function()
+ local v = vector.new(1, 2, 3.14)
+ assert.is_true(vector.check(vector.from_string("(1, 2, 3.14)")))
+ assert.same({v, 13}, {vector.from_string("(1, 2, 3.14)")})
+ assert.same({v, 12}, {vector.from_string("(1,2 ,3.14)")})
+ assert.same({v, 12}, {vector.from_string("(1,2,3.14,)")})
+ assert.same({v, 11}, {vector.from_string("(1 2 3.14)")})
+ assert.same({v, 15}, {vector.from_string("( 1, 2, 3.14 )")})
+ assert.same({v, 15}, {vector.from_string(" ( 1, 2, 3.14) ")})
+ assert.same({vector.new(), 8}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ")})
+ assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 8)})
+ assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 9)})
+ assert.same(nil, vector.from_string("nothing"))
end)
-- This function is needed because of floating point imprecision.
diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua
index d6437deda..581d014e0 100644
--- a/builtin/common/vector.lua
+++ b/builtin/common/vector.lua
@@ -1,73 +1,126 @@
+--[[
+Vector helpers
+Note: The vector.*-functions must be able to accept old vectors that had no metatables
+]]
+
+-- localize functions
+local setmetatable = setmetatable
vector = {}
+local metatable = {}
+vector.metatable = metatable
+
+local xyz = {"x", "y", "z"}
+
+-- only called when rawget(v, key) returns nil
+function metatable.__index(v, key)
+ return rawget(v, xyz[key]) or vector[key]
+end
+
+-- only called when rawget(v, key) returns nil
+function metatable.__newindex(v, key, value)
+ rawset(v, xyz[key] or key, value)
+end
+
+-- constructors
+
+local function fast_new(x, y, z)
+ return setmetatable({x = x, y = y, z = z}, metatable)
+end
+
function vector.new(a, b, c)
+ if a and b and c then
+ return fast_new(a, b, c)
+ end
+
+ -- deprecated, use vector.copy and vector.zero directly
if type(a) == "table" then
- assert(a.x and a.y and a.z, "Invalid vector passed to vector.new()")
- return {x=a.x, y=a.y, z=a.z}
- elseif a then
- assert(b and c, "Invalid arguments for vector.new()")
- return {x=a, y=b, z=c}
+ return vector.copy(a)
+ else
+ assert(not a, "Invalid arguments for vector.new()")
+ return vector.zero()
+ end
+end
+
+function vector.zero()
+ return fast_new(0, 0, 0)
+end
+
+function vector.copy(v)
+ assert(v.x and v.y and v.z, "Invalid vector passed to vector.copy()")
+ return fast_new(v.x, v.y, v.z)
+end
+
+function vector.from_string(s, init)
+ local x, y, z, np = string.match(s, "^%s*%(%s*([^%s,]+)%s*[,%s]%s*([^%s,]+)%s*[,%s]" ..
+ "%s*([^%s,]+)%s*[,%s]?%s*%)()", init)
+ x = tonumber(x)
+ y = tonumber(y)
+ z = tonumber(z)
+ if not (x and y and z) then
+ return nil
end
- return {x=0, y=0, z=0}
+ return fast_new(x, y, z), np
+end
+
+function vector.to_string(v)
+ return string.format("(%g, %g, %g)", v.x, v.y, v.z)
end
+metatable.__tostring = vector.to_string
function vector.equals(a, b)
return a.x == b.x and
a.y == b.y and
a.z == b.z
end
+metatable.__eq = vector.equals
+
+-- unary operations
function vector.length(v)
- return math.hypot(v.x, math.hypot(v.y, v.z))
+ return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
end
+-- Note: we can not use __len because it is already used for primitive table length
function vector.normalize(v)
local len = vector.length(v)
if len == 0 then
- return {x=0, y=0, z=0}
+ return fast_new(0, 0, 0)
else
return vector.divide(v, len)
end
end
function vector.floor(v)
- return {
- x = math.floor(v.x),
- y = math.floor(v.y),
- z = math.floor(v.z)
- }
+ return vector.apply(v, math.floor)
end
function vector.round(v)
- return {
- x = math.floor(v.x + 0.5),
- y = math.floor(v.y + 0.5),
- z = math.floor(v.z + 0.5)
- }
+ return fast_new(
+ math.round(v.x),
+ math.round(v.y),
+ math.round(v.z)
+ )
end
function vector.apply(v, func)
- return {
- x = func(v.x),
- y = func(v.y),
- z = func(v.z)
- }
+ return fast_new(
+ func(v.x),
+ func(v.y),
+ func(v.z)
+ )
end
function vector.distance(a, b)
local x = a.x - b.x
local y = a.y - b.y
local z = a.z - b.z
- return math.hypot(x, math.hypot(y, z))
+ return math.sqrt(x * x + y * y + z * z)
end
function vector.direction(pos1, pos2)
- return vector.normalize({
- x = pos2.x - pos1.x,
- y = pos2.y - pos1.y,
- z = pos2.z - pos1.z
- })
+ return vector.subtract(pos2, pos1):normalize()
end
function vector.angle(a, b)
@@ -82,70 +135,137 @@ function vector.dot(a, b)
end
function vector.cross(a, b)
- return {
- x = a.y * b.z - a.z * b.y,
- y = a.z * b.x - a.x * b.z,
- z = a.x * b.y - a.y * b.x
- }
+ return fast_new(
+ a.y * b.z - a.z * b.y,
+ a.z * b.x - a.x * b.z,
+ a.x * b.y - a.y * b.x
+ )
end
+function metatable.__unm(v)
+ return fast_new(-v.x, -v.y, -v.z)
+end
+
+-- add, sub, mul, div operations
+
function vector.add(a, b)
if type(b) == "table" then
- return {x = a.x + b.x,
- y = a.y + b.y,
- z = a.z + b.z}
+ return fast_new(
+ a.x + b.x,
+ a.y + b.y,
+ a.z + b.z
+ )
else
- return {x = a.x + b,
- y = a.y + b,
- z = a.z + b}
+ return fast_new(
+ a.x + b,
+ a.y + b,
+ a.z + b
+ )
end
end
+function metatable.__add(a, b)
+ return fast_new(
+ a.x + b.x,
+ a.y + b.y,
+ a.z + b.z
+ )
+end
function vector.subtract(a, b)
if type(b) == "table" then
- return {x = a.x - b.x,
- y = a.y - b.y,
- z = a.z - b.z}
+ return fast_new(
+ a.x - b.x,
+ a.y - b.y,
+ a.z - b.z
+ )
else
- return {x = a.x - b,
- y = a.y - b,
- z = a.z - b}
+ return fast_new(
+ a.x - b,
+ a.y - b,
+ a.z - b
+ )
end
end
+function metatable.__sub(a, b)
+ return fast_new(
+ a.x - b.x,
+ a.y - b.y,
+ a.z - b.z
+ )
+end
function vector.multiply(a, b)
if type(b) == "table" then
- return {x = a.x * b.x,
- y = a.y * b.y,
- z = a.z * b.z}
+ return fast_new(
+ a.x * b.x,
+ a.y * b.y,
+ a.z * b.z
+ )
+ else
+ return fast_new(
+ a.x * b,
+ a.y * b,
+ a.z * b
+ )
+ end
+end
+function metatable.__mul(a, b)
+ if type(a) == "table" then
+ return fast_new(
+ a.x * b,
+ a.y * b,
+ a.z * b
+ )
else
- return {x = a.x * b,
- y = a.y * b,
- z = a.z * b}
+ return fast_new(
+ a * b.x,
+ a * b.y,
+ a * b.z
+ )
end
end
function vector.divide(a, b)
if type(b) == "table" then
- return {x = a.x / b.x,
- y = a.y / b.y,
- z = a.z / b.z}
+ return fast_new(
+ a.x / b.x,
+ a.y / b.y,
+ a.z / b.z
+ )
else
- return {x = a.x / b,
- y = a.y / b,
- z = a.z / b}
+ return fast_new(
+ a.x / b,
+ a.y / b,
+ a.z / b
+ )
end
end
+function metatable.__div(a, b)
+ -- scalar/vector makes no sense
+ return fast_new(
+ a.x / b,
+ a.y / b,
+ a.z / b
+ )
+end
+
+-- misc stuff
function vector.offset(v, x, y, z)
- return {x = v.x + x,
- y = v.y + y,
- z = v.z + z}
+ return fast_new(
+ v.x + x,
+ v.y + y,
+ v.z + z
+ )
end
function vector.sort(a, b)
- return {x = math.min(a.x, b.x), y = math.min(a.y, b.y), z = math.min(a.z, b.z)},
- {x = math.max(a.x, b.x), y = math.max(a.y, b.y), z = math.max(a.z, b.z)}
+ return fast_new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)),
+ fast_new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z))
+end
+
+function vector.check(v)
+ return getmetatable(v) == metatable
end
local function sin(x)
@@ -213,7 +333,7 @@ end
function vector.dir_to_rotation(forward, up)
forward = vector.normalize(forward)
- local rot = {x = math.asin(forward.y), y = -math.atan2(forward.x, forward.z), z = 0}
+ local rot = vector.new(math.asin(forward.y), -math.atan2(forward.x, forward.z), 0)
if not up then
return rot
end
@@ -221,7 +341,7 @@ function vector.dir_to_rotation(forward, up)
"Invalid vectors passed to vector.dir_to_rotation().")
up = vector.normalize(up)
-- Calculate vector pointing up with roll = 0, just based on forward vector.
- local forwup = vector.rotate({x = 0, y = 1, z = 0}, rot)
+ local forwup = vector.rotate(vector.new(0, 1, 0), rot)
-- 'forwup' and 'up' are now in a plane with 'forward' as normal.
-- The angle between them is the absolute of the roll value we're looking for.
rot.z = vector.angle(forwup, up)