mirror of
https://github.com/appgurueu/modlib.git
synced 2024-11-22 07:13:45 +01:00
Indentation: Use only tabs
This commit is contained in:
parent
3acf09eed6
commit
adb4b4fdaf
644
b3d.lua
644
b3d.lua
@ -6,350 +6,350 @@ local metatable = {__index = getfenv(1)}
|
||||
--+ See `b3d_specification.txt` as well as https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp
|
||||
--> B3D model
|
||||
function read(stream)
|
||||
local left = 8
|
||||
local left = 8
|
||||
|
||||
local function byte()
|
||||
left = left - 1
|
||||
return assert(stream:read(1):byte())
|
||||
end
|
||||
local function byte()
|
||||
left = left - 1
|
||||
return assert(stream:read(1):byte())
|
||||
end
|
||||
|
||||
local function int()
|
||||
local value = byte() + byte() * 0x100 + byte() * 0x10000 + byte() * 0x1000000
|
||||
if value >= 2^31 then
|
||||
return value - 2^32
|
||||
end
|
||||
return value
|
||||
end
|
||||
local function int()
|
||||
local value = byte() + byte() * 0x100 + byte() * 0x10000 + byte() * 0x1000000
|
||||
if value >= 2^31 then
|
||||
return value - 2^32
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function id()
|
||||
return int() + 1
|
||||
end
|
||||
local function id()
|
||||
return int() + 1
|
||||
end
|
||||
|
||||
local function optional_id()
|
||||
local id = int()
|
||||
if id == -1 then
|
||||
return
|
||||
end
|
||||
return id + 1
|
||||
end
|
||||
local function optional_id()
|
||||
local id = int()
|
||||
if id == -1 then
|
||||
return
|
||||
end
|
||||
return id + 1
|
||||
end
|
||||
|
||||
local function string()
|
||||
local rope = {}
|
||||
while true do
|
||||
left = left - 1
|
||||
local char = assert(stream:read(1))
|
||||
if char == "\0" then
|
||||
return table.concat(rope)
|
||||
end
|
||||
table.insert(rope, char)
|
||||
end
|
||||
end
|
||||
local function string()
|
||||
local rope = {}
|
||||
while true do
|
||||
left = left - 1
|
||||
local char = assert(stream:read(1))
|
||||
if char == "\0" then
|
||||
return table.concat(rope)
|
||||
end
|
||||
table.insert(rope, char)
|
||||
end
|
||||
end
|
||||
|
||||
local read_single = modlib.binary.read_single
|
||||
local function float()
|
||||
return read_single(byte)
|
||||
end
|
||||
local read_single = modlib.binary.read_single
|
||||
local function float()
|
||||
return read_single(byte)
|
||||
end
|
||||
|
||||
local function float_array(length)
|
||||
local list = {}
|
||||
for index = 1, length do
|
||||
list[index] = float()
|
||||
end
|
||||
return list
|
||||
end
|
||||
local function float_array(length)
|
||||
local list = {}
|
||||
for index = 1, length do
|
||||
list[index] = float()
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
local function color()
|
||||
return {
|
||||
r = float(),
|
||||
g = float(),
|
||||
b = float(),
|
||||
a = float()
|
||||
}
|
||||
end
|
||||
local function color()
|
||||
return {
|
||||
r = float(),
|
||||
g = float(),
|
||||
b = float(),
|
||||
a = float()
|
||||
}
|
||||
end
|
||||
|
||||
local function vector3()
|
||||
return float_array(3)
|
||||
end
|
||||
local function vector3()
|
||||
return float_array(3)
|
||||
end
|
||||
|
||||
local function quaternion()
|
||||
return {[4] = float(), [1] = float(), [2] = float(), [3] = float()}
|
||||
end
|
||||
local function quaternion()
|
||||
return {[4] = float(), [1] = float(), [2] = float(), [3] = float()}
|
||||
end
|
||||
|
||||
local function content()
|
||||
assert(left >= 0, stream:seek())
|
||||
return left ~= 0
|
||||
end
|
||||
local function content()
|
||||
assert(left >= 0, stream:seek())
|
||||
return left ~= 0
|
||||
end
|
||||
|
||||
local chunk
|
||||
local chunks = {
|
||||
TEXS = function()
|
||||
local textures = {}
|
||||
while content() do
|
||||
table.insert(textures, {
|
||||
file = string(),
|
||||
flags = int(),
|
||||
blend = int(),
|
||||
pos = float_array(2),
|
||||
scale = float_array(2),
|
||||
rotation = float()
|
||||
})
|
||||
end
|
||||
return textures
|
||||
end,
|
||||
BRUS = function()
|
||||
local brushes = {}
|
||||
brushes.n_texs = int()
|
||||
assert(brushes.n_texs <= 8)
|
||||
while content() do
|
||||
local brush = {
|
||||
name = string(),
|
||||
color = color(),
|
||||
shininess = float(),
|
||||
blend = float(),
|
||||
fx = float(),
|
||||
texture_id = {}
|
||||
}
|
||||
for index = 1, brushes.n_texs do
|
||||
brush.texture_id[index] = optional_id()
|
||||
end
|
||||
table.insert(brushes, brush)
|
||||
end
|
||||
return brushes
|
||||
end,
|
||||
VRTS = function()
|
||||
local vertices = {
|
||||
flags = int(),
|
||||
tex_coord_sets = int(),
|
||||
tex_coord_set_size = int()
|
||||
}
|
||||
assert(vertices.tex_coord_sets <= 8 and vertices.tex_coord_set_size <= 4)
|
||||
local has_normal = (vertices.flags % 2 == 1) or nil
|
||||
local has_color = (math.floor(vertices.flags / 2) % 2 == 1) or nil
|
||||
while content() do
|
||||
local vertex = {
|
||||
pos = vector3(),
|
||||
normal = has_normal and vector3(),
|
||||
color = has_color and color(),
|
||||
tex_coords = {}
|
||||
}
|
||||
for tex_coord_set = 1, vertices.tex_coord_sets do
|
||||
local tex_coords = {}
|
||||
for tex_coord = 1, vertices.tex_coord_set_size do
|
||||
tex_coords[tex_coord] = float()
|
||||
end
|
||||
vertex.tex_coords[tex_coord_set] = tex_coords
|
||||
end
|
||||
table.insert(vertices, vertex)
|
||||
end
|
||||
return vertices
|
||||
end,
|
||||
TRIS = function()
|
||||
local tris = {
|
||||
brush_id = id(),
|
||||
vertex_ids = {}
|
||||
}
|
||||
while content() do
|
||||
table.insert(tris.vertex_ids, {id(), id(), id()})
|
||||
end
|
||||
return tris
|
||||
end,
|
||||
MESH = function()
|
||||
local mesh = {
|
||||
brush_id = optional_id(),
|
||||
vertices = chunk{VRTS = true}
|
||||
}
|
||||
mesh.triangle_sets = {}
|
||||
repeat
|
||||
local tris = chunk{TRIS = true}
|
||||
table.insert(mesh.triangle_sets, tris)
|
||||
until not content()
|
||||
return mesh
|
||||
end,
|
||||
BONE = function()
|
||||
local bone = {}
|
||||
while content() do
|
||||
local vertex_id = id()
|
||||
assert(not bone[vertex_id], "duplicate vertex weight")
|
||||
local weight = float()
|
||||
if weight > 0 then
|
||||
-- Many exporters include unneeded zero weights
|
||||
bone[vertex_id] = weight
|
||||
end
|
||||
end
|
||||
return bone
|
||||
end,
|
||||
KEYS = function()
|
||||
local flags = int()
|
||||
local _flags = flags % 8
|
||||
local rotation, scale, position
|
||||
if _flags >= 4 then
|
||||
rotation = true
|
||||
_flags = _flags - 4
|
||||
end
|
||||
if _flags >= 2 then
|
||||
scale = true
|
||||
_flags = _flags - 2
|
||||
end
|
||||
position = _flags >= 1
|
||||
local bone = {
|
||||
flags = flags
|
||||
}
|
||||
while content() do
|
||||
table.insert(bone, {
|
||||
frame = int(),
|
||||
position = position and vector3() or nil,
|
||||
scale = scale and vector3() or nil,
|
||||
rotation = rotation and quaternion() or nil
|
||||
})
|
||||
end
|
||||
-- Ensure frames are sorted ascending
|
||||
table.sort(bone, function(a, b) return a.frame < b.frame end)
|
||||
return bone
|
||||
end,
|
||||
ANIM = function()
|
||||
return {
|
||||
-- flags are unused
|
||||
flags = int(),
|
||||
frames = int(),
|
||||
fps = float()
|
||||
}
|
||||
end,
|
||||
NODE = function()
|
||||
local node = {
|
||||
name = string(),
|
||||
position = vector3(),
|
||||
scale = vector3(),
|
||||
keys = {},
|
||||
rotation = quaternion(),
|
||||
children = {}
|
||||
}
|
||||
local node_type
|
||||
-- See https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp#L263
|
||||
-- Order is not validated; double occurences of mutually exclusive node def are
|
||||
while content() do
|
||||
local elem, type = chunk()
|
||||
if type == "MESH" then
|
||||
assert(not node_type)
|
||||
node_type = "mesh"
|
||||
node.mesh = elem
|
||||
elseif type == "BONE" then
|
||||
assert(not node_type)
|
||||
node_type = "bone"
|
||||
node.bone = elem
|
||||
elseif type == "KEYS" then
|
||||
assert((node.keys[#node.keys] or {}).frame ~= (elem[1] or {}).frame, "duplicate frame")
|
||||
modlib.table.append(node.keys, elem)
|
||||
elseif type == "NODE" then
|
||||
table.insert(node.children, elem)
|
||||
elseif type == "ANIM" then
|
||||
node.animation = elem
|
||||
else
|
||||
assert(not node_type)
|
||||
node_type = "pivot"
|
||||
end
|
||||
end
|
||||
-- TODO somehow merge keys
|
||||
return node
|
||||
end,
|
||||
BB3D = function()
|
||||
local version = int()
|
||||
local self = {
|
||||
version = {
|
||||
major = math.floor(version / 100),
|
||||
minor = version % 100,
|
||||
raw = version
|
||||
},
|
||||
textures = {},
|
||||
brushes = {}
|
||||
}
|
||||
assert(self.version.major <= 2, "unsupported version: " .. self.version.major)
|
||||
while content() do
|
||||
local field, type = chunk{TEXS = true, BRUS = true, NODE = true}
|
||||
if type == "TEXS" then
|
||||
modlib.table.append(self.textures, field)
|
||||
elseif type == "BRUS" then
|
||||
modlib.table.append(self.brushes, field)
|
||||
else
|
||||
self.node = field
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
}
|
||||
local chunk
|
||||
local chunks = {
|
||||
TEXS = function()
|
||||
local textures = {}
|
||||
while content() do
|
||||
table.insert(textures, {
|
||||
file = string(),
|
||||
flags = int(),
|
||||
blend = int(),
|
||||
pos = float_array(2),
|
||||
scale = float_array(2),
|
||||
rotation = float()
|
||||
})
|
||||
end
|
||||
return textures
|
||||
end,
|
||||
BRUS = function()
|
||||
local brushes = {}
|
||||
brushes.n_texs = int()
|
||||
assert(brushes.n_texs <= 8)
|
||||
while content() do
|
||||
local brush = {
|
||||
name = string(),
|
||||
color = color(),
|
||||
shininess = float(),
|
||||
blend = float(),
|
||||
fx = float(),
|
||||
texture_id = {}
|
||||
}
|
||||
for index = 1, brushes.n_texs do
|
||||
brush.texture_id[index] = optional_id()
|
||||
end
|
||||
table.insert(brushes, brush)
|
||||
end
|
||||
return brushes
|
||||
end,
|
||||
VRTS = function()
|
||||
local vertices = {
|
||||
flags = int(),
|
||||
tex_coord_sets = int(),
|
||||
tex_coord_set_size = int()
|
||||
}
|
||||
assert(vertices.tex_coord_sets <= 8 and vertices.tex_coord_set_size <= 4)
|
||||
local has_normal = (vertices.flags % 2 == 1) or nil
|
||||
local has_color = (math.floor(vertices.flags / 2) % 2 == 1) or nil
|
||||
while content() do
|
||||
local vertex = {
|
||||
pos = vector3(),
|
||||
normal = has_normal and vector3(),
|
||||
color = has_color and color(),
|
||||
tex_coords = {}
|
||||
}
|
||||
for tex_coord_set = 1, vertices.tex_coord_sets do
|
||||
local tex_coords = {}
|
||||
for tex_coord = 1, vertices.tex_coord_set_size do
|
||||
tex_coords[tex_coord] = float()
|
||||
end
|
||||
vertex.tex_coords[tex_coord_set] = tex_coords
|
||||
end
|
||||
table.insert(vertices, vertex)
|
||||
end
|
||||
return vertices
|
||||
end,
|
||||
TRIS = function()
|
||||
local tris = {
|
||||
brush_id = id(),
|
||||
vertex_ids = {}
|
||||
}
|
||||
while content() do
|
||||
table.insert(tris.vertex_ids, {id(), id(), id()})
|
||||
end
|
||||
return tris
|
||||
end,
|
||||
MESH = function()
|
||||
local mesh = {
|
||||
brush_id = optional_id(),
|
||||
vertices = chunk{VRTS = true}
|
||||
}
|
||||
mesh.triangle_sets = {}
|
||||
repeat
|
||||
local tris = chunk{TRIS = true}
|
||||
table.insert(mesh.triangle_sets, tris)
|
||||
until not content()
|
||||
return mesh
|
||||
end,
|
||||
BONE = function()
|
||||
local bone = {}
|
||||
while content() do
|
||||
local vertex_id = id()
|
||||
assert(not bone[vertex_id], "duplicate vertex weight")
|
||||
local weight = float()
|
||||
if weight > 0 then
|
||||
-- Many exporters include unneeded zero weights
|
||||
bone[vertex_id] = weight
|
||||
end
|
||||
end
|
||||
return bone
|
||||
end,
|
||||
KEYS = function()
|
||||
local flags = int()
|
||||
local _flags = flags % 8
|
||||
local rotation, scale, position
|
||||
if _flags >= 4 then
|
||||
rotation = true
|
||||
_flags = _flags - 4
|
||||
end
|
||||
if _flags >= 2 then
|
||||
scale = true
|
||||
_flags = _flags - 2
|
||||
end
|
||||
position = _flags >= 1
|
||||
local bone = {
|
||||
flags = flags
|
||||
}
|
||||
while content() do
|
||||
table.insert(bone, {
|
||||
frame = int(),
|
||||
position = position and vector3() or nil,
|
||||
scale = scale and vector3() or nil,
|
||||
rotation = rotation and quaternion() or nil
|
||||
})
|
||||
end
|
||||
-- Ensure frames are sorted ascending
|
||||
table.sort(bone, function(a, b) return a.frame < b.frame end)
|
||||
return bone
|
||||
end,
|
||||
ANIM = function()
|
||||
return {
|
||||
-- flags are unused
|
||||
flags = int(),
|
||||
frames = int(),
|
||||
fps = float()
|
||||
}
|
||||
end,
|
||||
NODE = function()
|
||||
local node = {
|
||||
name = string(),
|
||||
position = vector3(),
|
||||
scale = vector3(),
|
||||
keys = {},
|
||||
rotation = quaternion(),
|
||||
children = {}
|
||||
}
|
||||
local node_type
|
||||
-- See https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp#L263
|
||||
-- Order is not validated; double occurences of mutually exclusive node def are
|
||||
while content() do
|
||||
local elem, type = chunk()
|
||||
if type == "MESH" then
|
||||
assert(not node_type)
|
||||
node_type = "mesh"
|
||||
node.mesh = elem
|
||||
elseif type == "BONE" then
|
||||
assert(not node_type)
|
||||
node_type = "bone"
|
||||
node.bone = elem
|
||||
elseif type == "KEYS" then
|
||||
assert((node.keys[#node.keys] or {}).frame ~= (elem[1] or {}).frame, "duplicate frame")
|
||||
modlib.table.append(node.keys, elem)
|
||||
elseif type == "NODE" then
|
||||
table.insert(node.children, elem)
|
||||
elseif type == "ANIM" then
|
||||
node.animation = elem
|
||||
else
|
||||
assert(not node_type)
|
||||
node_type = "pivot"
|
||||
end
|
||||
end
|
||||
-- TODO somehow merge keys
|
||||
return node
|
||||
end,
|
||||
BB3D = function()
|
||||
local version = int()
|
||||
local self = {
|
||||
version = {
|
||||
major = math.floor(version / 100),
|
||||
minor = version % 100,
|
||||
raw = version
|
||||
},
|
||||
textures = {},
|
||||
brushes = {}
|
||||
}
|
||||
assert(self.version.major <= 2, "unsupported version: " .. self.version.major)
|
||||
while content() do
|
||||
local field, type = chunk{TEXS = true, BRUS = true, NODE = true}
|
||||
if type == "TEXS" then
|
||||
modlib.table.append(self.textures, field)
|
||||
elseif type == "BRUS" then
|
||||
modlib.table.append(self.brushes, field)
|
||||
else
|
||||
self.node = field
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
}
|
||||
|
||||
local function chunk_header()
|
||||
left = left - 4
|
||||
return stream:read(4), int()
|
||||
end
|
||||
local function chunk_header()
|
||||
left = left - 4
|
||||
return stream:read(4), int()
|
||||
end
|
||||
|
||||
function chunk(possible_chunks)
|
||||
local type, new_left = chunk_header()
|
||||
local parent_left
|
||||
left, parent_left = new_left, left
|
||||
if possible_chunks and not possible_chunks[type] then
|
||||
error("expected one of " .. table.concat(modlib.table.keys(possible_chunks), ", ") .. ", found " .. type)
|
||||
end
|
||||
local res = assert(chunks[type])()
|
||||
assert(left == 0)
|
||||
left = parent_left - new_left
|
||||
return res, type
|
||||
end
|
||||
function chunk(possible_chunks)
|
||||
local type, new_left = chunk_header()
|
||||
local parent_left
|
||||
left, parent_left = new_left, left
|
||||
if possible_chunks and not possible_chunks[type] then
|
||||
error("expected one of " .. table.concat(modlib.table.keys(possible_chunks), ", ") .. ", found " .. type)
|
||||
end
|
||||
local res = assert(chunks[type])()
|
||||
assert(left == 0)
|
||||
left = parent_left - new_left
|
||||
return res, type
|
||||
end
|
||||
|
||||
local self = chunk{BB3D = true}
|
||||
return setmetatable(self, metatable)
|
||||
local self = chunk{BB3D = true}
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
-- TODO function write(self, stream)
|
||||
|
||||
local binary_search_frame = modlib.table.binary_search_comparator(function(a, b)
|
||||
return modlib.table.default_comparator(a, b.frame)
|
||||
return modlib.table.default_comparator(a, b.frame)
|
||||
end)
|
||||
|
||||
--> [bonename] = { position = vector, rotation = quaternion, scale = vector }
|
||||
function get_animated_bone_properties(self, keyframe, interpolate)
|
||||
local function get_frame_values(keys)
|
||||
local values = keys[keyframe]
|
||||
if values and values.frame == keyframe then
|
||||
return {
|
||||
position = values.position,
|
||||
rotation = values.rotation,
|
||||
scale = values.scale
|
||||
}
|
||||
end
|
||||
local index = binary_search_frame(keys, keyframe)
|
||||
if index > 0 then
|
||||
return keys[index]
|
||||
end
|
||||
index = -index
|
||||
assert(index > 1 and index <= #keys)
|
||||
local a, b = keys[index - 1], keys[index]
|
||||
if not interpolate then
|
||||
return a
|
||||
end
|
||||
local ratio = (keyframe - a.frame) / (b.frame - a.frame)
|
||||
return {
|
||||
position = (a.position and b.position and modlib.vector.interpolate(a.position, b.position, ratio)) or a.position or b.position,
|
||||
rotation = (a.rotation and b.rotation and modlib.quaternion.interpolate(a.rotation, b.rotation, ratio)) or a.rotation or b.rotation,
|
||||
scale = (a.scale and b.scale and modlib.vector.interpolate(a.scale, b.scale, ratio)) or a.scale or b.scale,
|
||||
}
|
||||
end
|
||||
local bone_properties = {}
|
||||
local function get_props(node)
|
||||
local properties = {}
|
||||
if node.keys and next(node.keys) ~= nil then
|
||||
properties = modlib.table.add_all(properties, get_frame_values(node.keys))
|
||||
end
|
||||
for _, property in pairs{"position", "rotation", "scale"} do
|
||||
properties[property] = properties[property] or modlib.table.copy(node[property])
|
||||
end
|
||||
if node.bone then
|
||||
assert(not bone_properties[node.name])
|
||||
bone_properties[node.name] = properties
|
||||
end
|
||||
for _, child in pairs(node.children or {}) do
|
||||
get_props(child)
|
||||
end
|
||||
end
|
||||
get_props(self.node)
|
||||
return bone_properties
|
||||
local function get_frame_values(keys)
|
||||
local values = keys[keyframe]
|
||||
if values and values.frame == keyframe then
|
||||
return {
|
||||
position = values.position,
|
||||
rotation = values.rotation,
|
||||
scale = values.scale
|
||||
}
|
||||
end
|
||||
local index = binary_search_frame(keys, keyframe)
|
||||
if index > 0 then
|
||||
return keys[index]
|
||||
end
|
||||
index = -index
|
||||
assert(index > 1 and index <= #keys)
|
||||
local a, b = keys[index - 1], keys[index]
|
||||
if not interpolate then
|
||||
return a
|
||||
end
|
||||
local ratio = (keyframe - a.frame) / (b.frame - a.frame)
|
||||
return {
|
||||
position = (a.position and b.position and modlib.vector.interpolate(a.position, b.position, ratio)) or a.position or b.position,
|
||||
rotation = (a.rotation and b.rotation and modlib.quaternion.interpolate(a.rotation, b.rotation, ratio)) or a.rotation or b.rotation,
|
||||
scale = (a.scale and b.scale and modlib.vector.interpolate(a.scale, b.scale, ratio)) or a.scale or b.scale,
|
||||
}
|
||||
end
|
||||
local bone_properties = {}
|
||||
local function get_props(node)
|
||||
local properties = {}
|
||||
if node.keys and next(node.keys) ~= nil then
|
||||
properties = modlib.table.add_all(properties, get_frame_values(node.keys))
|
||||
end
|
||||
for _, property in pairs{"position", "rotation", "scale"} do
|
||||
properties[property] = properties[property] or modlib.table.copy(node[property])
|
||||
end
|
||||
if node.bone then
|
||||
assert(not bone_properties[node.name])
|
||||
bone_properties[node.name] = properties
|
||||
end
|
||||
for _, child in pairs(node.children or {}) do
|
||||
get_props(child)
|
||||
end
|
||||
end
|
||||
get_props(self.node)
|
||||
return bone_properties
|
||||
end
|
168
binary.lua
168
binary.lua
@ -3,113 +3,113 @@
|
||||
--+ Reads doubles (f64) or floats (f32)
|
||||
--: double reads an f64 if true, f32 otherwise
|
||||
function read_float(read_byte, double)
|
||||
-- First read the mantissa
|
||||
local mantissa = 0
|
||||
for _ = 1, double and 6 or 2 do
|
||||
mantissa = (mantissa + read_byte()) / 0x100
|
||||
end
|
||||
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
|
||||
local byte_2, byte_1 = read_byte(), read_byte()
|
||||
local sign = 1
|
||||
if byte_1 >= 0x80 then
|
||||
sign = -1
|
||||
byte_1 = byte_1 - 0x80
|
||||
end
|
||||
local exponent = byte_1 * 2
|
||||
if byte_2 >= 0x80 then
|
||||
exponent = exponent + 1
|
||||
byte_2 = byte_2 - 0x80
|
||||
end
|
||||
mantissa = (mantissa + byte_2) / 0x80
|
||||
if exponent == 0xFF then
|
||||
if mantissa == 0 then
|
||||
return sign * math.huge
|
||||
end
|
||||
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
|
||||
-- HACK ((0/0)^1) yields nan, 0/0 yields -nan
|
||||
return sign == 1 and ((0/0)^1) or 0/0
|
||||
end
|
||||
assert(mantissa < 1)
|
||||
if exponent == 0 then
|
||||
-- subnormal value
|
||||
return sign * 2^-126 * mantissa
|
||||
end
|
||||
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
|
||||
-- First read the mantissa
|
||||
local mantissa = 0
|
||||
for _ = 1, double and 6 or 2 do
|
||||
mantissa = (mantissa + read_byte()) / 0x100
|
||||
end
|
||||
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
|
||||
local byte_2, byte_1 = read_byte(), read_byte()
|
||||
local sign = 1
|
||||
if byte_1 >= 0x80 then
|
||||
sign = -1
|
||||
byte_1 = byte_1 - 0x80
|
||||
end
|
||||
local exponent = byte_1 * 2
|
||||
if byte_2 >= 0x80 then
|
||||
exponent = exponent + 1
|
||||
byte_2 = byte_2 - 0x80
|
||||
end
|
||||
mantissa = (mantissa + byte_2) / 0x80
|
||||
if exponent == 0xFF then
|
||||
if mantissa == 0 then
|
||||
return sign * math.huge
|
||||
end
|
||||
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
|
||||
-- HACK ((0/0)^1) yields nan, 0/0 yields -nan
|
||||
return sign == 1 and ((0/0)^1) or 0/0
|
||||
end
|
||||
assert(mantissa < 1)
|
||||
if exponent == 0 then
|
||||
-- subnormal value
|
||||
return sign * 2^-126 * mantissa
|
||||
end
|
||||
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
|
||||
end
|
||||
|
||||
--+ Reads a single floating point number (f32)
|
||||
function read_single(read_byte)
|
||||
return read_float(read_byte)
|
||||
return read_float(read_byte)
|
||||
end
|
||||
|
||||
--+ Reads a double (f64)
|
||||
function read_double(read_byte)
|
||||
return read_float(read_byte, true)
|
||||
return read_float(read_byte, true)
|
||||
end
|
||||
|
||||
function read_uint(read_byte, bytes)
|
||||
local factor = 1
|
||||
local uint = 0
|
||||
for _ = 1, bytes do
|
||||
uint = uint + read_byte() * factor
|
||||
factor = factor * 0x100
|
||||
end
|
||||
return uint
|
||||
local factor = 1
|
||||
local uint = 0
|
||||
for _ = 1, bytes do
|
||||
uint = uint + read_byte() * factor
|
||||
factor = factor * 0x100
|
||||
end
|
||||
return uint
|
||||
end
|
||||
|
||||
function write_uint(write_byte, uint, bytes)
|
||||
for _ = 1, bytes do
|
||||
write_byte(uint % 0x100)
|
||||
uint = math.floor(uint / 0x100)
|
||||
end
|
||||
assert(uint == 0)
|
||||
for _ = 1, bytes do
|
||||
write_byte(uint % 0x100)
|
||||
uint = math.floor(uint / 0x100)
|
||||
end
|
||||
assert(uint == 0)
|
||||
end
|
||||
|
||||
--: on_write function(double)
|
||||
--: double set to true to force f64, false for f32, nil for auto
|
||||
function write_float(write_byte, number, on_write, double)
|
||||
local sign = 0
|
||||
if number < 0 then
|
||||
number = -number
|
||||
sign = 0x80
|
||||
end
|
||||
local mantissa, exponent = math.frexp(number)
|
||||
exponent = exponent + 127
|
||||
if exponent > 1 then
|
||||
-- TODO ensure this deals properly with subnormal numbers
|
||||
mantissa = mantissa * 2 - 1
|
||||
exponent = exponent - 1
|
||||
end
|
||||
local sign_byte = sign + math.floor(exponent / 2)
|
||||
mantissa = mantissa * 0x80
|
||||
local exponent_byte = (exponent % 2) * 0x80 + math.floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
local mantissa_bytes = {}
|
||||
-- TODO ensure this check is proper
|
||||
if double == nil then
|
||||
double = mantissa % 2^-23 > 0
|
||||
end
|
||||
if on_write then
|
||||
on_write(double)
|
||||
end
|
||||
local len = double and 6 or 2
|
||||
for index = len, 1, -1 do
|
||||
mantissa = mantissa * 0x100
|
||||
mantissa_bytes[index] = math.floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
end
|
||||
assert(mantissa == 0)
|
||||
for index = 1, len do
|
||||
write_byte(mantissa_bytes[index])
|
||||
end
|
||||
write_byte(exponent_byte)
|
||||
write_byte(sign_byte)
|
||||
local sign = 0
|
||||
if number < 0 then
|
||||
number = -number
|
||||
sign = 0x80
|
||||
end
|
||||
local mantissa, exponent = math.frexp(number)
|
||||
exponent = exponent + 127
|
||||
if exponent > 1 then
|
||||
-- TODO ensure this deals properly with subnormal numbers
|
||||
mantissa = mantissa * 2 - 1
|
||||
exponent = exponent - 1
|
||||
end
|
||||
local sign_byte = sign + math.floor(exponent / 2)
|
||||
mantissa = mantissa * 0x80
|
||||
local exponent_byte = (exponent % 2) * 0x80 + math.floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
local mantissa_bytes = {}
|
||||
-- TODO ensure this check is proper
|
||||
if double == nil then
|
||||
double = mantissa % 2^-23 > 0
|
||||
end
|
||||
if on_write then
|
||||
on_write(double)
|
||||
end
|
||||
local len = double and 6 or 2
|
||||
for index = len, 1, -1 do
|
||||
mantissa = mantissa * 0x100
|
||||
mantissa_bytes[index] = math.floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
end
|
||||
assert(mantissa == 0)
|
||||
for index = 1, len do
|
||||
write_byte(mantissa_bytes[index])
|
||||
end
|
||||
write_byte(exponent_byte)
|
||||
write_byte(sign_byte)
|
||||
end
|
||||
|
||||
function write_single(write_byte, number)
|
||||
return write_float(write_byte, number, nil, false)
|
||||
return write_float(write_byte, number, nil, false)
|
||||
end
|
||||
|
||||
function write_double(write_byte, number)
|
||||
return write_float(write_byte, number, nil, true)
|
||||
return write_float(write_byte, number, nil, true)
|
||||
end
|
520
bluon.lua
520
bluon.lua
@ -3,23 +3,23 @@ local bluon = getfenv(1)
|
||||
local metatable = {__index = bluon}
|
||||
|
||||
function new(self)
|
||||
return setmetatable(self or {}, metatable)
|
||||
return setmetatable(self or {}, metatable)
|
||||
end
|
||||
|
||||
function aux_is_valid()
|
||||
return false
|
||||
return false
|
||||
end
|
||||
|
||||
function aux_len(object)
|
||||
error("unsupported type: " .. type(object))
|
||||
error("unsupported type: " .. type(object))
|
||||
end
|
||||
|
||||
function aux_read(type)
|
||||
error(("unsupported type: 0x%02X"):format(type))
|
||||
error(("unsupported type: 0x%02X"):format(type))
|
||||
end
|
||||
|
||||
function aux_write(object)
|
||||
error("unsupported type: " .. type(object))
|
||||
error("unsupported type: " .. type(object))
|
||||
end
|
||||
|
||||
local uint_widths = {1, 2, 4, 8}
|
||||
@ -27,291 +27,291 @@ local uint_types = #uint_widths
|
||||
local type_ranges = {}
|
||||
local current = 0
|
||||
for _, type in ipairs{
|
||||
{"boolean", 2};
|
||||
-- 0, -nan, +inf, -inf: sign of nan can be ignored
|
||||
{"number_constant", 4};
|
||||
{"number_negative", uint_types};
|
||||
{"number_positive", uint_types};
|
||||
{"number_f32", 1};
|
||||
{"number", 1};
|
||||
{"string_constant", 1};
|
||||
{"string", uint_types};
|
||||
-- (T0, T8, T16, T32, T64) x (L0, L8, L16, L32, L64)
|
||||
{"table", (uint_types + 1) ^ 2};
|
||||
{"reference", uint_types}
|
||||
{"boolean", 2};
|
||||
-- 0, -nan, +inf, -inf: sign of nan can be ignored
|
||||
{"number_constant", 4};
|
||||
{"number_negative", uint_types};
|
||||
{"number_positive", uint_types};
|
||||
{"number_f32", 1};
|
||||
{"number", 1};
|
||||
{"string_constant", 1};
|
||||
{"string", uint_types};
|
||||
-- (T0, T8, T16, T32, T64) x (L0, L8, L16, L32, L64)
|
||||
{"table", (uint_types + 1) ^ 2};
|
||||
{"reference", uint_types}
|
||||
} do
|
||||
local typename, length = unpack(type)
|
||||
current = current + length
|
||||
type_ranges[typename] = current
|
||||
local typename, length = unpack(type)
|
||||
current = current + length
|
||||
type_ranges[typename] = current
|
||||
end
|
||||
|
||||
local constants = {
|
||||
[false] = "\0",
|
||||
[true] = "\1",
|
||||
[0] = "\2",
|
||||
-- not possible as table entry as Lua doesn't allow +/-nan as table key
|
||||
-- [0/0] = "\3",
|
||||
[math.huge] = "\4",
|
||||
[-math.huge] = "\5",
|
||||
[""] = "\20"
|
||||
[false] = "\0",
|
||||
[true] = "\1",
|
||||
[0] = "\2",
|
||||
-- not possible as table entry as Lua doesn't allow +/-nan as table key
|
||||
-- [0/0] = "\3",
|
||||
[math.huge] = "\4",
|
||||
[-math.huge] = "\5",
|
||||
[""] = "\20"
|
||||
}
|
||||
|
||||
local constant_nan = "\3"
|
||||
|
||||
local function uint_type(uint)
|
||||
--U8
|
||||
if uint <= 0xFF then return 1 end
|
||||
--U16
|
||||
if uint <= 0xFFFF then return 2 end
|
||||
--U32
|
||||
if uint <= 0xFFFFFFFF then return 3 end
|
||||
--U64
|
||||
return 4
|
||||
--U8
|
||||
if uint <= 0xFF then return 1 end
|
||||
--U16
|
||||
if uint <= 0xFFFF then return 2 end
|
||||
--U32
|
||||
if uint <= 0xFFFFFFFF then return 3 end
|
||||
--U64
|
||||
return 4
|
||||
end
|
||||
|
||||
local valid_types = modlib.table.set{"nil", "boolean", "number", "string"}
|
||||
function is_valid(self, object)
|
||||
local _type = type(object)
|
||||
if valid_types[_type] then
|
||||
return true
|
||||
end
|
||||
if _type == "table" then
|
||||
for key, value in pairs(object) do
|
||||
if not (is_valid(self, key) and is_valid(self, value)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return self.aux_is_valid(object)
|
||||
local _type = type(object)
|
||||
if valid_types[_type] then
|
||||
return true
|
||||
end
|
||||
if _type == "table" then
|
||||
for key, value in pairs(object) do
|
||||
if not (is_valid(self, key) and is_valid(self, value)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return self.aux_is_valid(object)
|
||||
end
|
||||
|
||||
local function uint_len(uint)
|
||||
return uint_widths[uint_type(uint)]
|
||||
return uint_widths[uint_type(uint)]
|
||||
end
|
||||
|
||||
local function is_map_key(key, list_len)
|
||||
return type(key) ~= "number" or (key < 1 or key > list_len or key % 1 ~= 0)
|
||||
return type(key) ~= "number" or (key < 1 or key > list_len or key % 1 ~= 0)
|
||||
end
|
||||
|
||||
function len(self, object)
|
||||
if constants[object] then
|
||||
return 1
|
||||
end
|
||||
local _type = type(object)
|
||||
if _type == "number" then
|
||||
if object ~= object then
|
||||
stream:write(constant_nan)
|
||||
return
|
||||
end
|
||||
if object % 1 == 0 then
|
||||
return 1 + uint_len(object > 0 and object or -object)
|
||||
end
|
||||
-- TODO ensure this check is proper
|
||||
if mantissa % 2^-23 > 0 then
|
||||
return 9
|
||||
end
|
||||
return 5
|
||||
end
|
||||
local id = object_ids[object]
|
||||
if id then
|
||||
return 1 + uint_len(id)
|
||||
end
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
if _type == "string" then
|
||||
local object_len = object:len()
|
||||
return 1 + uint_len(object_len) + object_len
|
||||
end
|
||||
if _type == "table" then
|
||||
if next(object) == nil then
|
||||
-- empty {} table
|
||||
byte(type_ranges.string + 1)
|
||||
return 1
|
||||
end
|
||||
local list_len = #object
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local table_len = 1 + uint_len(list_len) + uint_len(kv_len)
|
||||
for index = 1, list_len do
|
||||
table_len = table_len + len(self, object[index])
|
||||
end
|
||||
for key, value in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
table_len = table_len + len(self, key) + len(self, value)
|
||||
end
|
||||
end
|
||||
return len
|
||||
end
|
||||
return self.aux_len(object)
|
||||
if constants[object] then
|
||||
return 1
|
||||
end
|
||||
local _type = type(object)
|
||||
if _type == "number" then
|
||||
if object ~= object then
|
||||
stream:write(constant_nan)
|
||||
return
|
||||
end
|
||||
if object % 1 == 0 then
|
||||
return 1 + uint_len(object > 0 and object or -object)
|
||||
end
|
||||
-- TODO ensure this check is proper
|
||||
if mantissa % 2^-23 > 0 then
|
||||
return 9
|
||||
end
|
||||
return 5
|
||||
end
|
||||
local id = object_ids[object]
|
||||
if id then
|
||||
return 1 + uint_len(id)
|
||||
end
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
if _type == "string" then
|
||||
local object_len = object:len()
|
||||
return 1 + uint_len(object_len) + object_len
|
||||
end
|
||||
if _type == "table" then
|
||||
if next(object) == nil then
|
||||
-- empty {} table
|
||||
byte(type_ranges.string + 1)
|
||||
return 1
|
||||
end
|
||||
local list_len = #object
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local table_len = 1 + uint_len(list_len) + uint_len(kv_len)
|
||||
for index = 1, list_len do
|
||||
table_len = table_len + len(self, object[index])
|
||||
end
|
||||
for key, value in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
table_len = table_len + len(self, key) + len(self, value)
|
||||
end
|
||||
end
|
||||
return len
|
||||
end
|
||||
return self.aux_len(object)
|
||||
end
|
||||
|
||||
--: stream any object implementing :write(text)
|
||||
function write(self, object, stream)
|
||||
if object == nil then
|
||||
return
|
||||
end
|
||||
local object_ids = {}
|
||||
local current_id = 0
|
||||
local function byte(byte)
|
||||
stream:write(string.char(byte))
|
||||
end
|
||||
local write_uint = modlib.binary.write_uint
|
||||
local function uint(type, uint)
|
||||
write_uint(byte, uint, uint_widths[type])
|
||||
end
|
||||
local function uint_with_type(base, _uint)
|
||||
local type_offset = uint_type(_uint)
|
||||
byte(base + type_offset)
|
||||
uint(type_offset, _uint)
|
||||
end
|
||||
local write_float = modlib.binary.write_float
|
||||
local function float_on_write(double)
|
||||
byte(double and type_ranges.number or type_ranges.number_f32)
|
||||
end
|
||||
local function float(number)
|
||||
write_float(byte, number, float_on_write)
|
||||
end
|
||||
local aux_write = self.aux_write
|
||||
local function _write(object)
|
||||
local constant = constants[object]
|
||||
if constant then
|
||||
stream:write(constant)
|
||||
return
|
||||
end
|
||||
local _type = type(object)
|
||||
if _type == "number" then
|
||||
if object ~= object then
|
||||
stream:write(constant_nan)
|
||||
return
|
||||
end
|
||||
if object % 1 == 0 then
|
||||
uint_with_type(object > 0 and type_ranges.number_constant or type_ranges.number_negative, object > 0 and object or -object)
|
||||
return
|
||||
end
|
||||
float(object)
|
||||
return
|
||||
end
|
||||
local id = object_ids[object]
|
||||
if id then
|
||||
uint_with_type(type_ranges.table, id)
|
||||
return
|
||||
end
|
||||
if _type == "string" then
|
||||
local len = object:len()
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
uint_with_type(type_ranges.number, len)
|
||||
stream:write(object)
|
||||
return
|
||||
end
|
||||
if _type == "table" then
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
if next(object) == nil then
|
||||
-- empty {} table
|
||||
byte(type_ranges.string + 1)
|
||||
return
|
||||
end
|
||||
local list_len = #object
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local list_len_sig = uint_type(list_len)
|
||||
local kv_len_sig = uint_type(kv_len)
|
||||
byte(type_ranges.string + list_len_sig + kv_len_sig * 5 + 1)
|
||||
uint(list_len_sig, list_len)
|
||||
uint(kv_len_sig, kv_len)
|
||||
for index = 1, list_len do
|
||||
_write(object[index])
|
||||
end
|
||||
for key, value in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
_write(key)
|
||||
_write(value)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
aux_write(object, object_ids)
|
||||
end
|
||||
_write(object)
|
||||
if object == nil then
|
||||
return
|
||||
end
|
||||
local object_ids = {}
|
||||
local current_id = 0
|
||||
local function byte(byte)
|
||||
stream:write(string.char(byte))
|
||||
end
|
||||
local write_uint = modlib.binary.write_uint
|
||||
local function uint(type, uint)
|
||||
write_uint(byte, uint, uint_widths[type])
|
||||
end
|
||||
local function uint_with_type(base, _uint)
|
||||
local type_offset = uint_type(_uint)
|
||||
byte(base + type_offset)
|
||||
uint(type_offset, _uint)
|
||||
end
|
||||
local write_float = modlib.binary.write_float
|
||||
local function float_on_write(double)
|
||||
byte(double and type_ranges.number or type_ranges.number_f32)
|
||||
end
|
||||
local function float(number)
|
||||
write_float(byte, number, float_on_write)
|
||||
end
|
||||
local aux_write = self.aux_write
|
||||
local function _write(object)
|
||||
local constant = constants[object]
|
||||
if constant then
|
||||
stream:write(constant)
|
||||
return
|
||||
end
|
||||
local _type = type(object)
|
||||
if _type == "number" then
|
||||
if object ~= object then
|
||||
stream:write(constant_nan)
|
||||
return
|
||||
end
|
||||
if object % 1 == 0 then
|
||||
uint_with_type(object > 0 and type_ranges.number_constant or type_ranges.number_negative, object > 0 and object or -object)
|
||||
return
|
||||
end
|
||||
float(object)
|
||||
return
|
||||
end
|
||||
local id = object_ids[object]
|
||||
if id then
|
||||
uint_with_type(type_ranges.table, id)
|
||||
return
|
||||
end
|
||||
if _type == "string" then
|
||||
local len = object:len()
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
uint_with_type(type_ranges.number, len)
|
||||
stream:write(object)
|
||||
return
|
||||
end
|
||||
if _type == "table" then
|
||||
current_id = current_id + 1
|
||||
object_ids[object] = current_id
|
||||
if next(object) == nil then
|
||||
-- empty {} table
|
||||
byte(type_ranges.string + 1)
|
||||
return
|
||||
end
|
||||
local list_len = #object
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local list_len_sig = uint_type(list_len)
|
||||
local kv_len_sig = uint_type(kv_len)
|
||||
byte(type_ranges.string + list_len_sig + kv_len_sig * 5 + 1)
|
||||
uint(list_len_sig, list_len)
|
||||
uint(kv_len_sig, kv_len)
|
||||
for index = 1, list_len do
|
||||
_write(object[index])
|
||||
end
|
||||
for key, value in pairs(object) do
|
||||
if is_map_key(key, list_len) then
|
||||
_write(key)
|
||||
_write(value)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
aux_write(object, object_ids)
|
||||
end
|
||||
_write(object)
|
||||
end
|
||||
|
||||
local constants_flipped = modlib.table.flip(constants)
|
||||
|
||||
-- See https://www.lua.org/manual/5.1/manual.html#2.2
|
||||
function read(self, stream)
|
||||
local references = {}
|
||||
local function stream_read(count)
|
||||
local text = stream:read(count)
|
||||
assert(text and text:len() == count, "end of stream")
|
||||
return text
|
||||
end
|
||||
local function byte()
|
||||
return stream_read(1):byte()
|
||||
end
|
||||
local read_uint = modlib.binary.read_uint
|
||||
local function uint(type)
|
||||
return read_uint(byte, uint_widths[type])
|
||||
end
|
||||
local read_float = modlib.binary.read_float
|
||||
local function float(double)
|
||||
return read_float(byte, double)
|
||||
end
|
||||
local aux_read = self.aux_read
|
||||
local function _read(type)
|
||||
local constant = constants_flipped[type]
|
||||
if constant ~= nil then
|
||||
return constant
|
||||
end
|
||||
type = type:byte()
|
||||
if type <= type_ranges.number then
|
||||
if type <= type_ranges.number_negative then
|
||||
return uint(type - type_ranges.number_constant)
|
||||
end
|
||||
if type <= type_ranges.number_positive then
|
||||
return -uint(type - type_ranges.number_negative)
|
||||
end
|
||||
return float(type == type_ranges.number)
|
||||
end
|
||||
if type <= type_ranges.string then
|
||||
local string = stream_read(uint(type - type_ranges.number))
|
||||
table.insert(references, string)
|
||||
return string
|
||||
end
|
||||
if type <= type_ranges.table then
|
||||
type = type - type_ranges.string - 1
|
||||
local tab = {}
|
||||
table.insert(references, tab)
|
||||
if type == 0 then
|
||||
return tab
|
||||
end
|
||||
local list_len = uint(type % 5)
|
||||
local kv_len = uint(math.floor(type / 5))
|
||||
for index = 1, list_len do
|
||||
tab[index] = _read(stream_read(1))
|
||||
end
|
||||
for _ = 1, kv_len do
|
||||
tab[_read(stream_read(1))] = _read(stream_read(1))
|
||||
end
|
||||
return tab
|
||||
end
|
||||
if type <= type_ranges.reference then
|
||||
return references[uint(type - type_ranges.table)]
|
||||
end
|
||||
return aux_read(type, stream, references)
|
||||
end
|
||||
local type = stream:read(1)
|
||||
if type == nil then
|
||||
return
|
||||
end
|
||||
return _read(type)
|
||||
local references = {}
|
||||
local function stream_read(count)
|
||||
local text = stream:read(count)
|
||||
assert(text and text:len() == count, "end of stream")
|
||||
return text
|
||||
end
|
||||
local function byte()
|
||||
return stream_read(1):byte()
|
||||
end
|
||||
local read_uint = modlib.binary.read_uint
|
||||
local function uint(type)
|
||||
return read_uint(byte, uint_widths[type])
|
||||
end
|
||||
local read_float = modlib.binary.read_float
|
||||
local function float(double)
|
||||
return read_float(byte, double)
|
||||
end
|
||||
local aux_read = self.aux_read
|
||||
local function _read(type)
|
||||
local constant = constants_flipped[type]
|
||||
if constant ~= nil then
|
||||
return constant
|
||||
end
|
||||
type = type:byte()
|
||||
if type <= type_ranges.number then
|
||||
if type <= type_ranges.number_negative then
|
||||
return uint(type - type_ranges.number_constant)
|
||||
end
|
||||
if type <= type_ranges.number_positive then
|
||||
return -uint(type - type_ranges.number_negative)
|
||||
end
|
||||
return float(type == type_ranges.number)
|
||||
end
|
||||
if type <= type_ranges.string then
|
||||
local string = stream_read(uint(type - type_ranges.number))
|
||||
table.insert(references, string)
|
||||
return string
|
||||
end
|
||||
if type <= type_ranges.table then
|
||||
type = type - type_ranges.string - 1
|
||||
local tab = {}
|
||||
table.insert(references, tab)
|
||||
if type == 0 then
|
||||
return tab
|
||||
end
|
||||
local list_len = uint(type % 5)
|
||||
local kv_len = uint(math.floor(type / 5))
|
||||
for index = 1, list_len do
|
||||
tab[index] = _read(stream_read(1))
|
||||
end
|
||||
for _ = 1, kv_len do
|
||||
tab[_read(stream_read(1))] = _read(stream_read(1))
|
||||
end
|
||||
return tab
|
||||
end
|
||||
if type <= type_ranges.reference then
|
||||
return references[uint(type - type_ranges.table)]
|
||||
end
|
||||
return aux_read(type, stream, references)
|
||||
end
|
||||
local type = stream:read(1)
|
||||
if type == nil then
|
||||
return
|
||||
end
|
||||
return _read(type)
|
||||
end
|
542
conf.lua
542
conf.lua
@ -1,296 +1,296 @@
|
||||
-- not deprecated
|
||||
function build_tree(dict)
|
||||
local tree = {}
|
||||
for key, value in pairs(dict) do
|
||||
local path = modlib.text.split_unlimited(key, ".")
|
||||
local subtree = tree
|
||||
for i = 1, #path - 1 do
|
||||
local index = tonumber(path[i]) or path[i]
|
||||
subtree[index] = subtree[index] or {}
|
||||
subtree = subtree[index]
|
||||
end
|
||||
subtree[path[#path]] = value
|
||||
end
|
||||
return tree
|
||||
local tree = {}
|
||||
for key, value in pairs(dict) do
|
||||
local path = modlib.text.split_unlimited(key, ".")
|
||||
local subtree = tree
|
||||
for i = 1, #path - 1 do
|
||||
local index = tonumber(path[i]) or path[i]
|
||||
subtree[index] = subtree[index] or {}
|
||||
subtree = subtree[index]
|
||||
end
|
||||
subtree[path[#path]] = value
|
||||
end
|
||||
return tree
|
||||
end
|
||||
if minetest then
|
||||
function build_setting_tree()
|
||||
modlib.conf.settings = build_tree(minetest.settings:to_table())
|
||||
end
|
||||
-- deprecated, use modlib.mod.configuration instead
|
||||
minetest.mkdir(minetest.get_worldpath().."/config")
|
||||
function get_path(confname)
|
||||
return minetest.get_worldpath().."/config/"..confname
|
||||
end
|
||||
function build_setting_tree()
|
||||
modlib.conf.settings = build_tree(minetest.settings:to_table())
|
||||
end
|
||||
-- deprecated, use modlib.mod.configuration instead
|
||||
minetest.mkdir(minetest.get_worldpath().."/config")
|
||||
function get_path(confname)
|
||||
return minetest.get_worldpath().."/config/"..confname
|
||||
end
|
||||
end
|
||||
function read_conf(text)
|
||||
local lines = modlib.text.split_lines(text, nil, true)
|
||||
local dict = {}
|
||||
for i, line in ipairs(lines) do
|
||||
local error_base = "Line " .. (i+1) .. ": "
|
||||
line = modlib.text.trim_left(lines[i])
|
||||
if line ~= "" and line:sub(1,1) ~= "#" then
|
||||
line = modlib.text.split(line, "=", 2)
|
||||
if #line ~= 2 then
|
||||
error(error_base .. "No value given")
|
||||
end
|
||||
local prop = modlib.text.trim_right(line[1])
|
||||
if prop == "" then
|
||||
error(error_base .. "No key given")
|
||||
end
|
||||
local val = modlib.text.trim_left(line[2])
|
||||
if val == "" then
|
||||
error(error_base .. "No value given")
|
||||
end
|
||||
if modlib.text.starts_with(val, '"""') then
|
||||
val = val:sub(3)
|
||||
local total_val = {}
|
||||
local function readMultiline()
|
||||
while i < #lines do
|
||||
if modlib.text.ends_with(val, '"""') then
|
||||
val = val:sub(1, val:len() - 3)
|
||||
return
|
||||
end
|
||||
table.insert(total_val, val)
|
||||
i = i + 1
|
||||
val = lines[i]
|
||||
end
|
||||
i = i - 1
|
||||
error(error_base .. "Unclosed multiline block")
|
||||
end
|
||||
readMultiline()
|
||||
table.insert(total_val, val)
|
||||
val = table.concat(total_val, "\n")
|
||||
else
|
||||
val = modlib.text.trim_right(val)
|
||||
end
|
||||
if dict[prop] then
|
||||
error(error_base .. "Duplicate key")
|
||||
end
|
||||
dict[prop] = val
|
||||
end
|
||||
end
|
||||
return dict
|
||||
local lines = modlib.text.split_lines(text, nil, true)
|
||||
local dict = {}
|
||||
for i, line in ipairs(lines) do
|
||||
local error_base = "Line " .. (i+1) .. ": "
|
||||
line = modlib.text.trim_left(lines[i])
|
||||
if line ~= "" and line:sub(1,1) ~= "#" then
|
||||
line = modlib.text.split(line, "=", 2)
|
||||
if #line ~= 2 then
|
||||
error(error_base .. "No value given")
|
||||
end
|
||||
local prop = modlib.text.trim_right(line[1])
|
||||
if prop == "" then
|
||||
error(error_base .. "No key given")
|
||||
end
|
||||
local val = modlib.text.trim_left(line[2])
|
||||
if val == "" then
|
||||
error(error_base .. "No value given")
|
||||
end
|
||||
if modlib.text.starts_with(val, '"""') then
|
||||
val = val:sub(3)
|
||||
local total_val = {}
|
||||
local function readMultiline()
|
||||
while i < #lines do
|
||||
if modlib.text.ends_with(val, '"""') then
|
||||
val = val:sub(1, val:len() - 3)
|
||||
return
|
||||
end
|
||||
table.insert(total_val, val)
|
||||
i = i + 1
|
||||
val = lines[i]
|
||||
end
|
||||
i = i - 1
|
||||
error(error_base .. "Unclosed multiline block")
|
||||
end
|
||||
readMultiline()
|
||||
table.insert(total_val, val)
|
||||
val = table.concat(total_val, "\n")
|
||||
else
|
||||
val = modlib.text.trim_right(val)
|
||||
end
|
||||
if dict[prop] then
|
||||
error(error_base .. "Duplicate key")
|
||||
end
|
||||
dict[prop] = val
|
||||
end
|
||||
end
|
||||
return dict
|
||||
end
|
||||
function check_config_constraints(config, constraints, handler)
|
||||
local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end)
|
||||
if not no_error then
|
||||
handler(error_or_retval)
|
||||
end
|
||||
local no_error, error_or_retval = pcall(function() check_constraints(config, constraints) end)
|
||||
if not no_error then
|
||||
handler(error_or_retval)
|
||||
end
|
||||
end
|
||||
function load(filename, constraints)
|
||||
local config = minetest.parse_json(modlib.file.read(filename))
|
||||
if constraints then
|
||||
check_config_constraints(config, constraints, function(message)
|
||||
error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
|
||||
end)
|
||||
end
|
||||
return config
|
||||
local config = minetest.parse_json(modlib.file.read(filename))
|
||||
if constraints then
|
||||
check_config_constraints(config, constraints, function(message)
|
||||
error('Configuration of file "'..filename.."\" doesn't satisfy constraints: "..message)
|
||||
end)
|
||||
end
|
||||
return config
|
||||
end
|
||||
function load_or_create(filename, replacement_file, constraints)
|
||||
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
|
||||
return load(filename, constraints)
|
||||
modlib.file.create_if_not_exists_from_file(filename, replacement_file)
|
||||
return load(filename, constraints)
|
||||
end
|
||||
function import(modname, constraints, no_settingtypes)
|
||||
local default_config = modlib.mod.get_resource(modname, "default_config.json")
|
||||
local default_conf = minetest.parse_json(modlib.file.read(default_config))
|
||||
local config = load_or_create(get_path(modname)..".json", default_config, constraints)
|
||||
local formats = {
|
||||
{ extension = ".lua", load = minetest.deserialize },
|
||||
{ extension = ".luon", load = function(text) minetest.deserialize("return "..text) end },
|
||||
{ extension = ".conf", load = function(text) return fix_types(build_tree(read_conf(text)), constraints) end }
|
||||
}
|
||||
for _, format in ipairs(formats) do
|
||||
local conf = modlib.file.read(get_path(modname)..format.extension)
|
||||
if conf then
|
||||
config = merge_config(config, format.load(conf))
|
||||
end
|
||||
end
|
||||
if not no_settingtypes then
|
||||
constraints.name = modname
|
||||
local settingtypes = generate_settingtypes(default_conf, constraints)
|
||||
modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes)
|
||||
end
|
||||
local additional_settings = modlib.conf.settings[modname] or {}
|
||||
additional_settings = fix_types(additional_settings, constraints)
|
||||
-- TODO implement merge_config_legal(default_conf, ...)
|
||||
config = merge_config(config, additional_settings)
|
||||
if constraints then
|
||||
check_config_constraints(config, constraints, function(message)
|
||||
error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message)
|
||||
end)
|
||||
end
|
||||
return config
|
||||
local default_config = modlib.mod.get_resource(modname, "default_config.json")
|
||||
local default_conf = minetest.parse_json(modlib.file.read(default_config))
|
||||
local config = load_or_create(get_path(modname)..".json", default_config, constraints)
|
||||
local formats = {
|
||||
{ extension = ".lua", load = minetest.deserialize },
|
||||
{ extension = ".luon", load = function(text) minetest.deserialize("return "..text) end },
|
||||
{ extension = ".conf", load = function(text) return fix_types(build_tree(read_conf(text)), constraints) end }
|
||||
}
|
||||
for _, format in ipairs(formats) do
|
||||
local conf = modlib.file.read(get_path(modname)..format.extension)
|
||||
if conf then
|
||||
config = merge_config(config, format.load(conf))
|
||||
end
|
||||
end
|
||||
if not no_settingtypes then
|
||||
constraints.name = modname
|
||||
local settingtypes = generate_settingtypes(default_conf, constraints)
|
||||
modlib.file.write(modlib.mod.get_resource(modname, "settingtypes.txt"), settingtypes)
|
||||
end
|
||||
local additional_settings = modlib.conf.settings[modname] or {}
|
||||
additional_settings = fix_types(additional_settings, constraints)
|
||||
-- TODO implement merge_config_legal(default_conf, ...)
|
||||
config = merge_config(config, additional_settings)
|
||||
if constraints then
|
||||
check_config_constraints(config, constraints, function(message)
|
||||
error('Configuration of mod "'..modname.."\" doesn't satisfy constraints: "..message)
|
||||
end)
|
||||
end
|
||||
return config
|
||||
end
|
||||
function merge_config(config, additional_settings)
|
||||
if not config or type(additional_settings) ~= "table" then
|
||||
return additional_settings
|
||||
end
|
||||
for setting, value in pairs(additional_settings) do
|
||||
if config[setting] then
|
||||
config[setting] = merge_config(config[setting], value)
|
||||
end
|
||||
end
|
||||
return config
|
||||
if not config or type(additional_settings) ~= "table" then
|
||||
return additional_settings
|
||||
end
|
||||
for setting, value in pairs(additional_settings) do
|
||||
if config[setting] then
|
||||
config[setting] = merge_config(config[setting], value)
|
||||
end
|
||||
end
|
||||
return config
|
||||
end
|
||||
-- format: # comment
|
||||
-- name (Readable name) type type_args
|
||||
function generate_settingtypes(default_conf, constraints)
|
||||
local constraint_type = constraints.type
|
||||
if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then
|
||||
constraint_type = "table"
|
||||
end
|
||||
local settingtype, type_args
|
||||
local title = constraints.title
|
||||
if not title then
|
||||
title = modlib.text.split(constraints.name, "_")
|
||||
title[1] = modlib.text.upper_first(title[1])
|
||||
title = table.concat(title, " ")
|
||||
end
|
||||
if constraint_type == "boolean" then
|
||||
settingtype = "bool"
|
||||
default_conf = default_conf and "true" or "false"
|
||||
elseif constraint_type == "string" then
|
||||
settingtype = "string"
|
||||
elseif constraint_type == "number" then
|
||||
settingtype = constraints.int and "int" or "float"
|
||||
local range = constraints.range
|
||||
if range then
|
||||
-- TODO consider better max
|
||||
type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or (2 ^ 30))
|
||||
end
|
||||
-- HACK
|
||||
if not default_conf then default_conf = range[1] end
|
||||
elseif constraint_type == "table" then
|
||||
local handled = {}
|
||||
local settings = {}
|
||||
local function setting(key, value_constraints)
|
||||
if handled[key] then
|
||||
return
|
||||
end
|
||||
handled[key] = true
|
||||
value_constraints.name = constraints.name .. "." .. key
|
||||
value_constraints.title = title .. " " .. key
|
||||
table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints))
|
||||
end
|
||||
for _, table in ipairs{"children", "required_children", "possible_children"} do
|
||||
for key, constraints in pairs(constraints[table] or {}) do
|
||||
setting(key, constraints)
|
||||
end
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not constraint_type then
|
||||
return ""
|
||||
end
|
||||
local comment = constraints.comment
|
||||
if comment then
|
||||
comment = "# " .. comment .. "\n"
|
||||
else
|
||||
comment = ""
|
||||
end
|
||||
assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf))
|
||||
return comment .. constraints.name .. " (" .. title .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "")
|
||||
local constraint_type = constraints.type
|
||||
if constraints.children or constraints.possible_children or constraints.required_children or constraints.keys or constraints.values then
|
||||
constraint_type = "table"
|
||||
end
|
||||
local settingtype, type_args
|
||||
local title = constraints.title
|
||||
if not title then
|
||||
title = modlib.text.split(constraints.name, "_")
|
||||
title[1] = modlib.text.upper_first(title[1])
|
||||
title = table.concat(title, " ")
|
||||
end
|
||||
if constraint_type == "boolean" then
|
||||
settingtype = "bool"
|
||||
default_conf = default_conf and "true" or "false"
|
||||
elseif constraint_type == "string" then
|
||||
settingtype = "string"
|
||||
elseif constraint_type == "number" then
|
||||
settingtype = constraints.int and "int" or "float"
|
||||
local range = constraints.range
|
||||
if range then
|
||||
-- TODO consider better max
|
||||
type_args = (constraints.int and "%d %d" or "%f %f"):format(range[1], range[2] or (2 ^ 30))
|
||||
end
|
||||
-- HACK
|
||||
if not default_conf then default_conf = range[1] end
|
||||
elseif constraint_type == "table" then
|
||||
local handled = {}
|
||||
local settings = {}
|
||||
local function setting(key, value_constraints)
|
||||
if handled[key] then
|
||||
return
|
||||
end
|
||||
handled[key] = true
|
||||
value_constraints.name = constraints.name .. "." .. key
|
||||
value_constraints.title = title .. " " .. key
|
||||
table.insert(settings, generate_settingtypes(default_conf and default_conf[key], value_constraints))
|
||||
end
|
||||
for _, table in ipairs{"children", "required_children", "possible_children"} do
|
||||
for key, constraints in pairs(constraints[table] or {}) do
|
||||
setting(key, constraints)
|
||||
end
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not constraint_type then
|
||||
return ""
|
||||
end
|
||||
local comment = constraints.comment
|
||||
if comment then
|
||||
comment = "# " .. comment .. "\n"
|
||||
else
|
||||
comment = ""
|
||||
end
|
||||
assert(type(default_conf) == "string" or type(default_conf) == "number" or type(default_conf) == "nil", dump(default_conf))
|
||||
return comment .. constraints.name .. " (" .. title .. ") " .. settingtype .. " " .. (default_conf or "") ..(type_args and (" "..type_args) or "")
|
||||
end
|
||||
function fix_types(value, constraints)
|
||||
local type = type(value)
|
||||
local expected_type = constraints.type
|
||||
if expected_type and expected_type ~= type then
|
||||
assert(type == "string", "Can't fix non-string value")
|
||||
if expected_type == "boolean" then
|
||||
assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value)
|
||||
value = value == "true"
|
||||
elseif expected_type == "number" then
|
||||
assert(tonumber(value), "Not a number: " .. value)
|
||||
value = tonumber(value)
|
||||
end
|
||||
end
|
||||
if type == "table" then
|
||||
for key, val in pairs(value) do
|
||||
for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do
|
||||
child_constraints = (constraints[child_constraints] or {})[key]
|
||||
if child_constraints then
|
||||
val = fix_types(val, child_constraints)
|
||||
end
|
||||
end
|
||||
if constraints.values then
|
||||
val = fix_types(val, constraints.values)
|
||||
end
|
||||
if constraints.keys then
|
||||
value[key] = nil
|
||||
value[fix_types(key, constraints.keys)] = val
|
||||
else
|
||||
value[key] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
local type = type(value)
|
||||
local expected_type = constraints.type
|
||||
if expected_type and expected_type ~= type then
|
||||
assert(type == "string", "Can't fix non-string value")
|
||||
if expected_type == "boolean" then
|
||||
assert(value == "true" or value == "false", "Not a boolean (true or false): " .. value)
|
||||
value = value == "true"
|
||||
elseif expected_type == "number" then
|
||||
assert(tonumber(value), "Not a number: " .. value)
|
||||
value = tonumber(value)
|
||||
end
|
||||
end
|
||||
if type == "table" then
|
||||
for key, val in pairs(value) do
|
||||
for _, child_constraints in ipairs{"required_children", "children", "possible_children"} do
|
||||
child_constraints = (constraints[child_constraints] or {})[key]
|
||||
if child_constraints then
|
||||
val = fix_types(val, child_constraints)
|
||||
end
|
||||
end
|
||||
if constraints.values then
|
||||
val = fix_types(val, constraints.values)
|
||||
end
|
||||
if constraints.keys then
|
||||
value[key] = nil
|
||||
value[fix_types(key, constraints.keys)] = val
|
||||
else
|
||||
value[key] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
function check_constraints(value, constraints)
|
||||
local t = type(value)
|
||||
if constraints.type and constraints.type ~= t then
|
||||
error("Wrong type: Expected "..constraints.type..", found "..t)
|
||||
end
|
||||
if (t == "number" or t == "string") and constraints.range then
|
||||
if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then
|
||||
error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
if t == "number" and constraints.int and value % 1 ~= 0 then
|
||||
error("Not an integer number: " .. minetest.write_json(value))
|
||||
end
|
||||
if constraints.possible_values and not constraints.possible_values[value] then
|
||||
error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value))
|
||||
end
|
||||
if t == "table" then
|
||||
if constraints.children then
|
||||
for key, val in pairs(value) do
|
||||
local child_constraints = constraints.children[key]
|
||||
if not child_constraints then
|
||||
error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
|
||||
else
|
||||
check_constraints(val, child_constraints)
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(constraints.children) do
|
||||
if value[key] == nil then
|
||||
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.required_children then
|
||||
for key, value_constraints in pairs(constraints.required_children) do
|
||||
local val = value[key]
|
||||
if val then
|
||||
check_constraints(val, value_constraints)
|
||||
else
|
||||
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.possible_children then
|
||||
for key, value_constraints in pairs(constraints.possible_children) do
|
||||
local val = value[key]
|
||||
if val then
|
||||
check_constraints(val, value_constraints)
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.keys then
|
||||
for key,_ in pairs(value) do
|
||||
check_constraints(key, constraints.keys)
|
||||
end
|
||||
end
|
||||
if constraints.values then
|
||||
for _, val in pairs(value) do
|
||||
check_constraints(val, constraints.values)
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.func then
|
||||
local possible_errors = constraints.func(value)
|
||||
if possible_errors then
|
||||
error(possible_errors)
|
||||
end
|
||||
end
|
||||
local t = type(value)
|
||||
if constraints.type and constraints.type ~= t then
|
||||
error("Wrong type: Expected "..constraints.type..", found "..t)
|
||||
end
|
||||
if (t == "number" or t == "string") and constraints.range then
|
||||
if value < constraints.range[1] or (constraints.range[2] and value > constraints.range[2]) then
|
||||
error("Not inside range: Expected value >= "..constraints.range[1].." and <= "..(constraints.range[2] or "inf")..", found "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
if t == "number" and constraints.int and value % 1 ~= 0 then
|
||||
error("Not an integer number: " .. minetest.write_json(value))
|
||||
end
|
||||
if constraints.possible_values and not constraints.possible_values[value] then
|
||||
error("None of the possible values: Expected one of "..minetest.write_json(modlib.table.keys(constraints.possible_values))..", found "..minetest.write_json(value))
|
||||
end
|
||||
if t == "table" then
|
||||
if constraints.children then
|
||||
for key, val in pairs(value) do
|
||||
local child_constraints = constraints.children[key]
|
||||
if not child_constraints then
|
||||
error("Unexpected table entry: Expected one of "..minetest.write_json(modlib.table.keys(constraints.children))..", found "..minetest.write_json(key))
|
||||
else
|
||||
check_constraints(val, child_constraints)
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(constraints.children) do
|
||||
if value[key] == nil then
|
||||
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.required_children then
|
||||
for key, value_constraints in pairs(constraints.required_children) do
|
||||
local val = value[key]
|
||||
if val then
|
||||
check_constraints(val, value_constraints)
|
||||
else
|
||||
error("Table entry missing: Expected key "..minetest.write_json(key).." to be present in table "..minetest.write_json(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.possible_children then
|
||||
for key, value_constraints in pairs(constraints.possible_children) do
|
||||
local val = value[key]
|
||||
if val then
|
||||
check_constraints(val, value_constraints)
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.keys then
|
||||
for key,_ in pairs(value) do
|
||||
check_constraints(key, constraints.keys)
|
||||
end
|
||||
end
|
||||
if constraints.values then
|
||||
for _, val in pairs(value) do
|
||||
check_constraints(val, constraints.values)
|
||||
end
|
||||
end
|
||||
end
|
||||
if constraints.func then
|
||||
local possible_errors = constraints.func(value)
|
||||
if possible_errors then
|
||||
error(possible_errors)
|
||||
end
|
||||
end
|
||||
end
|
12
data.lua
12
data.lua
@ -1,19 +1,19 @@
|
||||
minetest.mkdir(minetest.get_worldpath().."/data")
|
||||
function create_mod_storage(modname)
|
||||
minetest.mkdir(minetest.get_worldpath().."/data/"..modname)
|
||||
end
|
||||
|
||||
function get_path(modname, filename)
|
||||
return minetest.get_worldpath().."/data/"..modname.."/"..filename
|
||||
end
|
||||
|
||||
function load(modname, filename)
|
||||
return minetest.deserialize(modlib.file.read(get_path(modname, filename)..".lua"))
|
||||
end
|
||||
|
||||
function save(modname, filename, stuff)
|
||||
return modlib.file.write(get_path(modname, filename)..".lua", minetest.serialize(stuff))
|
||||
end
|
||||
|
||||
function load_json(modname, filename)
|
||||
return minetest.parse_json(modlib.file.read(get_path(modname, filename)..".json") or "null")
|
||||
end
|
||||
|
||||
function save_json(modname, filename, stuff)
|
||||
return modlib.file.write(get_path(modname, filename)..".json", minetest.write_json(stuff))
|
||||
end
|
||||
|
10
file.lua
10
file.lua
@ -15,11 +15,11 @@ function write(filename, new_content)
|
||||
end
|
||||
|
||||
function ensure_content(filename, ensured_content)
|
||||
local content = read(filename)
|
||||
if content ~= ensured_content then
|
||||
return write(filename, ensured_content)
|
||||
end
|
||||
return true
|
||||
local content = read(filename)
|
||||
if content ~= ensured_content then
|
||||
return write(filename, ensured_content)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function append(filename, new_content)
|
||||
|
74
heap.lua
74
heap.lua
@ -4,47 +4,47 @@ function less_than(a, b) return a < b end
|
||||
|
||||
--> empty min heap
|
||||
function new(less_than)
|
||||
return setmetatable({less_than = less_than}, metatable)
|
||||
return setmetatable({less_than = less_than}, metatable)
|
||||
end
|
||||
|
||||
function push(self, value)
|
||||
table.insert(self, value)
|
||||
local function heapify(index)
|
||||
if index == 1 then
|
||||
return
|
||||
end
|
||||
local parent = math.floor(index / 2)
|
||||
if self.less_than(self[index], self[parent]) then
|
||||
self[parent], self[index] = self[index], self[parent]
|
||||
heapify(parent)
|
||||
end
|
||||
end
|
||||
heapify(#self)
|
||||
table.insert(self, value)
|
||||
local function heapify(index)
|
||||
if index == 1 then
|
||||
return
|
||||
end
|
||||
local parent = math.floor(index / 2)
|
||||
if self.less_than(self[index], self[parent]) then
|
||||
self[parent], self[index] = self[index], self[parent]
|
||||
heapify(parent)
|
||||
end
|
||||
end
|
||||
heapify(#self)
|
||||
end
|
||||
|
||||
function pop(self)
|
||||
local value = self[1]
|
||||
local last = #self
|
||||
if last == 1 then
|
||||
self[1] = nil
|
||||
return value
|
||||
end
|
||||
self[1], self[last] = self[last], nil
|
||||
last = last - 1
|
||||
local function heapify(index)
|
||||
local left_child = index * 2
|
||||
if left_child > last then
|
||||
return
|
||||
end
|
||||
local smallest_child = left_child + 1
|
||||
if smallest_child > last or self.less_than(self[left_child], self[smallest_child]) then
|
||||
smallest_child = left_child
|
||||
end
|
||||
if self.less_than(self[smallest_child], self[index]) then
|
||||
self[index], self[smallest_child] = self[smallest_child], self[index]
|
||||
heapify(smallest_child)
|
||||
end
|
||||
end
|
||||
heapify(1)
|
||||
return value
|
||||
local value = self[1]
|
||||
local last = #self
|
||||
if last == 1 then
|
||||
self[1] = nil
|
||||
return value
|
||||
end
|
||||
self[1], self[last] = self[last], nil
|
||||
last = last - 1
|
||||
local function heapify(index)
|
||||
local left_child = index * 2
|
||||
if left_child > last then
|
||||
return
|
||||
end
|
||||
local smallest_child = left_child + 1
|
||||
if smallest_child > last or self.less_than(self[left_child], self[smallest_child]) then
|
||||
smallest_child = left_child
|
||||
end
|
||||
if self.less_than(self[smallest_child], self[index]) then
|
||||
self[index], self[smallest_child] = self[smallest_child], self[index]
|
||||
heapify(smallest_child)
|
||||
end
|
||||
end
|
||||
heapify(1)
|
||||
return value
|
||||
end
|
212
init.lua
212
init.lua
@ -1,123 +1,123 @@
|
||||
-- Lua version check
|
||||
if _VERSION then
|
||||
if _VERSION < "Lua 5" then
|
||||
error("Outdated Lua version! modlib requires Lua 5 or greater.")
|
||||
end
|
||||
if _VERSION > "Lua 5.1" then
|
||||
-- not throwing error("Too new Lua version! modlib requires Lua 5.1 or smaller.") anymore
|
||||
unpack = unpack or table.unpack -- unpack was moved to table.unpack in Lua 5.2
|
||||
loadstring = load
|
||||
function setfenv(fn, env)
|
||||
local i = 1
|
||||
while true do
|
||||
name = debug.getupvalue(fn, i)
|
||||
if name == "_ENV" then
|
||||
debug.setupvalue(fn, i, env)
|
||||
break
|
||||
elseif not name then
|
||||
break
|
||||
end
|
||||
end
|
||||
return fn
|
||||
end
|
||||
function getfenv(fn)
|
||||
local i = 1
|
||||
local name, val
|
||||
repeat
|
||||
name, val = debug.getupvalue(fn, i)
|
||||
if name == "_ENV" then
|
||||
return val
|
||||
end
|
||||
i = i + 1
|
||||
until not name
|
||||
end
|
||||
end
|
||||
if _VERSION < "Lua 5" then
|
||||
error("Outdated Lua version! modlib requires Lua 5 or greater.")
|
||||
end
|
||||
if _VERSION > "Lua 5.1" then
|
||||
-- not throwing error("Too new Lua version! modlib requires Lua 5.1 or smaller.") anymore
|
||||
unpack = unpack or table.unpack -- unpack was moved to table.unpack in Lua 5.2
|
||||
loadstring = load
|
||||
function setfenv(fn, env)
|
||||
local i = 1
|
||||
while true do
|
||||
name = debug.getupvalue(fn, i)
|
||||
if name == "_ENV" then
|
||||
debug.setupvalue(fn, i, env)
|
||||
break
|
||||
elseif not name then
|
||||
break
|
||||
end
|
||||
end
|
||||
return fn
|
||||
end
|
||||
function getfenv(fn)
|
||||
local i = 1
|
||||
local name, val
|
||||
repeat
|
||||
name, val = debug.getupvalue(fn, i)
|
||||
if name == "_ENV" then
|
||||
return val
|
||||
end
|
||||
i = i + 1
|
||||
until not name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
modlib = {
|
||||
dir_delim = rawget(_G, "DIR_DELIM") or "/",
|
||||
_RG = setmetatable({}, {
|
||||
__index = function(_, index)
|
||||
return rawget(_G, index)
|
||||
end,
|
||||
__newindex = function(_, index, value)
|
||||
return rawset(_G, index, value)
|
||||
end
|
||||
}),
|
||||
assertdump = function(v, value)
|
||||
if not v then
|
||||
error(dump(value), 2)
|
||||
end
|
||||
end
|
||||
dir_delim = rawget(_G, "DIR_DELIM") or "/",
|
||||
_RG = setmetatable({}, {
|
||||
__index = function(_, index)
|
||||
return rawget(_G, index)
|
||||
end,
|
||||
__newindex = function(_, index, value)
|
||||
return rawset(_G, index, value)
|
||||
end
|
||||
}),
|
||||
assertdump = function(v, value)
|
||||
if not v then
|
||||
error(dump(value), 2)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
local function get_resource(modname, resource, ...)
|
||||
if not resource then
|
||||
resource = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
return table.concat({minetest.get_modpath(modname), resource, ...}, modlib.dir_delim)
|
||||
if not resource then
|
||||
resource = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
return table.concat({minetest.get_modpath(modname), resource, ...}, modlib.dir_delim)
|
||||
end
|
||||
|
||||
local function loadfile_exports(filename)
|
||||
local env = setmetatable({}, {__index = _G})
|
||||
local file = assert(loadfile(filename))
|
||||
setfenv(file, env)
|
||||
file()
|
||||
return env
|
||||
local env = setmetatable({}, {__index = _G})
|
||||
local file = assert(loadfile(filename))
|
||||
setfenv(file, env)
|
||||
file()
|
||||
return env
|
||||
end
|
||||
|
||||
local minetest_only = {
|
||||
mod = true,
|
||||
minetest = true,
|
||||
data = true,
|
||||
log = true,
|
||||
player = true,
|
||||
-- not actually minetest-only, but a deprecated component
|
||||
conf = true
|
||||
mod = true,
|
||||
minetest = true,
|
||||
data = true,
|
||||
log = true,
|
||||
player = true,
|
||||
-- not actually minetest-only, but a deprecated component
|
||||
conf = true
|
||||
}
|
||||
for _, component in ipairs{
|
||||
"mod",
|
||||
"conf",
|
||||
"schema",
|
||||
"data",
|
||||
"file",
|
||||
"func",
|
||||
"log",
|
||||
"math",
|
||||
"player",
|
||||
"table",
|
||||
"text",
|
||||
"vector",
|
||||
"quaternion",
|
||||
{
|
||||
name = "minetest",
|
||||
"misc",
|
||||
"collisionboxes",
|
||||
"liquid",
|
||||
"raycast",
|
||||
"wielditem_change",
|
||||
"colorspec"
|
||||
},
|
||||
"trie",
|
||||
"kdtree",
|
||||
"heap",
|
||||
"ranked_set",
|
||||
"binary",
|
||||
"b3d",
|
||||
"bluon"
|
||||
"mod",
|
||||
"conf",
|
||||
"schema",
|
||||
"data",
|
||||
"file",
|
||||
"func",
|
||||
"log",
|
||||
"math",
|
||||
"player",
|
||||
"table",
|
||||
"text",
|
||||
"vector",
|
||||
"quaternion",
|
||||
{
|
||||
name = "minetest",
|
||||
"misc",
|
||||
"collisionboxes",
|
||||
"liquid",
|
||||
"raycast",
|
||||
"wielditem_change",
|
||||
"colorspec"
|
||||
},
|
||||
"trie",
|
||||
"kdtree",
|
||||
"heap",
|
||||
"ranked_set",
|
||||
"binary",
|
||||
"b3d",
|
||||
"bluon"
|
||||
} do
|
||||
if component.name then
|
||||
if minetest then
|
||||
modlib[component.name] = loadfile_exports(get_resource(minetest.get_current_modname(), component.name, component[1] .. ".lua"))
|
||||
for index = 2, #component do
|
||||
modlib.mod.include_env(get_resource(minetest.get_current_modname(), component.name, component[index] .. ".lua"), modlib[component.name])
|
||||
end
|
||||
end
|
||||
elseif minetest or not minetest_only[component] then
|
||||
local path = minetest and get_resource(component .. ".lua") or component .. ".lua"
|
||||
modlib[component] = loadfile_exports(path)
|
||||
end
|
||||
if component.name then
|
||||
if minetest then
|
||||
modlib[component.name] = loadfile_exports(get_resource(minetest.get_current_modname(), component.name, component[1] .. ".lua"))
|
||||
for index = 2, #component do
|
||||
modlib.mod.include_env(get_resource(minetest.get_current_modname(), component.name, component[index] .. ".lua"), modlib[component.name])
|
||||
end
|
||||
end
|
||||
elseif minetest or not minetest_only[component] then
|
||||
local path = minetest and get_resource(component .. ".lua") or component .. ".lua"
|
||||
modlib[component] = loadfile_exports(path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Aliases
|
||||
@ -125,10 +125,10 @@ modlib.string = modlib.text
|
||||
modlib.number = modlib.math
|
||||
|
||||
if minetest then
|
||||
modlib.conf.build_setting_tree()
|
||||
modlib.conf.build_setting_tree()
|
||||
|
||||
modlib.mod.get_resource = get_resource
|
||||
modlib.mod.loadfile_exports = loadfile_exports
|
||||
modlib.mod.get_resource = get_resource
|
||||
modlib.mod.loadfile_exports = loadfile_exports
|
||||
end
|
||||
|
||||
_ml = modlib
|
||||
|
60
kdtree.lua
60
kdtree.lua
@ -5,9 +5,9 @@ distance = modlib.vector.distance
|
||||
--: vectors first vector is used to infer the dimension
|
||||
--: distance (vector, other_vector) -> number, default: modlib.vector.distance
|
||||
function new(vectors, distance)
|
||||
assert(#vectors > 0, "vector list must not be empty")
|
||||
local dimension = #vectors[1]
|
||||
local function builder(vectors, axis)
|
||||
assert(#vectors > 0, "vector list must not be empty")
|
||||
local dimension = #vectors[1]
|
||||
local function builder(vectors, axis)
|
||||
if #vectors == 1 then return { value = vectors[1] } end
|
||||
table.sort(vectors, function(a, b) return a[axis] > b[axis] end)
|
||||
local median = math.floor(#vectors / 2)
|
||||
@ -19,36 +19,36 @@ function new(vectors, distance)
|
||||
right = builder({ unpack(vectors, median + 1) }, next_axis)
|
||||
}, metatable)
|
||||
end
|
||||
local self = builder(vectors, 1)
|
||||
self.distance = distance
|
||||
return setmetatable(self, metatable)
|
||||
local self = builder(vectors, 1)
|
||||
self.distance = distance
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
function get_nearest_neighbor(self, vector)
|
||||
local min_distance = math.huge
|
||||
local nearest_neighbor
|
||||
local distance_func = self.distance
|
||||
local function visit(tree)
|
||||
local axis = tree.axis
|
||||
if tree.value ~= nil then
|
||||
local distance = distance_func(tree.value, vector)
|
||||
if distance < min_distance then
|
||||
min_distance = distance
|
||||
nearest_neighbor = tree.value
|
||||
end
|
||||
return
|
||||
else
|
||||
local this_side, other_side = tree.left, tree.right
|
||||
if vector[axis] < tree.pivot[axis] then this_side, other_side = other_side, this_side end
|
||||
visit(this_side)
|
||||
if tree.pivot then
|
||||
local dist = math.abs(tree.pivot[axis] - vector[axis])
|
||||
if dist <= min_distance then visit(other_side) end
|
||||
end
|
||||
end
|
||||
end
|
||||
visit(self)
|
||||
return nearest_neighbor, min_distance
|
||||
local min_distance = math.huge
|
||||
local nearest_neighbor
|
||||
local distance_func = self.distance
|
||||
local function visit(tree)
|
||||
local axis = tree.axis
|
||||
if tree.value ~= nil then
|
||||
local distance = distance_func(tree.value, vector)
|
||||
if distance < min_distance then
|
||||
min_distance = distance
|
||||
nearest_neighbor = tree.value
|
||||
end
|
||||
return
|
||||
else
|
||||
local this_side, other_side = tree.left, tree.right
|
||||
if vector[axis] < tree.pivot[axis] then this_side, other_side = other_side, this_side end
|
||||
visit(this_side)
|
||||
if tree.pivot then
|
||||
local dist = math.abs(tree.pivot[axis] - vector[axis])
|
||||
if dist <= min_distance then visit(other_side) end
|
||||
end
|
||||
end
|
||||
end
|
||||
visit(self)
|
||||
return nearest_neighbor, min_distance
|
||||
end
|
||||
|
||||
-- TODO insertion & deletion + rebalancing
|
94
log.lua
94
log.lua
@ -3,74 +3,74 @@ minetest.mkdir(minetest.get_worldpath() .. "/logs")
|
||||
channels = {}
|
||||
last_day = os.date("%d")
|
||||
function get_path(logname)
|
||||
return minetest.get_worldpath() .. "/logs/" .. logname
|
||||
return minetest.get_worldpath() .. "/logs/" .. logname
|
||||
end
|
||||
function create_channel(title)
|
||||
local dir = get_path(title)
|
||||
minetest.mkdir(dir)
|
||||
channels[title] = {dirname = dir, queue = {}}
|
||||
write(title, "Initialisation")
|
||||
local dir = get_path(title)
|
||||
minetest.mkdir(dir)
|
||||
channels[title] = {dirname = dir, queue = {}}
|
||||
write(title, "Initialisation")
|
||||
end
|
||||
function write(channelname, msg)
|
||||
local channel = channels[channelname]
|
||||
local current_day = os.date("%d")
|
||||
if current_day ~= last_day then
|
||||
last_day = current_day
|
||||
write_to_file(channelname, channel, os.date("%Y-%m-%d"))
|
||||
end
|
||||
table.insert(channel.queue, os.date("[%H:%M:%S] ") .. msg)
|
||||
local channel = channels[channelname]
|
||||
local current_day = os.date("%d")
|
||||
if current_day ~= last_day then
|
||||
last_day = current_day
|
||||
write_to_file(channelname, channel, os.date("%Y-%m-%d"))
|
||||
end
|
||||
table.insert(channel.queue, os.date("[%H:%M:%S] ") .. msg)
|
||||
end
|
||||
function write_to_all(msg)
|
||||
for channelname, _ in pairs(channels) do
|
||||
write(channelname, msg)
|
||||
end
|
||||
for channelname, _ in pairs(channels) do
|
||||
write(channelname, msg)
|
||||
end
|
||||
end
|
||||
function write_to_file(name, channel, current_date)
|
||||
if not channel then
|
||||
channel = channels[name]
|
||||
end
|
||||
if #(channel.queue) > 0 then
|
||||
local filename = channel.dirname .. "/" .. (current_date or os.date("%Y-%m-%d")) .. ".txt"
|
||||
local rope = {}
|
||||
for _, msg in ipairs(channel.queue) do
|
||||
table.insert(rope, msg)
|
||||
end
|
||||
modlib.file.append(filename, table.concat(rope, "\n") .. "\n")
|
||||
channels[name].queue = {}
|
||||
end
|
||||
if not channel then
|
||||
channel = channels[name]
|
||||
end
|
||||
if #(channel.queue) > 0 then
|
||||
local filename = channel.dirname .. "/" .. (current_date or os.date("%Y-%m-%d")) .. ".txt"
|
||||
local rope = {}
|
||||
for _, msg in ipairs(channel.queue) do
|
||||
table.insert(rope, msg)
|
||||
end
|
||||
modlib.file.append(filename, table.concat(rope, "\n") .. "\n")
|
||||
channels[name].queue = {}
|
||||
end
|
||||
end
|
||||
function write_all_to_file()
|
||||
local current_date = os.date("%Y-%m-%d")
|
||||
for name, channel in pairs(channels) do
|
||||
write_to_file(name, channel, current_date)
|
||||
end
|
||||
local current_date = os.date("%Y-%m-%d")
|
||||
for name, channel in pairs(channels) do
|
||||
write_to_file(name, channel, current_date)
|
||||
end
|
||||
end
|
||||
function write_safe(channelname, msg)
|
||||
write(channelname, msg)
|
||||
write_all_to_file()
|
||||
write(channelname, msg)
|
||||
write_all_to_file()
|
||||
end
|
||||
|
||||
local timer = 0
|
||||
|
||||
minetest.register_globalstep(
|
||||
function(dtime)
|
||||
timer = timer + dtime
|
||||
if timer > 5 then
|
||||
write_all_to_file()
|
||||
timer = 0
|
||||
end
|
||||
end
|
||||
function(dtime)
|
||||
timer = timer + dtime
|
||||
if timer > 5 then
|
||||
write_all_to_file()
|
||||
timer = 0
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
minetest.register_on_mods_loaded(
|
||||
function()
|
||||
write_to_all("Mods loaded")
|
||||
end
|
||||
function()
|
||||
write_to_all("Mods loaded")
|
||||
end
|
||||
)
|
||||
|
||||
minetest.register_on_shutdown(
|
||||
function()
|
||||
write_to_all("Shutdown")
|
||||
write_all_to_file()
|
||||
end
|
||||
function()
|
||||
write_to_all("Shutdown")
|
||||
write_all_to_file()
|
||||
end
|
||||
)
|
||||
|
@ -1,135 +1,135 @@
|
||||
-- Minetest allows shorthand collisionbox = {...} instead of {{...}}
|
||||
local function get_collisionboxes(box_or_boxes)
|
||||
return type(box_or_boxes[1]) == "number" and {box_or_boxes} or box_or_boxes
|
||||
return type(box_or_boxes[1]) == "number" and {box_or_boxes} or box_or_boxes
|
||||
end
|
||||
|
||||
--> list of collisionboxes in Minetest format
|
||||
function get_node_collisionboxes(pos)
|
||||
local node = minetest.get_node(pos)
|
||||
local node_def = minetest.registered_nodes[node.name]
|
||||
if (not node_def) or node_def.walkable == false then
|
||||
return {}
|
||||
end
|
||||
local boxes = {{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}}
|
||||
local def_collision_box = node_def.collision_box or (node_def.drawtype == "nodebox" and node_def.node_box)
|
||||
if def_collision_box then
|
||||
local box_type = def_collision_box.type
|
||||
if box_type == "regular" then
|
||||
return boxes
|
||||
end
|
||||
local fixed = def_collision_box.fixed
|
||||
boxes = get_collisionboxes(fixed or {})
|
||||
local paramtype2 = node_def.paramtype2
|
||||
if box_type == "leveled" then
|
||||
boxes = table.copy(boxes)
|
||||
local level = (paramtype2 == "leveled" and node.param2 or node_def.leveled or 0) / 255 - 0.5
|
||||
for _, box in pairs(boxes) do
|
||||
box[5] = level
|
||||
end
|
||||
elseif box_type == "wallmounted" then
|
||||
-- TODO complete if only wall_top is given
|
||||
local dir = minetest.wallmounted_to_dir((paramtype2 == "colorwallmounted" and node.param2 % 8 or node.param2) or 0)
|
||||
local box
|
||||
if dir.y > 0 then
|
||||
box = def_collision_box.wall_top
|
||||
elseif dir.y < 0 then
|
||||
box = def_collision_box.wall_bottom
|
||||
else
|
||||
box = def_collision_box.wall_side
|
||||
if dir.z > 0 then
|
||||
box = {box[3], box[2], -box[4], box[6], box[5], -box[1]}
|
||||
elseif dir.z < 0 then
|
||||
box = {-box[6], box[2], box[1], -box[3], box[5], box[4]}
|
||||
elseif dir.x > 0 then
|
||||
box = {-box[4], box[2], box[3], -box[1], box[5], box[6]}
|
||||
else
|
||||
box = {box[1], box[2], -box[6], box[4], box[5], -box[3]}
|
||||
end
|
||||
end
|
||||
return {assert(box, "incomplete wallmounted collisionbox definition of " .. node.name)}
|
||||
end
|
||||
if box_type == "connected" then
|
||||
boxes = table.copy(boxes)
|
||||
local connect_sides = {
|
||||
top = {x = 0, y = 1, z = 0},
|
||||
bottom = {x = 0, y = -1, z = 0},
|
||||
front = {x = 0, y = 0, z = -1},
|
||||
left = {x = -1, y = 0, z = 0},
|
||||
back = {x = 0, y = 0, z = 1},
|
||||
right = {x = 1, y = 0, z = 0}
|
||||
}
|
||||
if node_def.connect_sides then
|
||||
for side in pairs(connect_sides) do
|
||||
if not node_def.connect_sides[side] then
|
||||
connect_sides[side] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local function add_collisionbox(key)
|
||||
for _, box in ipairs(get_collisionboxes(def_collision_box[key] or {})) do
|
||||
table.insert(boxes, box)
|
||||
end
|
||||
end
|
||||
local matchers = {}
|
||||
for _, nodename_or_group in pairs(node_def.connects_to or {}) do
|
||||
table.insert(matchers, nodename_matcher(nodename_or_group))
|
||||
end
|
||||
local function connects_to(nodename)
|
||||
for _, matcher in pairs(matchers) do
|
||||
if matcher(nodename) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
local connected, connected_sides
|
||||
for side, direction in pairs(connect_sides) do
|
||||
local neighbor = minetest.get_node(vector.add(pos, direction))
|
||||
local connects = connects_to(neighbor.name)
|
||||
connected = connected or connects
|
||||
connected_sides = connected_sides or (side ~= "top" and side ~= "bottom")
|
||||
add_collisionbox((connects and "connect_" or "disconnected_") .. side)
|
||||
end
|
||||
if not connected then
|
||||
add_collisionbox("disconnected")
|
||||
end
|
||||
if not connected_sides then
|
||||
add_collisionbox("disconnected_sides")
|
||||
end
|
||||
return boxes
|
||||
end
|
||||
if box_type == "fixed" and paramtype2 == "facedir" or paramtype2 == "colorfacedir" then
|
||||
local param2 = paramtype2 == "colorfacedir" and node.param2 % 32 or node.param2 or 0
|
||||
if param2 ~= 0 then
|
||||
boxes = table.copy(boxes)
|
||||
local axis = ({5, 6, 3, 4, 1, 2})[math.floor(param2 / 4) + 1]
|
||||
local other_axis_1, other_axis_2 = (axis % 3) + 1, ((axis + 1) % 3) + 1
|
||||
local rotation = (param2 % 4) / 2 * math.pi
|
||||
local flip = axis > 3
|
||||
if flip then axis = axis - 3; rotation = -rotation end
|
||||
local sin, cos = math.sin(rotation), math.cos(rotation)
|
||||
if axis == 2 then
|
||||
sin = -sin
|
||||
end
|
||||
for _, box in pairs(boxes) do
|
||||
for off = 0, 3, 3 do
|
||||
local axis_1, axis_2 = other_axis_1 + off, other_axis_2 + off
|
||||
local value_1, value_2 = box[axis_1], box[axis_2]
|
||||
box[axis_1] = value_1 * cos - value_2 * sin
|
||||
box[axis_2] = value_1 * sin + value_2 * cos
|
||||
end
|
||||
if not flip then
|
||||
box[axis], box[axis + 3] = -box[axis + 3], -box[axis]
|
||||
end
|
||||
local function fix(coord)
|
||||
if box[coord] > box[coord + 3] then
|
||||
box[coord], box[coord + 3] = box[coord + 3], box[coord]
|
||||
end
|
||||
end
|
||||
fix(other_axis_1)
|
||||
fix(other_axis_2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return boxes
|
||||
local node = minetest.get_node(pos)
|
||||
local node_def = minetest.registered_nodes[node.name]
|
||||
if (not node_def) or node_def.walkable == false then
|
||||
return {}
|
||||
end
|
||||
local boxes = {{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}}
|
||||
local def_collision_box = node_def.collision_box or (node_def.drawtype == "nodebox" and node_def.node_box)
|
||||
if def_collision_box then
|
||||
local box_type = def_collision_box.type
|
||||
if box_type == "regular" then
|
||||
return boxes
|
||||
end
|
||||
local fixed = def_collision_box.fixed
|
||||
boxes = get_collisionboxes(fixed or {})
|
||||
local paramtype2 = node_def.paramtype2
|
||||
if box_type == "leveled" then
|
||||
boxes = table.copy(boxes)
|
||||
local level = (paramtype2 == "leveled" and node.param2 or node_def.leveled or 0) / 255 - 0.5
|
||||
for _, box in pairs(boxes) do
|
||||
box[5] = level
|
||||
end
|
||||
elseif box_type == "wallmounted" then
|
||||
-- TODO complete if only wall_top is given
|
||||
local dir = minetest.wallmounted_to_dir((paramtype2 == "colorwallmounted" and node.param2 % 8 or node.param2) or 0)
|
||||
local box
|
||||
if dir.y > 0 then
|
||||
box = def_collision_box.wall_top
|
||||
elseif dir.y < 0 then
|
||||
box = def_collision_box.wall_bottom
|
||||
else
|
||||
box = def_collision_box.wall_side
|
||||
if dir.z > 0 then
|
||||
box = {box[3], box[2], -box[4], box[6], box[5], -box[1]}
|
||||
elseif dir.z < 0 then
|
||||
box = {-box[6], box[2], box[1], -box[3], box[5], box[4]}
|
||||
elseif dir.x > 0 then
|
||||
box = {-box[4], box[2], box[3], -box[1], box[5], box[6]}
|
||||
else
|
||||
box = {box[1], box[2], -box[6], box[4], box[5], -box[3]}
|
||||
end
|
||||
end
|
||||
return {assert(box, "incomplete wallmounted collisionbox definition of " .. node.name)}
|
||||
end
|
||||
if box_type == "connected" then
|
||||
boxes = table.copy(boxes)
|
||||
local connect_sides = {
|
||||
top = {x = 0, y = 1, z = 0},
|
||||
bottom = {x = 0, y = -1, z = 0},
|
||||
front = {x = 0, y = 0, z = -1},
|
||||
left = {x = -1, y = 0, z = 0},
|
||||
back = {x = 0, y = 0, z = 1},
|
||||
right = {x = 1, y = 0, z = 0}
|
||||
}
|
||||
if node_def.connect_sides then
|
||||
for side in pairs(connect_sides) do
|
||||
if not node_def.connect_sides[side] then
|
||||
connect_sides[side] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local function add_collisionbox(key)
|
||||
for _, box in ipairs(get_collisionboxes(def_collision_box[key] or {})) do
|
||||
table.insert(boxes, box)
|
||||
end
|
||||
end
|
||||
local matchers = {}
|
||||
for _, nodename_or_group in pairs(node_def.connects_to or {}) do
|
||||
table.insert(matchers, nodename_matcher(nodename_or_group))
|
||||
end
|
||||
local function connects_to(nodename)
|
||||
for _, matcher in pairs(matchers) do
|
||||
if matcher(nodename) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
local connected, connected_sides
|
||||
for side, direction in pairs(connect_sides) do
|
||||
local neighbor = minetest.get_node(vector.add(pos, direction))
|
||||
local connects = connects_to(neighbor.name)
|
||||
connected = connected or connects
|
||||
connected_sides = connected_sides or (side ~= "top" and side ~= "bottom")
|
||||
add_collisionbox((connects and "connect_" or "disconnected_") .. side)
|
||||
end
|
||||
if not connected then
|
||||
add_collisionbox("disconnected")
|
||||
end
|
||||
if not connected_sides then
|
||||
add_collisionbox("disconnected_sides")
|
||||
end
|
||||
return boxes
|
||||
end
|
||||
if box_type == "fixed" and paramtype2 == "facedir" or paramtype2 == "colorfacedir" then
|
||||
local param2 = paramtype2 == "colorfacedir" and node.param2 % 32 or node.param2 or 0
|
||||
if param2 ~= 0 then
|
||||
boxes = table.copy(boxes)
|
||||
local axis = ({5, 6, 3, 4, 1, 2})[math.floor(param2 / 4) + 1]
|
||||
local other_axis_1, other_axis_2 = (axis % 3) + 1, ((axis + 1) % 3) + 1
|
||||
local rotation = (param2 % 4) / 2 * math.pi
|
||||
local flip = axis > 3
|
||||
if flip then axis = axis - 3; rotation = -rotation end
|
||||
local sin, cos = math.sin(rotation), math.cos(rotation)
|
||||
if axis == 2 then
|
||||
sin = -sin
|
||||
end
|
||||
for _, box in pairs(boxes) do
|
||||
for off = 0, 3, 3 do
|
||||
local axis_1, axis_2 = other_axis_1 + off, other_axis_2 + off
|
||||
local value_1, value_2 = box[axis_1], box[axis_2]
|
||||
box[axis_1] = value_1 * cos - value_2 * sin
|
||||
box[axis_2] = value_1 * sin + value_2 * cos
|
||||
end
|
||||
if not flip then
|
||||
box[axis], box[axis + 3] = -box[axis + 3], -box[axis]
|
||||
end
|
||||
local function fix(coord)
|
||||
if box[coord] > box[coord + 3] then
|
||||
box[coord], box[coord + 3] = box[coord + 3], box[coord]
|
||||
end
|
||||
end
|
||||
fix(other_axis_1)
|
||||
fix(other_axis_2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return boxes
|
||||
end
|
||||
|
@ -154,107 +154,107 @@ colorspec = {}
|
||||
local colorspec_metatable = {__index = colorspec}
|
||||
|
||||
function colorspec.new(table)
|
||||
return setmetatable({
|
||||
r = assert(table.r),
|
||||
g = assert(table.g),
|
||||
b = assert(table.b),
|
||||
a = table.a or 255
|
||||
}, colorspec_metatable)
|
||||
return setmetatable({
|
||||
r = assert(table.r),
|
||||
g = assert(table.g),
|
||||
b = assert(table.b),
|
||||
a = table.a or 255
|
||||
}, colorspec_metatable)
|
||||
end
|
||||
|
||||
colorspec.from_table = colorspec.new
|
||||
|
||||
function colorspec.from_string(string)
|
||||
local hex = "#([A-Fa-f%d]+)"
|
||||
local number, alpha = named_colors[string], 0xFF
|
||||
if not number then
|
||||
local name, alpha_text = string:match("^([a-z]+)" .. hex .. "$")
|
||||
local hex = "#([A-Fa-f%d]+)"
|
||||
local number, alpha = named_colors[string], 0xFF
|
||||
if not number then
|
||||
local name, alpha_text = string:match("^([a-z]+)" .. hex .. "$")
|
||||
if name then
|
||||
assert(alpha_text:len() == 2)
|
||||
number = assert(named_colors[name])
|
||||
alpha = tonumber(alpha_text, 16)
|
||||
end
|
||||
end
|
||||
if number then
|
||||
return colorspec.from_number(number * 0x100 + alpha)
|
||||
end
|
||||
local hex_text = string:match(hex)
|
||||
local len, num = hex_text:len(), tonumber(hex_text, 16)
|
||||
if len == 8 then
|
||||
return colorspec.from_number(num)
|
||||
end
|
||||
if len == 6 then
|
||||
return colorspec.from_number(num * 0x100 + 0xFF)
|
||||
end
|
||||
local floor = math.floor
|
||||
if len == 4 then
|
||||
return colorspec.from_table{
|
||||
a = (num % 16) * 17,
|
||||
b = (floor(num / 16) % 16) * 17,
|
||||
g = (floor(num / (16 ^ 2)) % 16) * 17,
|
||||
r = (floor(num / (16 ^ 3)) % 16) * 17
|
||||
}
|
||||
end
|
||||
if len == 3 then
|
||||
return colorspec.from_table{
|
||||
b = (num % 16) * 17,
|
||||
g = (floor(num / 16) % 16) * 17,
|
||||
r = (floor(num / (16 ^ 2)) % 16) * 17
|
||||
}
|
||||
end
|
||||
error("Invalid colorstring: " .. string)
|
||||
end
|
||||
if number then
|
||||
return colorspec.from_number(number * 0x100 + alpha)
|
||||
end
|
||||
local hex_text = string:match(hex)
|
||||
local len, num = hex_text:len(), tonumber(hex_text, 16)
|
||||
if len == 8 then
|
||||
return colorspec.from_number(num)
|
||||
end
|
||||
if len == 6 then
|
||||
return colorspec.from_number(num * 0x100 + 0xFF)
|
||||
end
|
||||
local floor = math.floor
|
||||
if len == 4 then
|
||||
return colorspec.from_table{
|
||||
a = (num % 16) * 17,
|
||||
b = (floor(num / 16) % 16) * 17,
|
||||
g = (floor(num / (16 ^ 2)) % 16) * 17,
|
||||
r = (floor(num / (16 ^ 3)) % 16) * 17
|
||||
}
|
||||
end
|
||||
if len == 3 then
|
||||
return colorspec.from_table{
|
||||
b = (num % 16) * 17,
|
||||
g = (floor(num / 16) % 16) * 17,
|
||||
r = (floor(num / (16 ^ 2)) % 16) * 17
|
||||
}
|
||||
end
|
||||
error("Invalid colorstring: " .. string)
|
||||
end
|
||||
|
||||
colorspec.from_text = colorspec.from_string
|
||||
|
||||
function colorspec.from_number(number)
|
||||
local floor = math.floor
|
||||
return colorspec.from_table{
|
||||
a = number % 0x100,
|
||||
b = floor(number / 0x100) % 0x100,
|
||||
g = floor(number / 0x10000) % 0x100,
|
||||
r = floor(number / 0x1000000)
|
||||
}
|
||||
local floor = math.floor
|
||||
return colorspec.from_table{
|
||||
a = number % 0x100,
|
||||
b = floor(number / 0x100) % 0x100,
|
||||
g = floor(number / 0x10000) % 0x100,
|
||||
r = floor(number / 0x1000000)
|
||||
}
|
||||
end
|
||||
|
||||
function colorspec.from_number_rgb(number)
|
||||
local floor = math.floor
|
||||
return colorspec.from_table{
|
||||
a = 0xFF,
|
||||
b = number % 0x100,
|
||||
g = floor(number / 0x100) % 0x100,
|
||||
r = floor(number / 0x10000)
|
||||
}
|
||||
local floor = math.floor
|
||||
return colorspec.from_table{
|
||||
a = 0xFF,
|
||||
b = number % 0x100,
|
||||
g = floor(number / 0x100) % 0x100,
|
||||
r = floor(number / 0x10000)
|
||||
}
|
||||
end
|
||||
|
||||
function colorspec.from_any(value)
|
||||
local type = type(value)
|
||||
if type == "table" then
|
||||
return colorspec.from_table(value)
|
||||
end
|
||||
if type == "string" then
|
||||
return colorspec.from_string(value)
|
||||
end
|
||||
if type == "number" then
|
||||
return colorspec.from_number(value)
|
||||
end
|
||||
error("Unsupported type " .. type)
|
||||
local type = type(value)
|
||||
if type == "table" then
|
||||
return colorspec.from_table(value)
|
||||
end
|
||||
if type == "string" then
|
||||
return colorspec.from_string(value)
|
||||
end
|
||||
if type == "number" then
|
||||
return colorspec.from_number(value)
|
||||
end
|
||||
error("Unsupported type " .. type)
|
||||
end
|
||||
|
||||
function colorspec:to_table()
|
||||
return self
|
||||
return self
|
||||
end
|
||||
|
||||
--> hex string, omits alpha if possible (if opaque)
|
||||
function colorspec:to_string()
|
||||
if self.a == 255 then
|
||||
return ("%02X02X02X"):format(self.r, self.g, self.b)
|
||||
end
|
||||
return ("%02X02X02X02X"):format(self.r, self.g, self.b, self.a)
|
||||
if self.a == 255 then
|
||||
return ("%02X02X02X"):format(self.r, self.g, self.b)
|
||||
end
|
||||
return ("%02X02X02X02X"):format(self.r, self.g, self.b, self.a)
|
||||
end
|
||||
|
||||
function colorspec:to_number()
|
||||
return self.r * 0x1000000 + self.g * 0x10000 + self.b * 0x100 + self.a
|
||||
return self.r * 0x1000000 + self.g * 0x10000 + self.b * 0x100 + self.a
|
||||
end
|
||||
|
||||
function colorspec:to_number_rgb()
|
||||
@ -262,5 +262,5 @@ function colorspec:to_number_rgb()
|
||||
end
|
||||
|
||||
colorspec_to_colorstring = minetest.colorspec_to_colorstring or function(spec)
|
||||
return colorspec.from_any(spec):to_string()
|
||||
return colorspec.from_any(spec):to_string()
|
||||
end
|
||||
|
@ -2,73 +2,73 @@ liquid_level_max = 8
|
||||
--+ Calculates the corner levels of a flowingliquid node
|
||||
--> 4 corner levels from -0.5 to 0.5 as list of `modlib.vector`
|
||||
function get_liquid_corner_levels(pos)
|
||||
local node = minetest.get_node(pos)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
local source, flowing = def.liquid_alternative_source, node.name
|
||||
local range = def.liquid_range or liquid_level_max
|
||||
local neighbors = {}
|
||||
for x = -1, 1 do
|
||||
neighbors[x] = {}
|
||||
for z = -1, 1 do
|
||||
local neighbor_pos = {x = pos.x + x, y = pos.y, z = pos.z + z}
|
||||
local neighbor_node = minetest.get_node(neighbor_pos)
|
||||
local level
|
||||
if neighbor_node.name == source then
|
||||
level = 1
|
||||
elseif neighbor_node.name == flowing then
|
||||
local neighbor_level = neighbor_node.param2 % 8
|
||||
level = (math.max(0, neighbor_level - liquid_level_max + range) + 0.5) / range
|
||||
end
|
||||
neighbor_pos.y = neighbor_pos.y + 1
|
||||
local node_above = minetest.get_node(neighbor_pos)
|
||||
neighbors[x][z] = {
|
||||
air = neighbor_node.name == "air",
|
||||
level = level,
|
||||
above_is_same_liquid = node_above.name == flowing or node_above.name == source
|
||||
}
|
||||
end
|
||||
end
|
||||
local function get_corner_level(x, z)
|
||||
local air_neighbor
|
||||
local levels = 0
|
||||
local neighbor_count = 0
|
||||
for nx = x - 1, x do
|
||||
for nz = z - 1, z do
|
||||
local neighbor = neighbors[nx][nz]
|
||||
if neighbor.above_is_same_liquid then
|
||||
return 1
|
||||
end
|
||||
local level = neighbor.level
|
||||
if level then
|
||||
if level == 1 then
|
||||
return 1
|
||||
end
|
||||
levels = levels + level
|
||||
neighbor_count = neighbor_count + 1
|
||||
elseif neighbor.air then
|
||||
if air_neighbor then
|
||||
return 0.02
|
||||
end
|
||||
air_neighbor = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if neighbor_count == 0 then
|
||||
return 0
|
||||
end
|
||||
return levels / neighbor_count
|
||||
end
|
||||
local corner_levels = {
|
||||
{0, nil, 0},
|
||||
{1, nil, 0},
|
||||
{1, nil, 1},
|
||||
{0, nil, 1}
|
||||
}
|
||||
for index, corner_level in pairs(corner_levels) do
|
||||
corner_level[2] = get_corner_level(corner_level[1], corner_level[3])
|
||||
corner_levels[index] = modlib.vector.subtract_scalar(modlib.vector.new(corner_level), 0.5)
|
||||
end
|
||||
return corner_levels
|
||||
local node = minetest.get_node(pos)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
local source, flowing = def.liquid_alternative_source, node.name
|
||||
local range = def.liquid_range or liquid_level_max
|
||||
local neighbors = {}
|
||||
for x = -1, 1 do
|
||||
neighbors[x] = {}
|
||||
for z = -1, 1 do
|
||||
local neighbor_pos = {x = pos.x + x, y = pos.y, z = pos.z + z}
|
||||
local neighbor_node = minetest.get_node(neighbor_pos)
|
||||
local level
|
||||
if neighbor_node.name == source then
|
||||
level = 1
|
||||
elseif neighbor_node.name == flowing then
|
||||
local neighbor_level = neighbor_node.param2 % 8
|
||||
level = (math.max(0, neighbor_level - liquid_level_max + range) + 0.5) / range
|
||||
end
|
||||
neighbor_pos.y = neighbor_pos.y + 1
|
||||
local node_above = minetest.get_node(neighbor_pos)
|
||||
neighbors[x][z] = {
|
||||
air = neighbor_node.name == "air",
|
||||
level = level,
|
||||
above_is_same_liquid = node_above.name == flowing or node_above.name == source
|
||||
}
|
||||
end
|
||||
end
|
||||
local function get_corner_level(x, z)
|
||||
local air_neighbor
|
||||
local levels = 0
|
||||
local neighbor_count = 0
|
||||
for nx = x - 1, x do
|
||||
for nz = z - 1, z do
|
||||
local neighbor = neighbors[nx][nz]
|
||||
if neighbor.above_is_same_liquid then
|
||||
return 1
|
||||
end
|
||||
local level = neighbor.level
|
||||
if level then
|
||||
if level == 1 then
|
||||
return 1
|
||||
end
|
||||
levels = levels + level
|
||||
neighbor_count = neighbor_count + 1
|
||||
elseif neighbor.air then
|
||||
if air_neighbor then
|
||||
return 0.02
|
||||
end
|
||||
air_neighbor = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if neighbor_count == 0 then
|
||||
return 0
|
||||
end
|
||||
return levels / neighbor_count
|
||||
end
|
||||
local corner_levels = {
|
||||
{0, nil, 0},
|
||||
{1, nil, 0},
|
||||
{1, nil, 1},
|
||||
{0, nil, 1}
|
||||
}
|
||||
for index, corner_level in pairs(corner_levels) do
|
||||
corner_level[2] = get_corner_level(corner_level[1], corner_level[3])
|
||||
corner_levels[index] = modlib.vector.subtract_scalar(modlib.vector.new(corner_level), 0.5)
|
||||
end
|
||||
return corner_levels
|
||||
end
|
||||
|
||||
flowing_downwards = modlib.vector.new{0, -1, 0}
|
||||
@ -76,40 +76,40 @@ flowing_downwards = modlib.vector.new{0, -1, 0}
|
||||
--> `modlib.minetest.flowing_downwards = modlib.vector.new{0, -1, 0}` if only flowing downwards
|
||||
--> surface direction as `modlib.vector` else
|
||||
function get_liquid_flow_direction(pos)
|
||||
local corner_levels = get_liquid_corner_levels(pos)
|
||||
local max_level = corner_levels[1][2]
|
||||
for index = 2, 4 do
|
||||
local level = corner_levels[index][2]
|
||||
if level > max_level then
|
||||
max_level = level
|
||||
end
|
||||
end
|
||||
local dir = modlib.vector.new{0, 0, 0}
|
||||
local count = 0
|
||||
for max_level_index, corner_level in pairs(corner_levels) do
|
||||
if corner_level[2] == max_level then
|
||||
for offset = 1, 3 do
|
||||
local index = (max_level_index + offset - 1) % 4 + 1
|
||||
local diff = corner_level - corner_levels[index]
|
||||
if diff[2] ~= 0 then
|
||||
diff[1] = diff[1] * diff[2]
|
||||
diff[3] = diff[3] * diff[2]
|
||||
if offset == 3 then
|
||||
diff = modlib.vector.divide_scalar(diff, math.sqrt(2))
|
||||
end
|
||||
dir = dir + diff
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if count ~= 0 then
|
||||
dir = modlib.vector.divide_scalar(dir, count)
|
||||
end
|
||||
if dir == modlib.vector.new{0, 0, 0} then
|
||||
if minetest.get_node(pos).param2 % 32 > 7 then
|
||||
return flowing_downwards
|
||||
end
|
||||
end
|
||||
return dir
|
||||
local corner_levels = get_liquid_corner_levels(pos)
|
||||
local max_level = corner_levels[1][2]
|
||||
for index = 2, 4 do
|
||||
local level = corner_levels[index][2]
|
||||
if level > max_level then
|
||||
max_level = level
|
||||
end
|
||||
end
|
||||
local dir = modlib.vector.new{0, 0, 0}
|
||||
local count = 0
|
||||
for max_level_index, corner_level in pairs(corner_levels) do
|
||||
if corner_level[2] == max_level then
|
||||
for offset = 1, 3 do
|
||||
local index = (max_level_index + offset - 1) % 4 + 1
|
||||
local diff = corner_level - corner_levels[index]
|
||||
if diff[2] ~= 0 then
|
||||
diff[1] = diff[1] * diff[2]
|
||||
diff[3] = diff[3] * diff[2]
|
||||
if offset == 3 then
|
||||
diff = modlib.vector.divide_scalar(diff, math.sqrt(2))
|
||||
end
|
||||
dir = dir + diff
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if count ~= 0 then
|
||||
dir = modlib.vector.divide_scalar(dir, count)
|
||||
end
|
||||
if dir == modlib.vector.new{0, 0, 0} then
|
||||
if minetest.get_node(pos).param2 % 32 > 7 then
|
||||
return flowing_downwards
|
||||
end
|
||||
end
|
||||
return dir
|
||||
end
|
@ -1,40 +1,40 @@
|
||||
max_wear = 2 ^ 16 - 1
|
||||
function override(function_name, function_builder)
|
||||
local func = minetest[function_name]
|
||||
minetest["original_" .. function_name] = func
|
||||
minetest[function_name] = function_builder(func)
|
||||
local func = minetest[function_name]
|
||||
minetest["original_" .. function_name] = func
|
||||
minetest[function_name] = function_builder(func)
|
||||
end
|
||||
|
||||
-- TODO fix modlib.minetest.get_gametime() messing up responsible "mod" determined by engine on crash
|
||||
get_gametime = minetest.get_gametime
|
||||
local get_gametime_initialized
|
||||
local function get_gametime_init(dtime)
|
||||
if get_gametime_initialized then
|
||||
-- if the profiler is being used, the globalstep can't be unregistered
|
||||
return
|
||||
end
|
||||
get_gametime_initialized = true
|
||||
assert(dtime == 0)
|
||||
local gametime = minetest.get_gametime()
|
||||
assert(gametime)
|
||||
function modlib.minetest.get_gametime()
|
||||
local imprecise_gametime = minetest.get_gametime()
|
||||
if imprecise_gametime > gametime then
|
||||
minetest.log("warning", "modlib.minetest.get_gametime(): Called after increment and before first globalstep")
|
||||
return imprecise_gametime
|
||||
end
|
||||
return gametime
|
||||
end
|
||||
for index, globalstep in pairs(minetest.registered_globalsteps) do
|
||||
if globalstep == get_gametime_init then
|
||||
table.remove(minetest.registered_globalsteps, index)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- globalsteps of mods which depend on modlib will execute after this
|
||||
minetest.register_globalstep(function(dtime)
|
||||
gametime = gametime + dtime
|
||||
end)
|
||||
if get_gametime_initialized then
|
||||
-- if the profiler is being used, the globalstep can't be unregistered
|
||||
return
|
||||
end
|
||||
get_gametime_initialized = true
|
||||
assert(dtime == 0)
|
||||
local gametime = minetest.get_gametime()
|
||||
assert(gametime)
|
||||
function modlib.minetest.get_gametime()
|
||||
local imprecise_gametime = minetest.get_gametime()
|
||||
if imprecise_gametime > gametime then
|
||||
minetest.log("warning", "modlib.minetest.get_gametime(): Called after increment and before first globalstep")
|
||||
return imprecise_gametime
|
||||
end
|
||||
return gametime
|
||||
end
|
||||
for index, globalstep in pairs(minetest.registered_globalsteps) do
|
||||
if globalstep == get_gametime_init then
|
||||
table.remove(minetest.registered_globalsteps, index)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- globalsteps of mods which depend on modlib will execute after this
|
||||
minetest.register_globalstep(function(dtime)
|
||||
gametime = gametime + dtime
|
||||
end)
|
||||
end
|
||||
minetest.register_globalstep(get_gametime_init)
|
||||
|
||||
@ -42,156 +42,156 @@ delta_times={}
|
||||
delays={}
|
||||
callbacks={}
|
||||
function register_globalstep(interval, callback)
|
||||
if type(callback) ~= "function" then
|
||||
return
|
||||
end
|
||||
table.insert(delta_times, 0)
|
||||
table.insert(delays, interval)
|
||||
table.insert(callbacks, callback)
|
||||
if type(callback) ~= "function" then
|
||||
return
|
||||
end
|
||||
table.insert(delta_times, 0)
|
||||
table.insert(delays, interval)
|
||||
table.insert(callbacks, callback)
|
||||
end
|
||||
function texture_modifier_inventorycube(face_1, face_2, face_3)
|
||||
return "[inventorycube{" .. string.gsub(face_1, "%^", "&")
|
||||
.. "{" .. string.gsub(face_2, "%^", "&")
|
||||
.. "{" .. string.gsub(face_3, "%^", "&")
|
||||
return "[inventorycube{" .. string.gsub(face_1, "%^", "&")
|
||||
.. "{" .. string.gsub(face_2, "%^", "&")
|
||||
.. "{" .. string.gsub(face_3, "%^", "&")
|
||||
end
|
||||
function get_node_inventory_image(nodename)
|
||||
local n = minetest.registered_nodes[nodename]
|
||||
if not n then
|
||||
return
|
||||
end
|
||||
local tiles = {}
|
||||
for l, tile in pairs(n.tiles or {}) do
|
||||
tiles[l] = (type(tile) == "string" and tile) or tile.name
|
||||
end
|
||||
local chosen_tiles = { tiles[1], tiles[3], tiles[5] }
|
||||
if #chosen_tiles == 0 then
|
||||
return false
|
||||
end
|
||||
if not chosen_tiles[2] then
|
||||
chosen_tiles[2] = chosen_tiles[1]
|
||||
end
|
||||
if not chosen_tiles[3] then
|
||||
chosen_tiles[3] = chosen_tiles[2]
|
||||
end
|
||||
local img = minetest.registered_items[nodename].inventory_image
|
||||
if string.len(img) == 0 then
|
||||
img = nil
|
||||
end
|
||||
return img or texture_modifier_inventorycube(chosen_tiles[1], chosen_tiles[2], chosen_tiles[3])
|
||||
local n = minetest.registered_nodes[nodename]
|
||||
if not n then
|
||||
return
|
||||
end
|
||||
local tiles = {}
|
||||
for l, tile in pairs(n.tiles or {}) do
|
||||
tiles[l] = (type(tile) == "string" and tile) or tile.name
|
||||
end
|
||||
local chosen_tiles = { tiles[1], tiles[3], tiles[5] }
|
||||
if #chosen_tiles == 0 then
|
||||
return false
|
||||
end
|
||||
if not chosen_tiles[2] then
|
||||
chosen_tiles[2] = chosen_tiles[1]
|
||||
end
|
||||
if not chosen_tiles[3] then
|
||||
chosen_tiles[3] = chosen_tiles[2]
|
||||
end
|
||||
local img = minetest.registered_items[nodename].inventory_image
|
||||
if string.len(img) == 0 then
|
||||
img = nil
|
||||
end
|
||||
return img or texture_modifier_inventorycube(chosen_tiles[1], chosen_tiles[2], chosen_tiles[3])
|
||||
end
|
||||
function get_color_int(color)
|
||||
return color.b + (color.g*256) + (color.r*256*256)
|
||||
return color.b + (color.g*256) + (color.r*256*256)
|
||||
end
|
||||
function check_player_privs(playername, privtable)
|
||||
local privs=minetest.get_player_privs(playername)
|
||||
local missing_privs={}
|
||||
local to_lose_privs={}
|
||||
for priv, expected_value in pairs(privtable) do
|
||||
local actual_value=privs[priv]
|
||||
if expected_value then
|
||||
if not actual_value then
|
||||
table.insert(missing_privs, priv)
|
||||
end
|
||||
else
|
||||
if actual_value then
|
||||
table.insert(to_lose_privs, priv)
|
||||
end
|
||||
end
|
||||
end
|
||||
return missing_privs, to_lose_privs
|
||||
local privs=minetest.get_player_privs(playername)
|
||||
local missing_privs={}
|
||||
local to_lose_privs={}
|
||||
for priv, expected_value in pairs(privtable) do
|
||||
local actual_value=privs[priv]
|
||||
if expected_value then
|
||||
if not actual_value then
|
||||
table.insert(missing_privs, priv)
|
||||
end
|
||||
else
|
||||
if actual_value then
|
||||
table.insert(to_lose_privs, priv)
|
||||
end
|
||||
end
|
||||
end
|
||||
return missing_privs, to_lose_privs
|
||||
end
|
||||
|
||||
minetest.register_globalstep(function(dtime)
|
||||
for k, v in pairs(delta_times) do
|
||||
local v=dtime+v
|
||||
if v > delays[k] then
|
||||
callbacks[k](v)
|
||||
v=0
|
||||
end
|
||||
delta_times[k]=v
|
||||
end
|
||||
for k, v in pairs(delta_times) do
|
||||
local v=dtime+v
|
||||
if v > delays[k] then
|
||||
callbacks[k](v)
|
||||
v=0
|
||||
end
|
||||
delta_times[k]=v
|
||||
end
|
||||
end)
|
||||
|
||||
form_listeners = {}
|
||||
function register_form_listener(formname, func)
|
||||
local current_listeners = form_listeners[formname] or {}
|
||||
table.insert(current_listeners, func)
|
||||
form_listeners[formname] = current_listeners
|
||||
local current_listeners = form_listeners[formname] or {}
|
||||
table.insert(current_listeners, func)
|
||||
form_listeners[formname] = current_listeners
|
||||
end
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
local handlers = form_listeners[formname]
|
||||
if handlers then
|
||||
for _, handler in pairs(handlers) do
|
||||
handler(player, fields)
|
||||
end
|
||||
end
|
||||
local handlers = form_listeners[formname]
|
||||
if handlers then
|
||||
for _, handler in pairs(handlers) do
|
||||
handler(player, fields)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--+ Improved base64 decode removing valid padding
|
||||
function decode_base64(base64)
|
||||
local len = base64:len()
|
||||
local padding_char = base64:sub(len, len) == "="
|
||||
if padding_char then
|
||||
if len % 4 ~= 0 then
|
||||
return
|
||||
end
|
||||
if base64:sub(len-1, len-1) == "=" then
|
||||
base64 = base64:sub(1, len-2)
|
||||
else
|
||||
base64 = base64:sub(1, len-1)
|
||||
end
|
||||
end
|
||||
return minetest.decode_base64(base64)
|
||||
local len = base64:len()
|
||||
local padding_char = base64:sub(len, len) == "="
|
||||
if padding_char then
|
||||
if len % 4 ~= 0 then
|
||||
return
|
||||
end
|
||||
if base64:sub(len-1, len-1) == "=" then
|
||||
base64 = base64:sub(1, len-2)
|
||||
else
|
||||
base64 = base64:sub(1, len-1)
|
||||
end
|
||||
end
|
||||
return minetest.decode_base64(base64)
|
||||
end
|
||||
|
||||
local object_refs = minetest.object_refs
|
||||
--+ Objects inside radius iterator. Uses a linear search.
|
||||
function objects_inside_radius(pos, radius)
|
||||
radius = radius^2
|
||||
local id, object, object_pos
|
||||
return function()
|
||||
repeat
|
||||
id, object = next(object_refs, id)
|
||||
object_pos = object:get_pos()
|
||||
until (not object) or ((pos.x-object_pos.x)^2 + (pos.y-object_pos.y)^2 + (pos.z-object_pos.z)^2) <= radius
|
||||
return object
|
||||
end
|
||||
radius = radius^2
|
||||
local id, object, object_pos
|
||||
return function()
|
||||
repeat
|
||||
id, object = next(object_refs, id)
|
||||
object_pos = object:get_pos()
|
||||
until (not object) or ((pos.x-object_pos.x)^2 + (pos.y-object_pos.y)^2 + (pos.z-object_pos.z)^2) <= radius
|
||||
return object
|
||||
end
|
||||
end
|
||||
|
||||
--+ Objects inside area iterator. Uses a linear search.
|
||||
function objects_inside_area(min, max)
|
||||
local id, object, object_pos
|
||||
return function()
|
||||
repeat
|
||||
id, object = next(object_refs, id)
|
||||
object_pos = object:get_pos()
|
||||
until (not object) or (
|
||||
(min.x <= object_pos.x and min.y <= object_pos.y and min.z <= object_pos.z)
|
||||
and
|
||||
(max.y >= object_pos.x and max.y >= object_pos.y and max.z >= object_pos.z)
|
||||
)
|
||||
return object
|
||||
end
|
||||
local id, object, object_pos
|
||||
return function()
|
||||
repeat
|
||||
id, object = next(object_refs, id)
|
||||
object_pos = object:get_pos()
|
||||
until (not object) or (
|
||||
(min.x <= object_pos.x and min.y <= object_pos.y and min.z <= object_pos.z)
|
||||
and
|
||||
(max.y >= object_pos.x and max.y >= object_pos.y and max.z >= object_pos.z)
|
||||
)
|
||||
return object
|
||||
end
|
||||
end
|
||||
|
||||
--: node_or_groupname "modname:nodename", "group:groupname[,groupname]"
|
||||
--> function(nodename) -> whether node matches
|
||||
function nodename_matcher(node_or_groupname)
|
||||
if modlib.text.starts_with(node_or_groupname, "group:") then
|
||||
-- TODO consider using modlib.text.split instead of Minetest's string.split
|
||||
local groups = node_or_groupname:sub(("group:"):len() + 1):split(",")
|
||||
return function(nodename)
|
||||
for _, groupname in pairs(groups) do
|
||||
if minetest.get_item_group(nodename, groupname) == 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
else
|
||||
return function(nodename)
|
||||
return nodename == node_or_groupname
|
||||
end
|
||||
end
|
||||
if modlib.text.starts_with(node_or_groupname, "group:") then
|
||||
-- TODO consider using modlib.text.split instead of Minetest's string.split
|
||||
local groups = node_or_groupname:sub(("group:"):len() + 1):split(",")
|
||||
return function(nodename)
|
||||
for _, groupname in pairs(groups) do
|
||||
if minetest.get_item_group(nodename, groupname) == 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
else
|
||||
return function(nodename)
|
||||
return nodename == node_or_groupname
|
||||
end
|
||||
end
|
||||
end
|
@ -1,132 +1,132 @@
|
||||
--+ Raycast wrapper with proper flowingliquid intersections
|
||||
function raycast(_pos1, _pos2, objects, liquids)
|
||||
local raycast = minetest.raycast(_pos1, _pos2, objects, liquids)
|
||||
if not liquids then
|
||||
return raycast
|
||||
end
|
||||
local pos1 = modlib.vector.from_minetest(_pos1)
|
||||
local _direction = vector.direction(_pos1, _pos2)
|
||||
local direction = modlib.vector.from_minetest(_direction)
|
||||
local length = vector.distance(_pos1, _pos2)
|
||||
local function next()
|
||||
for pointed_thing in raycast do
|
||||
if pointed_thing.type ~= "node" then
|
||||
return pointed_thing
|
||||
end
|
||||
local _pos = pointed_thing.under
|
||||
local pos = modlib.vector.from_minetest(_pos)
|
||||
local node = minetest.get_node(_pos)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
if not (def and def.drawtype == "flowingliquid") then return pointed_thing end
|
||||
local corner_levels = get_liquid_corner_levels(_pos)
|
||||
local full_corner_levels = true
|
||||
for _, corner_level in pairs(corner_levels) do
|
||||
if corner_level[2] < 0.5 then
|
||||
full_corner_levels = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if full_corner_levels then
|
||||
return pointed_thing
|
||||
end
|
||||
local relative = pos1 - pos
|
||||
local inside = true
|
||||
for _, prop in pairs(relative) do
|
||||
if prop <= -0.5 or prop >= 0.5 then
|
||||
inside = false
|
||||
break
|
||||
end
|
||||
end
|
||||
local function level(x, z)
|
||||
local function distance_squared(corner)
|
||||
return (x - corner[1]) ^ 2 + (z - corner[3]) ^ 2
|
||||
end
|
||||
local irrelevant_corner, distance = 1, distance_squared(corner_levels[1])
|
||||
for index = 2, 4 do
|
||||
local other_distance = distance_squared(corner_levels[index])
|
||||
if other_distance > distance then
|
||||
irrelevant_corner, distance = index, other_distance
|
||||
end
|
||||
end
|
||||
local function corner(off)
|
||||
return corner_levels[((irrelevant_corner + off) % 4) + 1]
|
||||
end
|
||||
local base = corner(2)
|
||||
local edge_1, edge_2 = corner(1) - base, corner(3) - base
|
||||
-- Properly selected edges will have a total length of 2
|
||||
assert(math.abs(edge_1[1] + edge_1[3]) + math.abs(edge_2[1] + edge_2[3]) == 2)
|
||||
if edge_1[1] == 0 then
|
||||
edge_1, edge_2 = edge_2, edge_1
|
||||
end
|
||||
local level = base[2] + (edge_1[2] * ((x - base[1]) / edge_1[1])) + (edge_2[2] * ((z - base[3]) / edge_2[3]))
|
||||
assert(level >= -0.5 and level <= 0.5)
|
||||
return level
|
||||
end
|
||||
inside = inside and (relative[2] < level(relative[1], relative[3]))
|
||||
if inside then
|
||||
-- pos1 is inside the liquid node
|
||||
pointed_thing.intersection_point = _pos1
|
||||
pointed_thing.intersection_normal = vector.new(0, 0, 0)
|
||||
return pointed_thing
|
||||
end
|
||||
local function intersection_normal(axis, dir)
|
||||
return {x = 0, y = 0, z = 0, [axis] = dir}
|
||||
end
|
||||
local function plane(axis, dir)
|
||||
local offset = dir * 0.5
|
||||
local diff_axis = (relative[axis] - offset) / -direction[axis]
|
||||
local intersection_point = {}
|
||||
for plane_axis = 1, 3 do
|
||||
if plane_axis ~= axis then
|
||||
local value = direction[plane_axis] * diff_axis + relative[plane_axis]
|
||||
if value < -0.5 or value > 0.5 then
|
||||
return
|
||||
end
|
||||
intersection_point[plane_axis] = value
|
||||
end
|
||||
end
|
||||
intersection_point[axis] = offset
|
||||
return intersection_point
|
||||
end
|
||||
if direction[2] > 0 then
|
||||
local intersection_point = plane(2, -1)
|
||||
if intersection_point then
|
||||
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
|
||||
pointed_thing.intersection_normal = intersection_normal("y", -1)
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
for coord, other in pairs{[1] = 3, [3] = 1} do
|
||||
if direction[coord] ~= 0 then
|
||||
local dir = direction[coord] > 0 and -1 or 1
|
||||
local intersection_point = plane(coord, dir)
|
||||
if intersection_point then
|
||||
local height = 0
|
||||
for _, corner in pairs(corner_levels) do
|
||||
if corner[coord] == dir * 0.5 then
|
||||
height = height + (math.abs(intersection_point[other] + corner[other])) * corner[2]
|
||||
end
|
||||
end
|
||||
if intersection_point[2] <= height then
|
||||
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
|
||||
pointed_thing.intersection_normal = intersection_normal(modlib.vector.index_aliases[coord], dir)
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, triangle in pairs{
|
||||
{corner_levels[3], corner_levels[2], corner_levels[1]},
|
||||
{corner_levels[4], corner_levels[3], corner_levels[1]}
|
||||
} do
|
||||
local pos_on_ray = modlib.vector.ray_triangle_intersection(relative, direction, triangle)
|
||||
if pos_on_ray and pos_on_ray <= length then
|
||||
pointed_thing.intersection_point = (pos1 + modlib.vector.multiply_scalar(direction, pos_on_ray)):to_minetest()
|
||||
pointed_thing.intersection_normal = modlib.vector.triangle_normal(triangle):to_minetest()
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable({next = next}, {__call = next})
|
||||
local raycast = minetest.raycast(_pos1, _pos2, objects, liquids)
|
||||
if not liquids then
|
||||
return raycast
|
||||
end
|
||||
local pos1 = modlib.vector.from_minetest(_pos1)
|
||||
local _direction = vector.direction(_pos1, _pos2)
|
||||
local direction = modlib.vector.from_minetest(_direction)
|
||||
local length = vector.distance(_pos1, _pos2)
|
||||
local function next()
|
||||
for pointed_thing in raycast do
|
||||
if pointed_thing.type ~= "node" then
|
||||
return pointed_thing
|
||||
end
|
||||
local _pos = pointed_thing.under
|
||||
local pos = modlib.vector.from_minetest(_pos)
|
||||
local node = minetest.get_node(_pos)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
if not (def and def.drawtype == "flowingliquid") then return pointed_thing end
|
||||
local corner_levels = get_liquid_corner_levels(_pos)
|
||||
local full_corner_levels = true
|
||||
for _, corner_level in pairs(corner_levels) do
|
||||
if corner_level[2] < 0.5 then
|
||||
full_corner_levels = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if full_corner_levels then
|
||||
return pointed_thing
|
||||
end
|
||||
local relative = pos1 - pos
|
||||
local inside = true
|
||||
for _, prop in pairs(relative) do
|
||||
if prop <= -0.5 or prop >= 0.5 then
|
||||
inside = false
|
||||
break
|
||||
end
|
||||
end
|
||||
local function level(x, z)
|
||||
local function distance_squared(corner)
|
||||
return (x - corner[1]) ^ 2 + (z - corner[3]) ^ 2
|
||||
end
|
||||
local irrelevant_corner, distance = 1, distance_squared(corner_levels[1])
|
||||
for index = 2, 4 do
|
||||
local other_distance = distance_squared(corner_levels[index])
|
||||
if other_distance > distance then
|
||||
irrelevant_corner, distance = index, other_distance
|
||||
end
|
||||
end
|
||||
local function corner(off)
|
||||
return corner_levels[((irrelevant_corner + off) % 4) + 1]
|
||||
end
|
||||
local base = corner(2)
|
||||
local edge_1, edge_2 = corner(1) - base, corner(3) - base
|
||||
-- Properly selected edges will have a total length of 2
|
||||
assert(math.abs(edge_1[1] + edge_1[3]) + math.abs(edge_2[1] + edge_2[3]) == 2)
|
||||
if edge_1[1] == 0 then
|
||||
edge_1, edge_2 = edge_2, edge_1
|
||||
end
|
||||
local level = base[2] + (edge_1[2] * ((x - base[1]) / edge_1[1])) + (edge_2[2] * ((z - base[3]) / edge_2[3]))
|
||||
assert(level >= -0.5 and level <= 0.5)
|
||||
return level
|
||||
end
|
||||
inside = inside and (relative[2] < level(relative[1], relative[3]))
|
||||
if inside then
|
||||
-- pos1 is inside the liquid node
|
||||
pointed_thing.intersection_point = _pos1
|
||||
pointed_thing.intersection_normal = vector.new(0, 0, 0)
|
||||
return pointed_thing
|
||||
end
|
||||
local function intersection_normal(axis, dir)
|
||||
return {x = 0, y = 0, z = 0, [axis] = dir}
|
||||
end
|
||||
local function plane(axis, dir)
|
||||
local offset = dir * 0.5
|
||||
local diff_axis = (relative[axis] - offset) / -direction[axis]
|
||||
local intersection_point = {}
|
||||
for plane_axis = 1, 3 do
|
||||
if plane_axis ~= axis then
|
||||
local value = direction[plane_axis] * diff_axis + relative[plane_axis]
|
||||
if value < -0.5 or value > 0.5 then
|
||||
return
|
||||
end
|
||||
intersection_point[plane_axis] = value
|
||||
end
|
||||
end
|
||||
intersection_point[axis] = offset
|
||||
return intersection_point
|
||||
end
|
||||
if direction[2] > 0 then
|
||||
local intersection_point = plane(2, -1)
|
||||
if intersection_point then
|
||||
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
|
||||
pointed_thing.intersection_normal = intersection_normal("y", -1)
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
for coord, other in pairs{[1] = 3, [3] = 1} do
|
||||
if direction[coord] ~= 0 then
|
||||
local dir = direction[coord] > 0 and -1 or 1
|
||||
local intersection_point = plane(coord, dir)
|
||||
if intersection_point then
|
||||
local height = 0
|
||||
for _, corner in pairs(corner_levels) do
|
||||
if corner[coord] == dir * 0.5 then
|
||||
height = height + (math.abs(intersection_point[other] + corner[other])) * corner[2]
|
||||
end
|
||||
end
|
||||
if intersection_point[2] <= height then
|
||||
pointed_thing.intersection_point = (intersection_point + pos):to_minetest()
|
||||
pointed_thing.intersection_normal = intersection_normal(modlib.vector.index_aliases[coord], dir)
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, triangle in pairs{
|
||||
{corner_levels[3], corner_levels[2], corner_levels[1]},
|
||||
{corner_levels[4], corner_levels[3], corner_levels[1]}
|
||||
} do
|
||||
local pos_on_ray = modlib.vector.ray_triangle_intersection(relative, direction, triangle)
|
||||
if pos_on_ray and pos_on_ray <= length then
|
||||
pointed_thing.intersection_point = (pos1 + modlib.vector.multiply_scalar(direction, pos_on_ray)):to_minetest()
|
||||
pointed_thing.intersection_normal = modlib.vector.triangle_normal(triangle):to_minetest()
|
||||
return pointed_thing
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable({next = next}, {__call = next})
|
||||
end
|
@ -1,13 +1,13 @@
|
||||
players = {}
|
||||
|
||||
registered_on_wielditem_changes = {function(...)
|
||||
local _, previous_item, _, item = ...
|
||||
if previous_item then
|
||||
((previous_item:get_definition()._modlib or {}).un_wield or modlib.func.no_op)(...)
|
||||
end
|
||||
if item then
|
||||
((item:get_definition()._modlib or {}).on_wield or modlib.func.no_op)(...)
|
||||
end
|
||||
local _, previous_item, _, item = ...
|
||||
if previous_item then
|
||||
((previous_item:get_definition()._modlib or {}).un_wield or modlib.func.no_op)(...)
|
||||
end
|
||||
if item then
|
||||
((item:get_definition()._modlib or {}).on_wield or modlib.func.no_op)(...)
|
||||
end
|
||||
end}
|
||||
|
||||
--+ Registers an on_wielditem_change callback: function(player, previous_item, previous_index, item)
|
||||
@ -15,32 +15,32 @@ end}
|
||||
register_on_wielditem_change = modlib.func.curry(table.insert, registered_on_wielditem_changes)
|
||||
|
||||
minetest.register_on_mods_loaded(function()
|
||||
-- Other on_joinplayer / on_leaveplayer callbacks should execute first
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local item, index = player:get_wielded_item(), player:get_wield_index()
|
||||
players[player:get_player_name()] = {
|
||||
wield = {
|
||||
item = item,
|
||||
index = index
|
||||
}
|
||||
}
|
||||
modlib.table.icall(registered_on_wielditem_changes, player, nil, index, item)
|
||||
end)
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
players[player:get_player_name()] = nil
|
||||
end)
|
||||
-- Other on_joinplayer / on_leaveplayer callbacks should execute first
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local item, index = player:get_wielded_item(), player:get_wield_index()
|
||||
players[player:get_player_name()] = {
|
||||
wield = {
|
||||
item = item,
|
||||
index = index
|
||||
}
|
||||
}
|
||||
modlib.table.icall(registered_on_wielditem_changes, player, nil, index, item)
|
||||
end)
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
players[player:get_player_name()] = nil
|
||||
end)
|
||||
end)
|
||||
|
||||
minetest.register_globalstep(function()
|
||||
for _, player in pairs(minetest.get_connected_players()) do
|
||||
local item, index = player:get_wielded_item(), player:get_wield_index()
|
||||
local playerdata = players[player:get_player_name()]
|
||||
if not playerdata then return end
|
||||
local previous_item, previous_index = playerdata.wield.item, playerdata.wield.index
|
||||
if item:get_name() ~= previous_item or index ~= previous_index then
|
||||
playerdata.wield.item = item
|
||||
playerdata.wield.index = index
|
||||
modlib.table.icall(registered_on_wielditem_changes, player, previous_item, previous_index, item)
|
||||
end
|
||||
end
|
||||
for _, player in pairs(minetest.get_connected_players()) do
|
||||
local item, index = player:get_wielded_item(), player:get_wield_index()
|
||||
local playerdata = players[player:get_player_name()]
|
||||
if not playerdata then return end
|
||||
local previous_item, previous_index = playerdata.wield.item, playerdata.wield.index
|
||||
if item:get_name() ~= previous_item or index ~= previous_index then
|
||||
playerdata.wield.item = item
|
||||
playerdata.wield.index = index
|
||||
modlib.table.icall(registered_on_wielditem_changes, player, previous_item, previous_index, item)
|
||||
end
|
||||
end
|
||||
end)
|
212
mod.lua
212
mod.lua
@ -1,130 +1,130 @@
|
||||
-- get resource + dofile
|
||||
function include(modname, file)
|
||||
if not file then
|
||||
file = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
return dofile(get_resource(modname, file))
|
||||
if not file then
|
||||
file = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
return dofile(get_resource(modname, file))
|
||||
end
|
||||
|
||||
function include_env(file_or_string, env, is_string)
|
||||
setfenv(assert((is_string and loadstring or loadfile)(file_or_string)), env)()
|
||||
setfenv(assert((is_string and loadstring or loadfile)(file_or_string)), env)()
|
||||
end
|
||||
|
||||
function create_namespace(namespace_name, parent_namespace)
|
||||
namespace_name = namespace_name or minetest.get_current_modname()
|
||||
parent_namespace = parent_namespace or _G
|
||||
local metatable = {__index = parent_namespace == _G and function(_, key) return rawget(_G, key) end or parent_namespace}
|
||||
local namespace = {}
|
||||
namespace = setmetatable(namespace, metatable)
|
||||
if parent_namespace == _G then
|
||||
rawset(parent_namespace, namespace_name, namespace)
|
||||
else
|
||||
parent_namespace[namespace_name] = namespace
|
||||
end
|
||||
return namespace
|
||||
namespace_name = namespace_name or minetest.get_current_modname()
|
||||
parent_namespace = parent_namespace or _G
|
||||
local metatable = {__index = parent_namespace == _G and function(_, key) return rawget(_G, key) end or parent_namespace}
|
||||
local namespace = {}
|
||||
namespace = setmetatable(namespace, metatable)
|
||||
if parent_namespace == _G then
|
||||
rawset(parent_namespace, namespace_name, namespace)
|
||||
else
|
||||
parent_namespace[namespace_name] = namespace
|
||||
end
|
||||
return namespace
|
||||
end
|
||||
|
||||
-- formerly extend_mod
|
||||
function extend(modname, file)
|
||||
if not file then
|
||||
file = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
include_env(get_resource(modname, file .. ".lua"), rawget(_G, modname))
|
||||
if not file then
|
||||
file = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
include_env(get_resource(modname, file .. ".lua"), rawget(_G, modname))
|
||||
end
|
||||
|
||||
-- runs main.lua in table env
|
||||
-- formerly include_mod
|
||||
function init(modname)
|
||||
modname = modname or minetest.get_current_modname()
|
||||
create_namespace(modname)
|
||||
extend(modname, "main")
|
||||
modname = modname or minetest.get_current_modname()
|
||||
create_namespace(modname)
|
||||
extend(modname, "main")
|
||||
end
|
||||
|
||||
--! deprecated
|
||||
function extend_string(modname, string)
|
||||
if not string then
|
||||
string = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
include_env(string, rawget(_G, modname), true)
|
||||
if not string then
|
||||
string = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
include_env(string, rawget(_G, modname), true)
|
||||
end
|
||||
|
||||
function configuration(modname)
|
||||
modname = modname or minetest.get_current_modname()
|
||||
local schema = modlib.schema.new(assert(include(modname, "schema.lua")))
|
||||
schema.name = schema.name or modname
|
||||
assert(schema.type == "table")
|
||||
local overrides = {}
|
||||
local conf
|
||||
local function add(path)
|
||||
for _, format in ipairs{
|
||||
{extension = "lua", read = function(text)
|
||||
assert(overrides._C == nil)
|
||||
local additions = setfenv(assert(loadstring(text)), setmetatable(overrides, {__index = {_C = overrides}}))()
|
||||
setmetatable(overrides, nil)
|
||||
if additions == nil then
|
||||
return overrides
|
||||
end
|
||||
return additions
|
||||
end},
|
||||
{extension = "luon", read = function(text)
|
||||
local value = {setfenv(assert(loadstring("return " .. text)), setmetatable(overrides, {}))()}
|
||||
assert(#value == 1)
|
||||
value = value[1]
|
||||
local function check_type(value)
|
||||
local type = type(value)
|
||||
if type == "table" then
|
||||
assert(getmetatable(value) == nil)
|
||||
for key, value in pairs(value) do
|
||||
check_type(key)
|
||||
check_type(value)
|
||||
end
|
||||
elseif not (type == "boolean" or type == "number" or type == "string") then
|
||||
error("disallowed type " .. type)
|
||||
end
|
||||
end
|
||||
check_type(value)
|
||||
return value
|
||||
end},
|
||||
{extension = "conf", read = function(text) return modlib.conf.build_setting_tree(Settings(text):to_table()) end, convert_strings = true},
|
||||
{extension = "json", read = minetest.parse_json}
|
||||
} do
|
||||
local content = modlib.file.read(path .. "." .. format.extension)
|
||||
if content then
|
||||
overrides = modlib.table.deep_add_all(overrides, format.read(content))
|
||||
conf = schema:load(overrides, {convert_strings = format.convert_strings, error_message = true})
|
||||
end
|
||||
end
|
||||
end
|
||||
add(minetest.get_worldpath() .. "/conf/" .. modname)
|
||||
add(get_resource(modname, "conf"))
|
||||
local minetest_conf = modlib.conf.settings[schema.name]
|
||||
if minetest_conf then
|
||||
overrides = modlib.table.deep_add_all(overrides, minetest_conf)
|
||||
conf = schema:load(overrides, {convert_strings = true, error_message = true})
|
||||
end
|
||||
modlib.file.ensure_content(get_resource(modname, "settingtypes.txt"), schema:generate_settingtypes())
|
||||
local readme_path = get_resource(modname, "Readme.md")
|
||||
local readme = modlib.file.read(readme_path)
|
||||
if readme then
|
||||
local modified = false
|
||||
readme = readme:gsub("<!%-%-modlib:conf:(%d)%-%->" .. "(.-)" .. "<!%-%-modlib:conf%-%->", function(level, content)
|
||||
schema._md_level = assert(tonumber(level)) + 1
|
||||
-- HACK: Newline between comment and heading (MD implementations don't handle comments properly)
|
||||
local markdown = "\n" .. schema:generate_markdown()
|
||||
if content ~= markdown then
|
||||
modified = true
|
||||
return "<!--modlib:conf:" .. level .. "-->" .. markdown .. "<!--modlib:conf-->"
|
||||
end
|
||||
end, 1)
|
||||
if modified then
|
||||
assert(modlib.file.write(readme_path, readme))
|
||||
end
|
||||
end
|
||||
if conf == nil then
|
||||
return schema:load({}, {error_message = true})
|
||||
end
|
||||
return conf
|
||||
end
|
||||
modname = modname or minetest.get_current_modname()
|
||||
local schema = modlib.schema.new(assert(include(modname, "schema.lua")))
|
||||
schema.name = schema.name or modname
|
||||
assert(schema.type == "table")
|
||||
local overrides = {}
|
||||
local conf
|
||||
local function add(path)
|
||||
for _, format in ipairs{
|
||||
{extension = "lua", read = function(text)
|
||||
assert(overrides._C == nil)
|
||||
local additions = setfenv(assert(loadstring(text)), setmetatable(overrides, {__index = {_C = overrides}}))()
|
||||
setmetatable(overrides, nil)
|
||||
if additions == nil then
|
||||
return overrides
|
||||
end
|
||||
return additions
|
||||
end},
|
||||
{extension = "luon", read = function(text)
|
||||
local value = {setfenv(assert(loadstring("return " .. text)), setmetatable(overrides, {}))()}
|
||||
assert(#value == 1)
|
||||
value = value[1]
|
||||
local function check_type(value)
|
||||
local type = type(value)
|
||||
if type == "table" then
|
||||
assert(getmetatable(value) == nil)
|
||||
for key, value in pairs(value) do
|
||||
check_type(key)
|
||||
check_type(value)
|
||||
end
|
||||
elseif not (type == "boolean" or type == "number" or type == "string") then
|
||||
error("disallowed type " .. type)
|
||||
end
|
||||
end
|
||||
check_type(value)
|
||||
return value
|
||||
end},
|
||||
{extension = "conf", read = function(text) return modlib.conf.build_setting_tree(Settings(text):to_table()) end, convert_strings = true},
|
||||
{extension = "json", read = minetest.parse_json}
|
||||
} do
|
||||
local content = modlib.file.read(path .. "." .. format.extension)
|
||||
if content then
|
||||
overrides = modlib.table.deep_add_all(overrides, format.read(content))
|
||||
conf = schema:load(overrides, {convert_strings = format.convert_strings, error_message = true})
|
||||
end
|
||||
end
|
||||
end
|
||||
add(minetest.get_worldpath() .. "/conf/" .. modname)
|
||||
add(get_resource(modname, "conf"))
|
||||
local minetest_conf = modlib.conf.settings[schema.name]
|
||||
if minetest_conf then
|
||||
overrides = modlib.table.deep_add_all(overrides, minetest_conf)
|
||||
conf = schema:load(overrides, {convert_strings = true, error_message = true})
|
||||
end
|
||||
modlib.file.ensure_content(get_resource(modname, "settingtypes.txt"), schema:generate_settingtypes())
|
||||
local readme_path = get_resource(modname, "Readme.md")
|
||||
local readme = modlib.file.read(readme_path)
|
||||
if readme then
|
||||
local modified = false
|
||||
readme = readme:gsub("<!%-%-modlib:conf:(%d)%-%->" .. "(.-)" .. "<!%-%-modlib:conf%-%->", function(level, content)
|
||||
schema._md_level = assert(tonumber(level)) + 1
|
||||
-- HACK: Newline between comment and heading (MD implementations don't handle comments properly)
|
||||
local markdown = "\n" .. schema:generate_markdown()
|
||||
if content ~= markdown then
|
||||
modified = true
|
||||
return "<!--modlib:conf:" .. level .. "-->" .. markdown .. "<!--modlib:conf-->"
|
||||
end
|
||||
end, 1)
|
||||
if modified then
|
||||
assert(modlib.file.write(readme_path, readme))
|
||||
end
|
||||
end
|
||||
if conf == nil then
|
||||
return schema:load({}, {error_message = true})
|
||||
end
|
||||
return conf
|
||||
end
|
@ -5,8 +5,8 @@ function from_euler_rotation(rotation)
|
||||
local cos = vector.apply(rotation, math.cos)
|
||||
local sin = vector.apply(rotation, math.sin)
|
||||
return {
|
||||
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
|
||||
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
|
||||
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
|
||||
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
|
||||
cos.z * cos.x * sin.y - sin.z * sin.x * cos.y,
|
||||
cos.z * cos.x * cos.y + sin.z * sin.x * sin.y
|
||||
}
|
||||
@ -76,23 +76,23 @@ end
|
||||
|
||||
--> {x = pitch, y = yaw, z = roll} euler rotation in degrees
|
||||
function to_euler_rotation(self)
|
||||
local rotation = {}
|
||||
local rotation = {}
|
||||
|
||||
local sinr_cosp = 2 * (self[4] * self[1] + self[2] * self[3])
|
||||
local cosr_cosp = 1 - 2 * (self[1] ^ 2 + self[2] ^ 2)
|
||||
rotation.x = math.atan2(sinr_cosp, cosr_cosp)
|
||||
local sinr_cosp = 2 * (self[4] * self[1] + self[2] * self[3])
|
||||
local cosr_cosp = 1 - 2 * (self[1] ^ 2 + self[2] ^ 2)
|
||||
rotation.x = math.atan2(sinr_cosp, cosr_cosp)
|
||||
|
||||
local sinp = 2 * (self[4] * self[2] - self[3] * self[1])
|
||||
if sinp <= -1 then
|
||||
rotation.z = -math.pi/2
|
||||
elseif sinp >= 1 then
|
||||
rotation.z = math.pi/2
|
||||
else
|
||||
rotation.z = math.asin(sinp)
|
||||
local sinp = 2 * (self[4] * self[2] - self[3] * self[1])
|
||||
if sinp <= -1 then
|
||||
rotation.z = -math.pi/2
|
||||
elseif sinp >= 1 then
|
||||
rotation.z = math.pi/2
|
||||
else
|
||||
rotation.z = math.asin(sinp)
|
||||
end
|
||||
|
||||
local siny_cosp = 2 * (self[4] * self[3] + self[1] * self[2])
|
||||
local cosy_cosp = 1 - 2 * (self[2] ^ 2 + self[3] ^ 2)
|
||||
local siny_cosp = 2 * (self[4] * self[3] + self[1] * self[2])
|
||||
local cosy_cosp = 1 - 2 * (self[2] ^ 2 + self[3] ^ 2)
|
||||
rotation.y = math.atan2(siny_cosp, cosy_cosp)
|
||||
|
||||
return vector.apply(rotation, math.deg)
|
||||
|
480
ranked_set.lua
480
ranked_set.lua
@ -5,308 +5,308 @@ comparator = modlib.table.default_comparator
|
||||
|
||||
--+ Uses a weight-balanced binary tree
|
||||
function new(comparator)
|
||||
return setmetatable({comparator = comparator, root = {total = 0}}, metatable)
|
||||
return setmetatable({comparator = comparator, root = {total = 0}}, metatable)
|
||||
end
|
||||
|
||||
function len(self)
|
||||
return self.root.total
|
||||
return self.root.total
|
||||
end
|
||||
metatable.__len = len
|
||||
|
||||
function is_empty(self)
|
||||
return len(self) == 0
|
||||
return len(self) == 0
|
||||
end
|
||||
|
||||
local function insert_all(tree, _table)
|
||||
if tree.left then
|
||||
insert_all(tree.left, _table)
|
||||
end
|
||||
table.insert(_table, tree.key)
|
||||
if tree.right then
|
||||
insert_all(tree.right, _table)
|
||||
end
|
||||
if tree.left then
|
||||
insert_all(tree.left, _table)
|
||||
end
|
||||
table.insert(_table, tree.key)
|
||||
if tree.right then
|
||||
insert_all(tree.right, _table)
|
||||
end
|
||||
end
|
||||
|
||||
function to_table(self)
|
||||
local table = {}
|
||||
if not is_empty(self) then
|
||||
insert_all(self.root, table)
|
||||
end
|
||||
return table
|
||||
local table = {}
|
||||
if not is_empty(self) then
|
||||
insert_all(self.root, table)
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
--> iterator: function() -> `rank, key` with ascending rank
|
||||
function ipairs(self, min, max)
|
||||
if is_empty(self) then
|
||||
return function() end
|
||||
end
|
||||
min = min or 1
|
||||
local tree = self.root
|
||||
local current_rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
if min == current_rank then
|
||||
break
|
||||
end
|
||||
local left, right = tree.left, tree.right
|
||||
if min < current_rank then
|
||||
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
|
||||
tree = left
|
||||
else
|
||||
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
|
||||
tree = right
|
||||
end
|
||||
until not tree
|
||||
max = max or len(self)
|
||||
local to_visit = {tree}
|
||||
tree = nil
|
||||
local rank = min - 1
|
||||
local function next()
|
||||
if not tree then
|
||||
local len = #to_visit
|
||||
if len == 0 then return end
|
||||
tree = to_visit[len]
|
||||
to_visit[len] = nil
|
||||
else
|
||||
while tree.left do
|
||||
table.insert(to_visit, tree)
|
||||
tree = tree.left
|
||||
end
|
||||
end
|
||||
local key = tree.key
|
||||
tree = tree.right
|
||||
return key
|
||||
end
|
||||
return function()
|
||||
if rank >= max then
|
||||
return
|
||||
end
|
||||
local key = next()
|
||||
if key == nil then
|
||||
return
|
||||
end
|
||||
rank = rank + 1
|
||||
return rank, key
|
||||
end
|
||||
if is_empty(self) then
|
||||
return function() end
|
||||
end
|
||||
min = min or 1
|
||||
local tree = self.root
|
||||
local current_rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
if min == current_rank then
|
||||
break
|
||||
end
|
||||
local left, right = tree.left, tree.right
|
||||
if min < current_rank then
|
||||
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
|
||||
tree = left
|
||||
else
|
||||
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
|
||||
tree = right
|
||||
end
|
||||
until not tree
|
||||
max = max or len(self)
|
||||
local to_visit = {tree}
|
||||
tree = nil
|
||||
local rank = min - 1
|
||||
local function next()
|
||||
if not tree then
|
||||
local len = #to_visit
|
||||
if len == 0 then return end
|
||||
tree = to_visit[len]
|
||||
to_visit[len] = nil
|
||||
else
|
||||
while tree.left do
|
||||
table.insert(to_visit, tree)
|
||||
tree = tree.left
|
||||
end
|
||||
end
|
||||
local key = tree.key
|
||||
tree = tree.right
|
||||
return key
|
||||
end
|
||||
return function()
|
||||
if rank >= max then
|
||||
return
|
||||
end
|
||||
local key = next()
|
||||
if key == nil then
|
||||
return
|
||||
end
|
||||
rank = rank + 1
|
||||
return rank, key
|
||||
end
|
||||
end
|
||||
|
||||
local function _right_rotation(parent, right, left)
|
||||
local new_parent = parent[left]
|
||||
parent[left] = new_parent[right]
|
||||
new_parent[right] = parent
|
||||
parent.total = (parent[left] and parent[left].total or 0) + (parent[right] and parent[right].total or 0) + 1
|
||||
assert(parent.total > 0 or (parent.left == nil and parent.right == nil))
|
||||
new_parent.total = (new_parent[left] and new_parent[left].total or 0) + parent.total + 1
|
||||
return new_parent
|
||||
local new_parent = parent[left]
|
||||
parent[left] = new_parent[right]
|
||||
new_parent[right] = parent
|
||||
parent.total = (parent[left] and parent[left].total or 0) + (parent[right] and parent[right].total or 0) + 1
|
||||
assert(parent.total > 0 or (parent.left == nil and parent.right == nil))
|
||||
new_parent.total = (new_parent[left] and new_parent[left].total or 0) + parent.total + 1
|
||||
return new_parent
|
||||
end
|
||||
|
||||
local function right_rotation(parent)
|
||||
return _right_rotation(parent, "right", "left")
|
||||
return _right_rotation(parent, "right", "left")
|
||||
end
|
||||
|
||||
local function left_rotation(parent)
|
||||
return _right_rotation(parent, "left", "right")
|
||||
return _right_rotation(parent, "left", "right")
|
||||
end
|
||||
|
||||
local function _rebalance(parent)
|
||||
local left_count, right_count = (parent.left and parent.left.total or 0), (parent.right and parent.right.total or 0)
|
||||
if right_count > 1 and left_count * 2 < right_count then
|
||||
return left_rotation(parent)
|
||||
end
|
||||
if left_count > 1 and right_count * 2 < left_count then
|
||||
return right_rotation(parent)
|
||||
end
|
||||
return parent
|
||||
local left_count, right_count = (parent.left and parent.left.total or 0), (parent.right and parent.right.total or 0)
|
||||
if right_count > 1 and left_count * 2 < right_count then
|
||||
return left_rotation(parent)
|
||||
end
|
||||
if left_count > 1 and right_count * 2 < left_count then
|
||||
return right_rotation(parent)
|
||||
end
|
||||
return parent
|
||||
end
|
||||
|
||||
-- Rebalances a parent chain
|
||||
local function rebalance(self, len, parents, sides)
|
||||
if len <= 1 then
|
||||
return
|
||||
end
|
||||
for i = len, 2, -1 do
|
||||
parents[i] = _rebalance(parents[i])
|
||||
parents[i - 1][sides[i - 1]] = parents[i]
|
||||
end
|
||||
self.root = parents[1]
|
||||
if len <= 1 then
|
||||
return
|
||||
end
|
||||
for i = len, 2, -1 do
|
||||
parents[i] = _rebalance(parents[i])
|
||||
parents[i - 1][sides[i - 1]] = parents[i]
|
||||
end
|
||||
self.root = parents[1]
|
||||
end
|
||||
|
||||
local function _insert(self, key, replace)
|
||||
assert(key ~= nil)
|
||||
if is_empty(self) then
|
||||
self.root = {key = key, total = 1}
|
||||
return
|
||||
end
|
||||
local comparator = self.comparator
|
||||
local parents, sides = {}, {}
|
||||
local tree = self.root
|
||||
repeat
|
||||
local tree_key = tree.key
|
||||
local compared = comparator(key, tree_key)
|
||||
if compared == 0 then
|
||||
if replace then
|
||||
tree.key = key
|
||||
return tree_key
|
||||
end
|
||||
return
|
||||
end
|
||||
table.insert(parents, tree)
|
||||
local side = compared < 0 and "left" or "right"
|
||||
table.insert(sides, side)
|
||||
tree = tree[side]
|
||||
until not tree
|
||||
local len = #parents
|
||||
parents[len][sides[len]] = {key = key, total = 1}
|
||||
for _, parent in pairs(parents) do
|
||||
parent.total = parent.total + 1
|
||||
end
|
||||
rebalance(self, len, parents, sides)
|
||||
assert(key ~= nil)
|
||||
if is_empty(self) then
|
||||
self.root = {key = key, total = 1}
|
||||
return
|
||||
end
|
||||
local comparator = self.comparator
|
||||
local parents, sides = {}, {}
|
||||
local tree = self.root
|
||||
repeat
|
||||
local tree_key = tree.key
|
||||
local compared = comparator(key, tree_key)
|
||||
if compared == 0 then
|
||||
if replace then
|
||||
tree.key = key
|
||||
return tree_key
|
||||
end
|
||||
return
|
||||
end
|
||||
table.insert(parents, tree)
|
||||
local side = compared < 0 and "left" or "right"
|
||||
table.insert(sides, side)
|
||||
tree = tree[side]
|
||||
until not tree
|
||||
local len = #parents
|
||||
parents[len][sides[len]] = {key = key, total = 1}
|
||||
for _, parent in pairs(parents) do
|
||||
parent.total = parent.total + 1
|
||||
end
|
||||
rebalance(self, len, parents, sides)
|
||||
end
|
||||
|
||||
function insert(self, key)
|
||||
return _insert(self, key)
|
||||
return _insert(self, key)
|
||||
end
|
||||
|
||||
function insert_or_replace(self, key)
|
||||
return _insert(self, key, true)
|
||||
return _insert(self, key, true)
|
||||
end
|
||||
|
||||
local function _delete(self, key, is_rank)
|
||||
assert(key ~= nil)
|
||||
if is_empty(self) then
|
||||
return
|
||||
end
|
||||
local comparator = self.comparator
|
||||
local parents, sides = {}, {}
|
||||
local tree = self.root
|
||||
local rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
local tree_key = tree.key
|
||||
local compared
|
||||
if is_rank then
|
||||
if key == rank then
|
||||
compared = 0
|
||||
elseif key < rank then
|
||||
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
|
||||
compared = -1
|
||||
else
|
||||
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
|
||||
compared = 1
|
||||
end
|
||||
else
|
||||
compared = comparator(key, tree_key)
|
||||
end
|
||||
if compared == 0 then
|
||||
local len = #parents
|
||||
local left, right = tree.left, tree.right
|
||||
if left then
|
||||
tree.total = tree.total - 1
|
||||
if right then
|
||||
-- Obtain successor
|
||||
local side = left.total > right.total and "left" or "right"
|
||||
local other_side = side == "left" and "right" or "left"
|
||||
local sidemost = tree[side]
|
||||
while sidemost[other_side] do
|
||||
sidemost.total = sidemost.total - 1
|
||||
table.insert(parents, sidemost)
|
||||
table.insert(sides, other_side)
|
||||
sidemost = sidemost[other_side]
|
||||
end
|
||||
-- Replace deleted key
|
||||
tree.key = rightmost.key
|
||||
-- Replace the successor by it's single child
|
||||
parents[len][sides[len]] = sidemost[side]
|
||||
else
|
||||
if len == 0 then
|
||||
self.root = left or {total = 0}
|
||||
else
|
||||
parents[len][sides[len]] = left
|
||||
end
|
||||
end
|
||||
elseif right then
|
||||
if len == 0 then
|
||||
self.root = right or {total = 0}
|
||||
else
|
||||
tree.total = tree.total - 1
|
||||
parents[len][sides[len]] = right
|
||||
end
|
||||
else
|
||||
if len == 0 then
|
||||
self.root = {total = 0}
|
||||
else
|
||||
parents[len][sides[len]] = nil
|
||||
end
|
||||
end
|
||||
for _, parent in pairs(parents) do
|
||||
parent.total = parent.total - 1
|
||||
end
|
||||
rebalance(self, len, parents, sides)
|
||||
if is_rank then
|
||||
return tree_key
|
||||
end
|
||||
return rank, tree_key
|
||||
end
|
||||
table.insert(parents, tree)
|
||||
local side
|
||||
if compared < 0 then
|
||||
side = "left"
|
||||
else
|
||||
side = "right"
|
||||
end
|
||||
table.insert(sides, side)
|
||||
tree = tree[side]
|
||||
until not tree
|
||||
assert(key ~= nil)
|
||||
if is_empty(self) then
|
||||
return
|
||||
end
|
||||
local comparator = self.comparator
|
||||
local parents, sides = {}, {}
|
||||
local tree = self.root
|
||||
local rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
local tree_key = tree.key
|
||||
local compared
|
||||
if is_rank then
|
||||
if key == rank then
|
||||
compared = 0
|
||||
elseif key < rank then
|
||||
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
|
||||
compared = -1
|
||||
else
|
||||
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
|
||||
compared = 1
|
||||
end
|
||||
else
|
||||
compared = comparator(key, tree_key)
|
||||
end
|
||||
if compared == 0 then
|
||||
local len = #parents
|
||||
local left, right = tree.left, tree.right
|
||||
if left then
|
||||
tree.total = tree.total - 1
|
||||
if right then
|
||||
-- Obtain successor
|
||||
local side = left.total > right.total and "left" or "right"
|
||||
local other_side = side == "left" and "right" or "left"
|
||||
local sidemost = tree[side]
|
||||
while sidemost[other_side] do
|
||||
sidemost.total = sidemost.total - 1
|
||||
table.insert(parents, sidemost)
|
||||
table.insert(sides, other_side)
|
||||
sidemost = sidemost[other_side]
|
||||
end
|
||||
-- Replace deleted key
|
||||
tree.key = rightmost.key
|
||||
-- Replace the successor by it's single child
|
||||
parents[len][sides[len]] = sidemost[side]
|
||||
else
|
||||
if len == 0 then
|
||||
self.root = left or {total = 0}
|
||||
else
|
||||
parents[len][sides[len]] = left
|
||||
end
|
||||
end
|
||||
elseif right then
|
||||
if len == 0 then
|
||||
self.root = right or {total = 0}
|
||||
else
|
||||
tree.total = tree.total - 1
|
||||
parents[len][sides[len]] = right
|
||||
end
|
||||
else
|
||||
if len == 0 then
|
||||
self.root = {total = 0}
|
||||
else
|
||||
parents[len][sides[len]] = nil
|
||||
end
|
||||
end
|
||||
for _, parent in pairs(parents) do
|
||||
parent.total = parent.total - 1
|
||||
end
|
||||
rebalance(self, len, parents, sides)
|
||||
if is_rank then
|
||||
return tree_key
|
||||
end
|
||||
return rank, tree_key
|
||||
end
|
||||
table.insert(parents, tree)
|
||||
local side
|
||||
if compared < 0 then
|
||||
side = "left"
|
||||
else
|
||||
side = "right"
|
||||
end
|
||||
table.insert(sides, side)
|
||||
tree = tree[side]
|
||||
until not tree
|
||||
end
|
||||
|
||||
function delete(self, key)
|
||||
return _delete(self, key)
|
||||
return _delete(self, key)
|
||||
end
|
||||
|
||||
delete_by_key = delete
|
||||
|
||||
function delete_by_rank(self, rank)
|
||||
return _delete(self, rank, true)
|
||||
return _delete(self, rank, true)
|
||||
end
|
||||
|
||||
--> `rank, key` if the key was found
|
||||
--> `rank` the key would have if inserted
|
||||
function get(self, key)
|
||||
if is_empty(self) then return end
|
||||
local comparator = self.comparator
|
||||
local tree = self.root
|
||||
local rank = (tree.left and tree.left.total or 0) + 1
|
||||
while tree do
|
||||
local compared = comparator(key, tree.key)
|
||||
if compared == 0 then
|
||||
return rank, tree.key
|
||||
end
|
||||
if compared < 0 then
|
||||
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
|
||||
tree = tree.left
|
||||
else
|
||||
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
|
||||
tree = tree.right
|
||||
end
|
||||
end
|
||||
return rank
|
||||
if is_empty(self) then return end
|
||||
local comparator = self.comparator
|
||||
local tree = self.root
|
||||
local rank = (tree.left and tree.left.total or 0) + 1
|
||||
while tree do
|
||||
local compared = comparator(key, tree.key)
|
||||
if compared == 0 then
|
||||
return rank, tree.key
|
||||
end
|
||||
if compared < 0 then
|
||||
rank = rank - (tree.left and tree.left.right and tree.left.right.total or 0) - 1
|
||||
tree = tree.left
|
||||
else
|
||||
rank = rank + (tree.right and tree.right.left and tree.right.left.total or 0) + 1
|
||||
tree = tree.right
|
||||
end
|
||||
end
|
||||
return rank
|
||||
end
|
||||
|
||||
get_by_key = get
|
||||
|
||||
--> key
|
||||
function get_by_rank(self, rank)
|
||||
local tree = self.root
|
||||
local current_rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
if rank == current_rank then
|
||||
return tree.key
|
||||
end
|
||||
local left, right = tree.left, tree.right
|
||||
if rank < current_rank then
|
||||
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
|
||||
tree = left
|
||||
else
|
||||
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
|
||||
tree = right
|
||||
end
|
||||
until not tree
|
||||
local tree = self.root
|
||||
local current_rank = (tree.left and tree.left.total or 0) + 1
|
||||
repeat
|
||||
if rank == current_rank then
|
||||
return tree.key
|
||||
end
|
||||
local left, right = tree.left, tree.right
|
||||
if rank < current_rank then
|
||||
current_rank = current_rank - (left and left.right and left.right.total or 0) - 1
|
||||
tree = left
|
||||
else
|
||||
current_rank = current_rank + (right and right.left and right.left.total or 0) + 1
|
||||
tree = right
|
||||
end
|
||||
until not tree
|
||||
end
|
544
schema.lua
544
schema.lua
@ -1,291 +1,291 @@
|
||||
local schema = getfenv(1)
|
||||
|
||||
function new(def)
|
||||
-- TODO type inference, sanity checking etc.
|
||||
return setmetatable(def, {__index = schema})
|
||||
-- TODO type inference, sanity checking etc.
|
||||
return setmetatable(def, {__index = schema})
|
||||
end
|
||||
|
||||
local function field_name_to_title(name)
|
||||
local title = modlib.text.split(name, "_")
|
||||
title[1] = modlib.text.upper_first(title[1])
|
||||
return table.concat(title, " ")
|
||||
local title = modlib.text.split(name, "_")
|
||||
title[1] = modlib.text.upper_first(title[1])
|
||||
return table.concat(title, " ")
|
||||
end
|
||||
|
||||
function generate_settingtypes(self)
|
||||
local typ = self.type
|
||||
local settingtype, type_args
|
||||
self.title = self.title or field_name_to_title(self.name)
|
||||
self._level = self._level or 0
|
||||
local default = self.default
|
||||
if typ == "boolean" then
|
||||
settingtype = "bool"
|
||||
default = default and "true" or "false"
|
||||
elseif typ == "string" then
|
||||
settingtype = "string"
|
||||
elseif typ == "number" then
|
||||
settingtype = self.int and "int" or "float"
|
||||
if self.min or self.max then
|
||||
-- TODO handle exclusive min/max
|
||||
type_args = (self.int and "%d %d" or "%f %f"):format(self.min or (2 ^ -30), self.max or (2 ^ 30))
|
||||
end
|
||||
elseif typ == "table" then
|
||||
local settings = {}
|
||||
if self._level > 0 then
|
||||
-- HACK: Minetest automatically adds the modname
|
||||
-- TODO simple names (not modname.field.other_field)
|
||||
settings = {"[" .. table.concat(modlib.table.repetition("*", self._level)) .. self.name .. "]"}
|
||||
end
|
||||
local function setting(key, value_scheme)
|
||||
assert(not key:find("[=%.%s]"))
|
||||
value_scheme.name = self.name .. "." .. key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._level = self._level + 1
|
||||
table.insert(settings, generate_settingtypes(value_scheme))
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local description = self.description
|
||||
-- TODO extend description by range etc.?
|
||||
-- TODO enum etc. support
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
description = {description}
|
||||
end
|
||||
description = "# " .. table.concat(description, "\n# ") .. "\n"
|
||||
else
|
||||
description = ""
|
||||
end
|
||||
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
|
||||
local typ = self.type
|
||||
local settingtype, type_args
|
||||
self.title = self.title or field_name_to_title(self.name)
|
||||
self._level = self._level or 0
|
||||
local default = self.default
|
||||
if typ == "boolean" then
|
||||
settingtype = "bool"
|
||||
default = default and "true" or "false"
|
||||
elseif typ == "string" then
|
||||
settingtype = "string"
|
||||
elseif typ == "number" then
|
||||
settingtype = self.int and "int" or "float"
|
||||
if self.min or self.max then
|
||||
-- TODO handle exclusive min/max
|
||||
type_args = (self.int and "%d %d" or "%f %f"):format(self.min or (2 ^ -30), self.max or (2 ^ 30))
|
||||
end
|
||||
elseif typ == "table" then
|
||||
local settings = {}
|
||||
if self._level > 0 then
|
||||
-- HACK: Minetest automatically adds the modname
|
||||
-- TODO simple names (not modname.field.other_field)
|
||||
settings = {"[" .. table.concat(modlib.table.repetition("*", self._level)) .. self.name .. "]"}
|
||||
end
|
||||
local function setting(key, value_scheme)
|
||||
assert(not key:find("[=%.%s]"))
|
||||
value_scheme.name = self.name .. "." .. key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._level = self._level + 1
|
||||
table.insert(settings, generate_settingtypes(value_scheme))
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local description = self.description
|
||||
-- TODO extend description by range etc.?
|
||||
-- TODO enum etc. support
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
description = {description}
|
||||
end
|
||||
description = "# " .. table.concat(description, "\n# ") .. "\n"
|
||||
else
|
||||
description = ""
|
||||
end
|
||||
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
|
||||
end
|
||||
|
||||
function generate_markdown(self)
|
||||
-- TODO address redundancies
|
||||
local typ = self.type
|
||||
self.title = self.title or field_name_to_title(self._md_name)
|
||||
self._md_level = self._md_level or 1
|
||||
if typ == "table" then
|
||||
local settings = {}
|
||||
local function setting(key, value_scheme)
|
||||
value_scheme._md_name = key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._md_level = self._md_level + 1
|
||||
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
|
||||
table.insert(settings, "")
|
||||
table.insert(settings, generate_markdown(value_scheme))
|
||||
table.insert(settings, "")
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local lines = {}
|
||||
local function line(text)
|
||||
table.insert(lines, "* " .. text)
|
||||
end
|
||||
local description = self.description
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
table.insert(lines, description)
|
||||
else
|
||||
modlib.table.append(lines, description)
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
line("Type: " .. self.type)
|
||||
if self.default ~= nil then
|
||||
line("Default: `" .. tostring(self.default) .. "`")
|
||||
end
|
||||
if self.int then
|
||||
line"Integer"
|
||||
elseif self.list then
|
||||
line"List"
|
||||
end
|
||||
if self.infinity then
|
||||
line"Infinities allowed"
|
||||
end
|
||||
if self.nan then
|
||||
line"Not-a-Number (NaN) allowed"
|
||||
end
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
line(">= " .. self.range.min)
|
||||
elseif self.range.min_exclusive then
|
||||
line("> " .. self.range.min_exclusive)
|
||||
end
|
||||
if self.range.max then
|
||||
line("<= " .. self.range.max)
|
||||
elseif self.range.max_exclusive then
|
||||
line("< " .. self.range.max_exclusive)
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
line("Possible values:")
|
||||
for value in pairs(self.values) do
|
||||
table.insert(lines, " * " .. value)
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
-- TODO address redundancies
|
||||
local typ = self.type
|
||||
self.title = self.title or field_name_to_title(self._md_name)
|
||||
self._md_level = self._md_level or 1
|
||||
if typ == "table" then
|
||||
local settings = {}
|
||||
local function setting(key, value_scheme)
|
||||
value_scheme._md_name = key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._md_level = self._md_level + 1
|
||||
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
|
||||
table.insert(settings, "")
|
||||
table.insert(settings, generate_markdown(value_scheme))
|
||||
table.insert(settings, "")
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local lines = {}
|
||||
local function line(text)
|
||||
table.insert(lines, "* " .. text)
|
||||
end
|
||||
local description = self.description
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
table.insert(lines, description)
|
||||
else
|
||||
modlib.table.append(lines, description)
|
||||
end
|
||||
end
|
||||
table.insert(lines, "")
|
||||
line("Type: " .. self.type)
|
||||
if self.default ~= nil then
|
||||
line("Default: `" .. tostring(self.default) .. "`")
|
||||
end
|
||||
if self.int then
|
||||
line"Integer"
|
||||
elseif self.list then
|
||||
line"List"
|
||||
end
|
||||
if self.infinity then
|
||||
line"Infinities allowed"
|
||||
end
|
||||
if self.nan then
|
||||
line"Not-a-Number (NaN) allowed"
|
||||
end
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
line(">= " .. self.range.min)
|
||||
elseif self.range.min_exclusive then
|
||||
line("> " .. self.range.min_exclusive)
|
||||
end
|
||||
if self.range.max then
|
||||
line("<= " .. self.range.max)
|
||||
elseif self.range.max_exclusive then
|
||||
line("< " .. self.range.max_exclusive)
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
line("Possible values:")
|
||||
for value in pairs(self.values) do
|
||||
table.insert(lines, " * " .. value)
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function settingtypes(self)
|
||||
self.settingtypes = self.settingtypes or generate_settingtypes(self)
|
||||
return self.settingtypes
|
||||
self.settingtypes = self.settingtypes or generate_settingtypes(self)
|
||||
return self.settingtypes
|
||||
end
|
||||
|
||||
function load(self, override, params)
|
||||
local converted
|
||||
if params.convert_strings and type(override) == "string" then
|
||||
converted = true
|
||||
if self.type == "boolean" then
|
||||
if override == "true" then
|
||||
override = true
|
||||
elseif override == "false" then
|
||||
override = false
|
||||
end
|
||||
elseif self.type == "number" then
|
||||
override = tonumber(override)
|
||||
else
|
||||
converted = false
|
||||
end
|
||||
end
|
||||
if override == nil and not converted then
|
||||
if self.default ~= nil then
|
||||
return self.default
|
||||
elseif self.type == "table" then
|
||||
override = {}
|
||||
end
|
||||
end
|
||||
local _error = error
|
||||
local function format_error(typ, ...)
|
||||
if typ == "type" then
|
||||
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
|
||||
end
|
||||
if typ == "range" then
|
||||
local conditions = {}
|
||||
local function push(condition, bound)
|
||||
if self.range[bound] then
|
||||
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
|
||||
end
|
||||
end
|
||||
push(">", "min_exclusive")
|
||||
push(">=", "min")
|
||||
push("<", "max_exclusive")
|
||||
push("<=", "max")
|
||||
return "out of range: expected value " .. table.concat(conditions, "and")
|
||||
end
|
||||
if typ == "int" then
|
||||
return "expected integer"
|
||||
end
|
||||
if typ == "infinity" then
|
||||
return "expected no infinity"
|
||||
end
|
||||
if typ == "nan" then
|
||||
return "expected no nan"
|
||||
end
|
||||
if typ == "required" then
|
||||
local key = ...
|
||||
return "required field " .. minetest.write_json(key) .. " missing"
|
||||
end
|
||||
if typ == "additional" then
|
||||
local key = ...
|
||||
return "superfluous field " .. minetest.write_json(key)
|
||||
end
|
||||
if typ == "list" then
|
||||
return "not a list"
|
||||
end
|
||||
if typ == "values" then
|
||||
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
|
||||
end
|
||||
_error("unknown error type")
|
||||
end
|
||||
local function error(type, ...)
|
||||
if params.error_message then
|
||||
local formatted = format_error(type, ...)
|
||||
settingtypes(self)
|
||||
_error("Invalid value: " .. self.name .. ": " .. formatted)
|
||||
end
|
||||
_error{
|
||||
type = type,
|
||||
self = self,
|
||||
override = override,
|
||||
converted = converted
|
||||
}
|
||||
end
|
||||
local function assert(value, ...)
|
||||
if not value then
|
||||
error(...)
|
||||
end
|
||||
return value
|
||||
end
|
||||
assert(self.type == type(override), "type")
|
||||
if self.type == "number" or self.type == "string" then
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
assert(self.range.min <= override, "range")
|
||||
elseif self.range.min_exclusive then
|
||||
assert(self.range.min_exclusive < override, "range")
|
||||
end
|
||||
if self.range.max then
|
||||
assert(self.range.max >= override, "range")
|
||||
elseif self.range.max_exclusive then
|
||||
assert(self.range.max_exclusive > override, "range")
|
||||
end
|
||||
end
|
||||
if self.type == "number" then
|
||||
assert((not self.int) or (override % 1 == 0), "int")
|
||||
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
|
||||
assert(self.nan or override == override, "nan")
|
||||
end
|
||||
elseif self.type == "table" then
|
||||
if self.entries then
|
||||
for key, schema in pairs(self.entries) do
|
||||
if schema.required and override[key] == nil then
|
||||
error("required", key)
|
||||
end
|
||||
override[key] = load(schema, override[key], params)
|
||||
end
|
||||
if self.additional == false then
|
||||
for key in pairs(override) do
|
||||
if self.entries[key] == nil then
|
||||
error("additional", key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.keys then
|
||||
for key, value in pairs(override) do
|
||||
override[load(self.keys, key, params)], override[key] = value, nil
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
for key, value in pairs(override) do
|
||||
override[key] = load(self.values, value, params)
|
||||
end
|
||||
end
|
||||
assert((not self.list) or modlib.table.count(override) == #override, "list")
|
||||
else
|
||||
assert((not self.values) or self.values[override], "values")
|
||||
end
|
||||
if self.func then self.func(override) end
|
||||
return override
|
||||
local converted
|
||||
if params.convert_strings and type(override) == "string" then
|
||||
converted = true
|
||||
if self.type == "boolean" then
|
||||
if override == "true" then
|
||||
override = true
|
||||
elseif override == "false" then
|
||||
override = false
|
||||
end
|
||||
elseif self.type == "number" then
|
||||
override = tonumber(override)
|
||||
else
|
||||
converted = false
|
||||
end
|
||||
end
|
||||
if override == nil and not converted then
|
||||
if self.default ~= nil then
|
||||
return self.default
|
||||
elseif self.type == "table" then
|
||||
override = {}
|
||||
end
|
||||
end
|
||||
local _error = error
|
||||
local function format_error(typ, ...)
|
||||
if typ == "type" then
|
||||
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
|
||||
end
|
||||
if typ == "range" then
|
||||
local conditions = {}
|
||||
local function push(condition, bound)
|
||||
if self.range[bound] then
|
||||
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
|
||||
end
|
||||
end
|
||||
push(">", "min_exclusive")
|
||||
push(">=", "min")
|
||||
push("<", "max_exclusive")
|
||||
push("<=", "max")
|
||||
return "out of range: expected value " .. table.concat(conditions, "and")
|
||||
end
|
||||
if typ == "int" then
|
||||
return "expected integer"
|
||||
end
|
||||
if typ == "infinity" then
|
||||
return "expected no infinity"
|
||||
end
|
||||
if typ == "nan" then
|
||||
return "expected no nan"
|
||||
end
|
||||
if typ == "required" then
|
||||
local key = ...
|
||||
return "required field " .. minetest.write_json(key) .. " missing"
|
||||
end
|
||||
if typ == "additional" then
|
||||
local key = ...
|
||||
return "superfluous field " .. minetest.write_json(key)
|
||||
end
|
||||
if typ == "list" then
|
||||
return "not a list"
|
||||
end
|
||||
if typ == "values" then
|
||||
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
|
||||
end
|
||||
_error("unknown error type")
|
||||
end
|
||||
local function error(type, ...)
|
||||
if params.error_message then
|
||||
local formatted = format_error(type, ...)
|
||||
settingtypes(self)
|
||||
_error("Invalid value: " .. self.name .. ": " .. formatted)
|
||||
end
|
||||
_error{
|
||||
type = type,
|
||||
self = self,
|
||||
override = override,
|
||||
converted = converted
|
||||
}
|
||||
end
|
||||
local function assert(value, ...)
|
||||
if not value then
|
||||
error(...)
|
||||
end
|
||||
return value
|
||||
end
|
||||
assert(self.type == type(override), "type")
|
||||
if self.type == "number" or self.type == "string" then
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
assert(self.range.min <= override, "range")
|
||||
elseif self.range.min_exclusive then
|
||||
assert(self.range.min_exclusive < override, "range")
|
||||
end
|
||||
if self.range.max then
|
||||
assert(self.range.max >= override, "range")
|
||||
elseif self.range.max_exclusive then
|
||||
assert(self.range.max_exclusive > override, "range")
|
||||
end
|
||||
end
|
||||
if self.type == "number" then
|
||||
assert((not self.int) or (override % 1 == 0), "int")
|
||||
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
|
||||
assert(self.nan or override == override, "nan")
|
||||
end
|
||||
elseif self.type == "table" then
|
||||
if self.entries then
|
||||
for key, schema in pairs(self.entries) do
|
||||
if schema.required and override[key] == nil then
|
||||
error("required", key)
|
||||
end
|
||||
override[key] = load(schema, override[key], params)
|
||||
end
|
||||
if self.additional == false then
|
||||
for key in pairs(override) do
|
||||
if self.entries[key] == nil then
|
||||
error("additional", key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.keys then
|
||||
for key, value in pairs(override) do
|
||||
override[load(self.keys, key, params)], override[key] = value, nil
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
for key, value in pairs(override) do
|
||||
override[key] = load(self.values, value, params)
|
||||
end
|
||||
end
|
||||
assert((not self.list) or modlib.table.count(override) == #override, "list")
|
||||
else
|
||||
assert((not self.values) or self.values[override], "values")
|
||||
end
|
||||
if self.func then self.func(override) end
|
||||
return override
|
||||
end
|
822
table.lua
822
table.lua
@ -1,79 +1,79 @@
|
||||
-- Table helpers
|
||||
|
||||
function map_index(table, func)
|
||||
local mapping_metatable = {
|
||||
__index = function(table, key)
|
||||
return rawget(table, func(key))
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
rawset(table, func(key), value)
|
||||
end
|
||||
}
|
||||
return setmetatable(table, mapping_metatable)
|
||||
local mapping_metatable = {
|
||||
__index = function(table, key)
|
||||
return rawget(table, func(key))
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
rawset(table, func(key), value)
|
||||
end
|
||||
}
|
||||
return setmetatable(table, mapping_metatable)
|
||||
end
|
||||
|
||||
function set_case_insensitive_index(table)
|
||||
return map_index(table, string.lower)
|
||||
return map_index(table, string.lower)
|
||||
end
|
||||
|
||||
--+ nilget(a, "b", "c") == a?.b?.c
|
||||
function nilget(value, key, ...)
|
||||
if value == nil or key == nil then
|
||||
return value
|
||||
end
|
||||
return nilget(value[key], ...)
|
||||
if value == nil or key == nil then
|
||||
return value
|
||||
end
|
||||
return nilget(value[key], ...)
|
||||
end
|
||||
|
||||
-- Fisher-Yates
|
||||
function shuffle(table)
|
||||
for index = 1, #table - 1 do
|
||||
local index_2 = math.random(index + 1, #table)
|
||||
table[index], table[index_2] = table[index_2], table[index]
|
||||
end
|
||||
return table
|
||||
for index = 1, #table - 1 do
|
||||
local index_2 = math.random(index + 1, #table)
|
||||
table[index], table[index_2] = table[index_2], table[index]
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
local rope_metatable = {__index = {
|
||||
write = function(self, text)
|
||||
table.insert(self, text)
|
||||
end,
|
||||
to_text = function(self)
|
||||
return table.concat(self)
|
||||
end
|
||||
write = function(self, text)
|
||||
table.insert(self, text)
|
||||
end,
|
||||
to_text = function(self)
|
||||
return table.concat(self)
|
||||
end
|
||||
}}
|
||||
--> rope with simple metatable (:write(text) and :to_text())
|
||||
function rope(table)
|
||||
return setmetatable(table or {}, rope_metatable)
|
||||
return setmetatable(table or {}, rope_metatable)
|
||||
end
|
||||
|
||||
local rope_len_metatable = {__index = {
|
||||
write = function(self, text)
|
||||
self.len = self.len + text:len()
|
||||
end
|
||||
write = function(self, text)
|
||||
self.len = self.len + text:len()
|
||||
end
|
||||
}}
|
||||
--> rope for determining length supporting :write(text), .len being the length
|
||||
function rope_len(len)
|
||||
return setmetatable({len = len or 0}, rope_len_metatable)
|
||||
return setmetatable({len = len or 0}, rope_len_metatable)
|
||||
end
|
||||
|
||||
function is_circular(table)
|
||||
assert(type(table) == "table")
|
||||
local known = {}
|
||||
local function _is_circular(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if known[value] then
|
||||
return true
|
||||
end
|
||||
known[value] = true
|
||||
for key, value in pairs(value) do
|
||||
if _is_circular(key) or _is_circular(value) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return _is_circular(table)
|
||||
assert(type(table) == "table")
|
||||
local known = {}
|
||||
local function _is_circular(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if known[value] then
|
||||
return true
|
||||
end
|
||||
known[value] = true
|
||||
for key, value in pairs(value) do
|
||||
if _is_circular(key) or _is_circular(value) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return _is_circular(table)
|
||||
end
|
||||
|
||||
--+ Simple table equality check. Stack overflow if tables are too deep or circular.
|
||||
@ -81,44 +81,44 @@ end
|
||||
--> Equality of noncircular tables if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_noncircular(table, other_table)
|
||||
local is_equal = table == other_table
|
||||
if is_equal or type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_table then
|
||||
return false
|
||||
end
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local value_2 = other_table[key]
|
||||
if not equals_noncircular(value, value_2) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found
|
||||
for table, value in pairs(table_keys) do
|
||||
if equals_noncircular(other_key, table) and equals_noncircular(other_value, value) then
|
||||
table_keys[table] = nil
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
local is_equal = table == other_table
|
||||
if is_equal or type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_table then
|
||||
return false
|
||||
end
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local value_2 = other_table[key]
|
||||
if not equals_noncircular(value, value_2) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found
|
||||
for table, value in pairs(table_keys) do
|
||||
if equals_noncircular(other_key, table) and equals_noncircular(other_value, value) then
|
||||
table_keys[table] = nil
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
equals = equals_noncircular
|
||||
@ -127,60 +127,60 @@ equals = equals_noncircular
|
||||
--> Table content equality if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_content(table, other_table)
|
||||
local equal_tables = {}
|
||||
local function _equals(table, other_equal_table)
|
||||
local function set_equal_tables(value)
|
||||
equal_tables[table] = equal_tables[table] or {}
|
||||
equal_tables[table][other_equal_table] = value
|
||||
return value
|
||||
end
|
||||
local is_equal = table == other_equal_table
|
||||
if is_equal or type(table) ~= "table" or type(other_equal_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_equal_table then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
local lookup_equal = (equal_tables[table] or {})[other_equal_table]
|
||||
if lookup_equal ~= nil then
|
||||
return lookup_equal
|
||||
end
|
||||
-- Premise
|
||||
set_equal_tables(true)
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local other_value = other_equal_table[key]
|
||||
if not _equals(value, other_value) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_equal_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found = false
|
||||
for table_key, value in pairs(table_keys) do
|
||||
if _equals(table_key, other_key) and _equals(value, other_value) then
|
||||
table_keys[table_key] = nil
|
||||
found = true
|
||||
-- Breaking is fine as per transitivity
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return _equals(table, other_table)
|
||||
local equal_tables = {}
|
||||
local function _equals(table, other_equal_table)
|
||||
local function set_equal_tables(value)
|
||||
equal_tables[table] = equal_tables[table] or {}
|
||||
equal_tables[table][other_equal_table] = value
|
||||
return value
|
||||
end
|
||||
local is_equal = table == other_equal_table
|
||||
if is_equal or type(table) ~= "table" or type(other_equal_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_equal_table then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
local lookup_equal = (equal_tables[table] or {})[other_equal_table]
|
||||
if lookup_equal ~= nil then
|
||||
return lookup_equal
|
||||
end
|
||||
-- Premise
|
||||
set_equal_tables(true)
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local other_value = other_equal_table[key]
|
||||
if not _equals(value, other_value) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_equal_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found = false
|
||||
for table_key, value in pairs(table_keys) do
|
||||
if _equals(table_key, other_key) and _equals(value, other_value) then
|
||||
table_keys[table_key] = nil
|
||||
found = true
|
||||
-- Breaking is fine as per transitivity
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return _equals(table, other_table)
|
||||
end
|
||||
|
||||
--+ Table equality check: content has to be equal, relations between tables as well
|
||||
@ -190,400 +190,400 @@ end
|
||||
--> equality (same tables after table reference substitution) of circular tables if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_references(table, other_table)
|
||||
local function _equals(table, other_table, equal_refs)
|
||||
if equal_refs[table] then
|
||||
return equal_refs[table] == other_table
|
||||
end
|
||||
local is_equal = table == other_table
|
||||
-- this check could be omitted if table key equality is being checked
|
||||
if type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if is_equal then
|
||||
equal_refs[table] = other_table
|
||||
return true
|
||||
end
|
||||
-- Premise: table = other table
|
||||
equal_refs[table] = other_table
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
local other_value = other_table[key]
|
||||
if not _equals(value, other_value, equal_refs) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
local other_table_keys = {}
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
other_table_keys[other_key] = other_value
|
||||
elseif table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function _next(current_key, equal_refs, available_keys)
|
||||
local key, value = next(table_keys, current_key)
|
||||
if key == nil then
|
||||
return true
|
||||
end
|
||||
for other_key, other_value in pairs(other_table_keys) do
|
||||
local copy_equal_refs = shallowcopy(equal_refs)
|
||||
if _equals(key, other_key, copy_equal_refs) and _equals(value, other_value, copy_equal_refs) then
|
||||
local copy_available_keys = shallowcopy(available_keys)
|
||||
copy_available_keys[other_key] = nil
|
||||
if _next(key, copy_equal_refs, copy_available_keys) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
return _next(nil, equal_refs, other_table_keys)
|
||||
end
|
||||
return _equals(table, other_table, {})
|
||||
local function _equals(table, other_table, equal_refs)
|
||||
if equal_refs[table] then
|
||||
return equal_refs[table] == other_table
|
||||
end
|
||||
local is_equal = table == other_table
|
||||
-- this check could be omitted if table key equality is being checked
|
||||
if type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if is_equal then
|
||||
equal_refs[table] = other_table
|
||||
return true
|
||||
end
|
||||
-- Premise: table = other table
|
||||
equal_refs[table] = other_table
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
local other_value = other_table[key]
|
||||
if not _equals(value, other_value, equal_refs) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
local other_table_keys = {}
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
other_table_keys[other_key] = other_value
|
||||
elseif table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function _next(current_key, equal_refs, available_keys)
|
||||
local key, value = next(table_keys, current_key)
|
||||
if key == nil then
|
||||
return true
|
||||
end
|
||||
for other_key, other_value in pairs(other_table_keys) do
|
||||
local copy_equal_refs = shallowcopy(equal_refs)
|
||||
if _equals(key, other_key, copy_equal_refs) and _equals(value, other_value, copy_equal_refs) then
|
||||
local copy_available_keys = shallowcopy(available_keys)
|
||||
copy_available_keys[other_key] = nil
|
||||
if _next(key, copy_equal_refs, copy_available_keys) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
return _next(nil, equal_refs, other_table_keys)
|
||||
end
|
||||
return _equals(table, other_table, {})
|
||||
end
|
||||
|
||||
function shallowcopy(table)
|
||||
local copy = {}
|
||||
for key, value in pairs(table) do
|
||||
copy[key] = value
|
||||
end
|
||||
return copy
|
||||
local copy = {}
|
||||
for key, value in pairs(table) do
|
||||
copy[key] = value
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
function deepcopy_noncircular(table)
|
||||
local function _copy(value)
|
||||
if type(value) == "table" then
|
||||
return deepcopy_noncircular(value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
local copy = {}
|
||||
for key, value in pairs(table) do
|
||||
copy[_copy(key)] = _copy(value)
|
||||
end
|
||||
return copy
|
||||
local function _copy(value)
|
||||
if type(value) == "table" then
|
||||
return deepcopy_noncircular(value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
local copy = {}
|
||||
for key, value in pairs(table) do
|
||||
copy[_copy(key)] = _copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
function deepcopy(table)
|
||||
local copies = {}
|
||||
local function _deepcopy(table)
|
||||
if copies[table] then
|
||||
return copies[table]
|
||||
end
|
||||
local copy = {}
|
||||
copies[table] = copy
|
||||
local function _copy(value)
|
||||
if type(value) == "table" then
|
||||
if copies[value] then
|
||||
return copies[value]
|
||||
end
|
||||
return _deepcopy(value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
for key, value in pairs(table) do
|
||||
copy[_copy(key)] = _copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
return _deepcopy(table)
|
||||
local copies = {}
|
||||
local function _deepcopy(table)
|
||||
if copies[table] then
|
||||
return copies[table]
|
||||
end
|
||||
local copy = {}
|
||||
copies[table] = copy
|
||||
local function _copy(value)
|
||||
if type(value) == "table" then
|
||||
if copies[value] then
|
||||
return copies[value]
|
||||
end
|
||||
return _deepcopy(value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
for key, value in pairs(table) do
|
||||
copy[_copy(key)] = _copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
return _deepcopy(table)
|
||||
end
|
||||
|
||||
tablecopy = deepcopy
|
||||
copy = deepcopy
|
||||
|
||||
function count(table)
|
||||
local count = 0
|
||||
for _ in pairs(table) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
local count = 0
|
||||
for _ in pairs(table) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function is_empty(table)
|
||||
return next(table) == nil
|
||||
return next(table) == nil
|
||||
end
|
||||
|
||||
function foreach(table, func)
|
||||
for k, v in pairs(table) do
|
||||
func(k, v)
|
||||
end
|
||||
for k, v in pairs(table) do
|
||||
func(k, v)
|
||||
end
|
||||
end
|
||||
|
||||
function foreach_value(table, func)
|
||||
for _, v in pairs(table) do
|
||||
func(v)
|
||||
end
|
||||
for _, v in pairs(table) do
|
||||
func(v)
|
||||
end
|
||||
end
|
||||
|
||||
function call(table, ...)
|
||||
for _, func in pairs(table) do
|
||||
func(...)
|
||||
end
|
||||
for _, func in pairs(table) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function icall(table, ...)
|
||||
for _, func in ipairs(table) do
|
||||
func(...)
|
||||
end
|
||||
for _, func in ipairs(table) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function foreach_key(table, func)
|
||||
for key, _ in pairs(table) do
|
||||
func(key)
|
||||
end
|
||||
for key, _ in pairs(table) do
|
||||
func(key)
|
||||
end
|
||||
end
|
||||
|
||||
function map(table, func)
|
||||
for key, value in pairs(table) do
|
||||
table[key] = func(value)
|
||||
end
|
||||
return table
|
||||
for key, value in pairs(table) do
|
||||
table[key] = func(value)
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function map_keys(table, func)
|
||||
local new_tab = {}
|
||||
for key, value in pairs(table) do
|
||||
new_tab[func(key)] = value
|
||||
end
|
||||
return new_tab
|
||||
local new_tab = {}
|
||||
for key, value in pairs(table) do
|
||||
new_tab[func(key)] = value
|
||||
end
|
||||
return new_tab
|
||||
end
|
||||
|
||||
function process(tab, func)
|
||||
local results = {}
|
||||
for key, value in pairs(tab) do
|
||||
table.insert(results, func(key,value))
|
||||
end
|
||||
return results
|
||||
local results = {}
|
||||
for key, value in pairs(tab) do
|
||||
table.insert(results, func(key,value))
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
function call(funcs, ...)
|
||||
for _, func in ipairs(funcs) do
|
||||
func(...)
|
||||
end
|
||||
for _, func in ipairs(funcs) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function find(list, value)
|
||||
for index, other_value in pairs(list) do
|
||||
if value == other_value then
|
||||
return index
|
||||
end
|
||||
end
|
||||
return
|
||||
for index, other_value in pairs(list) do
|
||||
if value == other_value then
|
||||
return index
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
contains = find
|
||||
|
||||
function to_add(table, after_additions)
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
end
|
||||
|
||||
difference = to_add
|
||||
|
||||
function deep_to_add(table, after_additions)
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
additions[key] = deep_to_add(table[key], value)
|
||||
elseif table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
additions[key] = deep_to_add(table[key], value)
|
||||
elseif table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
end
|
||||
|
||||
function add_all(table, additions)
|
||||
for key, value in pairs(additions) do
|
||||
table[key] = value
|
||||
end
|
||||
return table
|
||||
for key, value in pairs(additions) do
|
||||
table[key] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function deep_add_all(table, additions)
|
||||
for key, value in pairs(additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
deep_add_all(table[key], value)
|
||||
else
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
for key, value in pairs(additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
deep_add_all(table[key], value)
|
||||
else
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function complete(table, completions)
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function deepcomplete(table, completions)
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
elseif type(table[key]) == "table" and type(value) == "table" then
|
||||
deepcomplete(table[key], value)
|
||||
end
|
||||
end
|
||||
return table
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
elseif type(table[key]) == "table" and type(value) == "table" then
|
||||
deepcomplete(table[key], value)
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function merge_tables(table, other_table)
|
||||
return add_all(copy(table), other_table)
|
||||
return add_all(copy(table), other_table)
|
||||
end
|
||||
|
||||
union = merge_tables
|
||||
|
||||
function intersection(table, other_table)
|
||||
local result = {}
|
||||
for key, value in pairs(table) do
|
||||
if other_table[key] then
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
return result
|
||||
local result = {}
|
||||
for key, value in pairs(table) do
|
||||
if other_table[key] then
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function append(table, other_table)
|
||||
local length = #table
|
||||
for index, value in ipairs(other_table) do
|
||||
table[length + index] = value
|
||||
end
|
||||
return table
|
||||
local length = #table
|
||||
for index, value in ipairs(other_table) do
|
||||
table[length + index] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function keys(table)
|
||||
local keys = {}
|
||||
for key, _ in pairs(table) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
return keys
|
||||
local keys = {}
|
||||
for key, _ in pairs(table) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
function values(table)
|
||||
local values = {}
|
||||
for _, value in pairs(table) do
|
||||
values[#values + 1] = value
|
||||
end
|
||||
return values
|
||||
local values = {}
|
||||
for _, value in pairs(table) do
|
||||
values[#values + 1] = value
|
||||
end
|
||||
return values
|
||||
end
|
||||
|
||||
function flip(table)
|
||||
local flipped = {}
|
||||
for key, value in pairs(table) do
|
||||
flipped[value] = key
|
||||
end
|
||||
return flipped
|
||||
local flipped = {}
|
||||
for key, value in pairs(table) do
|
||||
flipped[value] = key
|
||||
end
|
||||
return flipped
|
||||
end
|
||||
|
||||
function set(table)
|
||||
local flipped = {}
|
||||
for _, value in pairs(table) do
|
||||
flipped[value] = true
|
||||
end
|
||||
return flipped
|
||||
local flipped = {}
|
||||
for _, value in pairs(table) do
|
||||
flipped[value] = true
|
||||
end
|
||||
return flipped
|
||||
end
|
||||
|
||||
function unique(table)
|
||||
return keys(set(table))
|
||||
return keys(set(table))
|
||||
end
|
||||
|
||||
function rpairs(table)
|
||||
local index = #table
|
||||
return function()
|
||||
if index >= 1 then
|
||||
local value = table[index]
|
||||
index = index - 1
|
||||
if value ~= nil then
|
||||
return index + 1, value
|
||||
end
|
||||
end
|
||||
end
|
||||
local index = #table
|
||||
return function()
|
||||
if index >= 1 then
|
||||
local value = table[index]
|
||||
index = index - 1
|
||||
if value ~= nil then
|
||||
return index + 1, value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function best_value(table, is_better_func)
|
||||
local best = next(table)
|
||||
if best == nil then
|
||||
return
|
||||
end
|
||||
local candidate = best
|
||||
while true do
|
||||
candidate = next(table, candidate)
|
||||
if candidate == nil then
|
||||
return best
|
||||
end
|
||||
if is_better_func(candidate, best) then
|
||||
best = candidate
|
||||
end
|
||||
end
|
||||
error()
|
||||
local best = next(table)
|
||||
if best == nil then
|
||||
return
|
||||
end
|
||||
local candidate = best
|
||||
while true do
|
||||
candidate = next(table, candidate)
|
||||
if candidate == nil then
|
||||
return best
|
||||
end
|
||||
if is_better_func(candidate, best) then
|
||||
best = candidate
|
||||
end
|
||||
end
|
||||
error()
|
||||
end
|
||||
|
||||
function min(table)
|
||||
return best_value(table, function(value, other_value) return value < other_value end)
|
||||
return best_value(table, function(value, other_value) return value < other_value end)
|
||||
end
|
||||
|
||||
function max(table)
|
||||
return best_value(table, function(value, other_value) return value > other_value end)
|
||||
return best_value(table, function(value, other_value) return value > other_value end)
|
||||
end
|
||||
|
||||
function default_comparator(value, other_value)
|
||||
if value == other_value then
|
||||
return 0
|
||||
end
|
||||
if value > other_value then
|
||||
return 1
|
||||
end
|
||||
return -1
|
||||
if value == other_value then
|
||||
return 0
|
||||
end
|
||||
if value > other_value then
|
||||
return 1
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
--> index if element found
|
||||
--> -index for insertion if not found
|
||||
function binary_search_comparator(comparator)
|
||||
return function(list, value)
|
||||
local min, max = 1, #list
|
||||
while min <= max do
|
||||
local pivot = min + math.floor((max - min) / 2)
|
||||
local element = list[pivot]
|
||||
local compared = comparator(value, element)
|
||||
if compared == 0 then
|
||||
return pivot
|
||||
elseif compared > 0 then
|
||||
min = pivot + 1
|
||||
else
|
||||
max = pivot - 1
|
||||
end
|
||||
end
|
||||
return -min
|
||||
end
|
||||
return function(list, value)
|
||||
local min, max = 1, #list
|
||||
while min <= max do
|
||||
local pivot = min + math.floor((max - min) / 2)
|
||||
local element = list[pivot]
|
||||
local compared = comparator(value, element)
|
||||
if compared == 0 then
|
||||
return pivot
|
||||
elseif compared > 0 then
|
||||
min = pivot + 1
|
||||
else
|
||||
max = pivot - 1
|
||||
end
|
||||
end
|
||||
return -min
|
||||
end
|
||||
end
|
||||
|
||||
binary_search = binary_search_comparator(default_comparator)
|
||||
|
||||
function reverse(table)
|
||||
local l = #table + 1
|
||||
for index = 1, math.floor(#table / 2) do
|
||||
table[l - index], table[index] = table[index], table[l - index]
|
||||
end
|
||||
return table
|
||||
local l = #table + 1
|
||||
for index = 1, math.floor(#table / 2) do
|
||||
table[l - index], table[index] = table[index], table[l - index]
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function repetition(value, count)
|
||||
local table = {}
|
||||
for index = 1, count do
|
||||
table[index] = value
|
||||
end
|
||||
return table
|
||||
local table = {}
|
||||
for index = 1, count do
|
||||
table[index] = value
|
||||
end
|
||||
return table
|
||||
end
|
422
test.lua
422
test.lua
@ -3,16 +3,16 @@
|
||||
local random, huge = math.random, math.huge
|
||||
local parent_env = getfenv(1)
|
||||
setfenv(1, setmetatable({}, {
|
||||
__index = function(_, key)
|
||||
local value = modlib[key]
|
||||
if value ~= nil then
|
||||
return value
|
||||
end
|
||||
return parent_env[key]
|
||||
end,
|
||||
__newindex = function(_, key, value)
|
||||
error(dump{key = key, value = value})
|
||||
end
|
||||
__index = function(_, key)
|
||||
local value = modlib[key]
|
||||
if value ~= nil then
|
||||
return value
|
||||
end
|
||||
return parent_env[key]
|
||||
end,
|
||||
__newindex = function(_, key, value)
|
||||
error(dump{key = key, value = value})
|
||||
end
|
||||
}))
|
||||
|
||||
-- string
|
||||
@ -20,104 +20,104 @@ assert(string.escape_magic_chars"%" == "%%")
|
||||
|
||||
-- table
|
||||
do
|
||||
local tab = {}
|
||||
tab[tab] = tab
|
||||
local table_copy = table.deepcopy(tab)
|
||||
assert(table_copy[table_copy] == table_copy)
|
||||
assert(table.is_circular(tab))
|
||||
assert(not table.is_circular{a = 1})
|
||||
assert(table.equals_noncircular({[{}]={}}, {[{}]={}}))
|
||||
assert(table.equals_content(tab, table_copy))
|
||||
local equals_references = table.equals_references
|
||||
assert(equals_references(tab, table_copy))
|
||||
assert(equals_references({}, {}))
|
||||
assert(not equals_references({a = 1, b = 2}, {a = 1, b = 3}))
|
||||
tab = {}
|
||||
tab.a, tab.b = tab, tab
|
||||
table_copy = table.deepcopy(tab)
|
||||
assert(equals_references(tab, table_copy))
|
||||
local x, y = {}, {}
|
||||
assert(not equals_references({[x] = x, [y] = y}, {[x] = y, [y] = x}))
|
||||
assert(equals_references({[x] = x, [y] = y}, {[x] = x, [y] = y}))
|
||||
local nilget = table.nilget
|
||||
assert(nilget({a = {b = {c = 42}}}, "a", "b", "c") == 42)
|
||||
assert(nilget({a = {}}, "a", "b", "c") == nil)
|
||||
assert(nilget(nil, "a", "b", "c") == nil)
|
||||
assert(nilget(nil, "a", nil, "c") == nil)
|
||||
local rope = table.rope{}
|
||||
rope:write"hello"
|
||||
rope:write" "
|
||||
rope:write"world"
|
||||
assert(rope:to_text() == "hello world", rope:to_text())
|
||||
local tab = {}
|
||||
tab[tab] = tab
|
||||
local table_copy = table.deepcopy(tab)
|
||||
assert(table_copy[table_copy] == table_copy)
|
||||
assert(table.is_circular(tab))
|
||||
assert(not table.is_circular{a = 1})
|
||||
assert(table.equals_noncircular({[{}]={}}, {[{}]={}}))
|
||||
assert(table.equals_content(tab, table_copy))
|
||||
local equals_references = table.equals_references
|
||||
assert(equals_references(tab, table_copy))
|
||||
assert(equals_references({}, {}))
|
||||
assert(not equals_references({a = 1, b = 2}, {a = 1, b = 3}))
|
||||
tab = {}
|
||||
tab.a, tab.b = tab, tab
|
||||
table_copy = table.deepcopy(tab)
|
||||
assert(equals_references(tab, table_copy))
|
||||
local x, y = {}, {}
|
||||
assert(not equals_references({[x] = x, [y] = y}, {[x] = y, [y] = x}))
|
||||
assert(equals_references({[x] = x, [y] = y}, {[x] = x, [y] = y}))
|
||||
local nilget = table.nilget
|
||||
assert(nilget({a = {b = {c = 42}}}, "a", "b", "c") == 42)
|
||||
assert(nilget({a = {}}, "a", "b", "c") == nil)
|
||||
assert(nilget(nil, "a", "b", "c") == nil)
|
||||
assert(nilget(nil, "a", nil, "c") == nil)
|
||||
local rope = table.rope{}
|
||||
rope:write"hello"
|
||||
rope:write" "
|
||||
rope:write"world"
|
||||
assert(rope:to_text() == "hello world", rope:to_text())
|
||||
end
|
||||
|
||||
-- heap
|
||||
do
|
||||
local n = 100
|
||||
local list = {}
|
||||
for index = 1, n do
|
||||
list[index] = index
|
||||
end
|
||||
table.shuffle(list)
|
||||
local heap = heap.new()
|
||||
for index = 1, #list do
|
||||
heap:push(list[index])
|
||||
end
|
||||
for index = 1, #list do
|
||||
local popped = heap:pop()
|
||||
assert(popped == index)
|
||||
end
|
||||
local n = 100
|
||||
local list = {}
|
||||
for index = 1, n do
|
||||
list[index] = index
|
||||
end
|
||||
table.shuffle(list)
|
||||
local heap = heap.new()
|
||||
for index = 1, #list do
|
||||
heap:push(list[index])
|
||||
end
|
||||
for index = 1, #list do
|
||||
local popped = heap:pop()
|
||||
assert(popped == index)
|
||||
end
|
||||
end
|
||||
|
||||
-- ranked set
|
||||
do
|
||||
local n = 100
|
||||
local ranked_set = ranked_set.new()
|
||||
local list = {}
|
||||
for i = 1, n do
|
||||
ranked_set:insert(i)
|
||||
list[i] = i
|
||||
end
|
||||
local n = 100
|
||||
local ranked_set = ranked_set.new()
|
||||
local list = {}
|
||||
for i = 1, n do
|
||||
ranked_set:insert(i)
|
||||
list[i] = i
|
||||
end
|
||||
|
||||
assert(table.equals(ranked_set:to_table(), list))
|
||||
assert(table.equals(ranked_set:to_table(), list))
|
||||
|
||||
local i = 0
|
||||
for rank, key in ranked_set:ipairs() do
|
||||
i = i + 1
|
||||
assert(i == key and i == rank)
|
||||
assert(ranked_set:get_by_rank(rank) == key)
|
||||
local rank, key = ranked_set:get(i)
|
||||
assert(key == i and i == rank)
|
||||
end
|
||||
assert(i == n)
|
||||
local i = 0
|
||||
for rank, key in ranked_set:ipairs() do
|
||||
i = i + 1
|
||||
assert(i == key and i == rank)
|
||||
assert(ranked_set:get_by_rank(rank) == key)
|
||||
local rank, key = ranked_set:get(i)
|
||||
assert(key == i and i == rank)
|
||||
end
|
||||
assert(i == n)
|
||||
|
||||
for i = 1, n do
|
||||
local _, v = ranked_set:delete(i)
|
||||
assert(v == i, i)
|
||||
end
|
||||
assert(not next(ranked_set:to_table()))
|
||||
for i = 1, n do
|
||||
local _, v = ranked_set:delete(i)
|
||||
assert(v == i, i)
|
||||
end
|
||||
assert(not next(ranked_set:to_table()))
|
||||
|
||||
local ranked_set = ranked_set.new()
|
||||
for i = 1, n do
|
||||
ranked_set:insert(i)
|
||||
end
|
||||
local ranked_set = ranked_set.new()
|
||||
for i = 1, n do
|
||||
ranked_set:insert(i)
|
||||
end
|
||||
|
||||
for rank, key in ranked_set:ipairs(10, 20) do
|
||||
assert(rank == key and key >= 10 and key <= 20)
|
||||
end
|
||||
for rank, key in ranked_set:ipairs(10, 20) do
|
||||
assert(rank == key and key >= 10 and key <= 20)
|
||||
end
|
||||
|
||||
for i = n, 1, -1 do
|
||||
local j = ranked_set:delete_by_rank(i)
|
||||
assert(j == i)
|
||||
end
|
||||
for i = n, 1, -1 do
|
||||
local j = ranked_set:delete_by_rank(i)
|
||||
assert(j == i)
|
||||
end
|
||||
end
|
||||
|
||||
-- colorspec
|
||||
local colorspec = minetest.colorspec
|
||||
local function test_from_string(string, number)
|
||||
local spec = colorspec.from_string(string)
|
||||
local expected = colorspec.from_number(number)
|
||||
assertdump(table.equals(spec, expected), {expected = expected, actual = spec})
|
||||
local spec = colorspec.from_string(string)
|
||||
local expected = colorspec.from_number(number)
|
||||
assertdump(table.equals(spec, expected), {expected = expected, actual = spec})
|
||||
end
|
||||
local spec = colorspec.from_number(0xDDCCBBAA)
|
||||
assertdump(table.equals(spec, {a = 0xAA, b = 0xBB, g = 0xCC, r = 0xDD}), spec)
|
||||
@ -130,147 +130,147 @@ test_from_string("#11223344", 0x11223344)
|
||||
-- k-d-tree
|
||||
local vectors = {}
|
||||
for _ = 1, 1000 do
|
||||
_G.table.insert(vectors, {random(), random(), random()})
|
||||
_G.table.insert(vectors, {random(), random(), random()})
|
||||
end
|
||||
local kdtree = kdtree.new(vectors)
|
||||
for _, v in ipairs(vectors) do
|
||||
local neighbor, distance = kdtree:get_nearest_neighbor(v)
|
||||
assert(vector.equals(v, neighbor), distance == 0)
|
||||
local neighbor, distance = kdtree:get_nearest_neighbor(v)
|
||||
assert(vector.equals(v, neighbor), distance == 0)
|
||||
end
|
||||
|
||||
for _ = 1, 1000 do
|
||||
local v = {random(), random(), random()}
|
||||
local _, distance = kdtree:get_nearest_neighbor(v)
|
||||
local min_distance = huge
|
||||
for _, w in ipairs(vectors) do
|
||||
local other_distance = vector.distance(v, w)
|
||||
if other_distance < min_distance then
|
||||
min_distance = other_distance
|
||||
end
|
||||
end
|
||||
assert(distance == min_distance)
|
||||
local v = {random(), random(), random()}
|
||||
local _, distance = kdtree:get_nearest_neighbor(v)
|
||||
local min_distance = huge
|
||||
for _, w in ipairs(vectors) do
|
||||
local other_distance = vector.distance(v, w)
|
||||
if other_distance < min_distance then
|
||||
min_distance = other_distance
|
||||
end
|
||||
end
|
||||
assert(distance == min_distance)
|
||||
end
|
||||
|
||||
-- bluon
|
||||
do
|
||||
local bluon = bluon
|
||||
local function assert_preserves(object)
|
||||
local rope = table.rope{}
|
||||
local written, read, input
|
||||
local _, err = pcall(function()
|
||||
bluon:write(object, rope)
|
||||
written = rope:to_text()
|
||||
input = text.inputstream(written)
|
||||
read = bluon:read(input)
|
||||
local remaining = input:read(1000)
|
||||
assert(not remaining)
|
||||
end)
|
||||
assertdump(table.equals_references(object, read) and not err, {
|
||||
object = object,
|
||||
read = read,
|
||||
written = written and text.hexdump(written),
|
||||
err = err
|
||||
})
|
||||
end
|
||||
for _, constant in pairs{true, false, huge, -huge} do
|
||||
assert_preserves(constant)
|
||||
end
|
||||
for i = 1, 1000 do
|
||||
assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i)))
|
||||
end
|
||||
for _ = 1, 1000 do
|
||||
local int = random(-2^50, 2^50)
|
||||
assert(int % 1 == 0)
|
||||
assert_preserves(int)
|
||||
assert_preserves((random() - 0.5) * 2^random(-20, 20))
|
||||
end
|
||||
assert_preserves{hello = "world", welt = "hallo"}
|
||||
assert_preserves{"hello", "hello", "hello"}
|
||||
local a = {}
|
||||
a[a] = a
|
||||
a[1] = a
|
||||
assert_preserves(a)
|
||||
local bluon = bluon
|
||||
local function assert_preserves(object)
|
||||
local rope = table.rope{}
|
||||
local written, read, input
|
||||
local _, err = pcall(function()
|
||||
bluon:write(object, rope)
|
||||
written = rope:to_text()
|
||||
input = text.inputstream(written)
|
||||
read = bluon:read(input)
|
||||
local remaining = input:read(1000)
|
||||
assert(not remaining)
|
||||
end)
|
||||
assertdump(table.equals_references(object, read) and not err, {
|
||||
object = object,
|
||||
read = read,
|
||||
written = written and text.hexdump(written),
|
||||
err = err
|
||||
})
|
||||
end
|
||||
for _, constant in pairs{true, false, huge, -huge} do
|
||||
assert_preserves(constant)
|
||||
end
|
||||
for i = 1, 1000 do
|
||||
assert_preserves(_G.table.concat(table.repetition(_G.string.char(i % 256), i)))
|
||||
end
|
||||
for _ = 1, 1000 do
|
||||
local int = random(-2^50, 2^50)
|
||||
assert(int % 1 == 0)
|
||||
assert_preserves(int)
|
||||
assert_preserves((random() - 0.5) * 2^random(-20, 20))
|
||||
end
|
||||
assert_preserves{hello = "world", welt = "hallo"}
|
||||
assert_preserves{"hello", "hello", "hello"}
|
||||
local a = {}
|
||||
a[a] = a
|
||||
a[1] = a
|
||||
assert_preserves(a)
|
||||
end
|
||||
|
||||
-- in-game tests & b3d testing
|
||||
local tests = {
|
||||
-- depends on player_api
|
||||
b3d = false,
|
||||
liquid_dir = false,
|
||||
liquid_raycast = false
|
||||
-- depends on player_api
|
||||
b3d = false,
|
||||
liquid_dir = false,
|
||||
liquid_raycast = false
|
||||
}
|
||||
if tests.b3d then
|
||||
local stream = io.open(mod.get_resource("player_api", "models", "character.b3d"), "r")
|
||||
assert(stream)
|
||||
local b3d = b3d.read(stream)
|
||||
--! dirty helper method to create truncate tables with 10+ number keys
|
||||
local function _b3d_truncate(table)
|
||||
local count = 1
|
||||
for key, value in pairs(table) do
|
||||
if type(key) == "table" then
|
||||
_b3d_truncate(key)
|
||||
end
|
||||
if type(value) == "table" then
|
||||
_b3d_truncate(value)
|
||||
end
|
||||
count = count + 1
|
||||
if type(key) == "number" and count >= 9 and next(table, key) then
|
||||
if count == 9 then
|
||||
table[key] = "TRUNCATED"
|
||||
else
|
||||
table[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(b3d))))
|
||||
stream:close()
|
||||
local stream = io.open(mod.get_resource("player_api", "models", "character.b3d"), "r")
|
||||
assert(stream)
|
||||
local b3d = b3d.read(stream)
|
||||
--! dirty helper method to create truncate tables with 10+ number keys
|
||||
local function _b3d_truncate(table)
|
||||
local count = 1
|
||||
for key, value in pairs(table) do
|
||||
if type(key) == "table" then
|
||||
_b3d_truncate(key)
|
||||
end
|
||||
if type(value) == "table" then
|
||||
_b3d_truncate(value)
|
||||
end
|
||||
count = count + 1
|
||||
if type(key) == "number" and count >= 9 and next(table, key) then
|
||||
if count == 9 then
|
||||
table[key] = "TRUNCATED"
|
||||
else
|
||||
table[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(b3d))))
|
||||
stream:close()
|
||||
end
|
||||
local vector, minetest, ml_mt = _G.vector, _G.minetest, minetest
|
||||
if tests.liquid_dir then
|
||||
minetest.register_abm{
|
||||
label = "get_liquid_corner_levels & get_liquid_direction test",
|
||||
nodenames = {"group:liquid"},
|
||||
interval = 1,
|
||||
chance = 1,
|
||||
action = function(pos, node)
|
||||
assert(type(node) == "table")
|
||||
for _, corner_level in pairs(ml_mt.get_liquid_corner_levels(pos, node)) do
|
||||
minetest.add_particle{
|
||||
pos = vector.add(pos, corner_level),
|
||||
size = 2,
|
||||
texture = "logo.png"
|
||||
}
|
||||
end
|
||||
local direction = ml_mt.get_liquid_flow_direction(pos, node)
|
||||
local start_pos = pos
|
||||
start_pos.y = start_pos.y + 1
|
||||
for i = 0, 5 do
|
||||
minetest.add_particle{
|
||||
pos = vector.add(start_pos, vector.multiply(direction, i/5)),
|
||||
size = i/2.5,
|
||||
texture = "logo.png"
|
||||
}
|
||||
end
|
||||
end
|
||||
}
|
||||
minetest.register_abm{
|
||||
label = "get_liquid_corner_levels & get_liquid_direction test",
|
||||
nodenames = {"group:liquid"},
|
||||
interval = 1,
|
||||
chance = 1,
|
||||
action = function(pos, node)
|
||||
assert(type(node) == "table")
|
||||
for _, corner_level in pairs(ml_mt.get_liquid_corner_levels(pos, node)) do
|
||||
minetest.add_particle{
|
||||
pos = vector.add(pos, corner_level),
|
||||
size = 2,
|
||||
texture = "logo.png"
|
||||
}
|
||||
end
|
||||
local direction = ml_mt.get_liquid_flow_direction(pos, node)
|
||||
local start_pos = pos
|
||||
start_pos.y = start_pos.y + 1
|
||||
for i = 0, 5 do
|
||||
minetest.add_particle{
|
||||
pos = vector.add(start_pos, vector.multiply(direction, i/5)),
|
||||
size = i/2.5,
|
||||
texture = "logo.png"
|
||||
}
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
if tests.liquid_raycast then
|
||||
minetest.register_globalstep(function()
|
||||
for _, player in pairs(minetest.get_connected_players()) do
|
||||
local eye_pos = vector.offset(player:get_pos(), 0, player:get_properties().eye_height, 0)
|
||||
local raycast = ml_mt.raycast(eye_pos, vector.add(eye_pos, vector.multiply(player:get_look_dir(), 3)), false, true)
|
||||
for pointed_thing in raycast do
|
||||
if pointed_thing.type == "node" and minetest.registered_nodes[minetest.get_node(pointed_thing.under).name].liquidtype == "flowing" then
|
||||
minetest.add_particle{
|
||||
pos = vector.add(pointed_thing.intersection_point, vector.multiply(pointed_thing.intersection_normal, 0.1)),
|
||||
size = 0.5,
|
||||
texture = "object_marker_red.png",
|
||||
expirationtime = 3
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
minetest.register_globalstep(function()
|
||||
for _, player in pairs(minetest.get_connected_players()) do
|
||||
local eye_pos = vector.offset(player:get_pos(), 0, player:get_properties().eye_height, 0)
|
||||
local raycast = ml_mt.raycast(eye_pos, vector.add(eye_pos, vector.multiply(player:get_look_dir(), 3)), false, true)
|
||||
for pointed_thing in raycast do
|
||||
if pointed_thing.type == "node" and minetest.registered_nodes[minetest.get_node(pointed_thing.under).name].liquidtype == "flowing" then
|
||||
minetest.add_particle{
|
||||
pos = vector.add(pointed_thing.intersection_point, vector.multiply(pointed_thing.intersection_normal, 0.1)),
|
||||
size = 0.5,
|
||||
texture = "object_marker_red.png",
|
||||
expirationtime = 3
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
224
vector.lua
224
vector.lua
@ -2,106 +2,106 @@ local mt_vector = vector
|
||||
local vector = getfenv(1)
|
||||
|
||||
index_aliases = {
|
||||
x = 1,
|
||||
y = 2,
|
||||
z = 3,
|
||||
w = 4
|
||||
x = 1,
|
||||
y = 2,
|
||||
z = 3,
|
||||
w = 4
|
||||
}
|
||||
|
||||
modlib.table.add_all(index_aliases, modlib.table.flip(index_aliases))
|
||||
|
||||
metatable = {
|
||||
__index = function(table, key)
|
||||
local index = index_aliases[key]
|
||||
if index ~= nil then
|
||||
return table[index]
|
||||
end
|
||||
return vector[key]
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
local index = letters[key]
|
||||
if index ~= nil then
|
||||
return rawset(table, index, value)
|
||||
end
|
||||
end
|
||||
__index = function(table, key)
|
||||
local index = index_aliases[key]
|
||||
if index ~= nil then
|
||||
return table[index]
|
||||
end
|
||||
return vector[key]
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
local index = letters[key]
|
||||
if index ~= nil then
|
||||
return rawset(table, index, value)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function new(v)
|
||||
return setmetatable(v, metatable)
|
||||
return setmetatable(v, metatable)
|
||||
end
|
||||
|
||||
function from_xyzw(v)
|
||||
return new{v.x, v.y, v.z, v.w}
|
||||
return new{v.x, v.y, v.z, v.w}
|
||||
end
|
||||
|
||||
function from_minetest(v)
|
||||
return new{v.x, v.y, v.z}
|
||||
return new{v.x, v.y, v.z}
|
||||
end
|
||||
|
||||
function to_xyzw(v)
|
||||
return {x = v[1], y = v[2], z = v[3], w = v[4]}
|
||||
return {x = v[1], y = v[2], z = v[3], w = v[4]}
|
||||
end
|
||||
|
||||
--+ not necessarily required, as Minetest respects the metatable
|
||||
function to_minetest(v)
|
||||
return mt_vector.new(unpack(v))
|
||||
return mt_vector.new(unpack(v))
|
||||
end
|
||||
|
||||
function equals(v, w)
|
||||
for k, v in pairs(v) do
|
||||
if v ~= w[k] then return false end
|
||||
end
|
||||
return true
|
||||
for k, v in pairs(v) do
|
||||
if v ~= w[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
metatable.__eq = equals
|
||||
|
||||
function less_than(v, w)
|
||||
for k, v in pairs(v) do
|
||||
if v >= w[k] then return false end
|
||||
end
|
||||
return true
|
||||
for k, v in pairs(v) do
|
||||
if v >= w[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
metatable.__lt = less_than
|
||||
|
||||
function less_or_equal(v, w)
|
||||
for k, v in pairs(v) do
|
||||
if v > w[k] then return false end
|
||||
end
|
||||
return true
|
||||
for k, v in pairs(v) do
|
||||
if v > w[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
metatable.__le = less_or_equal
|
||||
|
||||
function combine(v, w, f)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, w[key])
|
||||
end
|
||||
return new(new_vector)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, w[key])
|
||||
end
|
||||
return new(new_vector)
|
||||
end
|
||||
|
||||
function apply(v, f, ...)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, ...)
|
||||
end
|
||||
return new(new_vector)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, ...)
|
||||
end
|
||||
return new(new_vector)
|
||||
end
|
||||
|
||||
function combinator(f)
|
||||
return function(v, w)
|
||||
return combine(v, w, f)
|
||||
end, function(v, ...)
|
||||
return apply(v, f, ...)
|
||||
end
|
||||
return function(v, w)
|
||||
return combine(v, w, f)
|
||||
end, function(v, ...)
|
||||
return apply(v, f, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function invert(v)
|
||||
for key, value in pairs(v) do
|
||||
v[key] = -value
|
||||
end
|
||||
for key, value in pairs(v) do
|
||||
v[key] = -value
|
||||
end
|
||||
end
|
||||
|
||||
add, add_scalar = combinator(function(v, w) return v + w end)
|
||||
@ -119,107 +119,107 @@ metatable.__div = divide
|
||||
--+ linear interpolation
|
||||
--: ratio number from 0 (all the first vector) to 1 (all the second vector)
|
||||
function interpolate(v, w, ratio)
|
||||
return add(multiply(v, 1 - ratio), multiply(w, ratio))
|
||||
return add(multiply(v, 1 - ratio), multiply(w, ratio))
|
||||
end
|
||||
|
||||
function norm(v)
|
||||
local sum = 0
|
||||
for _, value in pairs(v) do
|
||||
sum = sum + value ^ 2
|
||||
end
|
||||
return sum
|
||||
local sum = 0
|
||||
for _, value in pairs(v) do
|
||||
sum = sum + value ^ 2
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
function length(v)
|
||||
return math.sqrt(norm(v))
|
||||
return math.sqrt(norm(v))
|
||||
end
|
||||
|
||||
-- Minor code duplication for the sake of performance
|
||||
function distance(v, w)
|
||||
local sum = 0
|
||||
for key, value in pairs(v) do
|
||||
sum = sum + (value - w[key]) ^ 2
|
||||
end
|
||||
return math.sqrt(sum)
|
||||
local sum = 0
|
||||
for key, value in pairs(v) do
|
||||
sum = sum + (value - w[key]) ^ 2
|
||||
end
|
||||
return math.sqrt(sum)
|
||||
end
|
||||
|
||||
function normalize(v)
|
||||
return divide_scalar(v, length(v))
|
||||
return divide_scalar(v, length(v))
|
||||
end
|
||||
|
||||
function floor(v)
|
||||
return apply(v, math.floor)
|
||||
return apply(v, math.floor)
|
||||
end
|
||||
|
||||
function ceil(v)
|
||||
return apply(v, math.ceil)
|
||||
return apply(v, math.ceil)
|
||||
end
|
||||
|
||||
function clamp(v, min, max)
|
||||
return apply(apply(v, math.max, min), math.min, max)
|
||||
return apply(apply(v, math.max, min), math.min, max)
|
||||
end
|
||||
|
||||
function cross3(v, w)
|
||||
assert(#v == 3 and #w == 3)
|
||||
return new{
|
||||
v[2] * w[3] - v[3] * w[2],
|
||||
v[3] * w[1] - v[1] * w[3],
|
||||
v[1] * w[2] - v[2] * w[1]
|
||||
}
|
||||
assert(#v == 3 and #w == 3)
|
||||
return new{
|
||||
v[2] * w[3] - v[3] * w[2],
|
||||
v[3] * w[1] - v[1] * w[3],
|
||||
v[1] * w[2] - v[2] * w[1]
|
||||
}
|
||||
end
|
||||
|
||||
function dot(v, w)
|
||||
local sum = 0
|
||||
for i, c in pairs(v) do
|
||||
sum = sum + c * w[i]
|
||||
end
|
||||
return sum
|
||||
local sum = 0
|
||||
for i, c in pairs(v) do
|
||||
sum = sum + c * w[i]
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
--+ Angle between two vectors
|
||||
--> Signed angle in radians
|
||||
function angle(v, w)
|
||||
-- Based on dot(v, w) = |v| * |w| * cos(x)
|
||||
return math.acos(dot(v, w) / length(v) / length(w))
|
||||
-- Based on dot(v, w) = |v| * |w| * cos(x)
|
||||
return math.acos(dot(v, w) / length(v) / length(w))
|
||||
end
|
||||
|
||||
function box_box_collision(diff, box, other_box)
|
||||
for index, diff in pairs(diff) do
|
||||
if box[index] + diff > other_box[index + 3] or other_box[index] > box[index + 3] + diff then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
for index, diff in pairs(diff) do
|
||||
if box[index] + diff > other_box[index + 3] or other_box[index] > box[index + 3] + diff then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--+ Möller-Trumbore
|
||||
function ray_triangle_intersection(origin, direction, triangle)
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
local h = cross3(direction, edge_2)
|
||||
local a = dot(edge_1, h)
|
||||
if math.abs(a) < 1e-9 then
|
||||
return
|
||||
end
|
||||
local f = 1 / a
|
||||
local diff = subtract(origin, point_1)
|
||||
local u = f * dot(diff, h)
|
||||
if u < 0 or u > 1 then
|
||||
return
|
||||
end
|
||||
local q = cross3(diff, edge_1)
|
||||
local v = f * dot(direction, q)
|
||||
if v < 0 or u + v > 1 then
|
||||
return
|
||||
end
|
||||
local pos_on_line = f * dot(edge_2, q)
|
||||
if pos_on_line >= 0 then
|
||||
return pos_on_line
|
||||
end
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
local h = cross3(direction, edge_2)
|
||||
local a = dot(edge_1, h)
|
||||
if math.abs(a) < 1e-9 then
|
||||
return
|
||||
end
|
||||
local f = 1 / a
|
||||
local diff = subtract(origin, point_1)
|
||||
local u = f * dot(diff, h)
|
||||
if u < 0 or u > 1 then
|
||||
return
|
||||
end
|
||||
local q = cross3(diff, edge_1)
|
||||
local v = f * dot(direction, q)
|
||||
if v < 0 or u + v > 1 then
|
||||
return
|
||||
end
|
||||
local pos_on_line = f * dot(edge_2, q)
|
||||
if pos_on_line >= 0 then
|
||||
return pos_on_line
|
||||
end
|
||||
end
|
||||
|
||||
function triangle_normal(triangle)
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
return normalize(cross3(edge_1, edge_2))
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
return normalize(cross3(edge_1, edge_2))
|
||||
end
|
Loading…
Reference in New Issue
Block a user