Indentation: Use only tabs

This commit is contained in:
Lars Mueller 2021-03-27 20:10:49 +01:00
parent 3acf09eed6
commit adb4b4fdaf
23 changed files with 3143 additions and 3143 deletions

644
b3d.lua

@ -6,350 +6,350 @@ local metatable = {__index = getfenv(1)}
--+ See `b3d_specification.txt` as well as https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp
--> B3D model
function read(stream)
local left = 8
local left = 8
local function byte()
left = left - 1
return assert(stream:read(1):byte())
end
local function byte()
left = left - 1
return assert(stream:read(1):byte())
end
local function int()
local value = byte() + byte() * 0x100 + byte() * 0x10000 + byte() * 0x1000000
if value >= 2^31 then
return value - 2^32
end
return value
end
local function int()
local value = byte() + byte() * 0x100 + byte() * 0x10000 + byte() * 0x1000000
if value >= 2^31 then
return value - 2^32
end
return value
end
local function id()
return int() + 1
end
local function id()
return int() + 1
end
local function optional_id()
local id = int()
if id == -1 then
return
end
return id + 1
end
local function optional_id()
local id = int()
if id == -1 then
return
end
return id + 1
end
local function string()
local rope = {}
while true do
left = left - 1
local char = assert(stream:read(1))
if char == "\0" then
return table.concat(rope)
end
table.insert(rope, char)
end
end
local function string()
local rope = {}
while true do
left = left - 1
local char = assert(stream:read(1))
if char == "\0" then
return table.concat(rope)
end
table.insert(rope, char)
end
end
local read_single = modlib.binary.read_single
local function float()
return read_single(byte)
end
local read_single = modlib.binary.read_single
local function float()
return read_single(byte)
end
local function float_array(length)
local list = {}
for index = 1, length do
list[index] = float()
end
return list
end
local function float_array(length)
local list = {}
for index = 1, length do
list[index] = float()
end
return list
end
local function color()
return {
r = float(),
g = float(),
b = float(),
a = float()
}
end
local function color()
return {
r = float(),
g = float(),
b = float(),
a = float()
}
end
local function vector3()
return float_array(3)
end
local function vector3()
return float_array(3)
end
local function quaternion()
return {[4] = float(), [1] = float(), [2] = float(), [3] = float()}
end
local function quaternion()
return {[4] = float(), [1] = float(), [2] = float(), [3] = float()}
end
local function content()
assert(left >= 0, stream:seek())
return left ~= 0
end
local function content()
assert(left >= 0, stream:seek())
return left ~= 0
end
local chunk
local chunks = {
TEXS = function()
local textures = {}
while content() do
table.insert(textures, {
file = string(),
flags = int(),
blend = int(),
pos = float_array(2),
scale = float_array(2),
rotation = float()
})
end
return textures
end,
BRUS = function()
local brushes = {}
brushes.n_texs = int()
assert(brushes.n_texs <= 8)
while content() do
local brush = {
name = string(),
color = color(),
shininess = float(),
blend = float(),
fx = float(),
texture_id = {}
}
for index = 1, brushes.n_texs do
brush.texture_id[index] = optional_id()
end
table.insert(brushes, brush)
end
return brushes
end,
VRTS = function()
local vertices = {
flags = int(),
tex_coord_sets = int(),
tex_coord_set_size = int()
}
assert(vertices.tex_coord_sets <= 8 and vertices.tex_coord_set_size <= 4)
local has_normal = (vertices.flags % 2 == 1) or nil
local has_color = (math.floor(vertices.flags / 2) % 2 == 1) or nil
while content() do
local vertex = {
pos = vector3(),
normal = has_normal and vector3(),
color = has_color and color(),
tex_coords = {}
}
for tex_coord_set = 1, vertices.tex_coord_sets do
local tex_coords = {}
for tex_coord = 1, vertices.tex_coord_set_size do
tex_coords[tex_coord] = float()
end
vertex.tex_coords[tex_coord_set] = tex_coords
end
table.insert(vertices, vertex)
end
return vertices
end,
TRIS = function()
local tris = {
brush_id = id(),
vertex_ids = {}
}
while content() do
table.insert(tris.vertex_ids, {id(), id(), id()})
end
return tris
end,
MESH = function()
local mesh = {
brush_id = optional_id(),
vertices = chunk{VRTS = true}
}
mesh.triangle_sets = {}
repeat
local tris = chunk{TRIS = true}
table.insert(mesh.triangle_sets, tris)
until not content()
return mesh
end,
BONE = function()
local bone = {}
while content() do
local vertex_id = id()
assert(not bone[vertex_id], "duplicate vertex weight")
local weight = float()
if weight > 0 then
-- Many exporters include unneeded zero weights
bone[vertex_id] = weight
end
end
return bone
end,
KEYS = function()
local flags = int()
local _flags = flags % 8
local rotation, scale, position
if _flags >= 4 then
rotation = true
_flags = _flags - 4
end
if _flags >= 2 then
scale = true
_flags = _flags - 2
end
position = _flags >= 1
local bone = {
flags = flags
}
while content() do
table.insert(bone, {
frame = int(),
position = position and vector3() or nil,
scale = scale and vector3() or nil,
rotation = rotation and quaternion() or nil
})
end
-- Ensure frames are sorted ascending
table.sort(bone, function(a, b) return a.frame < b.frame end)
return bone
end,
ANIM = function()
return {
-- flags are unused
flags = int(),
frames = int(),
fps = float()
}
end,
NODE = function()
local node = {
name = string(),
position = vector3(),
scale = vector3(),
keys = {},
rotation = quaternion(),
children = {}
}
local node_type
-- See https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp#L263
-- Order is not validated; double occurences of mutually exclusive node def are
while content() do
local elem, type = chunk()
if type == "MESH" then
assert(not node_type)
node_type = "mesh"
node.mesh = elem
elseif type == "BONE" then
assert(not node_type)
node_type = "bone"
node.bone = elem
elseif type == "KEYS" then
assert((node.keys[#node.keys] or {}).frame ~= (elem[1] or {}).frame, "duplicate frame")
modlib.table.append(node.keys, elem)
elseif type == "NODE" then
table.insert(node.children, elem)
elseif type == "ANIM" then
node.animation = elem
else
assert(not node_type)
node_type = "pivot"
end
end
-- TODO somehow merge keys
return node
end,
BB3D = function()
local version = int()
local self = {
version = {
major = math.floor(version / 100),
minor = version % 100,
raw = version
},
textures = {},
brushes = {}
}
assert(self.version.major <= 2, "unsupported version: " .. self.version.major)
while content() do
local field, type = chunk{TEXS = true, BRUS = true, NODE = true}
if type == "TEXS" then
modlib.table.append(self.textures, field)
elseif type == "BRUS" then
modlib.table.append(self.brushes, field)
else
self.node = field
end
end
return self
end
}
local chunk
local chunks = {
TEXS = function()
local textures = {}
while content() do
table.insert(textures, {
file = string(),
flags = int(),
blend = int(),
pos = float_array(2),
scale = float_array(2),
rotation = float()
})
end
return textures
end,
BRUS = function()
local brushes = {}
brushes.n_texs = int()
assert(brushes.n_texs <= 8)
while content() do
local brush = {
name = string(),
color = color(),
shininess = float(),
blend = float(),
fx = float(),
texture_id = {}
}
for index = 1, brushes.n_texs do
brush.texture_id[index] = optional_id()
end
table.insert(brushes, brush)
end
return brushes
end,
VRTS = function()
local vertices = {
flags = int(),
tex_coord_sets = int(),
tex_coord_set_size = int()
}
assert(vertices.tex_coord_sets <= 8 and vertices.tex_coord_set_size <= 4)
local has_normal = (vertices.flags % 2 == 1) or nil
local has_color = (math.floor(vertices.flags / 2) % 2 == 1) or nil
while content() do
local vertex = {
pos = vector3(),
normal = has_normal and vector3(),
color = has_color and color(),
tex_coords = {}
}
for tex_coord_set = 1, vertices.tex_coord_sets do
local tex_coords = {}
for tex_coord = 1, vertices.tex_coord_set_size do
tex_coords[tex_coord] = float()
end
vertex.tex_coords[tex_coord_set] = tex_coords
end
table.insert(vertices, vertex)
end
return vertices
end,
TRIS = function()
local tris = {
brush_id = id(),
vertex_ids = {}
}
while content() do
table.insert(tris.vertex_ids, {id(), id(), id()})
end
return tris
end,
MESH = function()
local mesh = {
brush_id = optional_id(),
vertices = chunk{VRTS = true}
}
mesh.triangle_sets = {}
repeat
local tris = chunk{TRIS = true}
table.insert(mesh.triangle_sets, tris)
until not content()
return mesh
end,
BONE = function()
local bone = {}
while content() do
local vertex_id = id()
assert(not bone[vertex_id], "duplicate vertex weight")
local weight = float()
if weight > 0 then
-- Many exporters include unneeded zero weights
bone[vertex_id] = weight
end
end
return bone
end,
KEYS = function()
local flags = int()
local _flags = flags % 8
local rotation, scale, position
if _flags >= 4 then
rotation = true
_flags = _flags - 4
end
if _flags >= 2 then
scale = true
_flags = _flags - 2
end
position = _flags >= 1
local bone = {
flags = flags
}
while content() do
table.insert(bone, {
frame = int(),
position = position and vector3() or nil,
scale = scale and vector3() or nil,
rotation = rotation and quaternion() or nil
})
end
-- Ensure frames are sorted ascending
table.sort(bone, function(a, b) return a.frame < b.frame end)
return bone
end,
ANIM = function()
return {
-- flags are unused
flags = int(),
frames = int(),
fps = float()
}
end,
NODE = function()
local node = {
name = string(),
position = vector3(),
scale = vector3(),
keys = {},
rotation = quaternion(),
children = {}
}
local node_type
-- See https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp#L263
-- Order is not validated; double occurences of mutually exclusive node def are
while content() do
local elem, type = chunk()
if type == "MESH" then
assert(not node_type)
node_type = "mesh"
node.mesh = elem
elseif type == "BONE" then
assert(not node_type)
node_type = "bone"
node.bone = elem
elseif type == "KEYS" then
assert((node.keys[#node.keys] or {}).frame ~= (elem[1] or {}).frame, "duplicate frame")
modlib.table.append(node.keys, elem)
elseif type == "NODE" then
table.insert(node.children, elem)
elseif type == "ANIM" then
node.animation = elem
else
assert(not node_type)
node_type = "pivot"
end
end
-- TODO somehow merge keys
return node
end,
BB3D = function()
local version = int()
local self = {
version = {
major = math.floor(version / 100),
minor = version % 100,
raw = version
},
textures = {},
brushes = {}
}
assert(self.version.major <= 2, "unsupported version: " .. self.version.major)
while content() do
local field, type = chunk{TEXS = true, BRUS = true, NODE = true}
if type == "TEXS" then
modlib.table.append(self.textures, field)
elseif type == "BRUS" then
modlib.table.append(self.brushes, field)
else
self.node = field
end
end
return self
end
}
local function chunk_header()
left = left - 4
return stream:read(4), int()
end
local function chunk_header()
left = left - 4
return stream:read(4), int()
end
function chunk(possible_chunks)
local type, new_left = chunk_header()
local parent_left
left, parent_left = new_left, left
if possible_chunks and not possible_chunks[type] then
error("expected one of " .. table.concat(modlib.table.keys(possible_chunks), ", ") .. ", found " .. type)
end
local res = assert(chunks[type])()
assert(left == 0)
left = parent_left - new_left
return res, type
end
function chunk(possible_chunks)
local type, new_left = chunk_header()
local parent_left
left, parent_left = new_left, left
if possible_chunks and not possible_chunks[type] then
error("expected one of " .. table.concat(modlib.table.keys(possible_chunks), ", ") .. ", found " .. type)
end
local res = assert(chunks[type])()
assert(left == 0)
left = parent_left - new_left
return res, type
end
local self = chunk{BB3D = true}
return setmetatable(self, metatable)
local self = chunk{BB3D = true}
return setmetatable(self, metatable)
end
-- TODO function write(self, stream)
local binary_search_frame = modlib.table.binary_search_comparator(function(a, b)
return modlib.table.default_comparator(a, b.frame)
return modlib.table.default_comparator(a, b.frame)
end)
--> [bonename] = { position = vector, rotation = quaternion, scale = vector }
function get_animated_bone_properties(self, keyframe, interpolate)
local function get_frame_values(keys)
local values = keys[keyframe]
if values and values.frame == keyframe then
return {
position = values.position,
rotation = values.rotation,
scale = values.scale
}
end
local index = binary_search_frame(keys, keyframe)
if index > 0 then
return keys[index]
end
index = -index
assert(index > 1 and index <= #keys)
local a, b = keys[index - 1], keys[index]
if not interpolate then
return a
end
local ratio = (keyframe - a.frame) / (b.frame - a.frame)
return {
position = (a.position and b.position and modlib.vector.interpolate(a.position, b.position, ratio)) or a.position or b.position,
rotation = (a.rotation and b.rotation and modlib.quaternion.interpolate(a.rotation, b.rotation, ratio)) or a.rotation or b.rotation,
scale = (a.scale and b.scale and modlib.vector.interpolate(a.scale, b.scale, ratio)) or a.scale or b.scale,
}
end
local bone_properties = {}
local function get_props(node)
local properties = {}
if node.keys and next(node.keys) ~= nil then
properties = modlib.table.add_all(properties, get_frame_values(node.keys))
end
for _, property in pairs{"position", "rotation", "scale"} do
properties[property] = properties[property] or modlib.table.copy(node[property])
end
if node.bone then
assert(not bone_properties[node.name])
bone_properties[node.name] = properties
end
for _, child in pairs(node.children or {}) do
get_props(child)
end
end
get_props(self.node)
return bone_properties
local function get_frame_values(keys)
local values = keys[keyframe]
if values and values.frame == keyframe then
return {
position = values.position,
rotation = values.rotation,
scale = values.scale
}
end
local index = binary_search_frame(keys, keyframe)
if index > 0 then
return keys[index]
end
index = -index
assert(index > 1 and index <= #keys)
local a, b = keys[index - 1], keys[index]
if not interpolate then
return a
end
local ratio = (keyframe - a.frame) / (b.frame - a.frame)
return {
position = (a.position and b.position and modlib.vector.interpolate(a.position, b.position, ratio)) or a.position or b.position,
rotation = (a.rotation and b.rotation and modlib.quaternion.interpolate(a.rotation, b.rotation, ratio)) or a.rotation or b.rotation,
scale = (a.scale and b.scale and modlib.vector.interpolate(a.scale, b.scale, ratio)) or a.scale or b.scale,
}
end
local bone_properties = {}
local function get_props(node)
local properties = {}
if node.keys and next(node.keys) ~= nil then
properties = modlib.table.add_all(properties, get_frame_values(node.keys))
end
for _, property in pairs{"position", "rotation", "scale"} do
properties[property] = properties[property] or modlib.table.copy(node[property])
end
if node.bone then
assert(not bone_properties[node.name])
bone_properties[node.name] = properties
end
for _, child in pairs(node.children or {}) do
get_props(child)
end
end
get_props(self.node)
return bone_properties
end

@ -3,113 +3,113 @@
--+ Reads doubles (f64) or floats (f32)
--: double reads an f64 if true, f32 otherwise
function read_float(read_byte, double)
-- First read the mantissa
local mantissa = 0
for _ = 1, double and 6 or 2 do
mantissa = (mantissa + read_byte()) / 0x100
end
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
local byte_2, byte_1 = read_byte(), read_byte()
local sign = 1
if byte_1 >= 0x80 then
sign = -1
byte_1 = byte_1 - 0x80
end
local exponent = byte_1 * 2
if byte_2 >= 0x80 then
exponent = exponent + 1
byte_2 = byte_2 - 0x80
end
mantissa = (mantissa + byte_2) / 0x80
if exponent == 0xFF then
if mantissa == 0 then
return sign * math.huge
end
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
-- HACK ((0/0)^1) yields nan, 0/0 yields -nan
return sign == 1 and ((0/0)^1) or 0/0
end
assert(mantissa < 1)
if exponent == 0 then
-- subnormal value
return sign * 2^-126 * mantissa
end
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
-- First read the mantissa
local mantissa = 0
for _ = 1, double and 6 or 2 do
mantissa = (mantissa + read_byte()) / 0x100
end
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
local byte_2, byte_1 = read_byte(), read_byte()
local sign = 1
if byte_1 >= 0x80 then
sign = -1
byte_1 = byte_1 - 0x80
end
local exponent = byte_1 * 2
if byte_2 >= 0x80 then
exponent = exponent + 1
byte_2 = byte_2 - 0x80
end
mantissa = (mantissa + byte_2) / 0x80
if exponent == 0xFF then
if mantissa == 0 then
return sign * math.huge
end
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
-- HACK ((0/0)^1) yields nan, 0/0 yields -nan
return sign == 1 and ((0/0)^1) or 0/0
end
assert(mantissa < 1)
if exponent == 0 then
-- subnormal value
return sign * 2^-126 * mantissa
end
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
end
--+ Reads a single floating point number (f32)
function read_single(read_byte)
return read_float(read_byte)
return read_float(read_byte)
end
--+ Reads a double (f64)
function read_double(read_byte)
return read_float(read_byte, true)
return read_float(read_byte, true)
end
function read_uint(read_byte, bytes)
local factor = 1
local uint = 0
for _ = 1, bytes do
uint = uint + read_byte() * factor
factor = factor * 0x100
end
return uint
local factor = 1
local uint = 0
for _ = 1, bytes do
uint = uint + read_byte() * factor
factor = factor * 0x100
end
return uint
end
function write_uint(write_byte, uint, bytes)
for _ = 1, bytes do
write_byte(uint % 0x100)
uint = math.floor(uint / 0x100)
end
assert(uint == 0)
for _ = 1, bytes do
write_byte(uint % 0x100)
uint = math.floor(uint / 0x100)
end
assert(uint == 0)
end
--: on_write function(double)
--: double set to true to force f64, false for f32, nil for auto
function write_float(write_byte, number, on_write, double)
local sign = 0
if number < 0 then
number = -number
sign = 0x80
end
local mantissa, exponent = math.frexp(number)
exponent = exponent + 127
if exponent > 1 then
-- TODO ensure this deals properly with subnormal numbers
mantissa = mantissa * 2 - 1
exponent = exponent - 1
end
local sign_byte = sign + math.floor(exponent / 2)
mantissa = mantissa * 0x80
local exponent_byte = (exponent % 2) * 0x80 + math.floor(mantissa)
mantissa = mantissa % 1
local mantissa_bytes = {}
-- TODO ensure this check is proper
if double == nil then
double = mantissa % 2^-23 > 0
end
if on_write then
on_write(double)
end
local len = double and 6 or 2
for index = len, 1, -1 do
mantissa = mantissa * 0x100
mantissa_bytes[index] = math.floor(mantissa)
mantissa = mantissa % 1
end
assert(mantissa == 0)
for index = 1, len do
write_byte(mantissa_bytes[index])
end
write_byte(exponent_byte)
write_byte(sign_byte)
local sign = 0
if number < 0 then
number = -number
sign = 0x80
end
local mantissa, exponent = math.frexp(number)
exponent = exponent + 127
if exponent > 1 then
-- TODO ensure this deals properly with subnormal numbers
mantissa = mantissa * 2 - 1
exponent = exponent - 1
end
local sign_byte = sign + math.floor(exponent / 2)
mantissa = mantissa * 0x80
local exponent_byte = (exponent % 2) * 0x80 + math.floor(mantissa)
mantissa = mantissa % 1
local mantissa_bytes = {}
-- TODO ensure this check is proper
if double == nil then
double = mantissa % 2^-23 > 0
end
if on_write then
on_write(double)
end
local len = double and 6 or 2
for index = len, 1, -1 do
mantissa = mantissa * 0x100
mantissa_bytes[index] = math.floor(mantissa)
mantissa = mantissa % 1
end
assert(mantissa == 0)
for index = 1, len do
write_byte(mantissa_bytes[index])
end
write_byte(exponent_byte)
write_byte(sign_byte)
end
function write_single(write_byte, number)
return write_float(write_byte, number, nil, false)
return write_float(write_byte, number, nil, false)
end
function write_double(write_byte, number)
return write_float(write_byte, number, nil, true)
return write_float(write_byte, number, nil, true)
end

520
bluon.lua

@ -3,23 +3,23 @@ local bluon = getfenv(1)
local metatable = {__index = bluon}
function new(self)
return setmetatable(self or {}, metatable)
return setmetatable(self or {}, metatable)
end
function aux_is_valid()
return false
return false
end
function aux_len(object)
error("unsupported type: " .. type(object))
error("unsupported type: " .. type(object))
end
function aux_read(type)
error(("unsupported type: 0x%02X"):format(type))
error(("unsupported type: 0x%02X"):format(type))
end
function aux_write(object)
error("unsupported type: " .. type(object))
error("unsupported type: " .. type(object))
end
local uint_widths = {1, 2, 4, 8}
@ -27,291 +27,291 @@ local uint_types = #uint_widths
local type_ranges = {}
local current = 0
for _, type in ipairs{
{"boolean", 2};
-- 0, -nan, +inf, -inf: sign of nan can be ignored
{"number_constant", 4};
{"number_negative", uint_types};
{"number_positive", uint_types};
{"number_f32", 1};
{"number", 1};
{"string_constant", 1};
{"string", uint_types};
-- (T0, T8, T16, T32, T64) x (L0, L8, L16, L32, L64)
{"table", (uint_types + 1) ^ 2};
{"reference", uint_types}
{"boolean", 2};
-- 0, -nan, +inf, -inf: sign of nan can be ignored
{"number_constant", 4};
{"number_negative", uint_types};
{"number_positive", uint_types};
{"number_f32", 1};
{"number", 1};
{"string_constant", 1};
{"string", uint_types};
-- (T0, T8, T16, T32, T64) x (L0, L8, L16, L32, L64)
{"table", (uint_types + 1) ^ 2};
{"reference", uint_types}
} do
local typename, length = unpack(type)
current = current + length
type_ranges[typename] = current
local typename, length = unpack(type)
current = current + length
type_ranges[typename] = current
end
local constants = {
[false] = "\0",
[true] = "\1",
[0] = "\2",
-- not possible as table entry as Lua doesn't allow +/-nan as table key
-- [0/0] = "\3",
[math.huge] = "\4",
[-math.huge] = "\5",
[""] = "\20"
[false] = "\0",
[true] = "\1",
[0] = "\2",
-- not possible as table entry as Lua doesn't allow +/-nan as table key
-- [0/0] = "\3",
[math.huge] = "\4",
[-math.huge] = "\5",
[""] = "\20"
}
local constant_nan = "\3"
local function uint_type(uint)
--U8
if uint <= 0xFF then return 1 end
--U16
if uint <= 0xFFFF then return 2 end
--U32
if uint <= 0xFFFFFFFF then return 3 end
--U64
return 4
--U8
if uint <= 0xFF then return 1 end
--U16
if uint <= 0xFFFF then return 2 end
--U32
if uint <= 0xFFFFFFFF then return 3 end
--U64
return 4
end
local valid_types = modlib.table.set{"nil", "boolean", "number", "string"}
function is_valid(self, object)
local _type = type(object)
if valid_types[_type] then
return true
end
if _type == "table" then
for key, value in pairs(object) do
if not (is_valid(self, key) and is_valid(self, value)) then
return false
end
end
return true
end
return self.aux_is_valid(object)
local _type = type(object)
if valid_types[_type] then
return true
end
if _type == "table" then
for key, value in pairs(object) do
if not (is_valid(self, key) and is_valid(self, value)) then
return false
end
end
return true
end
return self.aux_is_valid(object)
end
local function uint_len(uint)
return uint_widths[uint_type(uint)]
return uint_widths[uint_type(uint)]
end
local function is_map_key(key, list_len)
return type(key) ~= "number" or (key < 1 or key > list_len or key % 1 ~= 0)
return type(key) ~= "number" or (key < 1 or key > list_len or key % 1 ~= 0)
end
function len(self, object)
if constants[object] then
return 1
end
local _type = type(object)
if _type == "number" then
if object ~= object then
stream:write(constant_nan)
return
end
if object % 1 == 0 then
return 1 + uint_len(object > 0 and object or -object)
end
-- TODO ensure this check is proper
if mantissa % 2^-23 > 0 then
return 9
end
return 5
end
local id = object_ids[object]
if id then
return 1 + uint_len(id)
end
current_id = current_id + 1
object_ids[object] = current_id
if _type == "string" then
local object_len = object:len()
return 1 + uint_len(object_len) + object_len
end
if _type == "table" then
if next(object) == nil then
-- empty {} table
byte(type_ranges.string + 1)
return 1
end
local list_len = #object
local kv_len = 0
for key, _ in pairs(object) do
if is_map_key(key, list_len) then
kv_len = kv_len + 1
end
end
local table_len = 1 + uint_len(list_len) + uint_len(kv_len)
for index = 1, list_len do
table_len = table_len + len(self, object[index])
end
for key, value in pairs(object) do
if is_map_key(key, list_len) then
table_len = table_len + len(self, key) + len(self, value)
end
end
return len
end
return self.aux_len(object)
if constants[object] then
return 1
end
local _type = type(object)
if _type == "number" then
if object ~= object then
stream:write(constant_nan)
return
end
if object % 1 == 0 then
return 1 + uint_len(object > 0 and object or -object)
end
-- TODO ensure this check is proper
if mantissa % 2^-23 > 0 then
return 9
end
return 5
end
local id = object_ids[object]
if id then
return 1 + uint_len(id)
end
current_id = current_id + 1
object_ids[object] = current_id
if _type == "string" then
local object_len = object:len()
return 1 + uint_len(object_len) + object_len
end
if _type == "table" then
if next(object) == nil then
-- empty {} table
byte(type_ranges.string + 1)
return 1
end
local list_len = #object
local kv_len = 0
for key, _ in pairs(object) do
if is_map_key(key, list_len) then
kv_len = kv_len + 1
end
end
local table_len = 1 + uint_len(list_len) + uint_len(kv_len)
for index = 1, list_len do
table_len = table_len + len(self, object[index])
end
for key, value in pairs(object) do
if is_map_key(key, list_len) then
table_len = table_len + len(self, key) + len(self, value)
end
end
return len
end
return self.aux_len(object)
end
--: stream any object implementing :write(text)
function write(self, object, stream)
if object == nil then
return
end
local object_ids = {}
local current_id = 0
local function byte(byte)
stream:write(string.char(byte))
end
local write_uint = modlib.binary.write_uint
local function uint(type, uint)
write_uint(byte, uint, uint_widths[type])
end
local function uint_with_type(base, _uint)
local type_offset = uint_type(_uint)
byte(base + type_offset)
uint(type_offset, _uint)
end
local write_float = modlib.binary.write_float
local function float_on_write(double)
byte(double and type_ranges.number or type_ranges.number_f32)
end
local function float(number)
write_float(byte, number, float_on_write)
end
local aux_write = self.aux_write
local function _write(object)
local constant = constants[object]
if constant then
stream:write(constant)
return
end
local _type = type(object)
if _type == "number" then
if object ~= object then
stream:write(constant_nan)
return
end
if object % 1 == 0 then
uint_with_type(object > 0 and type_ranges.number_constant or type_ranges.number_negative, object > 0 and object or -object)
return
end
float(object)
return
end
local id = object_ids[object]
if id then
uint_with_type(type_ranges.table, id)
return
end
if _type == "string" then
local len = object:len()
current_id = current_id + 1
object_ids[object] = current_id
uint_with_type(type_ranges.number, len)
stream:write(object)
return
end
if _type == "table" then
current_id = current_id + 1
object_ids[object] = current_id
if next(object) == nil then
-- empty {} table
byte(type_ranges.string + 1)
return
end
local list_len = #object
local kv_len = 0
for key, _ in pairs(object) do
if is_map_key(key, list_len) then
kv_len = kv_len + 1
end
end
local list_len_sig = uint_type(list_len)
local kv_len_sig = uint_type(kv_len)
byte(type_ranges.string + list_len_sig + kv_len_sig * 5 + 1)
uint(list_len_sig, list_len)
uint(kv_len_sig, kv_len)
for index = 1, list_len do
_write(object[index])
end
for key, value in pairs(object) do
if is_map_key(key, list_len) then
_write(key)
_write(value)
end
end
return
end
aux_write(object, object_ids)
end
_write(object)
if object == nil then
return
end
local object_ids = {}
local current_id = 0
local function byte(byte)
stream:write(string.char(byte))
end
local write_uint = modlib.binary.write_uint
local function uint(type, uint)
write_uint(byte, uint, uint_widths[type])
end
local function uint_with_type(base, _uint)
local type_offset = uint_type(_uint)
byte(base + type_offset)
uint(type_offset, _uint)
end
local write_float = modlib.binary.write_float
local function float_on_write(double)
byte(double and type_ranges.number or type_ranges.number_f32)
end
local function float(number)
write_float(byte, number, float_on_write)
end
local aux_write = self.aux_write
local function _write(object)
local constant = constants[object]
if constant then
stream:write(constant)
return
end
local _type = type(object)
if _type == "number" then
if object ~= object then
stream:write(constant_nan)
return
end
if object % 1 == 0 then
uint_with_type(object > 0 and type_ranges.number_constant or type_ranges.number_negative, object > 0 and object or -object)
return
end
float(object)
return
end
local id = object_ids[object]
if id then
uint_with_type(type_ranges.table, id)
return
end
if _type == "string" then
local len = object:len()
current_id = current_id + 1
object_ids[object] = current_id
uint_with_type(type_ranges.number, len)
stream:write(object)
return
end
if _type == "table" then
current_id = current_id + 1
object_ids[object] = current_id
if next(object) == nil then
-- empty {} table
byte(type_ranges.string + 1)
return
end
local list_len = #object
local kv_len = 0
for key, _ in pairs(object) do
if is_map_key(key, list_len) then
kv_len = kv_len + 1
end
end
local list_len_sig = uint_type(list_len)
local kv_len_sig = uint_type(kv_len)
byte(type_ranges.string + list_len_sig + kv_len_sig * 5 + 1)
uint(list_len_sig, list_len)
uint(kv_len_sig, kv_len)
for index = 1, list_len do
_write(object[index])
end
for key, value in pairs(object) do
if is_map_key(key, list_len) then
_write(key)
_write(value)
end
end
return
end
aux_write(object, object_ids)
end
_write(object)
end
local constants_flipped = modlib.table.flip(constants)
-- See https://www.lua.org/manual/5.1/manual.html#2.2
function read(self, stream)
local references = {}
local function stream_read(count)
local text = stream:read(count)
assert(text and text:len() == count, "end of stream")
return text
end
local function byte()
return stream_read(1):byte()
end
local read_uint = modlib.binary.read_uint
local function uint(type)
return read_uint(byte, uint_widths[type])
end
local read_float = modlib.binary.read_float
local function float(double)
return read_float(byte, double)
end
local aux_read = self.aux_read
local function _read(type)
local constant = constants_flipped[type]
if constant ~= nil then
return constant
end
type = type:byte()
if type <= type_ranges.number then
if type <= type_ranges.number_negative then
return uint(type - type_ranges.number_constant)
end
if type <= type_ranges.number_positive then
return -uint(type - type_ranges.number_negative)
end
return float(type == type_ranges.number)
end
if type <= type_ranges.string then
local string = stream_read(uint(type - type_ranges.number))
table.insert(references, string)
return string
end
if type <= type_ranges.table then
type = type - type_ranges.string - 1
local tab = {}
table.insert(references, tab)
if type == 0 then
return tab
end
local list_len = uint(type % 5)
local kv_len = uint(math.floor(type / 5))
for index = 1, list_len do
tab[index] = _read(stream_read(1))
end
for _ = 1, kv_len do
tab[_read(stream_read(1))] = _read(stream_read(1))
end
return tab
end
if type <= type_ranges.reference then
return references[uint(type - type_ranges.table)]
end
return aux_read(type, stream, references)
end
local type = stream:read(1)
if type == nil then
return
end
return _read(type)
local references = {}
local function stream_read(count)
local text = stream:read(count)
assert(text and text:len() == count, "end of stream")
return text
end
local function byte()
return stream_read(1):byte()
end
local read_uint = modlib.binary.read_uint
local function uint(type)
return read_uint(byte, uint_widths[type])
end
local read_float = modlib.binary.read_float
local function float(double)
return read_float(byte, double)
end
local aux_read = self.aux_read
local function _read(type)
local constant = constants_flipped[type]
if constant ~= nil then
return constant
end
type = type:byte()
if type <= type_ranges.number then
if type <= type_ranges.number_negative then
return uint(type - type_ranges.number_constant)
end
if type <= type_ranges.number_positive then
return -uint(type - type_ranges.number_negative)
end
return float(type == type_ranges.number)
end
if type <= type_ranges.string then
local string = stream_read(uint(type - type_ranges.number))
table.insert(references, string)
return string
end
if type <= type_ranges.table then
type = type - type_ranges.string - 1
local tab = {}
table.insert(references, tab)
if type == 0 then
return tab
end
local list_len = uint(type % 5)
local kv_len = uint(math.floor(type / 5))
for index = 1, list_len do
tab[index] = _read(stream_read(1))
end
for _ = 1, kv_len do
tab[_read(stream_read(1))] = _read(stream_read(1))
end
return tab
end
if type <= type_ranges.reference then
return references[uint(type - type_ranges.table)]
end
return aux_read(type, stream, references)
end
local type = stream:read(1)
if type == nil then
return
end
return _read(type)
end

542
conf.lua

@ -1,296 +1,296 @@
-- not deprecated
function build_tree(dict)
local tree = {}
for key, value in pairs(dict) do
local path = modlib.text.split_unlimited(key, ".")
local subtree = tree
for i = 1, #path - 1 do
local index = tonumber(path[i]) or path[i]
subtree[index] = subtree[index] or {}
subtree = subtree[index]
end
subtree[path[#path]] = value
end
return tree
local tree = {}
for key, value in pairs(dict) do
local path = modlib.text.split_unlimited(key, ".")
local subtree = tree
for i = 1, #path - 1 do
local index = tonumber(path[i]) or path[i]
subtree[index] = subtree[index] or {}
subtree = subtree[index]
end
subtree[path[#path]] = value
end
return tree
end
if minetest then
function build_setting_tree()
modlib.conf.settings = build_tree(minetest.settings:to_table())
end
-- deprecated, use modlib.mod.configuration instead
minetest.mkdir(minetest.get_worldpath().."/config")
function get_path(confname)
return minetest.get_worldpath().."/config/"..confname
end
function build_setting_tree()
modlib.conf.settings = build_tree(minetest.settings:to_table())
end
-- deprecated, use modlib.mod.configuration instead
minetest.mkdir(minetest.get_worldpath().."/config")
function get_path(confname)
return minetest.get_worldpath().."/config/"..confname
end
end
function read_conf(text)
local lines = modlib.text.split_lines(text, nil, true)
local dict = {}
for i, line in ipairs(lines) do
local error_base = "Line " .. (i+1) .. ": "
line = modlib.text.trim_left(lines[i])
if line ~= "" and line:sub(1,1) ~= "#" then
line = modlib.text.split(line, "=", 2)
if #line ~= 2 then
error(error_base .. "No value given")
end
local prop = modlib.text.trim_right(line[1])
if prop == "" then
error(error_base .. "No key given")
end
local val = modlib.text.trim_left(line[2])
if val == "" then
error(error_base .. "No value given")
end
if modlib.text.starts_with(val, '"""') then
val = val:sub(3)
local total_val = {}
local function readMultiline()
while i < #lines do
if modlib.text.ends_with(val, '"""') then
val = val:sub(1, val:len() - 3)
return
end
table.insert(total_val, val)
i = i + 1
val = lines[i]
end
i = i - 1
error(error_base .. "Unclosed multiline block")
end
readMultiline()
table.insert(total_val, val)
val = table.concat(total_val, "\n")
else
val = modlib.text.trim_right(val)
end
if dict[prop] then
error(error_base .. "Duplicate key")
end
dict[prop] = val
end
end
return dict
local lines = modlib.text.split_lines(text, nil, true)
local dict = {}
for i, line in ipairs(lines) do
local error_base = "Line " .. (i+1) .. ": "
line = modlib.text.trim_left(lines[i])
if line ~= "" and line:sub(1,1) ~= "#" then
line = modlib.text.split(line, "=", 2)
if #line ~= 2 then
error(error_base .. "No value given")
end
local prop = modlib.text.trim_right(line[1])
if prop == "" then
error(error_base .. "No key given")
end
local val = modlib.text.trim_left(line[2])
if val == "" then
error(error_base .. "No value given")
end
if modlib.text.starts_with(val, '"""') then
val = val:sub(3)
local total_val = {}
local function readMultiline()
while i < #lines do
if modlib.text.ends_with(val, '"""') then
val = val:sub(1, val:len() - 3)
return
end
table.insert(total_val, val)
i = i + 1
val = lines[i]
end
i = i - 1
error(error_base .. "Unclosed multiline block")
end
readMultiline()
table.insert(total_val, val)
val = table.concat(total_val, "\n")
else
val = modlib.text.trim_right(val)
end
if dict[prop] then
error(error_base .. "Duplicate key")
end
dict[prop] = val
end
end
return dict
end
function check_config_constraints(config, constraints, handler)
local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end)
if not no_error then
handler(error_or_retval)
end
local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end)
if not no_error then
handler(error_or_retval)
end
end
function load(filename, constraints)
local config = minetest.parse_json(modlib.file.read(filename))
if constraints then
check_config_constraints(config, constraints, function(message)
error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
end)
end
return config
local config = minetest.parse_json(modlib.file.read(filename))
if constraints then
check_config_constraints(config, constraints, function(message)
error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
end)
end
return config
end
function load_or_create(filename, replacement_file, constraints)
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
return load(filename, constraints)
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
return load(filename, constraints)
end
function import(modname, constraints, no_settingtypes)
local default_config = modlib.mod.get_resource(modname, "default_config.json")
local default_conf = minetest.parse_json(modlib.file.read(default_config))
local config = load_or_create(get_path(modname)..".json", default_config, constraints)
local formats = {
{ extension = ".lua", load = minetest.deserialize },
{ extension = ".luon", load = function(text) minetest.deserialize("return "..text) end },
{ extension = ".conf", load = function(text) return fix_types(build_tree(read_conf(text)), constraints) end }
}
for _, format in ipairs(formats) do
local conf = modlib.file.read(get_path(modname)..format.extension)
if conf then
config = merge_config(config, format.load(conf))
end
end
if not no_settingtypes then
constraints.name = modname
local settingtypes = generate_settingtypes(default_conf, constraints)
modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes)
end
local additional_settings = modlib.conf.settings[modname] or {}
additional_settings = fix_types(additional_settings, constraints)
-- TODO implement merge_config_legal(default_conf, ...)
config = merge_config(config, additional_settings)
if constraints then
check_config_constraints(config, constraints, function(message)
error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message)
end)
end
return config
local default_config = modlib.mod.get_resource(modname, "default_config.json")
local default_conf = minetest.parse_json(modlib.file.read(default_config))
local config = load_or_create(get_path(modname)..".json", default_config, constraints)
local formats = {
{ extension = ".lua", load = minetest.deserialize },
{ extension = ".luon", load = function(text) minetest.deserialize("return "..text) end },
{ extension = ".conf", load = function(text) return fix_types(build_tree(read_conf(text)), constraints) end }
}
for _, format in ipairs(formats) do
local conf = modlib.file.read(get_path(modname)..format.extension)
if conf then
config = merge_config(config, format.load(conf))
end
end
if not no_settingtypes then
constraints.name = modname
local settingtypes = generate_settingtypes(default_conf, constraints)
modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes)
end
local additional_settings = modlib.conf.settings[modname] or {}
additional_settings = fix_types(additional_settings, constraints)
-- TODO implement merge_config_legal(default_conf, ...)
config = merge_config(config, additional_settings)
if constraints then
check_config_constraints(config, constraints, function(message)
error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message)
end)
end
return config
end
function merge_config(config, additional_settings)
if not config or type(additional_settings) ~= "table" then
return additional_settings
end
for setting, value in pairs(additional_settings) do
if config[setting] then
config[setting] = merge_config(config[setting], value)
end
end
return config
if not config or type(additional_settings) ~= "table" then
return additional_settings
end
for setting, value in pairs(additional_settings) do
if config[setting] then
config[setting] = merge_config(config[setting], value)
end
end
return config
end
-- format: # comment
-- name (Readable name) type type_args
function generate_settingtypes(default_conf, constraints)
local constraint_type = constraints.type
if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then
constraint_type = "table"
end
local settingtype, type_args
local title = constraints.title
if not title then
title = modlib.text.split(constraints.name, "_")
title[1] = modlib.text.upper_first(title[1])
title = table.concat(title, " ")
end
if constraint_type == "boolean" then
settingtype = "bool"
default_conf = default_conf and "true" or "false"
elseif constraint_type == "string" then
settingtype = "string"
elseif constraint_type == "number" then
settingtype = constraints.int and "int" or "float"
local range = constraints.range
if range then
-- TODO consider better max
type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or (2 ^ 30))
end
-- HACK
if not default_conf then default_conf = range[1] end
elseif constraint_type == "table" then
local handled = {}
local settings = {}
local function setting(key, value_constraints)
if handled[key] then
return
end
handled[key] = true
value_constraints.name = constraints.name .. "." .. key
value_constraints.title = title .. " " .. key
table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints))
end
for _, table in ipairs{"children", "required_children", "possible_children"} do
for key, constraints in pairs(constraints[table] or {}) do
setting(key, constraints)
end
end
return table.concat(settings, "\n")
end
if not constraint_type then
return ""
end
local comment = constraints.comment
if comment then
comment = "# " .. comment .. "\n"
else
comment = ""
end
assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf))
return comment .. constraints.name .. " (" .. title .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "")
local constraint_type = constraints.type
if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then
constraint_type = "table"
end
local settingtype, type_args
local title = constraints.title
if not title then
title = modlib.text.split(constraints.name, "_")
title[1] = modlib.text.upper_first(title[1])
title = table.concat(title, " ")
end
if constraint_type == "boolean" then
settingtype = "bool"
default_conf = default_conf and "true" or "false"
elseif constraint_type == "string" then
settingtype = "string"
elseif constraint_type == "number" then
settingtype = constraints.int and "int" or "float"
local range = constraints.range
if range then
-- TODO consider better max
type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or (2 ^ 30))
end
-- HACK
if not default_conf then default_conf = range[1] end
elseif constraint_type == "table" then
local handled = {}
local settings = {}
local function setting(key, value_constraints)
if handled[key] then
return
end
handled[key] = true
value_constraints.name = constraints.name .. "." .. key
value_constraints.title = title .. " " .. key
table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints))
end
for _, table in ipairs{"children", "required_children", "possible_children"} do
for key, constraints in pairs(constraints[table] or {}) do
setting(key, constraints)
end
end
return table.concat(settings, "\n")
end
if not constraint_type then
return ""
end
local comment = constraints.comment
if comment then
comment = "# " .. comment .. "\n"
else
comment = ""
end
assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf))
return comment .. constraints.name .. " (" .. title .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "")
end
function fix_types(value, constraints)
local type = type(value)
local expected_type = constraints.type
if expected_type and expected_type ~= type then
assert(type == "string", "Can't fix non-string value")
if expected_type == "boolean" then
assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value)
value = value == "true"
elseif expected_type == "number" then
assert(tonumber(value), "Not a number: " .. value)
value = tonumber(value)
end
end
if type == "table" then
for key, val in pairs(value) do
for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do
child_constraints = (constraints[child_constraints] or {})[key]
if child_constraints then
val = fix_types(val, child_constraints)
end
end
if constraints.values then
val = fix_types(val, constraints.values)
end
if constraints.keys then
value[key] = nil
value[fix_types(key, constraints.keys)] = val
else
value[key] = val
end
end
end
return value
local type = type(value)
local expected_type = constraints.type
if expected_type and expected_type ~= type then
assert(type == "string", "Can't fix non-string value")
if expected_type == "boolean" then
assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value)
value = value == "true"
elseif expected_type == "number" then
assert(tonumber(value), "Not a number: " .. value)
value = tonumber(value)
end
end
if type == "table" then
for key, val in pairs(value) do
for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do
child_constraints = (constraints[child_constraints] or {})[key]
if child_constraints then
val = fix_types(val, child_constraints)
end
end
if constraints.values then
val = fix_types(val, constraints.values)
end
if constraints.keys then
value[key] = nil
value[fix_types(key, constraints.keys)] = val
else
value[key] = val
end
end
end
return value
end
function check_constraints(value, constraints)
local t = type(value)
if constraints.type and constraints.type ~= t then
error("Wrong type: Expected "..constraints.type..", found "..t)
end
if (t == "number" or t == "string") and constraints.range then
if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then
error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value))
end
end
if t == "number" and constraints.int and value % 1 ~= 0 then
error("Not an integer number: " .. minetest.write_json(value))
end
if constraints.possible_values and not constraints.possible_values[value] then
error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value))
end
if t == "table" then
if constraints.children then
for key, val in pairs(value) do
local child_constraints = constraints.children[key]
if not child_constraints then
error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
else
check_constraints(val, child_constraints)
end
end
for key, _ in pairs(constraints.children) do
if value[key] == nil then
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
end
end
end
if constraints.required_children then
for key, value_constraints in pairs(constraints.required_children) do
local val = value[key]
if val then
check_constraints(val, value_constraints)
else
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
end
end
end
if constraints.possible_children then
for key, value_constraints in pairs(constraints.possible_children) do
local val = value[key]
if val then
check_constraints(val, value_constraints)
end
end
end
if constraints.keys then
for key,_ in pairs(value) do
check_constraints(key, constraints.keys)
end
end
if constraints.values then
for _, val in pairs(value) do
check_constraints(val, constraints.values)
end
end
end
if constraints.func then
local possible_errors = constraints.func(value)
if possible_errors then
error(possible_errors)
end
end
local t = type(value)
if constraints.type and constraints.type ~= t then
error("Wrong type: Expected "..constraints.type..", found "..t)
end
if (t == "number" or t == "string") and constraints.range then
if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then
error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value))
end
end
if t == "number" and constraints.int and value % 1 ~= 0 then
error("Not an integer number: " .. minetest.write_json(value))
end
if constraints.possible_values and not constraints.possible_values[value] then
error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value))
end
if t == "table" then
if constraints.children then
for key, val in pairs(value) do
local child_constraints = constraints.children[key]
if not child_constraints then
error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
else
check_constraints(val, child_constraints)
end
end
for key, _ in pairs(constraints.children) do
if value[key] == nil then
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
end
end
end
if constraints.required_children then
for key, value_constraints in pairs(constraints.required_children) do
local val = value[key]
if val then
check_constraints(val, value_constraints)
else
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
end
end
end
if constraints.possible_children then
for key, value_constraints in pairs(constraints.possible_children) do
local val = value[key]
if val then
check_constraints(val, value_constraints)
end
end
end
if constraints.keys then
for key,_ in pairs(value) do
check_constraints(key, constraints.keys)
end
end
if constraints.values then
for _, val in pairs(value) do
check_constraints(val, constraints.values)
end
end
end
if constraints.func then
local possible_errors = constraints.func(value)
if possible_errors then
error(possible_errors)
end
end
end

