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

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

520
bluon.lua

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

542
conf.lua

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

@ -1,19 +1,19 @@
minetest.mkdir(minetest.get_worldpath().."/data") minetest.mkdir(minetest.get_worldpath().."/data")
function create_mod_storage(modname) function create_mod_storage(modname)
minetest.mkdir(minetest.get_worldpath().."/data/"..modname) minetest.mkdir(minetest.get_worldpath().."/data/"..modname)
end
function get_path(modname, filename) function get_path(modname, filename)
return minetest.get_worldpath().."/data/"..modname.."/"..filename return minetest.get_worldpath().."/data/"..modname.."/"..filename
end
function load(modname, filename) function load(modname, filename)
return minetest.deserialize(modlib.file.read(get_path(modname, filename)..".lua")) return minetest.deserialize(modlib.file.read(get_path(modname, filename)..".lua"))
end
function save(modname, filename, stuff) function save(modname, filename, stuff)
return modlib.file.write(get_path(modname, filename)..".lua", minetest.serialize(stuff)) return modlib.file.write(get_path(modname, filename)..".lua", minetest.serialize(stuff))
end
function load_json(modname, filename) function load_json(modname, filename)
return minetest.parse_json(modlib.file.read(get_path(modname, filename)..".json") or "null") return minetest.parse_json(modlib.file.read(get_path(modname, filename)..".json") or "null")
end
function save_json(modname, filename, stuff) function save_json(modname, filename, stuff)
return modlib.file.write(get_path(modname, filename)..".json", minetest.write_json(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 end
function ensure_content(filename, ensured_content) function ensure_content(filename, ensured_content)
local content = read(filename) local content = read(filename)
if content ~= ensured_content then if content ~= ensured_content then
return write(filename, ensured_content) return write(filename, ensured_content)
end end
return true return true
end end
function append(filename, new_content) function append(filename, new_content)

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

212
init.lua

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

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

94
log.lua

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

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

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

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

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

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

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

212
mod.lua

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

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

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

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

822
table.lua

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

422
test.lua

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

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