From 8f085e02a107dd8092393935bfa1bba71d2546d2 Mon Sep 17 00:00:00 2001 From: DS Date: Fri, 4 Jun 2021 21:22:33 +0200 Subject: [PATCH] Add metatables to lua vectors (#11039) Add backwards-compatible metatable functions for vectors. --- builtin/client/init.lua | 1 - builtin/common/misc_helpers.lua | 26 +- builtin/common/tests/misc_helpers_spec.lua | 5 +- builtin/common/tests/serialize_spec.lua | 13 + builtin/common/tests/vector_spec.lua | 248 +++++++++++++++++- builtin/common/vector.lua | 212 ++++++++++----- builtin/game/chat.lua | 8 +- builtin/game/falling.lua | 53 ++-- builtin/game/init.lua | 2 - builtin/game/item.lua | 70 +++-- builtin/game/misc.lua | 11 +- builtin/game/voxelarea.lua | 14 +- builtin/init.lua | 1 + builtin/mainmenu/tests/serverlistmgr_spec.lua | 1 + doc/lua_api.txt | 59 ++++- src/script/common/c_converter.cpp | 25 ++ 16 files changed, 570 insertions(+), 179 deletions(-) diff --git a/builtin/client/init.lua b/builtin/client/init.lua index 9633a7c71..589fe8f24 100644 --- a/builtin/client/init.lua +++ b/builtin/client/init.lua @@ -7,5 +7,4 @@ dofile(clientpath .. "register.lua") dofile(commonpath .. "after.lua") dofile(commonpath .. "chatcommands.lua") dofile(clientpath .. "chatcommands.lua") -dofile(commonpath .. "vector.lua") dofile(clientpath .. "death_formspec.lua") diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua index d5f25f2fe..64c8c9a67 100644 --- a/builtin/common/misc_helpers.lua +++ b/builtin/common/misc_helpers.lua @@ -432,21 +432,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 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 104c656e9..9ebe69056 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,7 +31,89 @@ describe("vector", function() end) end) - it("equal()", function() + 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 @@ -35,22 +123,164 @@ describe("vector", function() 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({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("add()", function() - assert.same({ x = 2, y = 4, z = 6 }, vector.add(vector.new(1, 2, 3), { x = 1, y = 2, z = 3 })) + 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() diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua index 2ef8fc617..cbaa872dc 100644 --- a/builtin/common/vector.lua +++ b/builtin/common/vector.lua @@ -1,15 +1,43 @@ +--[[ +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 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} + return fast_new(a.x, a.y, a.z) elseif a then assert(b and c, "Invalid arguments for vector.new()") - return {x=a, y=b, z=c} + return fast_new(a, b, c) end - return {x=0, y=0, z=0} + return fast_new(0, 0, 0) end function vector.from_string(s, init) @@ -27,48 +55,49 @@ 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)) 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.round(v.x), - y = math.round(v.y), - z = math.round(v.z) - } + 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) @@ -79,11 +108,7 @@ function vector.distance(a, b) 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) @@ -98,70 +123,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 {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.__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 {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) @@ -229,7 +321,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 @@ -237,7 +329,7 @@ function vector.dir_to_rotation(forward, up) "Invalid vectors passed to vector.dir_to_rotation().") up = vector.normalize(up) -- Calculate vector pointing up with roll = 0, just based on forward vector. - local forwup = vector.rotate({x = 0, y = 1, z = 0}, rot) + local forwup = vector.rotate(vector.new(0, 1, 0), rot) -- 'forwup' and 'up' are now in a plane with 'forward' as normal. -- The angle between them is the absolute of the roll value we're looking for. rot.z = vector.angle(forwup, up) diff --git a/builtin/game/chat.lua b/builtin/game/chat.lua index 4dbcff1e2..e155fba70 100644 --- a/builtin/game/chat.lua +++ b/builtin/game/chat.lua @@ -499,10 +499,10 @@ core.register_chatcommand("remove_player", { -- pos may be a non-integer position local function find_free_position_near(pos) local tries = { - {x=1, y=0, z=0}, - {x=-1, y=0, z=0}, - {x=0, y=0, z=1}, - {x=0, y=0, z=-1}, + vector.new( 1, 0, 0), + vector.new(-1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), } for _, d in ipairs(tries) do local p = vector.add(pos, d) diff --git a/builtin/game/falling.lua b/builtin/game/falling.lua index 2cc0d8fac..a9dbc6ed5 100644 --- a/builtin/game/falling.lua +++ b/builtin/game/falling.lua @@ -39,7 +39,7 @@ local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81 core.register_entity(":__builtin:falling_node", { initial_properties = { visual = "item", - visual_size = {x = SCALE, y = SCALE, z = SCALE}, + visual_size = vector.new(SCALE, SCALE, SCALE), textures = {}, physical = true, is_visible = false, @@ -96,7 +96,7 @@ core.register_entity(":__builtin:falling_node", { local vsize if def.visual_scale then local s = def.visual_scale - vsize = {x = s, y = s, z = s} + vsize = vector.new(s, s, s) end self.object:set_properties({ is_visible = true, @@ -114,7 +114,7 @@ core.register_entity(":__builtin:falling_node", { local vsize if def.visual_scale then local s = def.visual_scale * SCALE - vsize = {x = s, y = s, z = s} + vsize = vector.new(s, s, s) end self.object:set_properties({ is_visible = true, @@ -227,7 +227,7 @@ core.register_entity(":__builtin:falling_node", { on_activate = function(self, staticdata) self.object:set_armor_groups({immortal = 1}) - self.object:set_acceleration({x = 0, y = -gravity, z = 0}) + self.object:set_acceleration(vector.new(0, -gravity, 0)) local ds = core.deserialize(staticdata) if ds and ds.node then @@ -303,7 +303,7 @@ core.register_entity(":__builtin:falling_node", { if self.floats then local pos = self.object:get_pos() - local bcp = vector.round({x = pos.x, y = pos.y - 0.7, z = pos.z}) + local bcp = pos:offset(0, -0.7, 0):round() local bcn = core.get_node(bcp) local bcd = core.registered_nodes[bcn.name] @@ -344,13 +344,12 @@ core.register_entity(":__builtin:falling_node", { -- TODO: this hack could be avoided in the future if objects -- could choose who to collide with local vel = self.object:get_velocity() - self.object:set_velocity({ - x = vel.x, - y = player_collision.old_velocity.y, - z = vel.z - }) - self.object:set_pos(vector.add(self.object:get_pos(), - {x = 0, y = -0.5, z = 0})) + self.object:set_velocity(vector.new( + vel.x, + player_collision.old_velocity.y, + vel.z + )) + self.object:set_pos(self.object:get_pos():offset(0, -0.5, 0)) end return elseif bcn.name == "ignore" then @@ -430,7 +429,7 @@ local function drop_attached_node(p) if def and def.preserve_metadata then local oldmeta = core.get_meta(p):to_table().fields -- Copy pos and node because the callback can modify them. - local pos_copy = {x=p.x, y=p.y, z=p.z} + local pos_copy = vector.new(p) local node_copy = {name=n.name, param1=n.param1, param2=n.param2} local drop_stacks = {} for k, v in pairs(drops) do @@ -455,14 +454,14 @@ end function builtin_shared.check_attached_node(p, n) local def = core.registered_nodes[n.name] - local d = {x = 0, y = 0, z = 0} + local d = vector.new() if def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted" then -- The fallback vector here is in case 'wallmounted to dir' is nil due -- to voxelmanip placing a wallmounted node without resetting a -- pre-existing param2 value that is out-of-range for wallmounted. -- The fallback vector corresponds to param2 = 0. - d = core.wallmounted_to_dir(n.param2) or {x = 0, y = 1, z = 0} + d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0) else d.y = -1 end @@ -482,7 +481,7 @@ end function core.check_single_for_falling(p) local n = core.get_node(p) if core.get_item_group(n.name, "falling_node") ~= 0 then - local p_bottom = {x = p.x, y = p.y - 1, z = p.z} + local p_bottom = vector.offset(p, 0, -1, 0) -- Only spawn falling node if node below is loaded local n_bottom = core.get_node_or_nil(p_bottom) local d_bottom = n_bottom and core.registered_nodes[n_bottom.name] @@ -521,17 +520,17 @@ end -- Down first as likely case, but always before self. The same with sides. -- Up must come last, so that things above self will also fall all at once. local check_for_falling_neighbors = { - {x = -1, y = -1, z = 0}, - {x = 1, y = -1, z = 0}, - {x = 0, y = -1, z = -1}, - {x = 0, y = -1, z = 1}, - {x = 0, y = -1, z = 0}, - {x = -1, y = 0, z = 0}, - {x = 1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = 0, y = 0, z = -1}, - {x = 0, y = 0, z = 0}, - {x = 0, y = 1, z = 0}, + vector.new(-1, -1, 0), + vector.new( 1, -1, 0), + vector.new( 0, -1, -1), + vector.new( 0, -1, 1), + vector.new( 0, -1, 0), + vector.new(-1, 0, 0), + vector.new( 1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), + vector.new( 0, 0, 0), + vector.new( 0, 1, 0), } function core.check_for_falling(p) diff --git a/builtin/game/init.lua b/builtin/game/init.lua index 1d62be019..bb007fabd 100644 --- a/builtin/game/init.lua +++ b/builtin/game/init.lua @@ -7,8 +7,6 @@ local gamepath = scriptpath .. "game".. DIR_DELIM -- not exposed to outer context local builtin_shared = {} -dofile(commonpath .. "vector.lua") - dofile(gamepath .. "constants.lua") assert(loadfile(gamepath .. "item.lua"))(builtin_shared) dofile(gamepath .. "register.lua") diff --git a/builtin/game/item.lua b/builtin/game/item.lua index 17746e9a8..99465e099 100644 --- a/builtin/game/item.lua +++ b/builtin/game/item.lua @@ -92,12 +92,12 @@ end -- Table of possible dirs local facedir_to_dir = { - {x= 0, y=0, z= 1}, - {x= 1, y=0, z= 0}, - {x= 0, y=0, z=-1}, - {x=-1, y=0, z= 0}, - {x= 0, y=-1, z= 0}, - {x= 0, y=1, z= 0}, + vector.new( 0, 0, 1), + vector.new( 1, 0, 0), + vector.new( 0, 0, -1), + vector.new(-1, 0, 0), + vector.new( 0, -1, 0), + vector.new( 0, 1, 0), } -- Mapping from facedir value to index in facedir_to_dir. local facedir_to_dir_map = { @@ -136,12 +136,12 @@ end -- table of dirs in wallmounted order local wallmounted_to_dir = { - [0] = {x = 0, y = 1, z = 0}, - {x = 0, y = -1, z = 0}, - {x = 1, y = 0, z = 0}, - {x = -1, y = 0, z = 0}, - {x = 0, y = 0, z = 1}, - {x = 0, y = 0, z = -1}, + [0] = vector.new( 0, 1, 0), + vector.new( 0, -1, 0), + vector.new( 1, 0, 0), + vector.new(-1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), } function core.wallmounted_to_dir(wallmounted) return wallmounted_to_dir[wallmounted % 8] @@ -152,7 +152,7 @@ function core.dir_to_yaw(dir) end function core.yaw_to_dir(yaw) - return {x = -math.sin(yaw), y = 0, z = math.cos(yaw)} + return vector.new(-math.sin(yaw), 0, math.cos(yaw)) end function core.is_colored_paramtype(ptype) @@ -290,12 +290,12 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2, end -- Place above pointed node - local place_to = {x = above.x, y = above.y, z = above.z} + local place_to = vector.new(above) -- If node under is buildable_to, place into it instead (eg. snow) if olddef_under.buildable_to then log("info", "node under is buildable to") - place_to = {x = under.x, y = under.y, z = under.z} + place_to = vector.new(under) end if core.is_protected(place_to, playername) then @@ -315,22 +315,14 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2, newnode.param2 = def.place_param2 elseif (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted") and not param2 then - local dir = { - x = under.x - above.x, - y = under.y - above.y, - z = under.z - above.z - } + local dir = vector.subtract(under, above) newnode.param2 = core.dir_to_wallmounted(dir) -- Calculate the direction for furnaces and chests and stuff elseif (def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir") and not param2 then local placer_pos = placer and placer:get_pos() if placer_pos then - local dir = { - x = above.x - placer_pos.x, - y = above.y - placer_pos.y, - z = above.z - placer_pos.z - } + local dir = vector.subtract(above, placer_pos) newnode.param2 = core.dir_to_facedir(dir) log("info", "facedir: " .. newnode.param2) end @@ -384,7 +376,7 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2, -- Run callback if def.after_place_node and not prevent_after_place then -- Deepcopy place_to and pointed_thing because callback can modify it - local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z} + local place_to_copy = vector.new(place_to) local pointed_thing_copy = copy_pointed_thing(pointed_thing) if def.after_place_node(place_to_copy, placer, itemstack, pointed_thing_copy) then @@ -395,7 +387,7 @@ function core.item_place_node(itemstack, placer, pointed_thing, param2, -- Run script hook for _, callback in ipairs(core.registered_on_placenodes) do -- Deepcopy pos, node and pointed_thing because callback can modify them - local place_to_copy = {x=place_to.x, y=place_to.y, z=place_to.z} + local place_to_copy = vector.new(place_to) local newnode_copy = {name=newnode.name, param1=newnode.param1, param2=newnode.param2} local oldnode_copy = {name=oldnode.name, param1=oldnode.param1, param2=oldnode.param2} local pointed_thing_copy = copy_pointed_thing(pointed_thing) @@ -541,11 +533,11 @@ function core.handle_node_drops(pos, drops, digger) for _, dropped_item in pairs(drops) do local left = give_item(dropped_item) if not left:is_empty() then - local p = { - x = pos.x + math.random()/2-0.25, - y = pos.y + math.random()/2-0.25, - z = pos.z + math.random()/2-0.25, - } + local p = vector.offset(pos, + math.random()/2-0.25, + math.random()/2-0.25, + math.random()/2-0.25 + ) core.add_item(p, left) end end @@ -604,7 +596,7 @@ function core.node_dig(pos, node, digger) if def and def.preserve_metadata then local oldmeta = core.get_meta(pos):to_table().fields -- Copy pos and node because the callback can modify them. - local pos_copy = {x=pos.x, y=pos.y, z=pos.z} + local pos_copy = vector.new(pos) local node_copy = {name=node.name, param1=node.param1, param2=node.param2} local drop_stacks = {} for k, v in pairs(drops) do @@ -636,7 +628,7 @@ function core.node_dig(pos, node, digger) -- Run callback if def and def.after_dig_node then -- Copy pos and node because callback can modify them - local pos_copy = {x=pos.x, y=pos.y, z=pos.z} + local pos_copy = vector.new(pos) local node_copy = {name=node.name, param1=node.param1, param2=node.param2} def.after_dig_node(pos_copy, node_copy, oldmetadata, digger) end @@ -649,7 +641,7 @@ function core.node_dig(pos, node, digger) end -- Copy pos and node because callback can modify them - local pos_copy = {x=pos.x, y=pos.y, z=pos.z} + local pos_copy = vector.new(pos) local node_copy = {name=node.name, param1=node.param1, param2=node.param2} callback(pos_copy, node_copy, digger) end @@ -692,7 +684,7 @@ core.nodedef_default = { groups = {}, inventory_image = "", wield_image = "", - wield_scale = {x=1,y=1,z=1}, + wield_scale = vector.new(1, 1, 1), stack_max = default_stack_max, usable = false, liquids_pointable = false, @@ -751,7 +743,7 @@ core.craftitemdef_default = { groups = {}, inventory_image = "", wield_image = "", - wield_scale = {x=1,y=1,z=1}, + wield_scale = vector.new(1, 1, 1), stack_max = default_stack_max, liquids_pointable = false, tool_capabilities = nil, @@ -770,7 +762,7 @@ core.tooldef_default = { groups = {}, inventory_image = "", wield_image = "", - wield_scale = {x=1,y=1,z=1}, + wield_scale = vector.new(1, 1, 1), stack_max = 1, liquids_pointable = false, tool_capabilities = nil, @@ -789,7 +781,7 @@ core.noneitemdef_default = { -- This is used for the hand and unknown items groups = {}, inventory_image = "", wield_image = "", - wield_scale = {x=1,y=1,z=1}, + wield_scale = vector.new(1, 1, 1), stack_max = default_stack_max, liquids_pointable = false, tool_capabilities = nil, diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index fcb86146d..c13a583f0 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -119,13 +119,12 @@ end function core.get_position_from_hash(hash) - local pos = {} - pos.x = (hash % 65536) - 32768 + local x = (hash % 65536) - 32768 hash = math.floor(hash / 65536) - pos.y = (hash % 65536) - 32768 + local y = (hash % 65536) - 32768 hash = math.floor(hash / 65536) - pos.z = (hash % 65536) - 32768 - return pos + local z = (hash % 65536) - 32768 + return vector.new(x, y, z) end @@ -215,7 +214,7 @@ function core.is_area_protected(minp, maxp, player_name, interval) local y = math.floor(yf + 0.5) for xf = minp.x, maxp.x, d.x do local x = math.floor(xf + 0.5) - local pos = {x = x, y = y, z = z} + local pos = vector.new(x, y, z) if core.is_protected(pos, player_name) then return pos end diff --git a/builtin/game/voxelarea.lua b/builtin/game/voxelarea.lua index 724761414..64436bf1a 100644 --- a/builtin/game/voxelarea.lua +++ b/builtin/game/voxelarea.lua @@ -1,6 +1,6 @@ VoxelArea = { - MinEdge = {x=1, y=1, z=1}, - MaxEdge = {x=0, y=0, z=0}, + MinEdge = vector.new(1, 1, 1), + MaxEdge = vector.new(0, 0, 0), ystride = 0, zstride = 0, } @@ -19,11 +19,11 @@ end function VoxelArea:getExtent() local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge - return { - x = MaxEdge.x - MinEdge.x + 1, - y = MaxEdge.y - MinEdge.y + 1, - z = MaxEdge.z - MinEdge.z + 1, - } + return vector.new( + MaxEdge.x - MinEdge.x + 1, + MaxEdge.y - MinEdge.y + 1, + MaxEdge.z - MinEdge.z + 1 + ) end function VoxelArea:getVolume() diff --git a/builtin/init.lua b/builtin/init.lua index 89b1fdc64..7a9b5c427 100644 --- a/builtin/init.lua +++ b/builtin/init.lua @@ -30,6 +30,7 @@ local clientpath = scriptdir .. "client" .. DIR_DELIM local commonpath = scriptdir .. "common" .. DIR_DELIM local asyncpath = scriptdir .. "async" .. DIR_DELIM +dofile(commonpath .. "vector.lua") dofile(commonpath .. "strict.lua") dofile(commonpath .. "serialize.lua") dofile(commonpath .. "misc_helpers.lua") diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua index 148e9b794..a091959fb 100644 --- a/builtin/mainmenu/tests/serverlistmgr_spec.lua +++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua @@ -2,6 +2,7 @@ _G.core = {} _G.unpack = table.unpack _G.serverlistmgr = {} +dofile("builtin/common/vector.lua") dofile("builtin/common/misc_helpers.lua") dofile("builtin/mainmenu/serverlistmgr.lua") diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 0f57f1f28..0c81ca911 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -1505,6 +1505,9 @@ Position/vector {x=num, y=num, z=num} + Note: it is highly recommended to construct a vector using the helper function: + vector.new(num, num, num) + For helper functions see [Spatial Vectors]. `pointed_thing` @@ -3168,15 +3171,35 @@ no particular point. Internally, it is implemented as a table with the 3 fields `x`, `y` and `z`. Example: `{x = 0, y = 1, z = 0}`. +However, one should *never* create a vector manually as above, such misbehavior +is deprecated. The vector helpers set a metatable for the created vectors which +allows indexing with numbers, calling functions directly on vectors and using +operators (like `+`). Furthermore, the internal implementation might change in +the future. +Old code might still use vectors without metatables, be aware of this! + +All these forms of addressing a vector `v` are valid: +`v[1]`, `v[3]`, `v.x`, `v[1] = 42`, `v.y = 13` + +Where `v` is a vector and `foo` stands for any function name, `v:foo(...)` does +the same as `vector.foo(v, ...)`, apart from deprecated functionality. + +The metatable that is used for vectors can be accessed via `vector.metatable`. +Do not modify it! + +All `vector.*` functions allow vectors `{x = X, y = Y, z = Z}` without metatables. +Returned vectors always have a metatable set. For the following functions, `v`, `v1`, `v2` are vectors, `p1`, `p2` are positions, -`s` is a scalar (a number): +`s` is a scalar (a number), +vectors are written like this: `(x, y, z)`: -* `vector.new(a[, b, c])`: +* `vector.new([a[, b, c]])`: * Returns a vector. * A copy of `a` if `a` is a vector. - * `{x = a, y = b, z = c}`, if all of `a`, `b`, `c` are defined numbers. + * `(a, b, c)`, if all of `a`, `b`, `c` are defined numbers. + * `(0, 0, 0)`, if no arguments are given. * `vector.from_string(s[, init])`: * Returns `v, np`, where `v` is a vector read from the given string `s` and `np` is the next position in the string after the vector. @@ -3189,14 +3212,14 @@ For the following functions, `v`, `v1`, `v2` are vectors, * Returns a string of the form `"(x, y, z)"`. * `vector.direction(p1, p2)`: * Returns a vector of length 1 with direction `p1` to `p2`. - * If `p1` and `p2` are identical, returns `{x = 0, y = 0, z = 0}`. + * If `p1` and `p2` are identical, returns `(0, 0, 0)`. * `vector.distance(p1, p2)`: * Returns zero or a positive number, the distance between `p1` and `p2`. * `vector.length(v)`: * Returns zero or a positive number, the length of vector `v`. * `vector.normalize(v)`: * Returns a vector of length 1 with direction of vector `v`. - * If `v` has zero length, returns `{x = 0, y = 0, z = 0}`. + * If `v` has zero length, returns `(0, 0, 0)`. * `vector.floor(v)`: * Returns a vector, each dimension rounded down. * `vector.round(v)`: @@ -3216,7 +3239,11 @@ For the following functions, `v`, `v1`, `v2` are vectors, * `vector.cross(v1, v2)`: * Returns the cross product of `v1` and `v2`. * `vector.offset(v, x, y, z)`: - * Returns the sum of the vectors `v` and `{x = x, y = y, z = z}`. + * Returns the sum of the vectors `v` and `(x, y, z)`. +* `vector.check()`: + * Returns a boolean value indicating whether `v` is a real vector, eg. created + by a `vector.*` function. + * Returns `false` for anything else, including tables like `{x=3,y=1,z=4}`. For the following functions `x` can be either a vector or a number: @@ -3235,14 +3262,30 @@ For the following functions `x` can be either a vector or a number: * Returns a scaled vector. * Deprecated: If `s` is a vector: Returns the Schur quotient. +Operators can be used if all of the involved vectors have metatables: +* `v1 == v2`: + * Returns whether `v1` and `v2` are identical. +* `-v`: + * Returns the additive inverse of v. +* `v1 + v2`: + * Returns the sum of both vectors. + * Note: `+` can not be used together with scalars. +* `v1 - v2`: + * Returns the difference of `v1` subtracted by `v2`. + * Note: `-` can not be used together with scalars. +* `v * s` or `s * v`: + * Returns `v` scaled by `s`. +* `v / s`: + * Returns `v` scaled by `1 / s`. + For the following functions `a` is an angle in radians and `r` is a rotation vector ({x = , y = , z = }) where pitch, yaw and roll are angles in radians. * `vector.rotate(v, r)`: * Applies the rotation `r` to `v` and returns the result. - * `vector.rotate({x = 0, y = 0, z = 1}, r)` and - `vector.rotate({x = 0, y = 1, z = 0}, r)` return vectors pointing + * `vector.rotate(vector.new(0, 0, 1), r)` and + `vector.rotate(vector.new(0, 1, 0), r)` return vectors pointing forward and up relative to an entity's rotation `r`. * `vector.rotate_around_axis(v1, v2, a)`: * Returns `v1` rotated around axis `v2` by `a` radians according to diff --git a/src/script/common/c_converter.cpp b/src/script/common/c_converter.cpp index c00401b58..d848b75b8 100644 --- a/src/script/common/c_converter.cpp +++ b/src/script/common/c_converter.cpp @@ -51,6 +51,29 @@ if (value < F1000_MIN || value > F1000_MAX) { \ #define CHECK_POS_TAB(index) CHECK_TYPE(index, "position", LUA_TTABLE) +/** + * A helper which sets (if available) the vector metatable from builtin as metatable + * for the table on top of the stack + */ +static void set_vector_metatable(lua_State *L) +{ + // get vector.metatable + lua_getglobal(L, "vector"); + if (!lua_istable(L, -1)) { + // there is no global vector table + lua_pop(L, 1); + errorstream << "set_vector_metatable in c_converter.cpp: " << + "missing global vector table" << std::endl; + return; + } + lua_getfield(L, -1, "metatable"); + // set the metatable + lua_setmetatable(L, -3); + // pop vector global + lua_pop(L, 1); +} + + void push_float_string(lua_State *L, float value) { std::stringstream ss; @@ -69,6 +92,7 @@ void push_v3f(lua_State *L, v3f p) lua_setfield(L, -2, "y"); lua_pushnumber(L, p.Z); lua_setfield(L, -2, "z"); + set_vector_metatable(L); } void push_v2f(lua_State *L, v2f p) @@ -281,6 +305,7 @@ void push_v3s16(lua_State *L, v3s16 p) lua_setfield(L, -2, "y"); lua_pushinteger(L, p.Z); lua_setfield(L, -2, "z"); + set_vector_metatable(L); } v3s16 read_v3s16(lua_State *L, int index)