Преглед на файлове

Add metatables to lua vectors (#11039)

Add backwards-compatible metatable functions for vectors.
DS преди 3 години
родител
ревизия
8f085e02a1

+ 0 - 1
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")

+ 13 - 15
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
-	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
+	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
+	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

+ 3 - 2
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)
 

+ 13 - 0
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)

+ 239 - 9
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()

+ 152 - 60
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)

+ 4 - 4
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)

+ 26 - 27
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)

+ 0 - 2
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")

+ 31 - 39
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,

+ 5 - 6
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

+ 7 - 7
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()

+ 1 - 0
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")

+ 1 - 0
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")
 

+ 51 - 8
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 = <pitch>, y = <yaw>, z = <roll>}) 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

+ 25 - 0
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)