@ -1,19 +1,19 @@
minetest.mkdir(minetest.get_worldpath().."/data")
function create_mod_storage(modname)
minetest.mkdir(minetest.get_worldpath().."/data/"..modname)
end
function get_path(modname, filename)
return minetest.get_worldpath().."/data/"..modname.."/"..filename
end
function load(modname, filename)
return minetest.deserialize(modlib.file.read(get_path(modname, filename)..".lua"))
end
function save(modname, filename, stuff)
return modlib.file.write(get_path(modname, filename)..".lua", minetest.serialize(stuff))
end
function load_json(modname, filename)
return minetest.parse_json(modlib.file.read(get_path(modname, filename)..".json") or "null")
end
function save_json(modname, filename, stuff)
return modlib.file.write(get_path(modname, filename)..".json", minetest.write_json(stuff))
end

@ -15,11 +15,11 @@ function write(filename, new_content)
end
function ensure_content(filename, ensured_content)
local content = read(filename)
if content ~= ensured_content then
return write(filename, ensured_content)
end
return true
local content = read(filename)
if content ~= ensured_content then
return write(filename, ensured_content)
end
return true
end
function append(filename, new_content)

@ -4,47 +4,47 @@ function less_than(a, b) return a < b end
--> empty min heap
function new(less_than)
return setmetatable({less_than = less_than}, metatable)
return setmetatable({less_than = less_than}, metatable)
end
function push(self, value)
table.insert(self, value)
local function heapify(index)
if index == 1 then
return
end
local parent = math.floor(index / 2)
if self.less_than(self[index], self[parent]) then
self[parent], self[index] = self[index], self[parent]
heapify(parent)
end
end
heapify(#self)
table.insert(self, value)
local function heapify(index)
if index == 1 then
return
end
local parent = math.floor(index / 2)
if self.less_than(self[index], self[parent]) then
self[parent], self[index] = self[index], self[parent]
heapify(parent)
end
end
heapify(#self)
end
function pop(self)
local value = self[1]
local last = #self
if last == 1 then
self[1] = nil
return value
end
self[1], self[last] = self[last], nil
last = last - 1
local function heapify(index)
local left_child = index * 2
if left_child > last then
return
end
local smallest_child = left_child + 1
if smallest_child > last or self.less_than(self[left_child], self[smallest_child]) then
smallest_child = left_child
end
if self.less_than(self[smallest_child], self[index]) then
self[index], self[smallest_child] = self[smallest_child], self[index]
heapify(smallest_child)
end
end
heapify(1)
return value
local value = self[1]
local last = #self
if last == 1 then
self[1] = nil
return value
end
self[1], self[last] = self[last], nil
last = last - 1
local function heapify(index)
local left_child = index * 2
if left_child > last then
return
end
local smallest_child = left_child + 1
if smallest_child > last or self.less_than(self[left_child], self[smallest_child]) then
smallest_child = left_child
end
if self.less_than(self[smallest_child], self[index]) then
self[index], self[smallest_child] = self[smallest_child], self[index]
heapify(smallest_child)
end
end
heapify(1)
return value
end

212
init.lua

@ -1,123 +1,123 @@
-- Lua version check
if _VERSION then
if _VERSION < "Lua 5" then
error("Outdated Lua version! modlib requires Lua 5 or greater.")
end
if _VERSION > "Lua 5.1" then
-- not throwing error("Too new Lua version! modlib requires Lua 5.1 or smaller.") anymore
unpack = unpack or table.unpack -- unpack was moved to table.unpack in Lua 5.2
loadstring = load
function setfenv(fn, env)
local i = 1
while true do
name = debug.getupvalue(fn, i)
if name == "_ENV" then
debug.setupvalue(fn, i, env)
break
elseif not name then
break
end
end
return fn
end
function getfenv(fn)
local i = 1
local name, val
repeat
name, val = debug.getupvalue(fn, i)
if name == "_ENV" then
return val
end
i = i + 1
until not name
end
end
if _VERSION < "Lua 5" then
error("Outdated Lua version! modlib requires Lua 5 or greater.")
end
if _VERSION > "Lua 5.1" then
-- not throwing error("Too new Lua version! modlib requires Lua 5.1 or smaller.") anymore
unpack = unpack or table.unpack -- unpack was moved to table.unpack in Lua 5.2
loadstring = load
function setfenv(fn, env)
local i = 1
while true do
name = debug.getupvalue(fn, i)
if name == "_ENV" then
debug.setupvalue(fn, i, env)
break
elseif not name then
break
end
end
return fn
end
function getfenv(fn)
local i = 1
local name, val
repeat
name, val = debug.getupvalue(fn, i)
if name == "_ENV" then
return val
end
i = i + 1
until not name
end
end
end
modlib = {
dir_delim = rawget(_G, "DIR_DELIM") or "/",
_RG = setmetatable({}, {
__index = function(_, index)
return rawget(_G, index)
end,
__newindex = function(_, index, value)
return rawset(_G, index, value)
end
}),
assertdump = function(v, value)
if not v then
error(dump(value), 2)
end
end
dir_delim = rawget(_G, "DIR_DELIM") or "/",
_RG = setmetatable({}, {
__index = function(_, index)
return rawget(_G, index)
end,
__newindex = function(_, index, value)
return rawset(_G, index, value)
end
}),
assertdump = function(v, value)
if not v then
error(dump(value), 2)
end
end
}
local function get_resource(modname, resource, ...)
if not resource then
resource = modname
modname = minetest.get_current_modname()
end
return table.concat({minetest.get_modpath(modname), resource, ...}, modlib.dir_delim)
if not resource then
resource = modname
modname = minetest.get_current_modname()
end
return table.concat({minetest.get_modpath(modname), resource, ...}, modlib.dir_delim)
end
local function loadfile_exports(filename)
local env = setmetatable({}, {__index = _G})
local file = assert(loadfile(filename))
setfenv(file, env)
file()
return env
local env = setmetatable({}, {__index = _G})
local file = assert(loadfile(filename))
setfenv(file, env)
file()
return env
end
local minetest_only = {
mod = true,
minetest = true,
data = true,
log = true,
player = true,
-- not actually minetest-only, but a deprecated component
conf = true
mod = true,
minetest = true,
data = true,
log = true,
player = true,
-- not actually minetest-only, but a deprecated component
conf = true
}
for _, component in ipairs{
"mod",
"conf",
"schema",
"data",
"file",
"func",
"log",
"math",
"player",
"table",
"text",
"vector",
"quaternion",
{
name = "minetest",
"misc",
"collisionboxes",
"liquid",
"raycast",
"wielditem_change",
"colorspec"
},
"trie",
"kdtree",
"heap",
"ranked_set",
"binary",
"b3d",
"bluon"
"mod",
"conf",
"schema",
"data",
"file",
"func",
"log",
"math",
"player",
"table",
"text",
"vector",
"quaternion",
{
name = "minetest",
"misc",
"collisionboxes",
"liquid",
"raycast",
"wielditem_change",
"colorspec"
},
"trie",
"kdtree",
"heap",
"ranked_set",
"binary",
"b3d",
"bluon"
} do
if component.name then
if minetest then
modlib[component.name] = loadfile_exports(get_resource(minetest.get_current_modname(), component.name, component[1] .. ".lua"))
for index = 2, #component do
modlib.mod.include_env(get_resource(minetest.get_current_modname(), component.name, component[index] .. ".lua"), modlib[component.name])
end
end
elseif minetest or not minetest_only[component] then
local path = minetest and get_resource(component .. ".lua") or component .. ".lua"
modlib[component] = loadfile_exports(path)
end
if component.name then
if minetest then
modlib[component.name] = loadfile_exports(get_resource(minetest.get_current_modname(), component.name, component[1] .. ".lua"))
for index = 2, #component do
modlib.mod.include_env(get_resource(minetest.get_current_modname(), component.name, component[index] .. ".lua"), modlib[component.name])
end
end
elseif minetest or not minetest_only[component] then
local path = minetest and get_resource(component .. ".lua") or component .. ".lua"
modlib[component] = loadfile_exports(path)
end
end
-- Aliases
@ -125,10 +125,10 @@ modlib.string = modlib.text
modlib.number = modlib.math
if minetest then
modlib.conf.build_setting_tree()
modlib.conf.build_setting_tree()
modlib.mod.get_resource = get_resource
modlib.mod.loadfile_exports = loadfile_exports
modlib.mod.get_resource = get_resource
modlib.mod.loadfile_exports = loadfile_exports
end
_ml = modlib

@ -5,9 +5,9 @@ distance = modlib.vector.distance
--: vectors first vector is used to infer the dimension
--: distance (vector, other_vector) -> number, default: modlib.vector.distance
function new(vectors, distance)
assert(#vectors > 0, "vector list must not be empty")
local dimension = #vectors[1]
local function builder(vectors, axis)
assert(#vectors > 0, "vector list must not be empty")
local dimension = #vectors[1]
local function builder(vectors, axis)
if #vectors == 1 then return { value = vectors[1] } end
table.sort(vectors, function(a, b) return a[axis] > b[axis] end)
local median = math.floor(#vectors / 2)
@ -19,36 +19,36 @@ function new(vectors, distance)
right = builder({ unpack(vectors, median + 1) }, next_axis)
}, metatable)
end
local self = builder(vectors, 1)
self.distance = distance
return setmetatable(self, metatable)
local self = builder(vectors, 1)
self.distance = distance
return setmetatable(self, metatable)
end
function get_nearest_neighbor(self, vector)
local min_distance = math.huge
local nearest_neighbor
local distance_func = self.distance
local function visit(tree)
local axis = tree.axis
if tree.value ~= nil then
local distance = distance_func(tree.value, vector)
if distance < min_distance then
min_distance = distance
nearest_neighbor = tree.value
end
return
else
local this_side, other_side = tree.left, tree.right
if vector[axis] < tree.pivot[axis] then this_side, other_side = other_side, this_side end
visit(this_side)
if tree.pivot then
local dist = math.abs(tree.pivot[axis] - vector[axis])
if dist <= min_distance then visit(other_side) end
end
end
end
visit(self)
return nearest_neighbor, min_distance
local min_distance = math.huge
local nearest_neighbor
local distance_func = self.distance
local function visit(tree)
local axis = tree.axis
if tree.value ~= nil then
local distance = distance_func(tree.value, vector)
if distance < min_distance then
min_distance = distance
nearest_neighbor = tree.value
end
return
else
local this_side, other_side = tree.left, tree.right
if vector[axis] < tree.pivot[axis] then this_side, other_side = other_side, this_side end
visit(this_side)
if tree.pivot then
local dist = math.abs(tree.pivot[axis] - vector[axis])
if dist <= min_distance then visit(other_side) end
end
end
end
visit(self)
return nearest_neighbor, min_distance
end
-- TODO insertion & deletion + rebalancing

94
log.lua

@ -3,74 +3,74 @@ minetest.mkdir(minetest.get_worldpath() .. "/logs")
channels = {}
last_day = os.date("%d")
function get_path(logname)
return minetest.get_worldpath() .. "/logs/" .. logname
return minetest.get_worldpath() .. "/logs/" .. logname
end
function create_channel(title)
local dir = get_path(title)
minetest.mkdir(dir)
channels[title] = {dirname = dir, queue = {}}
write(title, "Initialisation")
local dir = get_path(title)
minetest.mkdir(dir)
channels[title] = {dirname = dir, queue = {}}
write(title, "Initialisation")
end
function write(channelname, msg)
local channel = channels[channelname]
local current_day = os.date("%d")
if current_day ~= last_day then
last_day = current_day
write_to_file(channelname, channel, os.date("%Y-%m-%d"))
end
table.insert(channel.queue, os.date("[%H:%M:%S] ") .. msg)
local channel = channels[channelname]
local current_day = os.date("%d")
if current_day ~= last_day then
last_day = current_day
write_to_file(channelname, channel, os.date("%Y-%m-%d"))
end
table.insert(channel.queue, os.date("[%H:%M:%S] ") .. msg)
end
function write_to_all(msg)
for channelname, _ in pairs(channels) do
write(channelname, msg)
end
for channelname, _ in pairs(channels) do
write(channelname, msg)
end
end
function write_to_file(name, channel, current_date)
if not channel then
channel = channels[name]
end
if #(channel.queue) > 0 then
local filename = channel.dirname .. "/" .. (current_date or os.date("%Y-%m-%d")) .. ".txt"
local rope = {}
for _, msg in ipairs(channel.queue) do
table.insert(rope, msg)
end
modlib.file.append(filename, table.concat(rope, "\n") .. "\n")
channels[name].queue = {}
end
if not channel then
channel = channels[name]
end
if #(channel.queue) > 0 then
local filename = channel.dirname .. "/" .. (current_date or os.date("%Y-%m-%d")) .. ".txt"
local rope = {}
for _, msg in ipairs(channel.queue) do
table.insert(rope, msg)
end
modlib.file.append(filename, table.concat(rope, "\n") .. "\n")
channels[name].queue = {}
end
end
function write_all_to_file()
local current_date = os.date("%Y-%m-%d")
for name, channel in pairs(channels) do
write_to_file(name, channel, current_date)
end
local current_date = os.date("%Y-%m-%d")
for name, channel in pairs(channels) do
write_to_file(name, channel, current_date)
end
end
function write_safe(channelname, msg)
write(channelname, msg)
write_all_to_file()
write(channelname, msg)
write_all_to_file()
end
local timer = 0
minetest.register_globalstep(
function(dtime)
timer = timer + dtime
if timer > 5 then
write_all_to_file()
timer = 0
end
end
function(dtime)
timer = timer + dtime
if timer > 5 then
write_all_to_file()
timer = 0
end
end
)
minetest.register_on_mods_loaded(
function()
write_to_all("Mods loaded")
end
function()
write_to_all("Mods loaded")
end
)
minetest.register_on_shutdown(
function()
write_to_all("Shutdown")
write_all_to_file()
end
function()
write_to_all("Shutdown")
write_all_to_file()
end
)

@ -1,135 +1,135 @@
-- Minetest allows shorthand collisionbox = {...} instead of {{...}}
local function get_collisionboxes(box_or_boxes)
return type(box_or_boxes[1]) == "number" and {box_or_boxes} or box_or_boxes
return type(box_or_boxes[1]) == "number" and {box_or_boxes} or box_or_boxes
end
--> list of collisionboxes in Minetest format
function get_node_collisionboxes(pos)
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if (not node_def) or node_def.walkable == false then
return {}
end
local boxes = {{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}}
local def_collision_box = node_def.collision_box or (node_def.drawtype == "nodebox" and node_def.node_box)
if def_collision_box then
local box_type = def_collision_box.type
if box_type == "regular" then
return boxes
end
local fixed = def_collision_box.fixed
boxes = get_collisionboxes(fixed or {})
local paramtype2 = node_def.paramtype2
if box_type == "leveled" then
boxes = table.copy(boxes)
local level = (paramtype2 == "leveled" and node.param2 or node_def.leveled or 0) / 255 - 0.5
for _, box in pairs(boxes) do
box[5] = level
end
elseif box_type == "wallmounted" then
-- TODO complete if only wall_top is given
local dir = minetest.wallmounted_to_dir((paramtype2 == "colorwallmounted" and node.param2 % 8 or node.param2) or 0)
local box
if dir.y > 0 then
box = def_collision_box.wall_top
elseif dir.y < 0 then
box = def_collision_box.wall_bottom
else
box = def_collision_box.wall_side
if dir.z > 0 then
box = {box[3], box[2], -box[4], box[6], box[5], -box[1]}
elseif dir.z < 0 then
box = {-box[6], box[2], box[1], -box[3], box[5], box[4]}
elseif dir.x > 0 then
box = {-box[4], box[2], box[3], -box[1], box[5], box[6]}
else
box = {box[1], box[2], -box[6], box[4], box[5], -box[3]}
end
end
return {assert(box, "incomplete wallmounted collisionbox definition of " .. node.name)}
end
if box_type == "connected" then
boxes = table.copy(boxes)
local connect_sides = {
top = {x = 0, y = 1, z = 0},
bottom = {x = 0, y = -1, z = 0},
front = {x = 0, y = 0, z = -1},
left = {x = -1, y = 0, z = 0},
back = {x = 0, y = 0, z = 1},
right = {x = 1, y = 0, z = 0}
}
if node_def.connect_sides then
for side in pairs(connect_sides) do
if not node_def.connect_sides[side] then
connect_sides[side] = nil
end
end
end
local function add_collisionbox(key)
for _, box in ipairs(get_collisionboxes(def_collision_box[key] or {})) do
table.insert(boxes, box)
end
end
local matchers = {}
for _, nodename_or_group in pairs(node_def.connects_to or {}) do
table.insert(matchers, nodename_matcher(nodename_or_group))
end
local function connects_to(nodename)
for _, matcher in pairs(matchers) do
if matcher(nodename) then
return true
end
end
end
local connected, connected_sides
for side, direction in pairs(connect_sides) do
local neighbor = minetest.get_node(vector.add(pos, direction))
local connects = connects_to(neighbor.name)
connected = connected or connects
connected_sides = connected_sides or (side ~= "top" and side ~= "bottom")
add_collisionbox((connects and "connect_" or "disconnected_") .. side)
end
if not connected then
add_collisionbox("disconnected")
end
if not connected_sides then
add_collisionbox("disconnected_sides")
end
return boxes
end
if box_type == "fixed" and paramtype2 == "facedir" or paramtype2 == "colorfacedir" then
local param2 = paramtype2 == "colorfacedir" and node.param2 % 32 or node.param2 or 0
if param2 ~= 0 then
boxes = table.copy(boxes)
local axis = ({5, 6, 3, 4, 1, 2})[math.floor(param2 / 4) + 1]
local other_axis_1, other_axis_2 = (axis % 3) + 1, ((axis + 1) % 3) + 1
local rotation = (param2 % 4) / 2 * math.pi
local flip = axis > 3
if flip then axis = axis - 3; rotation = -rotation end
local sin, cos = math.sin(rotation), math.cos(rotation)
if axis == 2 then
sin = -sin
end
for _, box in pairs(boxes) do
for off = 0, 3, 3 do
local axis_1, axis_2 = other_axis_1 + off, other_axis_2 + off
local value_1, value_2 = box[axis_1], box[axis_2]
box[axis_1] = value_1 * cos - value_2 * sin
box[axis_2] = value_1 * sin + value_2 * cos
end
if not flip then
box[axis], box[axis + 3] = -box[axis + 3], -box[axis]
end
local function fix(coord)
if box[coord] > box[coord + 3] then
box[coord], box[coord + 3] = box[coord + 3], box[coord]
end
end
fix(other_axis_1)
fix(other_axis_2)
end
end
end
end
return boxes
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if (not node_def) or node_def.walkable == false then
return {}
end
local boxes = {{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}}
local def_collision_box = node_def.collision_box or (node_def.drawtype == "nodebox" and node_def.node_box)
if def_collision_box then
local box_type = def_collision_box.type
if box_type == "regular" then
return boxes
end
local fixed = def_collision_box.fixed
boxes = get_collisionboxes(fixed or {})
local paramtype2 = node_def.paramtype2
if box_type == "leveled" then
boxes = table.copy(boxes)
local level = (paramtype2 == "leveled" and node.param2 or node_def.leveled or 0) / 255 - 0.5
for _, box in pairs(boxes) do
box[5] = level
end
elseif box_type == "wallmounted" then
-- TODO complete if only wall_top is given
local dir = minetest.wallmounted_to_dir((paramtype2 == "colorwallmounted" and node.param2 % 8 or node.param2) or 0)
local box
if dir.y > 0 then
box = def_collision_box.wall_top
elseif dir.y < 0 then
box = def_collision_box.wall_bottom
else
box = def_collision_box.wall_side
if dir.z > 0 then
box = {box[3], box[2], -box[4], box[6], box[5], -box[1]}
elseif dir.z < 0 then
box = {-box[6], box[2], box[1], -box[3], box[5], box[4]}
elseif dir.x > 0 then
box = {-box[4], box[2], box[3], -box[1], box[5], box[6]}
else
box = {box[1], box[2], -box[6], box[4], box[5], -box[3]}
end
end
return {assert(box, "incomplete wallmounted collisionbox definition of " .. node.name)}
end
if box_type == "connected" then
boxes = table.copy(boxes)
local connect_sides = {
top = {x = 0, y = 1, z = 0},
bottom = {x = 0, y = -1, z = 0},
front = {x = 0, y = 0, z = -1},
left = {x = -1, y = 0, z = 0},
back = {x = 0, y = 0, z = 1},
right = {x = 1, y = 0, z = 0}
}
if node_def.connect_sides then
for side in pairs(connect_sides) do
if not node_def.connect_sides[side] then
connect_sides[side] = nil
end
end
end
local function add_collisionbox(key)
for _, box in ipairs(get_collisionboxes(def_collision_box[key] or {})) do
table.insert(boxes, box)
end
end
local matchers = {}
for _, nodename_or_group in pairs(node_def.connects_to or {}) do
table.insert(matchers, nodename_matcher(nodename_or_group))
end
local function connects_to(nodename)
for _, matcher in pairs(matchers) do
if matcher(nodename) then
return true
end
end
end
local connected, connected_sides
for side, direction in pairs(connect_sides) do
local neighbor = minetest.get_node(vector.add(pos, direction))
local connects = connects_to(neighbor.name)
connected = connected or connects
connected_sides = connected_sides or (side ~= "top" and side ~= "bottom")
add_collisionbox((connects and "connect_" or "disconnected_") .. side)
end
if not connected then
add_collisionbox("disconnected")
end
if not connected_sides then
add_collisionbox("disconnected_sides")
end
return boxes
end
if box_type == "fixed" and paramtype2 == "facedir" or paramtype2 == "colorfacedir" then
local param2 = paramtype2 == "colorfacedir" and node.param2 % 32 or node.param2 or 0
if param2 ~= 0 then
boxes = table.copy(boxes)
local axis = ({5, 6, 3, 4, 1, 2})[math.floor(param2 / 4) + 1]
local other_axis_1, other_axis_2 = (axis % 3) + 1, ((axis + 1) % 3) + 1
local rotation = (param2 % 4) / 2 * math.pi
local flip = axis > 3
if flip then axis = axis - 3; rotation = -rotation end
local sin, cos = math.sin(rotation), math.cos(rotation)
if axis == 2 then
sin = -sin
end
for _, box in pairs(boxes) do
for off = 0, 3, 3 do
local axis_1, axis_2 = other_axis_1 + off, other_axis_2 + off
local value_1, value_2 = box[axis_1], box[axis_2]
box[axis_1] = value_1 * cos - value_2 * sin
box[axis_2] = value_1 * sin + value_2 * cos
end
if not flip then
box[axis], box[axis + 3] = -box[axis + 3], -box[axis]
end
local function fix(coord)
if box[coord] > box[coord + 3] then
box[coord], box[coord + 3] = box[coord + 3], box[coord]
end
end
fix(other_axis_1)
fix(other_axis_2)
end
end
end
end
return boxes
end

@ -154,107 +154,107 @@ colorspec = {}
local colorspec_metatable = {__index = colorspec}
function colorspec.new(table)
return setmetatable({
r = assert(table.r),
g = assert(table.g),
b = assert(table.b),
a = table.a or 255
}, colorspec_metatable)
return setmetatable({
r = assert(table.r),
g = assert(table.g),
b = assert(table.b),
a = table.a or 255
}, colorspec_metatable)
end
colorspec.from_table = colorspec.new
function colorspec.from_string(string)
local hex = "#([A-Fa-f%d]+)"
local number, alpha = named_colors[string], 0xFF
if not number then
local name, alpha_text = string:match("^([a-z]+)" .. hex .. "$")
local hex = "#([A-Fa-f%d]+)"
local number, alpha = named_colors[string], 0xFF
if not number then
local name, alpha_text = string:match("^([a-z]+)" .. hex .. "$")
if name then
assert(alpha_text:len() == 2)
number = assert(named_colors[name])
alpha = tonumber(alpha_text, 16)
end
end
if number then
return colorspec.from_number(number * 0x100 + alpha)
end
local hex_text = string:match(hex)
local len, num = hex_text:len(), tonumber(hex_text, 16)
if len == 8 then
return colorspec.from_number(num)
end
if len == 6 then
return colorspec.from_number(num * 0x100 + 0xFF)
end
local floor = math.floor
if len == 4 then
return colorspec.from_table{
a = (num % 16) * 17,
b = (floor(num / 16) % 16) * 17,
g = (floor(num / (16 ^ 2)) % 16) * 17,
r = (floor(num / (16 ^ 3)) % 16) * 17
}
end
if len == 3 then
return colorspec.from_table{
b = (num % 16) * 17,
g = (floor(num / 16) % 16) * 17,
r = (floor(num / (16 ^ 2)) % 16) * 17
}
end
error("Invalid colorstring: " .. string)
end
if number then
return colorspec.from_number(number * 0x100 + alpha)
end
local hex_text = string:match(hex)
local len, num = hex_text:len(), tonumber(hex_text, 16)
if len == 8 then
return colorspec.from_number(num)
end
if len == 6 then
return colorspec.from_number(num * 0x100 + 0xFF)
end
local floor = math.floor
if len == 4 then
return colorspec.from_table{
a = (num % 16) * 17,
b = (floor(num / 16) % 16) * 17,
g = (floor(num / (16 ^ 2)) % 16) * 17,
r = (floor(num / (16 ^ 3)) % 16) * 17
}
end
if len == 3 then
return colorspec.from_table{
b = (num % 16) * 17,
g = (floor(num / 16) % 16) * 17,
r = (floor(num / (16 ^ 2)) % 16) * 17
}
end
error("Invalid colorstring: " .. string)
end
colorspec.from_text = colorspec.from_string
function colorspec.from_number(number)
local floor = math.floor
return colorspec.from_table{
a = number % 0x100,
b = floor(number / 0x100) % 0x100,
g = floor(number / 0x10000) % 0x100,
r = floor(number / 0x1000000)
}
local floor = math.floor
return colorspec.from_table{
a = number % 0x100,
b = floor(number / 0x100) % 0x100,
g = floor(number / 0x10000) % 0x100,
r = floor(number / 0x1000000)
}
end
function colorspec.from_number_rgb(number)
local floor = math.floor
return colorspec.from_table{
a = 0xFF,
b = number % 0x100,
g = floor(number / 0x100) % 0x100,
r = floor(number / 0x10000)
}
local floor = math.floor
return colorspec.from_table{
a = 0xFF,
b = number % 0x100,
g = floor(number / 0x100) % 0x100,
r = floor(number / 0x10000)
}
end
function colorspec.from_any(value)
local type = type(value)
if type == "table" then
return colorspec.from_table(value)
end
if type == "string" then
return colorspec.from_string(value)
end
if type == "number" then
return colorspec.from_number(value)
end
error("Unsupported type " .. type)
local type = type(value)
if type == "table" then
return colorspec.from_table(value)
end
if type == "string" then
return colorspec.from_string(value)
end
if type == "number" then
return colorspec.from_number(value)
end
error("Unsupported type " .. type)
end
function colorspec:to_table()
return self
return self
end
--> hex string, omits alpha if possible (if opaque)
function colorspec:to_string()
if self.a == 255 then
return ("%02X02X02X"):format(self.r, self.g, self.b)
end
return ("%02X02X02X02X"):format(self.r, self.g, self.b, self.a)
if self.a == 255 then
return ("%02X02X02X"):format(self.r, self.g, self.b)
end
return ("%02X02X02X02X"):format(self.r, self.g, self.b, self.a)
end
function colorspec:to_number()
return self.r * 0x1000000 + self.g * 0x10000 + self.b * 0x100 + self.a
return self.r * 0x1000000 + self.g * 0x10000 + self.b * 0x100 + self.a
end
function colorspec:to_number_rgb()
@ -262,5 +262,5 @@ function colorspec:to_number_rgb()
end
colorspec_to_colorstring = minetest.colorspec_to_colorstring or function(spec)
return colorspec.from_any(spec):to_string()
return colorspec.from_any(spec):to_string()
end

@ -2,73 +2,73 @@ liquid_level_max = 8
--+ Calculates the corner levels of a flowingliquid node
--> 4 corner levels from -0.5 to 0.5 as list of `modlib.vector`
function get_liquid_corner_levels(pos)
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
local source, flowing = def.liquid_alternative_source, node.name
local range = def.liquid_range or liquid_level_max
local neighbors = {}
for x = -1, 1 do
neighbors[x] = {}
for z = -1, 1 do
local neighbor_pos = {x = pos.x + x, y = pos.y, z = pos.z + z}
local neighbor_node = minetest.get_node(neighbor_pos)
local level
if neighbor_node.name == source then
level = 1
elseif neighbor_node.name == flowing then
local neighbor_level = neighbor_node.param2 % 8
level = (math.max(0, neighbor_level - liquid_level_max + range) + 0.5) / range
end
neighbor_pos.y = neighbor_pos.y + 1
local node_above = minetest.get_node(neighbor_pos)
neighbors[x][z] = {
air = neighbor_node.name == "air",
level = level,
above_is_same_liquid = node_above.name == flowing or node_above.name == source
}
end
end
local function get_corner_level(x, z)
local air_neighbor
local levels = 0
local neighbor_count = 0
for nx = x - 1, x do
for nz = z - 1, z do
local neighbor = neighbors[nx][nz]
if neighbor.above_is_same_liquid then
return 1
end
local level = neighbor.level
if level then
if level == 1 then
return 1
end
levels = levels + level
neighbor_count = neighbor_count + 1
elseif neighbor.air then
if air_neighbor then
return 0.02
end
air_neighbor = true
end
end
end
if neighbor_count == 0 then
return 0
end
return levels / neighbor_count
end
local corner_levels = {
{0, nil, 0},
{1, nil, 0},
{1, nil, 1},
{0, nil, 1}
}
for index, corner_level in pairs(corner_levels) do
corner_level[2] = get_corner_level(corner_level[1], corner_level[3])
corner_levels[index] = modlib.vector.subtract_scalar(modlib.vector.new(corner_level), 0.5)
end
return corner_levels
local node = minetest.get_node(pos)
local def = minetest.registered_nodes[node.name]
local source, flowing = def.liquid_alternative_source, node.name
local range = def.liquid_range or liquid_level_max
local neighbors = {}
for x = -1, 1 do
neighbors[x] = {}
for z = -1, 1 do
local neighbor_pos = {x = pos.x + x, y = pos.y, z = pos.z + z}
local neighbor_node = minetest.get_node(neighbor_pos)
local level
if neighbor_node.name == source then
level = 1
elseif neighbor_node.name == flowing then
local neighbor_level = neighbor_node.param2 % 8
level = (math.max(0, neighbor_level - liquid_level_max + range) + 0.5) / range
end
neighbor_pos.y = neighbor_pos.y + 1
local node_above = minetest.get_node(neighbor_pos)
neighbors[x][z] = {
air = neighbor_node.name == "air",
level = level,
above_is_same_liquid = node_above.name == flowing or node_above.name == source
}
end
end
local function get_corner_level(x, z)
local air_neighbor
local levels = 0
local neighbor_count = 0
for nx = x - 1, x do
for nz = z - 1, z do
local neighbor = neighbors[nx][nz]
if neighbor.above_is_same_liquid then
return 1
end
local level = neighbor.level
if level then
if level == 1 then
return 1
end
levels = levels + level
neighbor_count = neighbor_count + 1
elseif neighbor.air then
if air_neighbor then
return 0.02
end
air_neighbor = true
end
end
end
if neighbor_count == 0 then
return 0
end
return levels / neighbor_count
end
local corner_levels = {
{0, nil, 0},
{1, nil, 0},
{1, nil, 1},
{0, nil, 1}
}
for index, corner_level in pairs(corner_levels) do
corner_level[2] = get_corner_level(corner_level[1], corner_level[3])
corner_levels[index] = modlib.vector.subtract_scalar(modlib.vector.new(corner_level), 0.5)
end
return corner_levels
end
flowing_downwards = modlib.vector.new{0, -1, 0}
@ -76,40 +76,40 @@ flowing_downwards = modlib.vector.new{0, -1, 0}
--> `modlib.minetest.flowing_downwards = modlib.vector.new{0, -1, 0}` if only flowing downwards
--> surface direction as `modlib.vector` else
function get_liquid_flow_direction(pos)
local corner_levels = get_liquid_corner_levels(pos)
local max_level = corner_levels[1][2]
for index = 2, 4 do
local level = corner_levels[index][2]
if level > max_level then
max_level = level
end
end
local dir = modlib.vector.new{0, 0, 0}
local count = 0
for max_level_index, corner_level in pairs(corner_levels) do
if corner_level[2] == max_level then
for offset = 1, 3 do
local index = (max_level_index + offset - 1) % 4 + 1
local diff = corner_level - corner_levels[index]
if diff[2] ~= 0 then
diff[1] = diff[1] * diff[2]
diff[3] = diff[3] * diff[2]
if offset == 3 then
diff = modlib.vector.divide_scalar(diff, math.sqrt(2))
end
dir = dir + diff
count = count + 1
end
end
end
end
if count ~= 0 then
dir = modlib.vector.divide_scalar(dir, count)
end
if dir == modlib.vector.new{0, 0, 0} then
if minetest.get_node(pos).param2 % 32 > 7 then
return flowing_downwards
end
end
return dir
local corner_levels = get_liquid_corner_levels(pos)
local max_level = corner_levels[1][2]
for index = 2, 4 do
local level = corner_levels[index][2]
if level > max_level then
max_level = level
end
end
local dir = modlib.vector.new{0, 0, 0}
local count = 0
for max_level_index, corner_level in pairs(corner_levels) do
if corner_level[2] == max_level then
for offset = 1, 3 do
local index = (max_level_index + offset - 1) % 4 + 1
local diff = corner_level - corner_levels[index]
if diff[2] ~= 0 then
diff[1] = diff[1] * diff[2]
diff[3] = diff[3] * diff[2]
if offset == 3 then
diff = modlib.vector.divide_scalar(diff, math.sqrt(2))
end
dir = dir + diff
count = count + 1
end
end
end
end
if count ~= 0 then
dir = modlib.vector.divide_scalar(dir, count)
end
if dir == modlib.vector.new{0, 0, 0} then
if minetest.get_node(pos).param2 % 32 > 7 then
return flowing_downwards
end
end
return dir
end

@ -1,40 +1,40 @@
max_wear = 2 ^ 16 - 1
function override(function_name, function_builder)
local func = minetest[function_name]
minetest["original_" .. function_name] = func
minetest[function_name] = function_builder(func)
local func = minetest[function_name]
minetest["original_" .. function_name] = func
minetest[function_name] = function_builder(func)
end
-- TODO fix modlib.minetest.get_gametime() messing up responsible "mod" determined by engine on crash
get_gametime = minetest.get_gametime
local get_gametime_initialized
local function get_gametime_init(dtime)
if get_gametime_initialized then
-- if the profiler is being used, the globalstep can't be unregistered
return
end
get_gametime_initialized = true
assert(dtime == 0)
local gametime = minetest.get_gametime()
assert(gametime)
function modlib.minetest.get_gametime()
local imprecise_gametime = minetest.get_gametime()
if imprecise_gametime > gametime then
minetest.log("warning", "modlib.minetest.get_gametime(): Called after increment and before first globalstep")
return imprecise_gametime
end
return gametime
end
for index, globalstep in pairs(minetest.registered_globalsteps) do
if globalstep == get_gametime_init then
table.remove(minetest.registered_globalsteps, index)
break
end
end
-- globalsteps of mods which depend on modlib will execute after this
minetest.register_globalstep(function(dtime)
gametime = gametime + dtime
end)
if get_gametime_initialized then
-- if the profiler is being used, the globalstep can't be unregistered
return
end
get_gametime_initialized = true
assert(dtime == 0)
local gametime = minetest.get_gametime()
assert(gametime)
function modlib.minetest.get_gametime()
local imprecise_gametime = minetest.get_gametime()
if imprecise_gametime > gametime then
minetest.log("warning", "modlib.minetest.get_gametime(): Called after increment and before first globalstep")
return imprecise_gametime
end
return gametime
end
for index, globalstep in pairs(minetest.registered_globalsteps) do
if globalstep == get_gametime_init then
table.remove(minetest.registered_globalsteps, index)
break
end
end
-- globalsteps of mods which depend on modlib will execute after this
minetest.register_globalstep(function(dtime)
gametime = gametime + dtime
end)
end
minetest.register_globalstep(get_gametime_init)
@ -42,156 +42,156 @@ delta_times={}
delays={}
callbacks={}
function register_globalstep(interval, callback)
if type(callback) ~= "function" then
return
end
table.insert(delta_times, 0)
table.insert(delays, interval)
table.insert(callbacks, callback)
if type(callback) ~= "function" then
return
end
table.insert(delta_times, 0)
table.insert(delays, interval)
table.insert(callbacks, callback)
end
function texture_modifier_inventorycube(face_1, face_2, face_3)
return "[inventorycube{" .. string.gsub(face_1, "%^", "&")
.. "{" .. string.gsub(face_2, "%^", "&")
.. "{" .. string.gsub(face_3, "%^", "&")
return "[inventorycube{" .. string.gsub(face_1, "%^", "&")
.. "{" .. string.gsub(face_2, "%^", "&")
.. "{" .. string.gsub(face_3, "%^", "&")
end
function get_node_inventory_image(nodename)
local n = minetest.registered_nodes[nodename]
if not n then
return
end
local tiles = {}
for l, tile in pairs(n.tiles or {}) do
tiles[l] = (type(tile) == "string" and tile) or tile.name
end
local chosen_tiles = { tiles[1], tiles[3], tiles[5] }
if #chosen_tiles == 0 then
return false
end
if not chosen_tiles[2] then
chosen_tiles[2] = chosen_tiles[1]
end
if not chosen_tiles[3] then
chosen_tiles[3] = chosen_tiles[2]
end
local img = minetest.registered_items[nodename].inventory_image
if string.len(img) == 0 then
img = nil
end
return img or texture_modifier_inventorycube(chosen_tiles[1], chosen_tiles[2], chosen_tiles[3])
local n = minetest.registered_nodes[nodename]
if not n then
return
end
local tiles = {}
for l, tile in pairs(n.tiles or {}) do
tiles[l] = (type(tile) == "string" and tile) or tile.name
end
local chosen_tiles = { tiles[1], tiles[3], tiles[5] }
if #chosen_tiles == 0 then
return false
end
if not chosen_tiles[2] then
chosen_tiles[2] = chosen_tiles[1]
end
if not chosen_tiles[3] then
chosen_tiles[3] = chosen_tiles[2]
end
local img = minetest.registered_items[nodename].inventory_image
if string.len(img) == 0 then
img = nil
end
return img or texture_modifier_inventorycube(chosen_tiles[1], chosen_tiles[2], chosen_tiles[3])
end
function get_color_int(color)
return color.b + (color.g*256) + (color.r*256*256)
return color.b + (color.g*256) + (color.r*256*256)
end
function check_player_privs(playername, privtable)
local privs=minetest.get_player_privs(playername)
local missing_privs={}
local to_lose_privs={}
for priv, expected_value in pairs(privtable) do
local actual_value=privs[priv]
if expected_value then
if not actual_value then
table.insert(missing_privs, priv)
end
else
if actual_value then
table.insert(to_lose_privs, priv)
end
end
end
return missing_privs, to_lose_privs
local privs=minetest.get_player_privs(playername)
local missing_privs={}
local to_lose_privs={}
for priv, expected_value in pairs(privtable) do
local actual_value=privs[priv]
if expected_value then
if not actual_value then
table.insert(missing_privs, priv)
end
else
if actual_value then
table.insert(to_lose_privs, priv)
end
end
end
return missing_privs, to_lose_privs
end
minetest.register_globalstep(function(dtime)
for k, v in pairs(delta_times) do
local v=dtime+v
if v > delays[k] then
callbacks[k](v)
v=0
end
delta_times[k]=v
end
for k, v in pairs(delta_times) do
local v=dtime+v
if v > delays[k] then
callbacks[k](v)
v=0
end
delta_times[k]=v
end
end)
form_listeners = {}
function register_form_listener(formname, func)
local current_listeners = form_listeners[formname] or {}
table.insert(current_listeners, func)
form_listeners[formname] = current_listeners
local current_listeners = form_listeners[formname] or {}
table.insert(current_listeners, func)
form_listeners[formname] = current_listeners
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
local handlers = form_listeners[formname]
if handlers then
for _, handler in pairs(handlers) do
handler(player, fields)
end
end
local handlers = form_listeners[formname]
if handlers then
for _, handler in pairs(handlers) do
handler(player, fields)
end
end
end)
--+ Improved base64 decode removing valid padding
function decode_base64(base64)
local len = base64:len()
local padding_char = base64:sub(len, len) == "="
if padding_char then
if len % 4 ~= 0 then
return
end
if base64:sub(len-1, len-1) == "=" then
base64 = base64:sub(1, len-2)
else
base64 = base64:sub(1, len-1)
end
end
return minetest.decode_base64(base64)
local len = base64:len()
local padding_char = base64:sub(len, len) == "="
if padding_char then
if len % 4 ~= 0 then
return
end
if base64:sub(len-1, len-1) == "=" then
base64 = base64:sub(1, len-2)
else
base64 = base64:sub(1, len-1)
end
end
return minetest.decode_base64(base64)
end
local object_refs = minetest.object_refs
--+ Objects inside radius iterator. Uses a linear search.
function objects_inside_radius(pos, radius)
radius = radius^2
local id, object, object_pos
return function()
repeat
id, object = next(object_refs, id)
object_pos = object:get_pos()
until (not object) or ((pos.x-object_pos.x)^2 + (pos.y-object_pos.y)^2 + (pos.z-object_pos.z)^2) <= radius
return object
end
radius = radius^2
local id, object, object_pos
return function()
repeat
id, object = next(object_refs, id)
object_pos = object:get_pos()
until (not object) or ((pos.x-object_pos.x)^2 + (pos.y-object_pos.y)^2 + (pos.z-object_pos.z)^2) <= radius
return object
end
end
--+ Objects inside area iterator. Uses a linear search.
function objects_inside_area(min, max)
local id, object, object_pos
return function()
repeat
id, object = next(object_refs, id)
object_pos = object:get_pos()
until (not object) or (
(min.x <= object_pos.x and min.y <= object_pos.y and min.z <= object_pos.z)
and
(max.y >= object_pos.x and max.y >= object_pos.y and max.z >= object_pos.z)
)
return object
end
local id, object, object_pos
return function()
repeat
id, object = next(object_refs, id)
object_pos = object:get_pos()
until (not object) or (
(min.x <= object_pos.x and min.y <= object_pos.y and min.z <= object_pos.z)
and
(max.y >= object_pos.x and max.y >= object_pos.y and max.z >= object_pos.z)
)
return object
end
end
--: node_or_groupname "modname:nodename", "group:groupname[,groupname]"
--> function(nodename) -> whether node matches
function nodename_matcher(node_or_groupname)
if modlib.text.starts_with(node_or_groupname, "group:") then
-- TODO consider using modlib.text.split instead of Minetest's string.split
local groups = node_or_groupname:sub(("group:"):len() + 1):split(",")
return function(nodename)
for _, groupname in pairs(groups) do
if minetest.get_item_group(nodename, groupname) == 0 then
return false
end
end
return true
end
else
return function(nodename)
return nodename == node_or_groupname
end
end
if modlib.text.starts_with(node_or_groupname, "group:") then
-- TODO consider using modlib.text.split instead of Minetest's string.split
local groups = node_or_groupname:sub(("group:"):len() + 1):split(",")
return function(nodename)
for _, groupname in pairs(groups) do
if minetest.get_item_group(nodename, groupname) == 0 then
return false
end
end
return true
end
else
return function(nodename)
return nodename == node_or_groupname
end
end
end

@ -1,132 +1,132 @@
--+ Raycast wrapper with proper flowingliquid intersections
function raycast(_pos1, _pos2, objects, liquids)
local raycast = minetest.raycast(_pos1, _pos2, objects, liquids)
if not liquids then
return raycast
end
local pos1 = modlib.vector.from_minetest(_pos1)
local _direction = vector.direction(_pos1, _pos2)
local direction = modlib.vector.from_minetest(_direction)
local length = vector.distance(_pos1, _pos2)
local function next()
for pointed_thing in raycast do
if pointed_thing.type ~= "node" then
return pointed_thing
end
local _pos = pointed_thing.under
local pos = modlib.vector.from_minetest(_pos)
local node = minetest.get_node(_pos)
local def = minetest.registered_nodes[node.name]
if not (def and def.drawtype == "flowingliquid") then return pointed_thing end
local corner_levels = get_liquid_corner_levels(_pos)
local full_corner_levels = true
for _, corner_level in pairs(corner_levels) do
if corner_level[2] < 0.5 then
full_corner_levels = false
break
end
end
if full_corner_levels then
return pointed_thing
end
local relative = pos1 - pos
local inside = true
for _, prop in pairs(relative) do
if prop <= -0.5 or prop >= 0.5 then
inside = false
break
end
end
local function level(x, z)
local function distance_squared(corner)
return (x - corner[1]) ^ 2 + (z - corner[3]) ^ 2
end
local irrelevant_corner, distance = 1, distance_squared(corner_levels[1])
for index = 2, 4 do
local other_distance = distance_squared(corner_levels[index])
if other_distance > distance then
irrelevant_corner, distance = index, other_distance
end
end
local function corner(off)
return corner_levels[((irrelevant_corner + off) % 4) + 1]
end
local base = corner(2)
local edge_1, edge_2 = corner(1) - base, corner(3) - base
-- Properly selected edges will have a total length of 2
assert(math.abs(edge_1[1] + edge_1[3]) + math.abs(edge_2[1] + edge_2[3]) == 2)
if edge_1[1] == 0 then
edge_1, edge_2 = edge_2, edge_1
end
local level = base[2] + (edge_1[2] * ((x - base[1]) / edge_1[1])) + (edge_2[2] * ((z - base[3]) / edge_2[3]))
assert(level >= -0.5 and level <= 0.5)
return level
end
inside = inside and (relative[2] < level(relative[1], relative[3]))
if inside then
-- pos1 is inside the liquid node
pointed_thing.intersection_point = _pos1
pointed_thing.intersection_normal = vector.new(0, 0, 0)
return pointed_thing
end
local function intersection_normal(axis, dir)
return {x = 0, y = 0, z = 0, [axis] = dir}
end
local function plane(axis, dir)
local offset = dir * 0.5
local diff_axis = (relative[axis] - offset) / -direction[axis]
local intersection_point = {}
for plane_axis = 1, 3 do
if plane_axis ~= axis then
local value = direction[plane_axis] * diff_axis + relative[plane_axis]
if value < -0.5 or value > 0.5 then
return
end
intersection_point[plane_axis] = value
end
end
intersection_point[axis] = offset
return intersection_point
end
if direction[2] > 0 then
local intersection_point = plane(2, -1)
if intersection_point then
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
pointed_thing.intersection_normal = intersection_normal("y", -1)
return pointed_thing
end
end
for coord, other in pairs{[1] = 3, [3] = 1} do
if direction[coord] ~= 0 then
local dir = direction[coord] > 0 and -1 or 1
local intersection_point = plane(coord, dir)
if intersection_point then
local height = 0
for _, corner in pairs(corner_levels) do
if corner[coord] == dir * 0.5 then
height = height + (math.abs(intersection_point[other] + corner[other])) * corner[2]
end
end
if intersection_point[2] <= height then
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
pointed_thing.intersection_normal = intersection_normal(modlib.vector.index_aliases[coord], dir)
return pointed_thing
end
end
end
end
for _, triangle in pairs{
{corner_levels[3], corner_levels[2], corner_levels[1]},
{corner_levels[4], corner_levels[3], corner_levels[1]}
} do
local pos_on_ray = modlib.vector.ray_triangle_intersection(relative, direction, triangle)
if pos_on_ray and pos_on_ray <= length then
pointed_thing.intersection_point = (pos1 + modlib.vector.multiply_scalar(direction, pos_on_ray)):to_minetest()
pointed_thing.intersection_normal = modlib.vector.triangle_normal(triangle):to_minetest()
return pointed_thing
end
end
end
end
return setmetatable({next = next}, {__call = next})
local raycast = minetest.raycast(_pos1, _pos2, objects, liquids)
if not liquids then
return raycast
end
local pos1 = modlib.vector.from_minetest(_pos1)
local _direction = vector.direction(_pos1, _pos2)
local direction = modlib.vector.from_minetest(_direction)
local length = vector.distance(_pos1, _pos2)
local function next()
for pointed_thing in raycast do
if pointed_thing.type ~= "node" then
return pointed_thing
end
local _pos = pointed_thing.under
local pos = modlib.vector.from_minetest(_pos)
local node = minetest.get_node(_pos)
local def = minetest.registered_nodes[node.name]
if not (def and def.drawtype == "flowingliquid") then return pointed_thing end
local corner_levels = get_liquid_corner_levels(_pos)
local full_corner_levels = true
for _, corner_level in pairs(corner_levels) do
if corner_level[2] < 0.5 then
full_corner_levels = false
break
end
end
if full_corner_levels then
return pointed_thing
end
local relative = pos1 - pos
local inside = true
for _, prop in pairs(relative) do
if prop <= -0.5 or prop >= 0.5 then
inside = false
break
end
end
local function level(x, z)
local function distance_squared(corner)
return (x - corner[1]) ^ 2 + (z - corner[3]) ^ 2
end
local irrelevant_corner, distance = 1, distance_squared(corner_levels[1])
for index = 2, 4 do
local other_distance = distance_squared(corner_levels[index])
if other_distance > distance then
irrelevant_corner, distance = index, other_distance
end
end
local function corner(off)
return corner_levels[((irrelevant_corner + off) % 4) + 1]
end
local base = corner(2)
local edge_1, edge_2 = corner(1) - base, corner(3) - base
-- Properly selected edges will have a total length of 2
assert(math.abs(edge_1[1] + edge_1[3]) + math.abs(edge_2[1] + edge_2[3]) == 2)
if edge_1[1] == 0 then
edge_1, edge_2 = edge_2, edge_1
end
local level = base[2] + (edge_1[2] * ((x - base[1]) / edge_1[1])) + (edge_2[2] * ((z - base[3]) / edge_2[3]))
assert(level >= -0.5 and level <= 0.5)
return level
end
inside = inside and (relative[2] < level(relative[1], relative[3]))
if inside then
-- pos1 is inside the liquid node
pointed_thing.intersection_point = _pos1
pointed_thing.intersection_normal = vector.new(0, 0, 0)
return pointed_thing
end
local function intersection_normal(axis, dir)
return {x = 0, y = 0, z = 0, [axis] = dir}
end
local function plane(axis, dir)
local offset = dir * 0.5
local diff_axis = (relative[axis] - offset) / -direction[axis]
local intersection_point = {}
for plane_axis = 1, 3 do
if plane_axis ~= axis then
local value = direction[plane_axis] * diff_axis + relative[plane_axis]
if value < -0.5 or value > 0.5 then
return
end
intersection_point[plane_axis] = value
end
end
intersection_point[axis] = offset
return intersection_point
end
if direction[2] > 0 then
local intersection_point = plane(2, -1)
if intersection_point then
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
pointed_thing.intersection_normal = intersection_normal("y", -1)
return pointed_thing
end
end
for coord, other in pairs{[1] = 3, [3] = 1} do
if direction[coord] ~= 0 then
local dir = direction[coord] > 0 and -1 or 1
local intersection_point = plane(coord, dir)
if intersection_point then
local height = 0
for _, corner in pairs(corner_levels) do
if corner[coord] == dir * 0.5 then
height = height + (math.abs(intersection_point[other] + corner[other])) * corner[2]
end
end
if intersection_point[2] <= height then
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
pointed_thing.intersection_normal = intersection_normal(modlib.vector.index_aliases[coord], dir)
return pointed_thing
end
end
end
end
for _, triangle in pairs{
{corner_levels[3], corner_levels[2], corner_levels[1]},
{corner_levels[4], corner_levels[3], corner_levels[1]}
} do
local pos_on_ray = modlib.vector.ray_triangle_intersection(relative, direction, triangle)
if pos_on_ray and pos_on_ray <= length then
pointed_thing.intersection_point = (pos1 + modlib.vector.multiply_scalar(direction, pos_on_ray)):to_minetest()
pointed_thing.intersection_normal = modlib.vector.triangle_normal(triangle):to_minetest()
return pointed_thing
end
end
end
end
return setmetatable({next = next}, {__call = next})
end

@ -1,13 +1,13 @@
players = {}
registered_on_wielditem_changes = {function(...)
local _, previous_item, _, item = ...
if previous_item then
((previous_item:get_definition()._modlib or {}).un_wield or modlib.func.no_op)(...)
end
if item then
((item:get_definition()._modlib or {}).on_wield or modlib.func.no_op)(...)
end
local _, previous_item, _, item = ...
if previous_item then
((previous_item:get_definition()._modlib or {}).un_wield or modlib.func.no_op)(...)
end
if item then
((item:get_definition()._modlib or {}).on_wield or modlib.func.no_op)(...)
end
end}
--+ Registers an on_wielditem_change callback: function(player, previous_item, previous_index, item)
@ -15,32 +15,32 @@ end}
register_on_wielditem_change = modlib.func.curry(table.insert, registered_on_wielditem_changes)
minetest.register_on_mods_loaded(function()
-- Other on_joinplayer / on_leaveplayer callbacks should execute first
minetest.register_on_joinplayer(function(player)
local item, index = player:get_wielded_item(), player:get_wield_index()
players[player:get_player_name()] = {
wield = {
item = item,
index = index
}
}
modlib.table.icall(registered_on_wielditem_changes, player, nil, index, item)
end)
minetest.register_on_leaveplayer(function(player)
players[player:get_player_name()] = nil
end)
-- Other on_joinplayer / on_leaveplayer callbacks should execute first
minetest.register_on_joinplayer(function(player)
local item, index = player:get_wielded_item(), player:get_wield_index()
players[player:get_player_name()] = {
wield = {
item = item,
index = index
}
}
modlib.table.icall(registered_on_wielditem_changes, player, nil, index, item)
end)
minetest.register_on_leaveplayer(function(player)
players[player:get_player_name()] = nil
end)
end)
minetest.register_globalstep(function()
for _, player in pairs(minetest.get_connected_players()) do
local item, index = player:get_wielded_item(), player:get_wield_index()
local playerdata = players[player:get_player_name()]
if not playerdata then return end
local previous_item, previous_index = playerdata.wield.item, playerdata.wield.index
if item:get_name() ~= previous_item or index ~= previous_index then
playerdata.wield.item = item
playerdata.wield.index = index
modlib.table.icall(registered_on_wielditem_changes, player, previous_item, previous_index, item)
end
end
for _, player in pairs(minetest.get_connected_players()) do
local item, index = player:get_wielded_item(), player:get_wield_index()
local playerdata = players[player:get_player_name()]
if not playerdata then return end
local previous_item, previous_index = playerdata.wield.item, playerdata.wield.index
if item:get_name() ~= previous_item or index ~= previous_index then
playerdata.wield.item = item
playerdata.wield.index = index
modlib.table.icall(registered_on_wielditem_changes, player, previous_item, previous_index, item)
end
end
end)

212
mod.lua

@ -1,130 +1,130 @@
-- get resource + dofile
function include(modname, file)
if not file then
file = modname
modname = minetest.get_current_modname()
end
return dofile(get_resource(modname, file))
if not file then
file = modname
modname = minetest.get_current_modname()
end
return dofile(get_resource(modname, file))
end
function include_env(file_or_string, env, is_string)
setfenv(assert((is_string and loadstring or loadfile)(file_or_string)), env)()
setfenv(assert((is_string and loadstring or loadfile)(file_or_string)), env)()
end
function create_namespace(namespace_name, parent_namespace)
namespace_name = namespace_name or minetest.get_current_modname()
parent_namespace = parent_namespace or _G
local metatable = {__index = parent_namespace == _G and function(_, key) return rawget(_G, key) end or parent_namespace}
local namespace = {}
namespace = setmetatable(namespace, metatable)
if parent_namespace == _G then
rawset(parent_namespace, namespace_name, namespace)
else
parent_namespace[namespace_name] = namespace
end
return namespace
namespace_name = namespace_name or minetest.get_current_modname()
parent_namespace = parent_namespace or _G
local metatable = {__index = parent_namespace == _G and function(_, key) return rawget(_G, key) end or parent_namespace}
local namespace = {}
namespace = setmetatable(namespace, metatable)
if parent_namespace == _G then
rawset(parent_namespace, namespace_name, namespace)
else
parent_namespace[namespace_name] = namespace
end
return namespace
end
-- formerly extend_mod
function extend(modname, file)
if not file then
file = modname
modname = minetest.get_current_modname()
end
include_env(get_resource(modname, file .. ".lua"), rawget(_G, modname))
if not file then
file = modname
modname = minetest.get_current_modname()
end
include_env(get_resource(modname, file .. ".lua"), rawget(_G, modname))
end
-- runs main.lua in table env
-- formerly include_mod
function init(modname)
modname = modname or minetest.get_current_modname()
create_namespace(modname)
extend(modname, "main")
modname = modname or minetest.get_current_modname()
create_namespace(modname)
extend(modname, "main")
end
--! deprecated
function extend_string(modname, string)
if not string then
string = modname
modname = minetest.get_current_modname()
end
include_env(string, rawget(_G, modname), true)
if not string then
string = modname
modname = minetest.get_current_modname()
end
include_env(string, rawget(_G, modname), true)
end
function configuration(modname)
modname = modname or minetest.get_current_modname()
local schema = modlib.schema.new(assert(include(modname, "schema.lua")))
schema.name = schema.name or modname
assert(schema.type == "table")
local overrides = {}
local conf
local function add(path)
for _, format in ipairs{
{extension = "lua", read = function(text)
assert(overrides._C == nil)
local additions = setfenv(assert(loadstring(text)), setmetatable(overrides, {__index = {_C = overrides}}))()
setmetatable(overrides, nil)
if additions == nil then
return overrides
end
return additions
end},
{extension = "luon", read = function(text)
local value = {setfenv(assert(loadstring("return " .. text)), setmetatable(overrides, {}))()}
assert(#value == 1)
value = value[1]
local function check_type(value)
local type = type(value)
if type == "table" then
assert(getmetatable(value) == nil)
for key, value in pairs(value) do
check_type(key)
check_type(value)
end
elseif not (type == "boolean" or type == "number" or type == "string") then
error("disallowed type " .. type)
end
end
check_type(value)
return value
end},
{extension = "conf", read = function(text) return modlib.conf.build_setting_tree(Settings(text):to_table()) end, convert_strings = true},
{extension = "json", read = minetest.parse_json}
} do
local content = modlib.file.read(path .. "." .. format.extension)
if content then
overrides = modlib.table.deep_add_all(overrides, format.read(content))
conf = schema:load(overrides, {convert_strings = format.convert_strings, error_message = true})
end
end
end
add(minetest.get_worldpath() .. "/conf/" .. modname)
add(get_resource(modname, "conf"))
local minetest_conf = modlib.conf.settings[schema.name]
if minetest_conf then
overrides = modlib.table.deep_add_all(overrides, minetest_conf)
conf = schema:load(overrides, {convert_strings = true, error_message = true})
end
modlib.file.ensure_content(get_resource(modname, "settingtypes.txt"), schema:generate_settingtypes())
local readme_path = get_resource(modname, "Readme.md")
local readme = modlib.file.read(readme_path)
if readme then
local modified = false
readme = readme:gsub("<!%-%-modlib:conf:(%d)%-%->" .. "(.-)" .. "<!%-%-modlib:conf%-%->", function(level, content)
schema._md_level = assert(tonumber(level)) + 1
-- HACK: Newline between comment and heading (MD implementations don't handle comments properly)
local markdown = "\n" .. schema:generate_markdown()
if content ~= markdown then
modified = true
return "<!--modlib:conf:" .. level .. "-->" .. markdown .. "<!--modlib:conf-->"
end
end, 1)
if modified then
assert(modlib.file.write(readme_path, readme))
end
end
if conf == nil then
return schema:load({}, {error_message = true})
end
return conf
end
modname = modname or minetest.get_current_modname()
local schema = modlib.schema.new(assert(include(modname, "schema.lua")))
schema.name = schema.name or modname
assert(schema.type == "table")
local overrides = {}
local conf
local function add(path)
for _, format in ipairs{
{extension = "lua", read = function(text)
assert(overrides._C == nil)
local additions = setfenv(assert(loadstring(text)), setmetatable(overrides, {__index = {_C = overrides}}))()
setmetatable(overrides, nil)
if additions == nil then
return overrides
end
return additions
end},
{extension = "luon", read = function(text)
local value = {setfenv(assert(loadstring("return " .. text)), setmetatable(overrides, {}))()}
assert(#value == 1)
value = value[1]
local function check_type(value)
local type = type(value)
if type == "table" then
assert(getmetatable(value) == nil)
for key, value in pairs(value) do
check_type(key)
check_type(value)
end
elseif not (type == "boolean" or type == "number" or type == "string") then
error("disallowed type " .. type)
end
end
check_type(value)
return value
end},
{extension = "conf", read = function(text) return modlib.conf.build_setting_tree(Settings(text):to_table()) end, convert_strings = true},
{extension = "json", read = minetest.parse_json}
} do
local content = modlib.file.read(path .. "." .. format.extension)
if content then
overrides = modlib.table.deep_add_all(overrides, format.read(content))
conf = schema:load(overrides, {convert_strings = format.convert_strings, error_message = true})
end
end
end
add(minetest.get_worldpath() .. "/conf/" .. modname)
add(get_resource(modname, "conf"))
local minetest_conf = modlib.conf.settings[schema.name]
if minetest_conf then
overrides = modlib.table.deep_add_all(overrides, minetest_conf)
conf = schema:load(overrides, {convert_strings = true, error_message = true})
end
modlib.file.ensure_content(get_resource(modname, "settingtypes.txt"), schema:generate_settingtypes())
local readme_path = get_resource(modname, "Readme.md")
local readme = modlib.file.read(readme_path)
if readme then
local modified = false
readme = readme:gsub("<!%-%-modlib:conf:(%d)%-%->" .. "(.-)" .. "<!%-%-modlib:conf%-%->", function(level, content)
schema._md_level = assert(tonumber(level)) + 1
-- HACK: Newline between comment and heading (MD implementations don't handle comments properly)
local markdown = "\n" .. schema:generate_markdown()
if content ~= markdown then
modified = true
return "<!--modlib:conf:" .. level .. "-->" .. markdown .. "<!--modlib:conf-->"
end
end, 1)
if modified then
assert(modlib.file.write(readme_path, readme))
end
end
if conf == nil then
return schema:load({}, {error_message = true})
end
return conf
end

@ -5,8 +5,8 @@ function from_euler_rotation(rotation)
local cos = vector.apply(rotation, math.cos)
local sin = vector.apply(rotation, math.sin)
return {
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
cos.z * cos.x * sin.y - sin.z * sin.x * cos.y,
cos.z * cos.x * cos.y + sin.z * sin.x * sin.y
}
@ -76,23 +76,23 @@ end
--> {x = pitch, y = yaw, z = roll} euler rotation in degrees
function to_euler_rotation(self)
local rotation = {}
local rotation = {}
local sinr_cosp = 2 * (self[4] * self[1] + self[2] * self[3])
local cosr_cosp = 1 - 2 * (self[1] ^ 2 + self[2] ^ 2)
rotation.x = math.atan2(sinr_cosp, cosr_cosp)
local sinr_cosp = 2 * (self[4] * self[1] + self[2] * self[3])
local cosr_cosp = 1 - 2 * (self[1] ^ 2 + self[2] ^ 2)
rotation.x = math.atan2(sinr_cosp, cosr_cosp)
local sinp = 2 * (self[4] * self[2] - self[3] * self[1])
if sinp <= -1 then
rotation.z = -math.pi/2
elseif sinp >= 1 then
rotation.z = math.pi/2
else
rotation.z = math.asin(sinp)
local sinp = 2 * (self[4] * self[2] - self[3] * self[1])
if sinp <= -1 then
rotation.z = -math.pi/2
elseif sinp >= 1 then
rotation.z = math.pi/2
else
rotation.z = math.asin(sinp)
end
local siny_cosp = 2 * (self[4] * self[3] + self[1] * self[2])
local cosy_cosp = 1 - 2 * (self[2] ^ 2 + self[3] ^ 2)
local siny_cosp = 2 * (self[4] * self[3] + self[1] * self[2])
local cosy_cosp = 1 - 2 * (self[2] ^ 2 + self[3] ^ 2)
rotation.y = math.atan2(siny_cosp, cosy_cosp)
return vector.apply(rotation, math.deg)

@ -5,308 +5,308 @@ comparator = modlib.table.default_comparator
--+ Uses a weight-balanced binary tree
function new(comparator)
return setmetatable({comparator = comparator, root = {total = 0}}, metatable)
return setmetatable({comparator = comparator, root = {total = 0}}, metatable)
end
function len(self)
return self.root.total
return self.root.total
end
metatable.__len = len
function is_empty(self)
return len(self) == 0
return len(self) == 0
end
local function insert_all(tree, _table)
if tree.left then
insert_all(tree.left, _table)
end
table.insert(_table, tree.key)
if tree.right then
insert_all(tree.right, _table)
end
if tree.left then
insert_all(tree.left, _table)
end
table.insert(_table, tree.key)
if tree.right then
insert_all(tree.right, _table)
end
end
function to_table(self)
local table = {}
if not is_empty(self) then
insert_all(self.root, table)
end
return table
local table = {}
if not is_empty(self) then
insert_all(self.root, table)
end
return table
end
--> iterator: function() -> `rank, key` with ascending rank
function ipairs(self, min, max)
if is_empty(self) then
return function() end
end
min = min or 1
local tree = self.root
local current_rank = (tree.left and tree.left.total or 0) + 1
repeat
if min == current_rank then
break
end
local left, right = tree.left, tree.right
if min < current_rank then
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
tree = left
else
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
tree = right
end
until not tree
max = max or len(self)
local to_visit = {tree}
tree = nil
local rank = min - 1
local function next()
if not tree then
local len = #to_visit
if len == 0 then return end
tree = to_visit[len]
to_visit[len] = nil
else
while tree.left do
table.insert(to_visit, tree)
tree = tree.left
end
end
local key = tree.key
tree = tree.right
return key
end
return function()
if rank >= max then
return
end
local key = next()
if key == nil then
return
end
rank = rank + 1
return rank, key
end
if is_empty(self) then
return function() end
end
min = min or 1
local tree = self.root
local current_rank = (tree.left and tree.left.total or 0) + 1
repeat
if min == current_rank then
break
end
local left, right = tree.left, tree.right
if min < current_rank then
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
tree = left
else
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
tree = right
end
until not tree
max = max or len(self)
local to_visit = {tree}
tree = nil
local rank = min - 1
local function next()
if not tree then
local len = #to_visit
if len == 0 then return end
tree = to_visit[len]
to_visit[len] = nil
else
while tree.left do
table.insert(to_visit, tree)
tree = tree.left
end
end
local key = tree.key
tree = tree.right
return key
end
return function()
if rank >= max then
return
end
local key = next()
if key == nil then
return
end
rank = rank + 1
return rank, key
end
end
local function _right_rotation(parent, right, left)
local new_parent = parent[left]
parent[left] = new_parent[right]
new_parent[right] = parent
parent.total = (parent[left] and parent[left].total or 0) + (parent[right] and parent[right].total or 0) + 1
assert(parent.total > 0 or (parent.left == nil and parent.right == nil))
new_parent.total = (new_parent[left] and new_parent[left].total or 0) + parent.total + 1
return new_parent
local new_parent = parent[left]
parent[left] = new_parent[right]
new_parent[right] = parent
parent.total = (parent[left] and parent[left].total or 0) + (parent[right] and parent[right].total or 0) + 1
assert(parent.total > 0 or (parent.left == nil and parent.right == nil))
new_parent.total = (new_parent[left] and new_parent[left].total or 0) + parent.total + 1
return new_parent
end
local function right_rotation(parent)
return _right_rotation(parent, "right", "left")
return _right_rotation(parent, "right", "left")
end
local function left_rotation(parent)
return _right_rotation(parent, "left", "right")
return _right_rotation(parent, "left", "right")
end
local function _rebalance(parent)
local left_count, right_count = (parent.left and parent.left.total or 0), (parent.right and parent.right.total or 0)
if right_count > 1 and left_count * 2 < right_count then
return left_rotation(parent)
end
if left_count > 1 and right_count * 2 < left_count then
return right_rotation(parent)
end
return parent
local left_count, right_count = (parent.left and parent.left.total or 0), (parent.right and parent.right.total or 0)
if right_count > 1 and left_count * 2 < right_count then
return left_rotation(parent)
end
if left_count > 1 and right_count * 2 < left_count then
return right_rotation(parent)
end
return parent
end
-- Rebalances a parent chain
local function rebalance(self, len, parents, sides)
if len <= 1 then
return
end
for i = len, 2, -1 do
parents[i] = _rebalance(parents[i])
parents[i - 1][sides[i - 1]] = parents[i]
end
self.root = parents[1]
if len <= 1 then
return
end
for i = len, 2, -1 do
parents[i] = _rebalance(parents[i])
parents[i - 1][sides[i - 1]] = parents[i]
end
self.root = parents[1]
end
local function _insert(self, key, replace)
assert(key ~= nil)
if is_empty(self) then
self.root = {key = key, total = 1}
return
end
local comparator = self.comparator
local parents, sides = {}, {}
local tree = self.root
repeat
local tree_key = tree.key
local compared = comparator(key, tree_key)
if compared == 0 then
if replace then
tree.key = key
return tree_key
end
return
end
table.insert(parents, tree)
local side = compared < 0 and "left" or "right"
table.insert(sides, side)
tree = tree[side]
until not tree
local len = #parents
parents[len][sides[len]] = {key = key, total = 1}
for _, parent in pairs(parents) do
parent.total = parent.total + 1
end
rebalance(self, len, parents, sides)
assert(key ~= nil)
if is_empty(self) then
self.root = {key = key, total = 1}
return
end
local comparator = self.comparator
local parents, sides = {}, {}
local tree = self.root
repeat
local tree_key = tree.key
local compared = comparator(key, tree_key)
if compared == 0 then
if replace then
tree.key = key
return tree_key
end
return
end
table.insert(parents, tree)
local side = compared < 0 and "left" or "right"
table.insert(sides, side)
tree = tree[side]
until not tree
local len = #parents
parents[len][sides[len]] = {key = key, total = 1}
for _, parent in pairs(parents) do
parent.total = parent.total + 1
end
rebalance(self, len, parents, sides)
end
function insert(self, key)
return _insert(self, key)
return _insert(self, key)
end
function insert_or_replace(self, key)
return _insert(self, key, true)
return _insert(self, key, true)
end
local function _delete(self, key, is_rank)
assert(key ~= nil)
if is_empty(self) then
return
end
local comparator = self.comparator
local parents, sides = {}, {}
local tree = self.root
local rank = (tree.left and tree.left.total or 0) + 1
repeat
local tree_key = tree.key
local compared
if is_rank then
if key == rank then
compared = 0
elseif key < rank then
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
compared = -1
else
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
compared = 1
end
else
compared = comparator(key, tree_key)
end
if compared == 0 then
local len = #parents
local left, right = tree.left, tree.right
if left then
tree.total = tree.total - 1
if right then
-- Obtain successor
local side = left.total > right.total and "left" or "right"
local other_side = side == "left" and "right" or "left"
local sidemost = tree[side]
while sidemost[other_side] do
sidemost.total = sidemost.total - 1
table.insert(parents, sidemost)
table.insert(sides, other_side)
sidemost = sidemost[other_side]
end
-- Replace deleted key
tree.key = rightmost.key
-- Replace the successor by it's single child
parents[len][sides[len]] = sidemost[side]
else
if len == 0 then
self.root = left or {total = 0}
else
parents[len][sides[len]] = left
end
end
elseif right then
if len == 0 then
self.root = right or {total = 0}
else
tree.total = tree.total - 1
parents[len][sides[len]] = right
end
else
if len == 0 then
self.root = {total = 0}
else
parents[len][sides[len]] = nil
end
end
for _, parent in pairs(parents) do
parent.total = parent.total - 1
end
rebalance(self, len, parents, sides)
if is_rank then
return tree_key
end
return rank, tree_key
end
table.insert(parents, tree)
local side
if compared < 0 then
side = "left"
else
side = "right"
end
table.insert(sides, side)
tree = tree[side]
until not tree
assert(key ~= nil)
if is_empty(self) then
return
end
local comparator = self.comparator
local parents, sides = {}, {}
local tree = self.root
local rank = (tree.left and tree.left.total or 0) + 1
repeat
local tree_key = tree.key
local compared
if is_rank then
if key == rank then
compared = 0
elseif key < rank then
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
compared = -1
else
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
compared = 1
end
else
compared = comparator(key, tree_key)
end
if compared == 0 then
local len = #parents
local left, right = tree.left, tree.right
if left then
tree.total = tree.total - 1
if right then
-- Obtain successor
local side = left.total > right.total and "left" or "right"
local other_side = side == "left" and "right" or "left"
local sidemost = tree[side]
while sidemost[other_side] do
sidemost.total = sidemost.total - 1
table.insert(parents, sidemost)
table.insert(sides, other_side)
sidemost = sidemost[other_side]
end
-- Replace deleted key
tree.key = rightmost.key
-- Replace the successor by it's single child
parents[len][sides[len]] = sidemost[side]
else
if len == 0 then
self.root = left or {total = 0}
else
parents[len][sides[len]] = left
end
end
elseif right then
if len == 0 then
self.root = right or {total = 0}
else
tree.total = tree.total - 1
parents[len][sides[len]] = right
end
else
if len == 0 then
self.root = {total = 0}
else
parents[len][sides[len]] = nil
end
end
for _, parent in pairs(parents) do
parent.total = parent.total - 1
end
rebalance(self, len, parents, sides)
if is_rank then
return tree_key
end
return rank, tree_key
end
table.insert(parents, tree)
local side
if compared < 0 then
side = "left"
else
side = "right"
end
table.insert(sides, side)
tree = tree[side]
until not tree
end
function delete(self, key)
return _delete(self, key)
return _delete(self, key)
end
delete_by_key = delete
function delete_by_rank(self, rank)
return _delete(self, rank, true)
return _delete(self, rank, true)
end
--> `rank, key` if the key was found
--> `rank` the key would have if inserted
function get(self, key)
if is_empty(self) then return end
local comparator = self.comparator
local tree = self.root
local rank = (tree.left and tree.left.total or 0) + 1
while tree do
local compared = comparator(key, tree.key)
if compared == 0 then
return rank, tree.key
end
if compared < 0 then
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
tree = tree.left
else
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
tree = tree.right
end
end
return rank
if is_empty(self) then return end
local comparator = self.comparator
local tree = self.root
local rank = (tree.left and tree.left.total or 0) + 1
while tree do
local compared = comparator(key, tree.key)
if compared == 0 then
return rank, tree.key
end
if compared < 0 then
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
tree = tree.left
else
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
tree = tree.right
end
end
return rank
end
get_by_key = get
--> key
function get_by_rank(self, rank)
local tree = self.root
local current_rank = (tree.left and tree.left.total or 0) + 1
repeat
if rank == current_rank then
return tree.key
end
local left, right = tree.left, tree.right
if rank < current_rank then
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
tree = left
else
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
tree = right
end
until not tree
local tree = self.root
local current_rank = (tree.left and tree.left.total or 0) + 1
repeat
if rank == current_rank then
return tree.key
end
local left, right = tree.left, tree.right
if rank < current_rank then
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
tree = left
else
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
tree = right
end
until not tree
end

@ -1,291 +1,291 @@
local schema = getfenv(1)
function new(def)
-- TODO type inference, sanity checking etc.
return setmetatable(def, {__index = schema})
-- TODO type inference, sanity checking etc.
return setmetatable(def, {__index = schema})
end
local function field_name_to_title(name)
local title = modlib.text.split(name, "_")
title[1] = modlib.text.upper_first(title[1])
return table.concat(title, " ")
local title = modlib.text.split(name, "_")
title[1] = modlib.text.upper_first(title[1])
return table.concat(title, " ")
end
function generate_settingtypes(self)
local typ = self.type
local settingtype, type_args
self.title = self.title or field_name_to_title(self.name)
self._level = self._level or 0
local default = self.default
if typ == "boolean" then
settingtype = "bool"
default = default and "true" or "false"
elseif typ == "string" then
settingtype = "string"
elseif typ == "number" then
settingtype = self.int and "int" or "float"
if self.min or self.max then
-- TODO handle exclusive min/max
type_args = (self.int and "%d %d" or "%f %f"):format(self.min or (2 ^ -30), self.max or (2 ^ 30))
end
elseif typ == "table" then
local settings = {}
if self._level > 0 then
-- HACK: Minetest automatically adds the modname
-- TODO simple names (not modname.field.other_field)
settings = {"[" .. table.concat(modlib.table.repetition("*", self._level)) .. self.name .. "]"}
end
local function setting(key, value_scheme)
assert(not key:find("[=%.%s]"))
value_scheme.name = self.name .. "." .. key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._level = self._level + 1
table.insert(settings, generate_settingtypes(value_scheme))
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n")
end
if not typ then
return ""
end
local description = self.description
-- TODO extend description by range etc.?
-- TODO enum etc. support
if description then
if type(description) ~= "table" then
description = {description}
end
description = "# " .. table.concat(description, "\n# ") .. "\n"
else
description = ""
end
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
local typ = self.type
local settingtype, type_args
self.title = self.title or field_name_to_title(self.name)
self._level = self._level or 0
local default = self.default
if typ == "boolean" then
settingtype = "bool"
default = default and "true" or "false"
elseif typ == "string" then
settingtype = "string"
elseif typ == "number" then
settingtype = self.int and "int" or "float"
if self.min or self.max then
-- TODO handle exclusive min/max
type_args = (self.int and "%d %d" or "%f %f"):format(self.min or (2 ^ -30), self.max or (2 ^ 30))
end
elseif typ == "table" then
local settings = {}
if self._level > 0 then
-- HACK: Minetest automatically adds the modname
-- TODO simple names (not modname.field.other_field)
settings = {"[" .. table.concat(modlib.table.repetition("*", self._level)) .. self.name .. "]"}
end
local function setting(key, value_scheme)
assert(not key:find("[=%.%s]"))
value_scheme.name = self.name .. "." .. key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._level = self._level + 1
table.insert(settings, generate_settingtypes(value_scheme))
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n")
end
if not typ then
return ""
end
local description = self.description
-- TODO extend description by range etc.?
-- TODO enum etc. support
if description then
if type(description) ~= "table" then
description = {description}
end
description = "# " .. table.concat(description, "\n# ") .. "\n"
else
description = ""
end
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
end
function generate_markdown(self)
-- TODO address redundancies
local typ = self.type
self.title = self.title or field_name_to_title(self._md_name)
self._md_level = self._md_level or 1
if typ == "table" then
local settings = {}
local function setting(key, value_scheme)
value_scheme._md_name = key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._md_level = self._md_level + 1
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
table.insert(settings, "")
table.insert(settings, generate_markdown(value_scheme))
table.insert(settings, "")
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n")
end
if not typ then
return ""
end
local lines = {}
local function line(text)
table.insert(lines, "* " .. text)
end
local description = self.description
if description then
if type(description) ~= "table" then
table.insert(lines, description)
else
modlib.table.append(lines, description)
end
end
table.insert(lines, "")
line("Type: " .. self.type)
if self.default ~= nil then
line("Default: `" .. tostring(self.default) .. "`")
end
if self.int then
line"Integer"
elseif self.list then
line"List"
end
if self.infinity then
line"Infinities allowed"
end
if self.nan then
line"Not-a-Number (NaN) allowed"
end
if self.range then
if self.range.min then
line("&gt;= " .. self.range.min)
elseif self.range.min_exclusive then
line("&gt; " .. self.range.min_exclusive)
end
if self.range.max then
line("&lt;= " .. self.range.max)
elseif self.range.max_exclusive then
line("&lt; " .. self.range.max_exclusive)
end
end
if self.values then
line("Possible values:")
for value in pairs(self.values) do
table.insert(lines, " * " .. value)
end
end
return table.concat(lines, "\n")
-- TODO address redundancies
local typ = self.type
self.title = self.title or field_name_to_title(self._md_name)
self._md_level = self._md_level or 1
if typ == "table" then
local settings = {}
local function setting(key, value_scheme)
value_scheme._md_name = key
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
value_scheme._md_level = self._md_level + 1
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
table.insert(settings, "")
table.insert(settings, generate_markdown(value_scheme))
table.insert(settings, "")
end
local keys = {}
for key in pairs(self.entries or {}) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n")
end
if not typ then
return ""
end
local lines = {}
local function line(text)
table.insert(lines, "* " .. text)
end
local description = self.description
if description then
if type(description) ~= "table" then
table.insert(lines, description)
else
modlib.table.append(lines, description)
end
end
table.insert(lines, "")
line("Type: " .. self.type)
if self.default ~= nil then
line("Default: `" .. tostring(self.default) .. "`")
end
if self.int then
line"Integer"
elseif self.list then
line"List"
end
if self.infinity then
line"Infinities allowed"
end
if self.nan then
line"Not-a-Number (NaN) allowed"
end
if self.range then
if self.range.min then
line("&gt;= " .. self.range.min)
elseif self.range.min_exclusive then
line("&gt; " .. self.range.min_exclusive)
end
if self.range.max then
line("&lt;= " .. self.range.max)
elseif self.range.max_exclusive then
line("&lt; " .. self.range.max_exclusive)
end
end
if self.values then
line("Possible values:")
for value in pairs(self.values) do
table.insert(lines, " * " .. value)
end
end
return table.concat(lines, "\n")
end
function settingtypes(self)
self.settingtypes = self.settingtypes or generate_settingtypes(self)
return self.settingtypes
self.settingtypes = self.settingtypes or generate_settingtypes(self)
return self.settingtypes
end
function load(self, override, params)
local converted
if params.convert_strings and type(override) == "string" then
converted = true
if self.type == "boolean" then
if override == "true" then
override = true
elseif override == "false" then
override = false
end
elseif self.type == "number" then
override = tonumber(override)
else
converted = false
end
end
if override == nil and not converted then
if self.default ~= nil then
return self.default
elseif self.type == "table" then
override = {}
end
end
local _error = error
local function format_error(typ, ...)
if typ == "type" then
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
end
if typ == "range" then
local conditions = {}
local function push(condition, bound)
if self.range[bound] then
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
end
end
push(">", "min_exclusive")
push(">=", "min")
push("<", "max_exclusive")
push("<=", "max")
return "out of range: expected value " .. table.concat(conditions, "and")
end
if typ == "int" then
return "expected integer"
end
if typ == "infinity" then
return "expected no infinity"
end
if typ == "nan" then
return "expected no nan"
end
if typ == "required" then
local key = ...
return "required field " .. minetest.write_json(key) .. " missing"
end
if typ == "additional" then
local key = ...
return "superfluous field " .. minetest.write_json(key)
end
if typ == "list" then
return "not a list"
end
if typ == "values" then
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
end
_error("unknown error type")
end
local function error(type, ...)
if params.error_message then
local formatted = format_error(type, ...)
settingtypes(self)
_error("Invalid value: " .. self.name .. ": " .. formatted)
end
_error{
type = type,
self = self,
override = override,
converted = converted
}
end
local function assert(value, ...)
if not value then
error(...)
end
return value
end
assert(self.type == type(override), "type")
if self.type == "number" or self.type == "string" then
if self.range then
if self.range.min then
assert(self.range.min <= override, "range")
elseif self.range.min_exclusive then
assert(self.range.min_exclusive < override, "range")
end
if self.range.max then
assert(self.range.max >= override, "range")
elseif self.range.max_exclusive then
assert(self.range.max_exclusive > override, "range")
end
end
if self.type == "number" then
assert((not self.int) or (override % 1 == 0), "int")
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
assert(self.nan or override == override, "nan")
end
elseif self.type == "table" then
if self.entries then
for key, schema in pairs(self.entries) do
if schema.required and override[key] == nil then
error("required", key)
end
override[key] = load(schema, override[key], params)
end
if self.additional == false then
for key in pairs(override) do
if self.entries[key] == nil then
error("additional", key)
end
end
end
end
if self.keys then
for key, value in pairs(override) do
override[load(self.keys, key, params)], override[key] = value, nil
end
end
if self.values then
for key, value in pairs(override) do
override[key] = load(self.values, value, params)
end
end
assert((not self.list) or modlib.table.count(override) == #override, "list")
else
assert((not self.values) or self.values[override], "values")
end
if self.func then self.func(override) end
return override
local converted
if params.convert_strings and type(override) == "string" then
converted = true
if self.type == "boolean" then
if override == "true" then
override = true
elseif override == "false" then
override = false
end
elseif self.type == "number" then
override = tonumber(override)
else
converted = false
end
end
if override == nil and not converted then
if self.default ~= nil then
return self.default
elseif self.type == "table" then
override = {}
end
end
local _error = error
local function format_error(typ, ...)
if typ == "type" then
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
end
if typ == "range" then
local conditions = {}
local function push(condition, bound)
if self.range[bound] then
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
end
end
push(">", "min_exclusive")
push(">=", "min")
push("<", "max_exclusive")
push("<=", "max")
return "out of range: expected value " .. table.concat(conditions, "and")
end
if typ == "int" then
return "expected integer"
end
if typ == "infinity" then
return "expected no infinity"
end
if typ == "nan" then
return "expected no nan"
end
if typ == "required" then
local key = ...
return "required field " .. minetest.write_json(key) .. " missing"
end
if typ == "additional" then
local key = ...
return "superfluous field " .. minetest.write_json(key)
end
if typ == "list" then
return "not a list"
end
if typ == "values" then
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
end
_error("unknown error type")
end
local function error(type, ...)
if params.error_message then
local formatted = format_error(type, ...)
settingtypes(self)
_error("Invalid value: " .. self.name .. ": " .. formatted)
end
_error{
type = type,
self = self,
override = override,
converted = converted
}
end
local function assert(value, ...)
if not value then
error(...)
end
return value
end
assert(self.type == type(override), "type")
if self.type == "number" or self.type == "string" then
if self.range then
if self.range.min then
assert(self.range.min <= override, "range")
elseif self.range.min_exclusive then
assert(self.range.min_exclusive < override, "range")
end
if self.range.max then
assert(self.range.max >= override, "range")
elseif self.range.max_exclusive then
assert(self.range.max_exclusive > override, "range")
end
end
if self.type == "number" then
assert((not self.int) or (override % 1 == 0), "int")
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
assert(self.nan or override == override, "nan")
end
elseif self.type == "table" then
if self.entries then
for key, schema in pairs(self.entries) do
if schema.required and override[key] == nil then
error("required", key)
end
override[key] = load(schema, override[key], params)
end
if self.additional == false then
for key in pairs(override) do
if self.entries[key] == nil then
error("additional", key)
end
end
end
end
if self.keys then
for key, value in pairs(override) do
override[load(self.keys, key, params)], override[key] = value, nil
end
end
if self.values then
for key, value in pairs(override) do
override[key] = load(self.values, value, params)
end
end
assert((not self.list) or modlib.table.count(override) == #override, "list")
else
assert((not self.values) or self.values[override], "values")
end
if self.func then self.func(override) end
return override
end

822
table.lua

@ -1,79 +1,79 @@
-- Table helpers
function map_index(table, func)
local mapping_metatable = {
__index = function(table, key)
return rawget(table, func(key))
end,
__newindex = function(table, key, value)
rawset(table, func(key), value)
end
}
return setmetatable(table, mapping_metatable)
local mapping_metatable = {
__index = function(table, key)
return rawget(table, func(key))
end,
__newindex = function(table, key, value)
rawset(table, func(key), value)
end
}
return setmetatable(table, mapping_metatable)
end
function set_case_insensitive_index(table)
return map_index(table, string.lower)
return map_index(table, string.lower)
end
--+ nilget(a, "b", "c") == a?.b?.c
function nilget(value, key, ...)
if value == nil or key == nil then
return value
end
return nilget(value[key], ...)
if value == nil or key == nil then
return value
end
return nilget(value[key], ...)
end
-- Fisher-Yates
function shuffle(table)
for index = 1, #table - 1 do
local index_2 = math.random(index + 1, #table)
table[index], table[index_2] = table[index_2], table[index]
end
return table
for index = 1, #table - 1 do
local index_2 = math.random(index + 1, #table)
table[index], table[index_2] = table[index_2], table[index]
end
return table
end
local rope_metatable = {__index = {
write = function(self, text)
table.insert(self, text)
end,
to_text = function(self)
return table.concat(self)
end
write = function(self, text)
table.insert(self, text)
end,
to_text = function(self)
return table.concat(self)
end
}}
--> rope with simple metatable (:write(text) and :to_text())
function rope(table)
return setmetatable(table or {}, rope_metatable)
return setmetatable(table or {}, rope_metatable)
end
local rope_len_metatable = {__index = {
write = function(self, text)
self.len = self.len + text:len()
end
write = function(self, text)
self.len = self.len + text:len()
end
}}
--> rope for determining length supporting :write(text), .len being the length
function rope_len(len)
return setmetatable({len = len or 0}, rope_len_metatable)
return setmetatable({len = len or 0}, rope_len_metatable)
end
function is_circular(table)
assert(type(table) == "table")
local known = {}
local function _is_circular(value)
if type(value) ~= "table" then
return false
end
if known[value] then
return true
end
known[value] = true
for key, value in pairs(value) do
if _is_circular(key) or _is_circular(value) then
return true
end
end
end
return _is_circular(table)
assert(type(table) == "table")
local known = {}
local function _is_circular(value)
if type(value) ~= "table" then
return false
end
if known[value] then
return true
end
known[value] = true
for key, value in pairs(value) do
if _is_circular(key) or _is_circular(value) then
return true
end
end
end
return _is_circular(table)
end
--+ Simple table equality check. Stack overflow if tables are too deep or circular.
@ -81,44 +81,44 @@ end
--> Equality of noncircular tables if `table` and `other_table` are tables
--> `table == other_table` else
function equals_noncircular(table, other_table)
local is_equal = table == other_table
if is_equal or type(table) ~= "table" or type(other_table) ~= "table" then
return is_equal
end
if #table ~= #other_table then
return false
end
local table_keys = {}
for key, value in pairs(table) do
local value_2 = other_table[key]
if not equals_noncircular(value, value_2) then
if type(key) == "table" then
table_keys[key] = value
else
return false
end
end
end
for other_key, other_value in pairs(other_table) do
if type(other_key) == "table" then
local found
for table, value in pairs(table_keys) do
if equals_noncircular(other_key, table) and equals_noncircular(other_value, value) then
table_keys[table] = nil
found = true
break
end
end
if not found then
return false
end
else
if table[other_key] == nil then
return false
end
end
end
return true
local is_equal = table == other_table
if is_equal or type(table) ~= "table" or type(other_table) ~= "table" then
return is_equal
end
if #table ~= #other_table then
return false
end
local table_keys = {}
for key, value in pairs(table) do
local value_2 = other_table[key]
if not equals_noncircular(value, value_2) then
if type(key) == "table" then
table_keys[key] = value
else
return false
end
end
end
for other_key, other_value in pairs(other_table) do
if type(other_key) == "table" then
local found
for table, value in pairs(table_keys) do
if equals_noncircular(other_key, table) and equals_noncircular(other_value, value) then
table_keys[table] = nil
found = true
break
end
end
if not found then
return false
end
else
if table[other_key] == nil then
return false
end
end
end
return true
end
equals = equals_noncircular
@ -127,60 +127,60 @@ equals = equals_noncircular
--> Table content equality if `table` and `other_table` are tables
--> `table == other_table` else
function equals_content(table, other_table)
local equal_tables = {}
local function _equals(table, other_equal_table)
local function set_equal_tables(value)
equal_tables[table] = equal_tables[table] or {}
equal_tables[table][other_equal_table] = value
return value
end
local is_equal = table == other_equal_table
if is_equal or type(table) ~= "table" or type(other_equal_table) ~= "table" then
return is_equal
end
if #table ~= #other_equal_table then
return set_equal_tables(false)
end
local lookup_equal = (equal_tables[table] or {})[other_equal_table]
if lookup_equal ~= nil then
return lookup_equal
end
-- Premise
set_equal_tables(true)
local table_keys = {}
for key, value in pairs(table) do
local other_value = other_equal_table[key]
if not _equals(value, other_value) then
if type(key) == "table" then
table_keys[key] = value
else
return set_equal_tables(false)
end
end
end
for other_key, other_value in pairs(other_equal_table) do
if type(other_key) == "table" then
local found = false
for table_key, value in pairs(table_keys) do
if _equals(table_key, other_key) and _equals(value, other_value) then
table_keys[table_key] = nil
found = true
-- Breaking is fine as per transitivity
break
end
end
if not found then
return set_equal_tables(false)
end
else
if table[other_key] == nil then
return set_equal_tables(false)
end
end
end
return true
end
return _equals(table, other_table)
local equal_tables = {}
local function _equals(table, other_equal_table)
local function set_equal_tables(value)
equal_tables[table] = equal_tables[table] or {}
equal_tables[table][other_equal_table] = value
return value
end
local is_equal = table == other_equal_table
if is_equal or type(table) ~= "table" or type(other_equal_table) ~= "table" then
return is_equal
end
if #table ~= #other_equal_table then
return set_equal_tables(false)
end
local lookup_equal = (equal_tables[table] or {})[other_equal_table]
if lookup_equal ~= nil then
return lookup_equal
end
-- Premise
set_equal_tables(true)
local table_keys = {}
for key, value in pairs(table) do
local other_value = other_equal_table[key]
if not _equals(value, other_value) then
if type(key) == "table" then
table_keys[key] = value
else
return set_equal_tables(false)
end
end
end
for other_key, other_value in pairs(other_equal_table) do
if type(other_key) == "table" then
local found = false
for table_key, value in pairs(table_keys) do
if _equals(table_key, other_key) and _equals(value, other_value) then
table_keys[table_key] = nil
found = true
-- Breaking is fine as per transitivity
break
end
end
if not found then
return set_equal_tables(false)
end
else
if table[other_key] == nil then
return set_equal_tables(false)
end
end
end
return true
end
return _equals(table, other_table)
end
--+ Table equality check: content has to be equal, relations between tables as well
@ -190,400 +190,400 @@ end
--> equality (same tables after table reference substitution) of circular tables if `table` and `other_table` are tables
--> `table == other_table` else
function equals_references(table, other_table)
local function _equals(table, other_table, equal_refs)
if equal_refs[table] then
return equal_refs[table] == other_table
end
local is_equal = table == other_table
-- this check could be omitted if table key equality is being checked
if type(table) ~= "table" or type(other_table) ~= "table" then
return is_equal
end
if is_equal then
equal_refs[table] = other_table
return true
end
-- Premise: table = other table
equal_refs[table] = other_table
local table_keys = {}
for key, value in pairs(table) do
if type(key) == "table" then
table_keys[key] = value
else
local other_value = other_table[key]
if not _equals(value, other_value, equal_refs) then
return false
end
end
end
local other_table_keys = {}
for other_key, other_value in pairs(other_table) do
if type(other_key) == "table" then
other_table_keys[other_key] = other_value
elseif table[other_key] == nil then
return false
end
end
local function _next(current_key, equal_refs, available_keys)
local key, value = next(table_keys, current_key)
if key == nil then
return true
end
for other_key, other_value in pairs(other_table_keys) do
local copy_equal_refs = shallowcopy(equal_refs)
if _equals(key, other_key, copy_equal_refs) and _equals(value, other_value, copy_equal_refs) then
local copy_available_keys = shallowcopy(available_keys)
copy_available_keys[other_key] = nil
if _next(key, copy_equal_refs, copy_available_keys) then
return true
end
end
end
return false
end
return _next(nil, equal_refs, other_table_keys)
end
return _equals(table, other_table, {})
local function _equals(table, other_table, equal_refs)
if equal_refs[table] then
return equal_refs[table] == other_table
end
local is_equal = table == other_table
-- this check could be omitted if table key equality is being checked
if type(table) ~= "table" or type(other_table) ~= "table" then
return is_equal
end
if is_equal then
equal_refs[table] = other_table
return true
end
-- Premise: table = other table
equal_refs[table] = other_table
local table_keys = {}
for key, value in pairs(table) do
if type(key) == "table" then
table_keys[key] = value
else
local other_value = other_table[key]
if not _equals(value, other_value, equal_refs) then
return false
end
end
end
local other_table_keys = {}
for other_key, other_value in pairs(other_table) do
if type(other_key) == "table" then
other_table_keys[other_key] = other_value
elseif table[other_key] == nil then
return false
end
end
local function _next(current_key, equal_refs, available_keys)
local key, value = next(table_keys, current_key)
if key == nil then
return true
end
for other_key, other_value in pairs(other_table_keys) do
local copy_equal_refs = shallowcopy(equal_refs)
if _equals(key, other_key, copy_equal_refs) and _equals(value, other_value, copy_equal_refs) then
local copy_available_keys = shallowcopy(available_keys)
copy_available_keys[other_key] = nil
if _next(key, copy_equal_refs, copy_available_keys) then
return true
end
end
end
return false
end
return _next(nil, equal_refs, other_table_keys)
end
return _equals(table, other_table, {})
end
function shallowcopy(table)
local copy = {}
for key, value in pairs(table) do
copy[key] = value
end
return copy
local copy = {}
for key, value in pairs(table) do
copy[key] = value
end
return copy
end
function deepcopy_noncircular(table)
local function _copy(value)
if type(value) == "table" then
return deepcopy_noncircular(value)
end
return value
end
local copy = {}
for key, value in pairs(table) do
copy[_copy(key)] = _copy(value)
end
return copy
local function _copy(value)
if type(value) == "table" then
return deepcopy_noncircular(value)
end
return value
end
local copy = {}
for key, value in pairs(table) do
copy[_copy(key)] = _copy(value)
end
return copy
end
function deepcopy(table)
local copies = {}
local function _deepcopy(table)
if copies[table] then
return copies[table]
end
local copy = {}
copies[table] = copy
local function _copy(value)
if type(value) == "table" then
if copies[value] then
return copies[value]
end
return _deepcopy(value)
end
return value
end
for key, value in pairs(table) do
copy[_copy(key)] = _copy(value)
end
return copy
end
return _deepcopy(table)
local copies = {}
local function _deepcopy(table)
if copies[table] then
return copies[table]
end
local copy = {}
copies[table] = copy
local function _copy(value)
if type(value) == "table" then
if copies[value] then
return copies[value]
end
return _deepcopy(value)
end
return value
end
for key, value in pairs(table) do
copy[_copy(key)] = _copy(value)
end
return copy
end
return _deepcopy(table)
end
tablecopy = deepcopy
copy = deepcopy
function count(table)
local count = 0
for _ in pairs(table) do
count = count + 1
end
return count
local count = 0
for _ in pairs(table) do
count = count + 1
end
return count
end
function is_empty(table)
return next(table) == nil
return next(table) == nil
end
function foreach(table, func)
for k, v in pairs(table) do
func(k, v)
end
for k, v in pairs(table) do
func(k, v)
end
end
function foreach_value(table, func)
for _, v in pairs(table) do
func(v)
end
for _, v in pairs(table) do
func(v)
end
end
function call(table, ...)
for _, func in pairs(table) do
func(...)
end
for _, func in pairs(table) do
func(...)
end
end
function icall(table, ...)
for _, func in ipairs(table) do
func(...)
end
for _, func in ipairs(table) do
func(...)
end
end
function foreach_key(table, func)
for key, _ in pairs(table) do
func(key)
end
for key, _ in pairs(table) do
func(key)
end
end
function map(table, func)
for key, value in pairs(table) do
table[key] = func(value)
end
return table
for key, value in pairs(table) do
table[key] = func(value)
end
return table
end
function map_keys(table, func)
local new_tab = {}
for key, value in pairs(table) do
new_tab[func(key)] = value
end
return new_tab
local new_tab = {}
for key, value in pairs(table) do
new_tab[func(key)] = value
end
return new_tab
end
function process(tab, func)
local results = {}
for key, value in pairs(tab) do
table.insert(results, func(key,value))
end
return results
local results = {}
for key, value in pairs(tab) do
table.insert(results, func(key,value))
end
return results
end
function call(funcs, ...)
for _, func in ipairs(funcs) do
func(...)
end
for _, func in ipairs(funcs) do
func(...)
end
end
function find(list, value)
for index, other_value in pairs(list) do
if value == other_value then
return index
end
end
return
for index, other_value in pairs(list) do
if value == other_value then
return index
end
end
return
end
contains = find
function to_add(table, after_additions)
local additions = {}
for key, value in pairs(after_additions) do
if table[key] ~= value then
additions[key] = value
end
end
return additions
local additions = {}
for key, value in pairs(after_additions) do
if table[key] ~= value then
additions[key] = value
end
end
return additions
end
difference = to_add
function deep_to_add(table, after_additions)
local additions = {}
for key, value in pairs(after_additions) do
if type(table[key]) == "table" and type(value) == "table" then
additions[key] = deep_to_add(table[key], value)
elseif table[key] ~= value then
additions[key] = value
end
end
return additions
local additions = {}
for key, value in pairs(after_additions) do
if type(table[key]) == "table" and type(value) == "table" then
additions[key] = deep_to_add(table[key], value)
elseif table[key] ~= value then
additions[key] = value
end
end
return additions
end
function add_all(table, additions)
for key, value in pairs(additions) do
table[key] = value
end
return table
for key, value in pairs(additions) do
table[key] = value
end
return table
end
function deep_add_all(table, additions)
for key, value in pairs(additions) do
if type(table[key]) == "table" and type(value) == "table" then
deep_add_all(table[key], value)
else
table[key] = value
end
end
return table
for key, value in pairs(additions) do
if type(table[key]) == "table" and type(value) == "table" then
deep_add_all(table[key], value)
else
table[key] = value
end
end
return table
end
function complete(table, completions)
for key, value in pairs(completions) do
if table[key] == nil then
table[key] = value
end
end
return table
for key, value in pairs(completions) do
if table[key] == nil then
table[key] = value
end
end
return table
end
function deepcomplete(table, completions)
for key, value in pairs(completions) do
if table[key] == nil then
table[key] = value
elseif type(table[key]) == "table" and type(value) == "table" then
deepcomplete(table[key], value)
end
end
return table
for key, value in pairs(completions) do
if table[key] == nil then
table[key] = value
elseif type(table[key]) == "table" and type(value) == "table" then
deepcomplete(table[key], value)
end
end
return table
end
function merge_tables(table, other_table)
return add_all(copy(table), other_table)
return add_all(copy(table), other_table)
end
union = merge_tables
function intersection(table, other_table)
local result = {}
for key, value in pairs(table) do
if other_table[key] then
result[key] = value
end
end
return result
local result = {}
for key, value in pairs(table) do
if other_table[key] then
result[key] = value
end
end
return result
end
function append(table, other_table)
local length = #table
for index, value in ipairs(other_table) do
table[length + index] = value
end
return table
local length = #table
for index, value in ipairs(other_table) do
table[length + index] = value
end
return table
end
function keys(table)
local keys = {}
for key, _ in pairs(table) do
keys[#keys + 1] = key
end
return keys
local keys = {}
for key, _ in pairs(table) do
keys[#keys + 1] = key
end
return keys
end
function values(table)
local values = {}
for _, value in pairs(table) do
values[#values + 1] = value
end
return values
local values = {}
for _, value in pairs(table) do
values[#values + 1] = value
end
return values
end
function flip(table)
local flipped = {}
for key, value in pairs(table) do
flipped[value] = key
end
return flipped
local flipped = {}
for key, value in pairs(table) do
flipped[value] = key
end
return flipped
end
function set(table)
local flipped = {}
for _, value in pairs(table) do
flipped[value] = true
end
return flipped
local flipped = {}
for _, value in pairs(table) do
flipped[value] = true
end
return flipped
end
function unique(table)
return keys(set(table))
return keys(set(table))
end
function rpairs(table)
local index = #table
return function()
if index >= 1 then
local value = table[index]
index = index - 1
if value ~= nil then
return index + 1, value
end
end
end
local index = #table
return function()
if index >= 1 then
local value = table[index]
index = index - 1
if value ~= nil then
return index + 1, value
end
end
end
end
function best_value(table, is_better_func)
local best = next(table)
if best == nil then
return
end
local candidate = best
while true do
candidate = next(table, candidate)
if candidate == nil then
return best
end
if is_better_func(candidate, best) then
best = candidate
end
end
error()
local best = next(table)
if best == nil then
return
end
local candidate = best
while true do
candidate = next(table, candidate)
if candidate == nil then
return best
end
if is_better_func(candidate, best) then
best = candidate
end
end
error()
end
function min(table)
return best_value(table, function(value, other_value) return value < other_value end)
return best_value(table, function(value, other_value) return value < other_value end)
end
function max(table)
return best_value(table, function(value, other_value) return value > other_value end)
return best_value(table, function(value, other_value) return value > other_value end)
end
function default_comparator(value, other_value)
if value == other_value then
return 0
end
if value > other_value then
return 1
end
return -1
if value == other_value then
return 0
end
if value > other_value then
return 1
end
return -1
end
--> index if element found
--> -index for insertion if not found
function binary_search_comparator(comparator)
return function(list, value)
local min, max = 1, #list
while min <= max do
local pivot = min + math.floor((max - min) / 2)
local element = list[pivot]
local compared = comparator(value, element)
if compared == 0 then
return pivot
elseif compared > 0 then
min = pivot + 1
else
max = pivot - 1
end
end
return -min
end
return function(list, value)
local min, max = 1, #list
while min <= max do
local pivot = min + math.floor((max - min) / 2)
local element = list[pivot]
local compared = comparator(value, element)
if compared == 0 then
return pivot
elseif compared > 0 then
min = pivot + 1
else
max = pivot - 1
end
end
return -min
end
end
binary_search = binary_search_comparator(default_comparator)
function reverse(table)
local l = #table + 1
for index = 1, math.floor(#table / 2) do
table[l - index], table[index] = table[index], table[l - index]
end
return table
local l = #table + 1
for index = 1, math.floor(#table / 2) do
table[l - index], table[index] = table[index], table[l - index]
end
return table
end
function repetition(value, count)
local table = {}
for index = 1, count do
table[index] = value
end
return table
local table = {}
for index = 1, count do
table[index] = value
end
return table
end

422
test.lua

@ -3,16 +3,16 @@
local random, huge = math.random, math.huge
local parent_env = getfenv(1)
setfenv(1, setmetatable({}, {
__index = function(_, key)
local value = modlib[key]
if value ~= nil then
return value
end
return parent_env[key]
end,
__newindex = function(_, key, value)
error(dump{key = key, value = value})
end
__index = function(_, key)
local value = modlib[key]
if value ~= nil then
return value
end
return parent_env[key]
end,
__newindex = function(_, key, value)
error(dump{key = key, value = value})
end
}))
-- string
@ -20,104 +20,104 @@ assert(string.escape_magic_chars"%" == "%%")
-- table
do
local tab = {}
tab[tab] = tab
local table_copy = table.deepcopy(tab)
assert(table_copy[table_copy] == table_copy)
assert(table.is_circular(tab))
assert(not table.is_circular{a = 1})
assert(table.equals_noncircular({[{}]={}}, {[{}]={}}))
assert(table.equals_content(tab, table_copy))
local equals_references = table.equals_references
assert(equals_references(tab, table_copy))
assert(equals_references({}, {}))
assert(not equals_references({a = 1, b = 2}, {a = 1, b = 3}))
tab = {}
tab.a, tab.b = tab, tab
table_copy = table.deepcopy(tab)
assert(equals_references(tab, table_copy))
local x, y = {}, {}
assert(not equals_references({[x] = x, [y] = y}, {[x] = y, [y] = x}))
assert(equals_references({[x] = x, [y] = y}, {[x] = x, [y] = y}))
local nilget = table.nilget
assert(nilget({a = {b = {c = 42}}}, "a", "b", "c") == 42)
assert(nilget({a = {}}, "a", "b", "c") == nil)
assert(nilget(nil, "a", "b", "c") == nil)
assert(nilget(nil, "a", nil, "c") == nil)
local rope = table.rope{}
rope:write"hello"
rope:write" "
rope:write"world"
assert(rope:to_text() == "hello world", rope:to_text())
local tab = {}
tab[tab] = tab
local table_copy = table.deepcopy(tab)
assert(table_copy[table_copy] == table_copy)
assert(table.is_circular(tab))
assert(not table.is_circular{a = 1})
assert(table.equals_noncircular({[{}]={}}, {[{}]={}}))
assert(table.equals_content(tab, table_copy))
local equals_references = table.equals_references
assert(equals_references(tab, table_copy))
assert(equals_references({}, {}))
assert(not equals_references({a = 1, b = 2}, {a = 1, b = 3}))
tab = {}
tab.a, tab.b = tab, tab
table_copy = table.deepcopy(tab)
assert(equals_references(tab, table_copy))
local x, y = {}, {}
assert(not equals_references({[x] = x, [y] = y}, {[x] = y, [y] = x}))
assert(equals_references({[x] = x, [y] = y}, {[x] = x, [y] = y}))
local nilget = table.nilget
assert(nilget({a = {b = {c = 42}}}, "a", "b", "c") == 42)
assert(nilget({a = {}}, "a", "b", "c") == nil)
assert(nilget(nil, "a", "b", "c") == nil)
assert(nilget(nil, "a", nil, "c") == nil)
local rope = table.rope{}
rope:write"hello"
rope:write" "
rope:write"world"
assert(rope:to_text() == "hello world", rope:to_text())
end
-- heap
do
local n = 100
local list = {}
for index = 1, n do
list[index] = index
end
table.shuffle(list)
local heap = heap.new()
for index = 1, #list do
heap:push(list[index])
end
for index = 1, #list do
local popped = heap:pop()
assert(popped == index)
end
local n = 100
local list = {}
for index = 1, n do
list[index] = index
end
table.shuffle(list)
local heap = heap.new()
for index = 1, #list do
heap:push(list[index])
end
for index = 1, #list do
local popped = heap:pop()
assert(popped == index)
end
end
-- ranked set
do
local n = 100
local ranked_set = ranked_set.new()
local list = {}
for i = 1, n do
ranked_set:insert(i)
list[i] = i
end
local n = 100
local ranked_set = ranked_set.new()
local list = {}
for i = 1, n do
ranked_set:insert(i)
list[i] = i
end
assert(table.equals(ranked_set:to_table(), list))
assert(table.equals(ranked_set:to_table(), list))
local i = 0
for rank, key in ranked_set:ipairs() do
i = i + 1
assert(i == key and i == rank)
assert(ranked_set:get_by_rank(rank) == key)
local rank, key = ranked_set:get(i)
assert(key == i and i == rank)
end
assert(i == n)
local i = 0
for rank, key in ranked_set:ipairs() do
i = i + 1
assert(i == key and i == rank)
assert(ranked_set:get_by_rank(rank) == key)
local rank, key = ranked_set:get(i)
assert(key == i and i == rank)
end
assert(i == n)
for i = 1, n do
local _, v = ranked_set:delete(i)
assert(v == i, i)
end
assert(not next(ranked_set:to_table()))
for i = 1, n do
local _, v = ranked_set:delete(i)
assert(v == i, i)
end
assert(not next(ranked_set:to_table()))
local ranked_set = ranked_set.new()
for i = 1, n do
ranked_set:insert(i)
end
local ranked_set = ranked_set.new()
for i = 1, n do
ranked_set:insert(i)
end
for rank, key in ranked_set:ipairs(10, 20) do
assert(rank == key and key >= 10 and key <= 20)
end
for rank, key in ranked_set:ipairs(10, 20) do
assert(rank == key and key >= 10 and key <= 20)
end
for i = n, 1, -1 do
local j = ranked_set:delete_by_rank(i)
assert(j == i)
end
for i = n, 1, -1 do
local j = ranked_set:delete_by_rank(i)
assert(j == i)
end
end
-- colorspec
local colorspec = minetest.colorspec
local function test_from_string(string, number)
local spec = colorspec.from_string(string)
local expected = colorspec.from_number(number)
assertdump(table.equals(spec, expected), {expected = expected, actual = spec})
local spec = colorspec.from_string(string)
local expected = colorspec.from_number(number)
assertdump(table.equals(spec, expected), {expected = expected, actual = spec})
end
local spec = colorspec.from_number(0xDDCCBBAA)
assertdump(table.equals(spec, {a = 0xAA, b = 0xBB, g = 0xCC, r = 0xDD}), spec)
@ -130,147 +130,147 @@ test_from_string("#11223344", 0x11223344)
-- k-d-tree
local vectors = {}
for _ = 1, 1000 do
_G.table.insert(vectors, {random(), random(), random()})
_G.table.insert(vectors, {random(), random(), random()})
end
local kdtree = kdtree.new(vectors)
for _, v in ipairs(vectors) do
local neighbor, distance = kdtree:get_nearest_neighbor(v)
assert(vector.equals(v, neighbor), distance == 0)
local neighbor, distance = kdtree:get_nearest_neighbor(v)
assert(vector.equals(v, neighbor), distance == 0)
end
for _ = 1, 1000 do
local v = {random(), random(), random()}
local _, distance = kdtree:get_nearest_neighbor(v)
local min_distance = huge
for _, w in ipairs(vectors) do
local other_distance = vector.distance(v, w)
if other_distance < min_distance then
min_distance = other_distance
end
end
assert(distance == min_distance)
local v = {random(), random(), random()}
local _, distance = kdtree:get_nearest_neighbor(v)
local min_distance = huge
for _, w in ipairs(vectors) do
local other_distance = vector.distance(v, w)
if other_distance < min_distance then
min_distance = other_distance
end
end
assert(distance == min_distance)
end
-- bluon
do
local bluon = bluon
local function assert_preserves(object)
local rope = table.rope{}
local written, read, input
local _, err = pcall(function()
bluon:write(object, rope)
written = rope:to_text()
input = text.inputstream(written)
read = bluon:read(input)
local remaining = input:read(1000)
assert(not remaining)
end)
assertdump(table.equals_references(object, read) and not err, {
object = object,
read = read,
written = written and text.hexdump(written),
err = err
})
end
for _, constant in pairs{true, false, huge, -huge} do
assert_preserves(constant)
end
for i = 1, 1000 do
assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i)))
end
for _ = 1, 1000 do
local int = random(-2^50, 2^50)
assert(int % 1 == 0)
assert_preserves(int)
assert_preserves((random() - 0.5) * 2^random(-20, 20))
end
assert_preserves{hello = "world", welt = "hallo"}
assert_preserves{"hello", "hello", "hello"}
local a = {}
a[a] = a
a[1] = a
assert_preserves(a)
local bluon = bluon
local function assert_preserves(object)
local rope = table.rope{}
local written, read, input
local _, err = pcall(function()
bluon:write(object, rope)
written = rope:to_text()
input = text.inputstream(written)
read = bluon:read(input)
local remaining = input:read(1000)
assert(not remaining)
end)
assertdump(table.equals_references(object, read) and not err, {
object = object,
read = read,
written = written and text.hexdump(written),
err = err
})
end
for _, constant in pairs{true, false, huge, -huge} do
assert_preserves(constant)
end
for i = 1, 1000 do
assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i)))
end
for _ = 1, 1000 do
local int = random(-2^50, 2^50)
assert(int % 1 == 0)
assert_preserves(int)
assert_preserves((random() - 0.5) * 2^random(-20, 20))
end
assert_preserves{hello = "world", welt = "hallo"}
assert_preserves{"hello", "hello", "hello"}
local a = {}
a[a] = a
a[1] = a
assert_preserves(a)
end
-- in-game tests & b3d testing
local tests = {
-- depends on player_api
b3d = false,
liquid_dir = false,
liquid_raycast = false
-- depends on player_api
b3d = false,
liquid_dir = false,
liquid_raycast = false
}
if tests.b3d then
local stream = io.open(mod.get_resource("player_api", "models", "character.b3d"), "r")
assert(stream)
local b3d = b3d.read(stream)
--! dirty helper method to create truncate tables with 10+ number keys
local function _b3d_truncate(table)
local count = 1
for key, value in pairs(table) do
if type(key) == "table" then
_b3d_truncate(key)
end
if type(value) == "table" then
_b3d_truncate(value)
end
count = count + 1
if type(key) == "number" and count >= 9 and next(table, key) then
if count == 9 then
table[key] = "TRUNCATED"
else
table[key] = nil
end
end
end
return table
end
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(b3d))))
stream:close()
local stream = io.open(mod.get_resource("player_api", "models", "character.b3d"), "r")
assert(stream)
local b3d = b3d.read(stream)
--! dirty helper method to create truncate tables with 10+ number keys
local function _b3d_truncate(table)
local count = 1
for key, value in pairs(table) do
if type(key) == "table" then
_b3d_truncate(key)
end
if type(value) == "table" then
_b3d_truncate(value)
end
count = count + 1
if type(key) == "number" and count >= 9 and next(table, key) then
if count == 9 then
table[key] = "TRUNCATED"
else
table[key] = nil
end
end
end
return table
end
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(b3d))))
stream:close()
end
local vector, minetest, ml_mt = _G.vector, _G.minetest, minetest
if tests.liquid_dir then
minetest.register_abm{
label = "get_liquid_corner_levels & get_liquid_direction test",
nodenames = {"group:liquid"},
interval = 1,
chance = 1,
action = function(pos, node)
assert(type(node) == "table")
for _, corner_level in pairs(ml_mt.get_liquid_corner_levels(pos, node)) do
minetest.add_particle{
pos = vector.add(pos, corner_level),
size = 2,
texture = "logo.png"
}
end
local direction = ml_mt.get_liquid_flow_direction(pos, node)
local start_pos = pos
start_pos.y = start_pos.y + 1
for i = 0, 5 do
minetest.add_particle{
pos = vector.add(start_pos, vector.multiply(direction, i/5)),
size = i/2.5,
texture = "logo.png"
}
end
end
}
minetest.register_abm{
label = "get_liquid_corner_levels & get_liquid_direction test",
nodenames = {"group:liquid"},
interval = 1,
chance = 1,
action = function(pos, node)
assert(type(node) == "table")
for _, corner_level in pairs(ml_mt.get_liquid_corner_levels(pos, node)) do
minetest.add_particle{
pos = vector.add(pos, corner_level),
size = 2,
texture = "logo.png"
}
end
local direction = ml_mt.get_liquid_flow_direction(pos, node)
local start_pos = pos
start_pos.y = start_pos.y + 1
for i = 0, 5 do
minetest.add_particle{
pos = vector.add(start_pos, vector.multiply(direction, i/5)),
size = i/2.5,
texture = "logo.png"
}
end
end
}
end
if tests.liquid_raycast then
minetest.register_globalstep(function()
for _, player in pairs(minetest.get_connected_players()) do
local eye_pos = vector.offset(player:get_pos(), 0, player:get_properties().eye_height, 0)
local raycast = ml_mt.raycast(eye_pos, vector.add(eye_pos, vector.multiply(player:get_look_dir(), 3)), false, true)
for pointed_thing in raycast do
if pointed_thing.type == "node" and minetest.registered_nodes[minetest.get_node(pointed_thing.under).name].liquidtype == "flowing" then
minetest.add_particle{
pos = vector.add(pointed_thing.intersection_point, vector.multiply(pointed_thing.intersection_normal, 0.1)),
size = 0.5,
texture = "object_marker_red.png",
expirationtime = 3
}
end
end
end
end)
minetest.register_globalstep(function()
for _, player in pairs(minetest.get_connected_players()) do
local eye_pos = vector.offset(player:get_pos(), 0, player:get_properties().eye_height, 0)
local raycast = ml_mt.raycast(eye_pos, vector.add(eye_pos, vector.multiply(player:get_look_dir(), 3)), false, true)
for pointed_thing in raycast do
if pointed_thing.type == "node" and minetest.registered_nodes[minetest.get_node(pointed_thing.under).name].liquidtype == "flowing" then
minetest.add_particle{
pos = vector.add(pointed_thing.intersection_point, vector.multiply(pointed_thing.intersection_normal, 0.1)),
size = 0.5,
texture = "object_marker_red.png",
expirationtime = 3
}
end
end
end
end)
end

@ -2,106 +2,106 @@ local mt_vector = vector
local vector = getfenv(1)
index_aliases = {
x = 1,
y = 2,
z = 3,
w = 4
x = 1,
y = 2,
z = 3,
w = 4
}
modlib.table.add_all(index_aliases, modlib.table.flip(index_aliases))
metatable = {
__index = function(table, key)
local index = index_aliases[key]
if index ~= nil then
return table[index]
end
return vector[key]
end,
__newindex = function(table, key, value)
local index = letters[key]
if index ~= nil then
return rawset(table, index, value)
end
end
__index = function(table, key)
local index = index_aliases[key]
if index ~= nil then
return table[index]
end
return vector[key]
end,
__newindex = function(table, key, value)
local index = letters[key]
if index ~= nil then
return rawset(table, index, value)
end
end
}
function new(v)
return setmetatable(v, metatable)
return setmetatable(v, metatable)
end
function from_xyzw(v)
return new{v.x, v.y, v.z, v.w}
return new{v.x, v.y, v.z, v.w}
end
function from_minetest(v)
return new{v.x, v.y, v.z}
return new{v.x, v.y, v.z}
end
function to_xyzw(v)
return {x = v[1], y = v[2], z = v[3], w = v[4]}
return {x = v[1], y = v[2], z = v[3], w = v[4]}
end
--+ not necessarily required, as Minetest respects the metatable
function to_minetest(v)
return mt_vector.new(unpack(v))
return mt_vector.new(unpack(v))
end
function equals(v, w)
for k, v in pairs(v) do
if v ~= w[k] then return false end
end
return true
for k, v in pairs(v) do
if v ~= w[k] then return false end
end
return true
end
metatable.__eq = equals
function less_than(v, w)
for k, v in pairs(v) do
if v >= w[k] then return false end
end
return true
for k, v in pairs(v) do
if v >= w[k] then return false end
end
return true
end
metatable.__lt = less_than
function less_or_equal(v, w)
for k, v in pairs(v) do
if v > w[k] then return false end
end
return true
for k, v in pairs(v) do
if v > w[k] then return false end
end
return true
end
metatable.__le = less_or_equal
function combine(v, w, f)
local new_vector = {}
for key, value in pairs(v) do
new_vector[key] = f(value, w[key])
end
return new(new_vector)
local new_vector = {}
for key, value in pairs(v) do
new_vector[key] = f(value, w[key])
end
return new(new_vector)
end
function apply(v, f, ...)
local new_vector = {}
for key, value in pairs(v) do
new_vector[key] = f(value, ...)
end
return new(new_vector)
local new_vector = {}
for key, value in pairs(v) do
new_vector[key] = f(value, ...)
end
return new(new_vector)
end
function combinator(f)
return function(v, w)
return combine(v, w, f)
end, function(v, ...)
return apply(v, f, ...)
end
return function(v, w)
return combine(v, w, f)
end, function(v, ...)
return apply(v, f, ...)
end
end
function invert(v)
for key, value in pairs(v) do
v[key] = -value
end
for key, value in pairs(v) do
v[key] = -value
end
end
add, add_scalar = combinator(function(v, w) return v + w end)
@ -119,107 +119,107 @@ metatable.__div = divide
--+ linear interpolation
--: ratio number from 0 (all the first vector) to 1 (all the second vector)
function interpolate(v, w, ratio)
return add(multiply(v, 1 - ratio), multiply(w, ratio))
return add(multiply(v, 1 - ratio), multiply(w, ratio))
end
function norm(v)
local sum = 0
for _, value in pairs(v) do
sum = sum + value ^ 2
end
return sum
local sum = 0
for _, value in pairs(v) do
sum = sum + value ^ 2
end
return sum
end
function length(v)
return math.sqrt(norm(v))
return math.sqrt(norm(v))
end
-- Minor code duplication for the sake of performance
function distance(v, w)
local sum = 0
for key, value in pairs(v) do
sum = sum + (value - w[key]) ^ 2
end
return math.sqrt(sum)
local sum = 0
for key, value in pairs(v) do
sum = sum + (value - w[key]) ^ 2
end
return math.sqrt(sum)
end
function normalize(v)
return divide_scalar(v, length(v))
return divide_scalar(v, length(v))
end
function floor(v)
return apply(v, math.floor)
return apply(v, math.floor)
end
function ceil(v)
return apply(v, math.ceil)
return apply(v, math.ceil)
end
function clamp(v, min, max)
return apply(apply(v, math.max, min), math.min, max)
return apply(apply(v, math.max, min), math.min, max)
end
function cross3(v, w)
assert(#v == 3 and #w == 3)
return new{
v[2] * w[3] - v[3] * w[2],
v[3] * w[1] - v[1] * w[3],
v[1] * w[2] - v[2] * w[1]
}
assert(#v == 3 and #w == 3)
return new{
v[2] * w[3] - v[3] * w[2],
v[3] * w[1] - v[1] * w[3],
v[1] * w[2] - v[2] * w[1]
}
end
function dot(v, w)
local sum = 0
for i, c in pairs(v) do
sum = sum + c * w[i]
end
return sum
local sum = 0
for i, c in pairs(v) do
sum = sum + c * w[i]
end
return sum
end
--+ Angle between two vectors
--> Signed angle in radians
function angle(v, w)
-- Based on dot(v, w) = |v| * |w| * cos(x)
return math.acos(dot(v, w) / length(v) / length(w))
-- Based on dot(v, w) = |v| * |w| * cos(x)
return math.acos(dot(v, w) / length(v) / length(w))
end
function box_box_collision(diff, box, other_box)
for index, diff in pairs(diff) do
if box[index] + diff > other_box[index + 3] or other_box[index] > box[index + 3] + diff then
return false
end
end
return true
for index, diff in pairs(diff) do
if box[index] + diff > other_box[index + 3] or other_box[index] > box[index + 3] + diff then
return false
end
end
return true
end
--+ Möller-Trumbore
function ray_triangle_intersection(origin, direction, triangle)
local point_1, point_2, point_3 = unpack(triangle)
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
local h = cross3(direction, edge_2)
local a = dot(edge_1, h)
if math.abs(a) < 1e-9 then
return
end
local f = 1 / a
local diff = subtract(origin, point_1)
local u = f * dot(diff, h)
if u < 0 or u > 1 then
return
end
local q = cross3(diff, edge_1)
local v = f * dot(direction, q)
if v < 0 or u + v > 1 then
return
end
local pos_on_line = f * dot(edge_2, q)
if pos_on_line >= 0 then
return pos_on_line
end
local point_1, point_2, point_3 = unpack(triangle)
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
local h = cross3(direction, edge_2)
local a = dot(edge_1, h)
if math.abs(a) < 1e-9 then
return
end
local f = 1 / a
local diff = subtract(origin, point_1)
local u = f * dot(diff, h)
if u < 0 or u > 1 then
return
end
local q = cross3(diff, edge_1)
local v = f * dot(direction, q)
if v < 0 or u + v > 1 then
return
end
local pos_on_line = f * dot(edge_2, q)
if pos_on_line >= 0 then
return pos_on_line
end
end
function triangle_normal(triangle)
local point_1, point_2, point_3 = unpack(triangle)
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
return normalize(cross3(edge_1, edge_2))
local point_1, point_2, point_3 = unpack(triangle)
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
return normalize(cross3(edge_1, edge_2))
end