Körperbewegung

This commit is contained in:
N-Nachtigal 2025-05-13 23:14:13 +02:00
parent b16b24e4f7
commit 95945c0306
78 changed files with 12503 additions and 0 deletions

7
mods/modlib/License.txt Normal file
View file

@ -0,0 +1,7 @@
Copyright 2019 - 2021 Lars Mueller alias LMD or appguru(eu)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

36
mods/modlib/Readme.md Normal file
View file

@ -0,0 +1,36 @@
# ![Logo](logo.svg) Modding Library (`modlib`)
Multipurpose Minetest Modding Library
## At a glance
No dependencies. Licensed under the MIT License. Written by Lars Mueller aka LMD or appguru(eu). Requires Lua 5.1 / LuaJIT.
### Acknowledgement
* [luk3yx](https://github.com/luk3yx): Various suggestions, bug reports and fixes
* [grorp](https://github.com/grorp) (Gregor Parzefall): [Bug reports & proposed fixes for node box code](https://github.com/appgurueu/modlib/pull/8)
* [NobWow](https://github.com/NobWow/): [Another bugfix](https://github.com/appgurueu/modlib/pull/7)
### Principles
* Game-agnostic: Modlib aims to provide nothing game-specific;
* Minimal invasiveness: Modlib should not disrupt other mods;
even at the expense of syntactic sugar, changes to the global
environment - apart from the addition of the modlib scope - are forbidden
* Architecture: Modlib is organized hierarchically
* Performance: Modlib tries to not compromise performance for convenience; modlib loads lazily
## Tests
The tests are located in a different repo, [`modlib_test`](https://github.com/appgurueu/modlib_test), as they are quite heavy due to testing the PNG reader using PngSuite. Reading the tests for examples of API usage is recommended.
## API
(Incomplete) documentation resides in the `doc` folder; you'll have to dive into the code for everything else.
The mod namespace is `modlib`, containing all modules which in turn contain variables & functions.
Modules are lazily loaded by indexing the `modlib` table. Do `_ = modlib.<module>` to avoid file load spikes at run time.
Localizing modules (`local <module> = modlib.<module>`) is recommended.

1137
mods/modlib/b3d.lua Normal file

File diff suppressed because it is too large Load diff

132
mods/modlib/base64.lua Normal file
View file

@ -0,0 +1,132 @@
local assert, floor, char, insert, concat = assert, math.floor, string.char, table.insert, table.concat
local base64 = {}
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
--! This is currently 5 - 10x slower than a C(++) implementation like Minetest's `minetest.encode_base64`
function base64.encode(
str, -- byte string to encode
padding -- whether to add padding, defaults to `true`
)
local res = {}
for i = 1, #str - 2, 3 do
-- Convert 3 bytes to 4 sextets
local b1, b2, b3 = str:byte(i, i + 2)
insert(res, char(
alphabet:byte(floor(b1 / 4) + 1), -- high 6 bits of first byte
alphabet:byte(16 * (b1 % 4) + floor(b2 / 16) + 1), -- low 2 bits of first byte & high 4 bits of second byte
alphabet:byte(4 * (b2 % 16) + floor(b3 / 64) + 1), -- low 4 bits of second byte & high 2 bits of third byte
alphabet:byte((b3 % 64) + 1) -- low 6 bits of third byte
))
end
-- Handle remaining 1 or 2 bytes:
-- Treat "missing" bytes to a multiple of 3 as "0" bytes, add appropriate padding.
local bytes_left = #str % 3
if bytes_left == 1 then
local b = str:byte(#str) -- b2 and b3 are missing ("= 0")
insert(res, char(
alphabet:byte(floor(b / 4) + 1),
alphabet:byte(16 * (b % 4) + 1)
))
-- Last two sextets depend only on missing bytes => padding
if padding ~= false then
insert(res, "==")
end
elseif bytes_left == 2 then
local b1, b2 = str:byte(#str - 1, #str) -- b3 is missing ("= 0")
insert(res, char(
alphabet:byte(floor(b1 / 4) + 1),
alphabet:byte(16 * (b1 % 4) + floor(b2 / 16) + 1),
alphabet:byte(4 * (b2 % 16) + 1)
))
-- Last sextet depends only on missing byte => padding
if padding ~= false then
insert(res, "=")
end
end
return concat(res)
end
-- Build reverse lookup table
local values = {}
for i = 1, #alphabet do
values[alphabet:byte(i)] = i - 1
end
local function decode_sextets_2(b1, b2)
local v1, v2 = values[b1], values[b2]
assert(v1 and v2)
assert(v2 % 16 == 0) -- 4 low bits from second sextet must be 0
return char(4 * v1 + floor(v2 / 16)) -- first sextet + 2 high bits from second sextet
end
local function decode_sextets_3(b1, b2, b3)
local v1, v2, v3 = values[b1], values[b2], values[b3]
assert(v1 and v2 and v3)
assert(v3 % 4 == 0) -- 2 low bits from third sextet must be 0
return char(
4 * v1 + floor(v2 / 16), -- first sextet + 2 high bits from second sextet
16 * (v2 % 16) + floor(v3 / 4) -- 4 low bits from second sextet + 4 high bits from third sextet
)
end
local function decode_sextets_4(b1, b2, b3, b4)
local v1, v2, v3, v4 = values[b1], values[b2], values[b3], values[b4]
assert(v1 and v2 and v3 and v4)
return char(
4 * v1 + floor(v2 / 16), -- first sextet + 2 high bits from second sextet
16 * (v2 % 16) + floor(v3 / 4), -- 4 low bits from second sextet + 4 high bits from third sextet
64 * (v3 % 4) + v4 -- 2 low bits from third sextet + fourth sextet
)
end
--! This is also about 10x slower than a C(++) implementation like Minetest's `minetest.decode_base64`
function base64.decode(
-- base64-encoded string to decode
str,
-- Whether to expect padding:
-- * `nil` (default) - may (or may not) be padded,
-- * `false` - must not be padded,
-- * `true` - must be padded
padding
)
-- Handle the empty string - the below code expects a nonempty string
if str == "" then return "" end
local res = {}
-- Note: the last (up to) 4 sextets are deliberately excluded, since they may contain padding
for i = 1, #str - 4, 4 do
-- Convert 4 sextets to 3 bytes
insert(res, decode_sextets_4(str:byte(i, i + 3)))
end
local sextets_left = #str % 4
if sextets_left == 0 then -- possibly padded
-- Convert 4 sextets to 3 bytes, taking padding into account
local b3, b4 = str:byte(#str - 1, #str)
if b3 == ("="):byte() then
assert(b4 == ("="):byte())
assert(padding ~= false, "got padding")
insert(res, decode_sextets_2(str:byte(#str - 3, #str - 2)))
elseif b4 == ("="):byte() then
assert(padding ~= false, "got padding")
insert(res, decode_sextets_3(str:byte(#str - 3, #str - 1)))
else -- no padding necessary
assert(#str >= 4)
assert(#({str:byte(#str - 3, #str)}) == 4)
insert(res, decode_sextets_4(str:byte(#str - 3, #str)))
end
else -- no padding and length not divisible by 4
assert(padding ~= true, "missing/invalid padding")
assert(sextets_left ~= 1)
if sextets_left == 2 then
insert(res, decode_sextets_2(str:byte(#str - 1, #str)))
elseif sextets_left == 3 then
insert(res, decode_sextets_3(str:byte(#str - 2, #str)))
end
end
return concat(res)
end
return base64

236
mods/modlib/binary.lua Normal file
View file

@ -0,0 +1,236 @@
-- Localize globals
local assert, math_huge, math_frexp, math_floor
= assert, math.huge, math.frexp, math.floor
local positive_nan, negative_nan = modlib.math.positive_nan, modlib.math.negative_nan
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
-- All little endian
--+ Reads an IEEE 754 single-precision floating point number (f32)
function read_single(read_byte)
-- First read the mantissa
local mantissa = read_byte() / 0x100
mantissa = (mantissa + read_byte()) / 0x100
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
local exponent_byte = read_byte()
local sign_byte = read_byte()
local sign = 1
if sign_byte >= 0x80 then
sign = -1
sign_byte = sign_byte - 0x80
end
local exponent = sign_byte * 2
if exponent_byte >= 0x80 then
exponent = exponent + 1
exponent_byte = exponent_byte - 0x80
end
mantissa = (mantissa + exponent_byte) / 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
return sign == 1 and positive_nan or negative_nan
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 an IEEE 754 double-precision floating point number (f64)
function read_double(read_byte)
-- First read the mantissa
local mantissa = 0
for _ = 1, 6 do
mantissa = (mantissa + read_byte()) / 0x100
end
-- Second and first byte in big endian: last 4 bits of exponent + 4 bits of mantissa; sign bit + 7 bits of exponent
local exponent_byte = read_byte()
local sign_byte = read_byte()
local sign = 1
if sign_byte >= 0x80 then
sign = -1
sign_byte = sign_byte - 0x80
end
local exponent = sign_byte * 0x10
local mantissa_bits = exponent_byte % 0x10
exponent = exponent + (exponent_byte - mantissa_bits) / 0x10
mantissa = (mantissa + mantissa_bits) / 0x10
if exponent == 0x7FF 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
return sign == 1 and positive_nan or negative_nan
end
assert(mantissa < 1)
if exponent == 0 then
-- subnormal value
return sign * 2^-1022 * mantissa
end
return sign * 2 ^ (exponent - 1023) * (1 + mantissa)
end
--+ Reads doubles (f64) or floats (f32)
--: double reads an f64 if true, f32 otherwise
function read_float(read_byte, double)
return (double and read_double or read_single)(read_byte)
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
end
function read_int(read_byte, bytes)
local uint = read_uint(read_byte, bytes)
local max = 0x100 ^ bytes
if uint >= max / 2 then
return uint - max
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)
end
function write_int(write_byte, int, bytes)
local max = 0x100 ^ bytes
if int < 0 then
assert(-int <= max / 2)
int = max + int
else
assert(int < max / 2)
end
return write_uint(write_byte, int, bytes)
end
function write_single(write_byte, number)
if number ~= number then -- nan: all ones
for _ = 1, 4 do write_byte(0xFF) end
return
end
local sign_byte, exponent_byte, mantissa_byte_1, mantissa_byte_2
local sign_bit = 0
if number < 0 then
number = -number
sign_bit = 0x80
end
if number == math_huge then -- inf: exponent = all 1, mantissa = all 0
sign_byte, exponent_byte, mantissa_byte_1, mantissa_byte_2 = sign_bit + 0x7F, 0x80, 0, 0
else -- real number
local mantissa, exponent = math_frexp(number)
if exponent <= -126 or number == 0 then -- must write a subnormal number
mantissa = mantissa * 2 ^ (exponent + 126)
exponent = 0
else -- normal numbers are stored as 1.<mantissa>
mantissa = mantissa * 2 - 1
exponent = exponent - 1 + 127 -- mantissa << 1 <=> exponent--
assert(exponent < 0xFF)
end
local exp_lowest_bit = exponent % 2
sign_byte = sign_bit + (exponent - exp_lowest_bit) / 2
mantissa = mantissa * 0x80
exponent_byte = exp_lowest_bit * 0x80 + math_floor(mantissa)
mantissa = mantissa % 1
mantissa = mantissa * 0x100
mantissa_byte_1 = math_floor(mantissa)
mantissa = mantissa % 1
mantissa = mantissa * 0x100
mantissa_byte_2 = math_floor(mantissa)
mantissa = mantissa % 1
assert(mantissa == 0) -- no truncation allowed: round numbers properly using modlib.math.fround
end
write_byte(mantissa_byte_2)
write_byte(mantissa_byte_1)
write_byte(exponent_byte)
write_byte(sign_byte)
end
function write_double(write_byte, number)
if number ~= number then -- nan: all ones
for _ = 1, 8 do write_byte(0xFF) end
return
end
local sign_byte, exponent_byte, mantissa_bytes
local sign_bit = 0
if number < 0 then
number = -number
sign_bit = 0x80
end
if number == math_huge then -- inf: exponent = all 1, mantissa = all 0
sign_byte, exponent_byte, mantissa_bytes = sign_bit + 0x7F, 0xF0, {0, 0, 0, 0, 0, 0}
else -- real number
local mantissa, exponent = math_frexp(number)
if exponent <= -1022 or number == 0 then -- must write a subnormal number
mantissa = mantissa * 2 ^ (exponent + 1022)
exponent = 0
else -- normal numbers are stored as 1.<mantissa>
mantissa = mantissa * 2 - 1
exponent = exponent - 1 + 1023 -- mantissa << 1 <=> exponent--
assert(exponent < 0x7FF)
end
local exp_low_nibble = exponent % 0x10
sign_byte = sign_bit + (exponent - exp_low_nibble) / 0x10
mantissa = mantissa * 0x10
exponent_byte = exp_low_nibble * 0x10 + math_floor(mantissa)
mantissa = mantissa % 1
mantissa_bytes = {}
for i = 1, 6 do
mantissa = mantissa * 0x100
mantissa_bytes[i] = math_floor(mantissa)
mantissa = mantissa % 1
end
assert(mantissa == 0)
end
for i = 6, 1, -1 do
write_byte(mantissa_bytes[i])
end
write_byte(exponent_byte)
write_byte(sign_byte)
end
--: on_write function(double)
--: double true - f64, false - f32
function write_float(write_byte, number, double)
(double and write_double or write_single)(write_byte, number)
end
-- Export environment
return _ENV

333
mods/modlib/bluon.lua Normal file
View file

@ -0,0 +1,333 @@
-- Localize globals
local assert, error, ipairs, math_floor, math_abs, math_huge, modlib, next, pairs, setmetatable, string, table_insert, type, unpack
= assert, error, ipairs, math.floor, math.abs, math.huge, modlib, next, pairs, setmetatable, string, table.insert, type, unpack
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local fround = modlib.math.fround
local write_single, write_double = modlib.binary.write_single, modlib.binary.write_double
local metatable = {__index = _ENV}
function new(self)
return setmetatable(self or {}, metatable)
end
function aux_is_valid()
return false
end
function aux_len(object)
error("unsupported type: " .. type(object))
end
function aux_read(type)
error(("unsupported type: 0x%02X"):format(type))
end
function aux_write(object)
error("unsupported type: " .. type(object))
end
local uint_widths = {1, 2, 4, 8}
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};
-- (M0, M8, M16, M32, M64) 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
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"
}
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
end
local valid_types = modlib.table.set{"nil", "boolean", "number", "string"}
function is_valid(self, value)
local _type = type(value)
if valid_types[_type] then
return true
end
if _type == "table" then
for key, value in pairs(value) 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(value)
end
local function uint_len(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)
end
function len(self, value)
if value == nil then
return 0
end
if constants[value] then
return 1
end
local object_ids = {}
local current_id = 0
local _type = type(value)
if _type == "number" then
if value ~= value then
return 1
end
if value % 1 == 0 then
return 1 + uint_len(value > 0 and value or -value)
end
local bytes = 4
if fround(value) ~= value then bytes = 8 end
return 1 + bytes
end
local id = object_ids[value]
if id then
return 1 + uint_len(id)
end
current_id = current_id + 1
object_ids[value] = current_id
if _type == "string" then
local object_len = value:len()
return 1 + uint_len(object_len) + object_len
end
if _type == "table" then
if next(value) == nil then
-- empty {} table
return 1
end
local list_len = #value
local kv_len = 0
for key, _ in pairs(value) 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 + self:len(value[index])
end
for key, value in pairs(value) do
if is_map_key(key, list_len) then
table_len = table_len + self:len(key) + self:len(value)
end
end
return kv_len + table_len
end
return self.aux_len(value)
end
--: stream any object implementing :write(text)
function write(self, value, stream)
if value == 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 function float(number)
if fround(number) == number then
byte(type_ranges.number_f32)
write_single(byte, number)
else
byte(type_ranges.number)
write_double(byte, number)
end
end
local aux_write = self.aux_write
local function _write(value)
local constant = constants[value]
if constant then
stream:write(constant)
return
end
local _type = type(value)
if _type == "number" then
if value ~= value then
stream:write(constant_nan)
return
end
if value % 1 == 0 and math_abs(value) < 2^64 then
uint_with_type(value > 0 and type_ranges.number_constant or type_ranges.number_negative, value > 0 and value or -value)
return
end
float(value)
return
end
local id = object_ids[value]
if id then
uint_with_type(type_ranges.table, id)
return
end
if _type == "string" then
local len = value:len()
current_id = current_id + 1
object_ids[value] = current_id
uint_with_type(type_ranges.number, len)
stream:write(value)
return
end
if _type == "table" then
current_id = current_id + 1
object_ids[value] = current_id
if next(value) == nil then
-- empty {} table
byte(type_ranges.string + 1)
return
end
local list_len = #value
local kv_len = 0
for key, _ in pairs(value) 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(value[index])
end
for key, value in pairs(value) do
if is_map_key(key, list_len) then
_write(key)
_write(value)
end
end
return
end
aux_write(value, object_ids)
end
_write(value)
end
local constants_flipped = modlib.table.flip(constants)
constants_flipped[constant_nan] = 0/0
-- 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)
end
-- Export environment
return _ENV

View file

@ -0,0 +1,19 @@
-- Generate lookup table for HTML entities out of https://html.spec.whatwg.org/entities.json
-- Requires https://github.com/brunoos/luasec to fetch the JSON
local https = require 'ssl.https'
local res, code = https.request"https://html.spec.whatwg.org/entities.json"
assert(code == 200)
local entity_map = {}
for entity, chars in pairs(assert(modlib.json:read_string(res))) do
entity_map[entity:sub(2, #entity - 1)] = table.concat(modlib.table.map(chars.codepoints, modlib.utf8.char))
end
local entries = {}
for entity, chars in pairs(entity_map) do
table.insert(entries, ("[%q] = %q"):format(entity, chars))
end
local serialized = [[-- HTML entity lookup table generated by build/html_entities.lua. Do not edit.
return {]] .. table.concat(entries, ", ") .. "}"
local loaded = assert(loadstring(serialized))
setfenv(loaded, {})
assert(modlib.table.equals(entity_map, loaded()))
modlib.file.write(modlib.mod.get_resource("modlib", "web", "html", "entities.lua"), serialized)

79
mods/modlib/doc/b3d.md Normal file
View file

@ -0,0 +1,79 @@
# B3D Reader & Writer
## `b3d.read(file)`
Reads from `file`, which is expected to provide `file:read(nbytes)`. `file` is not closed.
Returns a B3D model object.
## `:write(file)`
Writes the B3D model object `self` to `file`.
`file` must provide `file:write(bytestr)`. It should be in binary mode.
It is not closed after writing.
## `:write_string()`
Writes the B3D model object to a bytestring, which is returned.
## `:to_gltf()`
Returns a glTF JSON representation of `self` in Lua table format.
## `:write_gltf(file)`
Convenience function to write the glTF representation to `file` using modlib's `json` writer.
`file` must provide `file:write(str)`. It is not closed afterwards.
## Examples
### Converting B3D to glTF
This example loops over all files in `dir_path`, converting them to glTFs which are stored in `out_dir_path`.
```lua
local modpath = minetest.get_modpath(minetest.get_current_modname())
local dir_path = modpath .. "/b3d"
local out_dir_path = modpath .. "/gltf"
for _, filename in ipairs(minetest.get_dir_list(dir_path, false --[[only files]])) do
-- First read the B3D
local in_file = assert(io.open(dir_path .. "/" .. filename, "rb"))
local model = assert(b3d.read(in_file))
in_file:close()
-- Then write the glTF
local out_file = io.open(out_dir_path .. "/" .. filename .. ".gltf", "wb")
model:write_gltf(out_file)
out_file:close()
end
```
### [Round-trip (minifying B3Ds)](https://github.com/appgurueu/modlib_test/blob/f11c8e580e90454bc1adaa11a58e0c0217217d90/b3d.lua)
This example from [`modlib_test`](https://github.com/appgurueu/modlib_test) reads, writes, and then reads again,
in order to verify that no data is lost through writing.
Simply re-writing a model using modlib's B3D writer often reduces model sizes,
since for example modlib does not write `0` weights for bones.
Keep in mind to use the `rb` and `wb` modes for I/O operations
to force Windows systems to not perform a line feed normalization.
### [Extracting triangle sets](https://github.com/appgurueu/ghosts/blob/42a9eb9ee81fc6760a0278d23e4c47bc68bb4919/init.lua#L41-L79)
The [Ghosts](https://github.com/appgurueu/ghosts/) mod extracts triangle sets using the B3D module
to then approximate the player shape using particles picked from these triangles.
### [Animating the player](https://github.com/appgurueu/character_anim/blob/c48b282c0b42b32294ec2fddc03aa93141cbd894/init.lua#L213)
[`character_anim`](https://github.com/appgurueu/character_anim/) uses the B3D module to determine the bone overrides required
for animating the player entirely Lua-side using bone overrides.
### [Generating a Go board](https://github.com/appgurueu/go/blob/997ce85260d232a05dd668c32c6854bf34e3d5be/build/generate_models.lua)
This example from the [Go](https://github.com/appgurueu/go) mod generates a Go board
where for each spot on the board there are two pieces (black and white),
both of which can be moved out of the board using a bone.
It demonstrates how to use the writer (and how the table structure roughly looks like).

View file

@ -0,0 +1,260 @@
************************************************************************************
* Blitz3d file format V0.01 *
************************************************************************************
This document and the information contained within is placed in the Public Domain.
Please visit http://www.blitzbasic.co.nz for the latest version of this document.
Please contact marksibly@blitzbasic.co.nz for more information and general inquiries.
************************************************************************************
* Introduction *
************************************************************************************
The Blitz3D file format specifies a format for storing texture, brush and entity descriptions for
use with the Blitz3D programming language.
The rationale behind the creation of this format is to allow for the generation of much richer and
more complex Blitz3D scenes than is possible using established file formats - many of which do not
support key features of Blitz3D, and all of which miss out on at least some features!
A Blitz3D (.b3d) file is split up into a sequence of 'chunks', each of which can contain data
and/or other chunks.
Each chunk is preceded by an eight byte header:
char tag[4] ;4 byte chunk 'tag'
int length ;4 byte chunk length (not including *this* header!)
If a chunk contains both data and other chunks, the data always appears first and is of a fixed
length.
A file parser should ignore unrecognized chunks.
Blitz3D files are stored little endian (intel) style.
Many aspects of the file format are not quite a 'perfect fit' for the way Blitz3D works. This has
been done mainly to keep the file format simple, and to make life easier for the authors of third
party importers/exporters.
************************************************************************************
* Chunk Types *
************************************************************************************
This lists the types of chunks that can appear in a b3d file, and the data they contain.
Color values are always in the range 0 to 1.
string (char[]) values are 'C' style null terminated strings.
Quaternions are used to specify general orientations. The first value is the quaternion 'w' value,
the next 3 are the quaternion 'vector'. A 'null' rotation should be specified as 1,0,0,0.
Anything that is referenced 'by index' always appears EARLIER in the file than anything that
references it.
brush_id references can be -1: no brush.
In the following descriptions, {} is used to signify 'repeating until end of chunk'. Also, a chunk
name enclosed in '[]' signifies the chunk is optional.
Here we go!
BB3D
int version ;file format version: default=1
[TEXS] ;optional textures chunk
[BRUS] ;optional brushes chunk
[NODE] ;optional node chunk
The BB3D chunk appears first in a b3d file, and its length contains the rest of the file.
Version is in major*100+minor format. To check the version, just divide by 100 and compare it with
the major version your software supports, eg:
if file_version/100>my_version/100
RuntimeError "Can't handle this file version!"
EndIf
if file_version Mod 100>my_version Mod 100
;file is a more recent version, but should still be backwardly compatbile with what we can
handle!
EndIf
TEXS
{
char file[] ;texture file name
int flags,blend ;blitz3D TextureFLags and TextureBlend: default=1,2
float x_pos,y_pos ;x and y position of texture: default=0,0
float x_scale,y_scale ;x and y scale of texture: default=1,1
float rotation ;rotation of texture (in radians): default=0
}
The TEXS chunk contains a list of all textures used in the file.
The flags field value can conditional an additional flag value of '65536'. This is used to indicate that the texture uses secondary UV values, ala the TextureCoords command. Yes, I forgot about this one.
BRUS
int n_texs
{
char name[] ;eg "WATER" - just use texture name by default
float red,green,blue,alpha ;Blitz3D Brushcolor and Brushalpha: default=1,1,1,1
float shininess ;Blitz3D BrushShininess: default=0
int blend,fx ;Blitz3D Brushblend and BrushFX: default=1,0
int texture_id[n_texs] ;textures used in brush
}
The BRUS chunk contains a list of all brushes used in the file.
VRTS:
int flags ;1=normal values present, 2=rgba values present
int tex_coord_sets ;texture coords per vertex (eg: 1 for simple U/V) max=8
int tex_coord_set_size ;components per set (eg: 2 for simple U/V) max=4
{
float x,y,z ;always present
float nx,ny,nz ;vertex normal: present if (flags&1)
float red,green,blue,alpha ;vertex color: present if (flags&2)
float tex_coords[tex_coord_sets][tex_coord_set_size] ;tex coords
}
The VRTS chunk contains a list of vertices. The 'flags' value is used to indicate how much extra
data (normal/color) is stored with each vertex, and the tex_coord_sets and tex_coord_set_size
values describe texture coordinate information stored with each vertex.
TRIS:
int brush_id ;brush applied to these TRIs: default=-1
{
int vertex_id[3] ;vertex indices
}
The TRIS chunk contains a list of triangles that all share a common brush.
MESH:
int brush_id ;'master' brush: default=-1
VRTS ;vertices
TRIS[,TRIS...] ;1 or more sets of triangles
The MESH chunk describes a mesh. A mesh only has one VRTS chunk, but potentially many TRIS chunks.
BONE:
{
int vertex_id ;vertex affected by this bone
float weight ;how much the vertex is affected
}
The BONE chunk describes a bone. Weights are applied to the mesh described in the enclosing ANIM -
in 99% of cases, this will simply be the MESH contained in the root NODE chunk.
KEYS:
int flags ;1=position, 2=scale, 4=rotation
{
int frame ;where key occurs
float position[3] ;present if (flags&1)
float scale[3] ;present if (flags&2)
float rotation[4] ;present if (flags&4)
}
The KEYS chunk is a list of animation keys. The 'flags' value describes what kind of animation
info is stored in the chunk - position, scale, rotation, or any combination of.
ANIM:
int flags ;unused: default=0
int frames ;how many frames in anim
float fps ;default=60
The ANIM chunk describes an animation.
NODE:
char name[] ;name of node
float position[3] ;local...
float scale[3] ;coord...
float rotation[4] ;system...
[MESH|BONE] ;what 'kind' of node this is - if unrecognized, just use a Blitz3D
pivot.
[KEYS[,KEYS...]] ;optional animation keys
[NODE[,NODE...]] ;optional child nodes
[ANIM] ;optional animation
The NODE chunk describes a Blitz3D Entity. The scene hierarchy is expressed by the nesting of NODE
chunks.
NODE kinds are currently mutually exclusive - ie: a node can be a MESH, or a BONE, but not both!
However, it can be neither...if no kind is specified, the node is just a 'null' node - in Blitz3D
speak, a pivot.
The presence of an ANIM chunk in a NODE indicates that an animation starts here in the hierarchy.
This allows animations of differing speeds/lengths to be potentially nested.
There are many more 'kind' chunks coming, including camera, light, sprite, plane etc. For now, the
use of a Pivot in cases where the node kind is unknown will allow for backward compatibility.
************************************************************************************
* Examples *
************************************************************************************
A typical b3d file will contain 1 TEXS chunk, 1 BRUS chunk and 1 NODE chunk, like this:
BB3D
1
TEXS
...list of textures...
BRUS
...list of brushes...
NODE
...stuff in the node...
A simple, non-animating, non-textured etc mesh might look like this:
BB3D
1 ;version
NODE
"root_node" ;node name
0,0,0 ;position
1,1,1 ;scale
1,0,0,0 ;rotation
MESH ;the mesh
-1 ;brush: no brush
VRTS ;vertices in the mesh
0 ;no normal/color info in verts
0,0 ;no texture coords in verts
{x,y,z...} ;vertex coordinates
TRIS ;triangles in the mesh
-1 ;no brush for this triangle
{v0,v1,v2...} ;vertices
A more complex 'skinned mesh' might look like this (only chunks shown):
BB3D
TEXS ;texture list
BRUS ;brush list
NODE ;root node
MESH ;mesh - the 'skin'
ANIM ;anim
NODE ;first child of root node - eg: "pelvis"
BONE ;vertex weights for pelvis
KEYS ;anim keys for pelvis
NODE ;first child of pelvis - eg: "left-thigh"
BONE ;bone
KEYS ;anim keys for left-thigh
NODE ;second child of pelvis - eg: "right-thigh"
BONE ;vertex weights for right-thigh
KEYS ;anim keys for right-thigh
...and so on.

132
mods/modlib/doc/bluon.md Normal file
View file

@ -0,0 +1,132 @@
# Bluon
Binary Lua object notation.
## `new(def)`
```lua
def = {
aux_is_valid = function(object)
return is_valid
end,
aux_len = function(object)
return length_in_bytes
end,
-- read type byte, stream providing :read(count), map of references -> id
aux_read = function(type, stream, references)
... = stream:read(...)
return object
end,
-- object to be written, stream providing :write(text), list of references
aux_write = function(object, stream, references)
stream:write(...)
end
}
```
## `:is_valid(object)`
Returns whether the given object can be represented by the instance as boolean.
## `:len(object)`
Returns the expected length of the object if serialized by the current instance in bytes.
## `:write(object, stream)`
Writes the object to a stream supporting `:write(text)`. Throws an error if invalid.
## `:read(stream)`
Reads a single bluon object from a stream supporting `:read(count)`. Throws an error if invalid bluon.
Checking whether the stream has been fully consumed by doing `assert(not stream:read(1))` is left up to the user.
## Format
Bluon uses a "tagged union" binary format:
Values are stored as a one-byte tag followed by the contents of the union.
For frequently used "constants", only a tag is used.
`nil` is an exception; since it can't appear in tables, it gets no tag.
If the value to be written by Bluon is `nil`, Bluon simply writes *nothing*.
The following is an enumeration of tag numbers, which are assigned *in this order*.
* `false`: 0
* `true`: 1
* Numbers:
* Constants: 0, nan, +inf, -inf
* Integers: Little endian:
* Unsigned: `U8`, `U16`, `U32`, `U64`
* Negative: `-U8`, `-U16`, `-U32`, `-U64`
* Floats: Little endian `F32`, `F64`
* Strings:
* Constant: `""`
* Length is written as unsigned integer according to the tag: `S8`, `S16`, `S32`, `S64`
* followed by the raw bytes
* Tables:
* Tags: `M0`, `M8`, `M16`, `M32`, `M64` times `L0`, `L8`, `L16`, `L32`, `L64`
* `M` is more significant than `L`: The order of the cartesian product is `M0L0`, `M0L1`, ...
* List and map part count encoded as unsigned integers according to the tag,
list part count comes first
* followed by all values in the list part written as Bluon
* followed by all key-value pairs in the map part written as Bluon
(first the key is written as Bluon, then the value)
* Reference:
* Reference ID as unsigned integer: `R8`, `R16`, `R32`, `R64`
* References a previously encountered table or string by an index:
All tables and strings are numbered in the order they occur in the Bluon
* Reserved tags:
* All tags <= 55 are reserved. This gives 200 free tags.
## Features
* Embeddable: Written in pure Lua
* Storage efficient: No duplication of strings or reference-equal tables
* Flexible: Can serialize circular references and strings containing null
## Simple example
```lua
local object = ...
-- Write to file
local file = io.open(..., "wb")
modlib.bluon:write(object, file)
file:close()
-- Write to text
local rope = modlib.table.rope{}
modlib.bluon:write(object, rope)
text = rope:to_text()
-- Read from text
local inputstream = modlib.text.inputstream"\1"
assert(modlib.bluon:read(object, rope) == true)
```
## Advanced example
```lua
-- Serializes all userdata to a constant string:
local custom_bluon = bluon.new{
aux_is_valid = function(object)
return type(object) == "userdata"
end,
aux_len = function(object)
return 1 + ("userdata"):len())
end,
aux_read = function(type, stream, references)
assert(type == 100, "unsupported type")
assert(stream:read(("userdata"):len()) == "userdata")
return userdata()
end,
-- object to be written, stream providing :write(text), list of references
aux_write = function(object, stream, references)
assert(type(object) == "userdata")
stream:write"\100userdata"
end
}
-- Write to text
local rope = modlib.table.rope{}
custom_bluon:write(userdata(), rope)
assert(rope:to_text() == "\100userdata")
```

View file

@ -0,0 +1,76 @@
# Minetest Wavefront `.obj` file format specification
Minetest Wavefront `.obj` is a subset of [Wavefront `.obj`](http://paulbourke.net/dataformats/obj/).
It is inferred from the [Minetest Irrlicht `.obj` reader](https://github.com/minetest/irrlicht/blob/master/source/Irrlicht/COBJMeshFileLoader.cpp).
`.mtl` files are not supported since Minetest's media loading process ignores them due to the extension.
## Lines / "Commands"
Irrlicht only looks at the first characters needed to tell commands apart (imagine a prefix tree of commands).
Superfluous parameters are ignored.
Numbers are formatted as either:
* Float: An optional minus sign (`-`), one or more decimal digits, followed by the decimal dot (`.`) then again one or more digits
* Integer: An optional minus sign (`-`) followed by one or more decimal digits
Indexing starts at one. Indices are formatted as integers. Negative indices relative to the end of a buffer are supported.
* Comments: `# ...`; unsupported commands are silently ignored as well
* Groups: `g <name>` or `usemtl <name>`
* Subsequent faces belong to a new group / material, no matter the supplied names
* Each group gets their own material (texture); indices are determined by order of appearance
* Empty groups (groups without faces) are ignored
* Vertices (all numbers): `v <x> <y> <z>`, global to the model
* Texture Coordinates (all numbers): `vt <x> <y>`, global to the model
* Normals (all numbers): `vn <x> <y> <z>`, global to the model
* Faces (all vertex/texcoord/normal indices); always local to the current group:
* `f <v1> <v2> <v3>`
* `f <v1>/<t1> <v2>/<t2> ... <vn>/<tn>`
* `f <v1>//<n1> <v2>/<n2> ... <vn>/<nn>`
* `f <v1>/<t1>/<n1> <v2>/<t2>/<n2> ... <vn>/<tn>/<nn>`
## Coordinate system orientation ("handedness")
Vertex & normal X-coordinates are inverted ($x' = -x$);
texture Y-coordinates are inverted as well ($y' = 1 - y$).
## Example
```obj
# A simple 2³ cube centered at the origin; each face receives a separate texture / tile
# no care was taken to ensure "proper" texture orientation
v -1 -1 -1
v -1 -1 1
v -1 1 -1
v -1 1 1
v 1 -1 -1
v 1 -1 1
v 1 1 -1
v 1 1 1
vn -1 0 0
vn 0 -1 0
vn 0 0 -1
vn 1 0 0
vn 0 1 0
vn 0 0 1
vt 0 0
vt 1 0
vt 0 1
vt 1 1
g negative_x
f 1/1/1 3/3/1 2/2/1 4/4/1
g negative_y
f 1/1/2 5/3/2 2/2/2 6/4/2
g negative_z
f 1/1/3 5/3/3 3/2/3 7/4/3
g positive_x
f 5/1/4 7/3/4 2/2/4 8/4/4
g positive_y
f 3/1/5 7/3/5 4/2/5 8/4/5
g positive_z
f 2/1/6 6/3/6 4/2/6 8/4/6
```

9
mods/modlib/doc/json.md Normal file
View file

@ -0,0 +1,9 @@
# JSON
Advantages over `minetest.write_json`/`minetest.parse_json`:
* Twice as fast in most benchmarks (for pre-5.6 at least)
* Uses streams instead of strings
* Customizable
* Useful error messages
* Pure Lua

View file

@ -0,0 +1,31 @@
# Configuration
## Legacy
1. Configuration is loaded from `<worldpath>/config/<modname>.<extension>`, the following extensions are supported and loaded (in the given order), with loaded configurations overriding properties of previous ones:
1. [`json`](https://json.org)
2. [`lua`](https://lua.org)
3. [`luon`](https://github.com/appgurueu/luon), Lua but without the `return`
4. [`conf`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)
2. Settings are loaded from `minetest.conf` and override configuration values
## Locations
0. Default configuration: `<modfolder>/conf.lua`
1. World configuration: `config/<modname>.<format>`
2. Mod configuration: `<modfolder>/conf.<format>`
3. Minetest configuration: `minetest.conf`
## Formats
1. [`lua`](https://lua.org)
* Lua, with the environment being the configuration object
* `field = value` works
* Return new configuration object to replace
2. [`luon`](https://github.com/appgurueu/luon)
* Single Lua literal
* Booleans, numbers, strings and tables
3. [`conf`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)
* Minetest-like configuration files
4. [`json`](https://json.org)
* Not recommended

View file

@ -0,0 +1,79 @@
# Schematic
A schematic format with support for metadata and baked light data.
## Table Format
The table format uses a table with the following mandatory fields:
* `size`: Size of the schematic in nodes, vector
* `node_names`: List of node names
* `nodes`: List of node indices (into the `node_names` table)
* `param2s`: List of node `param2` values (numbers)
and the following optional fields:
* `light_values`: List of node `param1` (light) values (numbers)
* `metas`: Map from indices in the cuboid to metadata tables as produced by `minetest.get_meta(pos):to_table()`
A "vector" is a table with fields `x`, `y`, `z` for the 3 coordinates.
The `nodes`, `param2s` and `light_values` lists are in the order dictated by `VoxelArea:iterp` (Z-Y-X).
The cuboid indices for the `metas` table are calculated as `(z * size.y) + y * size.x + x` where `x`, `y`, `z` are relative to the min pos of the cuboid.
## Binary Format
The binary format uses modlib's Bluon to write the table format.
Since `param2s` (and optionally `light_values`) are all bytes, they are converted from lists of numbers to (byte)strings before writing.
For uncompressed files, it uses `MLBS` (short for "ModLib Bluon Schematic") for the magic bytes,
followed by the raw Bluon binary data.
For compressed files, it uses `MLZS` (short for "ModLib Zlib-compressed Schematic") for the magic bytes,
followed by the zlib-compressed Bluon binary data.
## API
### `schematic.setmetatable(obj)`
Sets the metatable of a table `obj` to the schematic metatable.
Useful if you've deserialized a schematic or want to create a schematic from the table format.
### `schematic.create(params, pos_min, pos_max)`
Creates a schematic from a map cuboid
* `params`: Table with fields
* `metas` (default `true`): Whether to store metadata
* `light_values`: Whether to bake light values (`param1`).
Usually not recommended, default `false`.
* `pos_min`: Minimum position of the cuboid, inclusive
* `pos_max`: Maximum position of the cuboid, inclusive
### `schematic:place(pos_min)`
"Inverse" to `schematic.create`: Places the schematic `self` starting at `pos_min`.
Content IDs (nodes), param1s, param2s, and metadata in the area will be completely erased and replaced; if light data is present, param1s will simply be set, otherwise they will be recalculated.
### `schematic:write_zlib_bluon(path)`
Write a binary file containing the schematic in *zlib-compressed* binary format to `path`.
**You should generally prefer this over `schematic:write_bluon`: zlib compression comes with massive size reductions.**
### `schematic.read_zlib_bluon(path)`
"Inverse": Read a binary file containing a schematic in *zlib-compressed* binary format from `path`, returning a `schematic` instance.
**You should generally prefer this over `schematic.read_bluon`: zlib compression comes with massive size reductions.**
### `schematic:write_bluon(path)`
Write a binary file containing the schematic in uncompressed binary format to `path`.
Useful only if you want to eliminate the time spent compressing.
### `schematic.read_bluon(path)`
"Inverse": Read a binary file containing a schematic in uncompressed binary format from `path`, returning a `schematic` instance.
Useful only if you want to eliminate the time spent decompressing.

View file

@ -0,0 +1,39 @@
# Texture Modifiers
## Specification
Refer to the following "specifications", in this order of precedence:
1. [Minetest Docs](https://github.com/minetest/minetest_docs/blob/master/doc/texture_modifiers.adoc)
2. [Minetest Lua API](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt), section "texture modifiers"
3. [Minetest Sources](https://github.com/minetest/minetest/blob/master/src/client/tile.cpp)
## Implementation
### Constructors ("DSL")
Constructors are kept close to the original forms and perform basic validation. Additionally, texture modifiers can directly be created using `texmod{type = "...", ...}`, bypassing the checks.
### Writing
The naive way to implement string building would be to have a
`tostring` function recursively `tostring`ing the sub-modifiers of the current modifier;
each writer would only need a stream (often passed in the form of a `write` function).
The problem with this is that applying escaping quickly makes this run in quadratic time.
A more efficient approach passes the escaping along with the `write` function. Thus a "writer" object `w` encapsulating this state is passed around.
The writer won't necessarily produce the *shortest* or most readable texture modifier possible; for example, colors will be converted to hexadecimal representation, and texture modifiers with optional parameters may have the default values be written.
You should not rely on the writer to produce any particular of the various valid outputs.
### Reading
**The reader does not attempt to precisely match the behavior of Minetest's shotgun "parser".** It *may* be more strict in some instances, rejecting insane constructs Minetest's parser allows.
It *may* however sometimes also be more lenient (though I haven't encountered an instance of this yet), accepting sane constructs which Minetest's parser rejects due to shortcomings in its implementation.
The parser is written *to spec*, in the given order of precedence.
If a documented construct is not working, that's a bug. If a construct which is incorrect according to the docs is accepted, that's a bug too.
Compatibility with Minetest's parser for all reasonable inputs is greatly valued. If an invalid input is notably used in the wild (or it is reasonable that it may occur in the wild) and supported by Minetest, this parser ought to support it too.
Recursive descent parsing is complicated by the two forms of escaping texture modifiers support: Reading each character needs to handle escaping. The current depth and whether the parser is inside an inventorycube need to be saved in state variables. These could be passed on the stack, but it's more comfortable (and possibly more efficient) to just share them across all functions and restore them after leaving an inventorycube / moving to a lower level.

View file

@ -0,0 +1,23 @@
# Lua Log Files
A data log file based on Lua statements. High performance. Example from `test.lua`:
```lua
local logfile = persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {})
logfile:init()
logfile.root = {}
logfile:rewrite()
logfile:set_root({a = 1}, {b = 2, c = 3})
logfile:close()
logfile:init()
assert(table.equals(logfile.root, {[{a = 1}] = {b = 2, c = 3}}))
```
Both strings and tables are stored in a reference table. Unused strings won't be garbage collected as Lua doesn't allow marking them as weak references.
This means that setting lots of temporary strings will waste memory until you call `:rewrite()` on the log file. An alternative is to set the third parameter, `reference_strings`, to `false` (default value is `true`):
```lua
persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {}, false)
```
This will prevent strings from being referenced, possibly bloating file size, but saving memory.

View file

@ -0,0 +1,41 @@
# SQLite3 Database Persistence
Uses a SQLite3 database to persistently store a Lua table. Obtaining it is a bit trickier, as it requires access to the `lsqlite3` library, which may be passed:
```lua
local modlib_sqlite3 = persistence.sqlite3(require"lsqlite3")
```
(assuming `require` is that of an insecure environment if Minetest is used)
Alternatively, if you are not running Minetest, mod security is disabled, you have (temporarily) provided `require` globally, or added `modlib` to `secure.trusted_mods`, you can simply do the following:
```lua
local modlib_sqlite3 = persistence.sqlite3()
```
Modlib will then simply call `require"lsqlite3"` for you.
Then, you can proceed to create a new database:
```lua
local database = persistence.modlib_sqlite3.new(mod.get_resource"database.test.sqlite3", {})
-- Create or load
database:init()
-- Use it
database:set_root("key", {nested = true})
database:close()
```
It uses a similar API to Lua log files:
* `new(filename, root)` - without `reference_strings` however (strings aren't referenced currently)
* `init`
* `set`
* `set_root`
* `rewrite`
* `close`
The advantage over Lua log files is that the SQlite3 database keeps disk usage minimal. Unused tables are dropped from the database immediately through reference counting. The downside of this is that this, combined with the overhead of using SQLite3, of course takes time, making updates on the SQLite3 database slower than Lua log file updates (which just append to an append-only file).
As simple and fast reference counting doesn't handle cycles, an additional `collectgarbage` stop-the-world method performing a full garbage collection on the database is provided which is called during `init`.
The method `defragment_ids` should not have to be used in practice (if it has to be, it happens automatically) and should be used solely for debugging purposes (neater IDs).

47
mods/modlib/doc/schema.md Normal file
View file

@ -0,0 +1,47 @@
# Schema
Place a file `schema.lua` in your mod, returning a schema table.
## Non-string entries and `minetest.conf`
Suppose you have the following schema:
```lua
return {
type = "table",
entries = {
[42] = {
type = "boolean",
description = "The Answer"
default = true
}
}
}
```
And a user sets the following config:
```conf
mod.42 = false
```
It won't work, as the resulting table will be `{["42"] = false}` instead of `{[42] = false}`. In order to make this work, you have to convert the keys yourself:
```lua
return {
type = "table",
keys = {
-- this will convert all keys to numbers
type = "number"
},
entries = {
[42] = {
type = "boolean",
description = "The Answer"
default = true
}
}
}
```
This is best left explicit. First, you shouldn't be using numbered field keys if you want decent `minetest.conf` support, and second, `modlib`'s schema module could only guess in this case, attempting conversion to number / boolean. What if both number and string field were set as possible entries? Should the string field be deleted? And so on.

164
mods/modlib/file.lua Normal file
View file

@ -0,0 +1,164 @@
local dir_delim = ...
-- Localize globals
local assert, io, minetest, modlib, string, pcall = assert, io, minetest, modlib, string, pcall
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
_ENV.dir_delim = dir_delim
function get_name(filepath)
return filepath:match("([^%" .. dir_delim .. "]+)$") or filepath
end
function split_extension(filename)
return filename:match"^(.*)%.(.*)$"
end
--! deprecated
get_extension = split_extension
function split_path(filepath)
return modlib.text.split_unlimited(filepath, dir_delim, true)
end
-- concat_path is set by init.lua to avoid code duplication
-- Lua 5.4 has `<close>` for this, but we're restricted to 5.1,
-- so we need to roll our own `try f = io.open(...); return func(f) finally f:close() end`.
function with_open(filename, mode, func --[[function(file), called with `file = io.open(filename, mode)`]])
local file = assert(io.open(filename, mode or "r"))
-- Throw away the stacktrace. The alternative would be to use `xpcall`
-- to bake the stack trace into the error string using `debug.traceback`.
-- Lua will have prepended `<source>:<line>:` already however.
return (function(status, ...)
file:close()
assert(status, ...)
return ...
end)(pcall(func, file))
end
function read(filename)
local file, err = io.open(filename, "r")
if file == nil then return nil, err end
local content = file:read"*a"
file:close()
return content
end
function read_binary(filename)
local file, err = io.open(filename, "rb")
if file == nil then return nil, err end
local content = file:read"*a"
file:close()
return content
end
function write_unsafe(filename, new_content)
local file, err = io.open(filename, "w")
if file == nil then return false, err end
file:write(new_content)
file:close()
return true
end
write = minetest and minetest.safe_file_write or write_unsafe
function write_binary_unsafe(filename, new_content)
local file, err = io.open(filename, "wb")
if file == nil then return false, err end
file:write(new_content)
file:close()
return true
end
write_binary = minetest and minetest.safe_file_write or write_binary_unsafe
function ensure_content(filename, ensured_content)
local content = read(filename)
if content ~= ensured_content then
return write(filename, ensured_content)
end
return true
end
function append(filename, new_content)
local file, err = io.open(filename, "a")
if file == nil then return false, err end
file:write(new_content)
file:close()
return true
end
function exists(filename)
local file, err = io.open(filename, "r")
if file == nil then return false, err end
file:close()
return true
end
function create_if_not_exists(filename, content)
if not exists(filename) then
return write(filename, content or "")
end
return false
end
function create_if_not_exists_from_file(filename, src_filename) return create_if_not_exists(filename, read(src_filename)) end
if not minetest then return end
-- Process Bridge Helpers
process_bridges = {}
function process_bridge_build(name, input, output, logs)
if not input or not output or not logs then
minetest.mkdir(minetest.get_worldpath() .. "/bridges/" .. name)
end
input = input or minetest.get_worldpath() .. "/bridges/" .. name .. "/input.txt"
output = output or minetest.get_worldpath() .. "/bridges/" .. name .. "/output.txt"
logs = logs or minetest.get_worldpath() .. "/bridges/" .. name .. "/logs.txt"
-- Clear input
write(input, "")
-- Clear output
write(output, "")
-- Create logs if not exists
create_if_not_exists(logs, "")
process_bridges[name] = {
input = input,
output = output,
logs = logs,
output_file = io.open(output, "a")
}
end
function process_bridge_listen(name, line_consumer, step)
local bridge = process_bridges[name]
modlib.minetest.register_globalstep(step or 0.1, function()
for line in io.lines(bridge.input) do
line_consumer(line)
end
write(bridge.input, "")
end)
end
function process_bridge_serve(name, step)
local bridge = process_bridges[name]
modlib.minetest.register_globalstep(step or 0.1, function()
bridge.output_file:close()
process_bridges[name].output_file = io.open(bridge.output, "a")
end)
end
function process_bridge_write(name, message)
local bridge = process_bridges[name]
bridge.output_file:write(message .. "\n")
end
function process_bridge_start(name, command, os_execute)
local bridge = process_bridges[name]
os_execute(string.format(command, bridge.output, bridge.input, bridge.logs))
end
-- Export environment
return _ENV

126
mods/modlib/func.lua Normal file
View file

@ -0,0 +1,126 @@
-- Localize globals
local modlib, unpack, select, setmetatable
= modlib, unpack, select, setmetatable
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
function no_op() end
function identity(...) return ... end
-- TODO switch all of these to proper vargs
function curry(func, ...)
local args = { ... }
return function(...) return func(unpack(args), ...) end
end
function curry_tail(func, ...)
local args = { ... }
return function(...) return func(unpack(modlib.table.concat({...}, args))) end
end
function curry_full(func, ...)
local args = { ... }
return function() return func(unpack(args)) end
end
function args(...)
local args = { ... }
return function(func) return func(unpack(args)) end
end
function value(val) return function() return val end end
function values(...)
local args = { ... }
return function() return unpack(args) end
end
function memoize(func)
return setmetatable({}, {
__index = function(self, key)
local value = func(key)
self[key] = value
return value
end,
__call = function(self, arg)
return self[arg]
end,
__mode = "k"
})
end
function compose(func, other_func)
return function(...)
return func(other_func(...))
end
end
function override_chain(func, override)
return function(...)
func(...)
return override(...)
end
end
--+ Calls func using the provided arguments, deepcopies all arguments
function call_by_value(func, ...)
return func(unpack(modlib.table.deepcopy{...}, 1, select("#", ...)))
end
-- Functional wrappers for Lua's builtin metatable operators (arithmetic, concatenation, length, comparison, indexing, call)
-- TODO (?) add operator table `["+"] = add, ...`
function add(a, b) return a + b end
function sub(a, b) return a - b end
function mul(a, b) return a * b end
function div(a, b) return a / b end
function mod(a, b) return a % b end
function pow(a, b) return a ^ b end
function unm(a) return -a end
function concat(a, b) return a .. b end
function len(a) return #a end
function eq(a, b) return a == b end
function neq(a, b) return a ~= b end
function lt(a, b) return a < b end
function gt(a, b) return a > b end
function le(a, b) return a <= b end
function ge(a, b) return a >= b end
function index(object, key) return object[key] end
function newindex(object, key, value) object[key] = value end
function call(object, ...) object(...) end
-- Functional wrappers for logical operators, suffixed with _ for syntactical convenience
function not_(a) return not a end
_ENV["not"] = not_
function and_(a, b) return a and b end
_ENV["and"] = and_
function or_(a, b) return a or b end
_ENV["or"] = or_
-- Export environment
return _ENV

117
mods/modlib/hashheap.lua Normal file
View file

@ -0,0 +1,117 @@
-- Localize globals
local assert, math_floor, setmetatable, table_insert = assert, math.floor, setmetatable, table.insert
-- Set environment
-- Min. heap + Lua hash table to allow updating the stored values
local _ENV = {}
setfenv(1, _ENV)
local metatable = { __index = _ENV }
function less_than(a, b)
return a < b
end
--> empty, duplicate-free min heap with priority queue functionality
function new(less_than)
return setmetatable({ less_than = less_than, indices = {} }, metatable)
end
local function swap(self, child_index, parent_index)
local child_value, parent_value = self[child_index], self[parent_index]
self.indices[parent_value], self.indices[child_value] = child_index, parent_index
self[parent_index], self[child_index] = child_value, parent_value
end
local function heapify_up(self, index)
if index == 1 then
return
end
local parent_index = math_floor(index / 2)
if self.less_than(self[index], self[parent_index]) then
swap(self, index, parent_index)
heapify_up(self, parent_index)
end
end
local function heapify_down(self, index)
local left_child = index * 2
if left_child > #self then
return
end
local smallest_child = left_child + 1
if smallest_child > #self 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
swap(self, smallest_child, index)
heapify_down(self, smallest_child)
end
end
function push(self, value)
table_insert(self, value)
local last = #self
self.indices[value] = last
heapify_up(self, last)
end
function top(self)
return self[1]
end
-- TODO what if empty?
function pop(self)
local value = self[1]
self.indices[value] = nil
local last = #self
if last == 1 then
self[1] = nil
return value
end
self[1], self[last] = self[last], nil
heapify_down(self, 1)
return value
end
function find_index(self, element)
return self.indices[element]
end
-- Notify heap that the element has been decreased
function decrease(self, element)
heapify_up(self, assert(self:find_index(element)))
end
-- Notify heap that the element has been increased
function increase(self, element)
heapify_down(self, assert(self:find_index(element)))
end
-- Replaces the specified element - by identity - with the new element
function replace(self, element, new_element)
local index = assert(self:find_index(element))
assert(self:find_index(new_element) == nil, "no duplicates allowed")
self[index] = new_element
self.indices[element] = nil
self.indices[new_element] = index;
(self.less_than(new_element, element) and heapify_up or heapify_down)(self, index)
end
function remove(self, element)
local index = assert(self:find_index(element), "element not found")
self.indices[element] = nil
if index == #self then
self[index] = nil
else
local last_index = #self
local last_element = self[last_index]
self[last_index] = nil
self[index] = last_element
self.indices[last_element] = index;
(self.less_than(last_element, element) and heapify_up or heapify_down)(self, index)
end
end
-- Export environment
return _ENV

93
mods/modlib/hashlist.lua Normal file
View file

@ -0,0 +1,93 @@
-- Localize globals
local setmetatable = setmetatable
-- Table based list, can handle at most 2^52 pushes
local list = {}
-- TODO use __len for Lua version > 5.1
local metatable = {__index = list}
list.metatable = metatable
-- Takes a list
function list:new()
self.head = 0
self.length = #self
return setmetatable(self, metatable)
end
function list:in_bounds(index)
return index >= 1 and index <= self.length
end
function list:get(index)
return self[self.head + index]
end
function list:set(index, value)
assert(value ~= nil)
self[self.head + index] = value
end
function list:len()
return self.length
end
function list:ipairs()
local index = 0
return function()
index = index + 1
if index > self.length then
return
end
return index, self[self.head + index]
end
end
function list:rpairs()
local index = self.length + 1
return function()
index = index - 1
if index < 1 then
return
end
return index, self[self.head + index]
end
end
function list:push_tail(value)
assert(value ~= nil)
self.length = self.length + 1
self[self.head + self.length] = value
end
function list:get_tail()
return self[self.head + self.length]
end
function list:pop_tail()
if self.length == 0 then return end
local value = self:get_tail()
self[self.head + self.length] = nil
self.length = self.length - 1
return value
end
function list:push_head(value)
self[self.head] = value
self.head = self.head - 1
self.length = self.length + 1
end
function list:get_head()
return self[self.head + 1]
end
function list:pop_head()
if self.length == 0 then return end
local value = self:get_head()
self.length = self.length - 1
self.head = self.head + 1
self[self.head] = nil
return value
end
return list

60
mods/modlib/heap.lua Normal file
View file

@ -0,0 +1,60 @@
-- Localize globals
local math_floor, setmetatable, table_insert = math.floor, setmetatable, table.insert
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
function less_than(a, b) return a < b end
--> empty min heap
function new(less_than)
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)
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
end
-- Export environment
return _ENV

147
mods/modlib/init.lua Normal file
View file

@ -0,0 +1,147 @@
local modules = {}
for _, file in pairs{
"schema",
"file",
"func",
"less_than",
"iterator",
"math",
"table",
"vararg",
"text",
"utf8",
"vector",
"matrix4",
"quaternion",
"trie",
"kdtree",
"hashlist",
"hashheap",
"heap",
"binary",
"b3d",
"json",
"luon",
"bluon",
"base64",
"persistence",
"debug",
"web",
"tex"
} do
modules[file] = file
end
if minetest then
modules.minetest = "minetest"
end
-- modlib.mod is an alias for modlib.minetest.mod
modules.string = "text"
modules.number = "math"
local parent_dir
if not minetest then
-- TOFIX
local init_path = arg and arg[0]
parent_dir = init_path and init_path:match"^.[/\\]" or ""
end
local dir_delim = rawget(_G, "DIR_DELIM") -- Minetest
or (rawget(_G, "package") and package.config and assert(package.config:match("^(.-)[\r\n]"))) or "/"
local function concat_path(path)
return table.concat(path, dir_delim)
end
-- only used if Minetest is available
local function get_resource(modname, resource, ...)
if not resource then
resource = modname
modname = minetest.get_current_modname()
end
return concat_path{minetest.get_modpath(modname), resource, ...}
end
local function load_module(self, module_name_or_alias)
local module_name = modules[module_name_or_alias]
if not module_name then
-- no such module
return
end
local environment
if module_name ~= module_name_or_alias then
-- alias handling
environment = self[module_name]
else
environment = dofile(minetest
and get_resource(self.modname, module_name .. ".lua")
or (parent_dir .. module_name .. ".lua"))
end
self[module_name_or_alias] = environment
return environment
end
local rawget, rawset = rawget, rawset
modlib = setmetatable({}, { __index = load_module })
-- TODO bump on release
modlib.version = 103
if minetest then
modlib.modname = minetest.get_current_modname()
end
-- Raw globals
modlib._RG = setmetatable({}, {
__index = function(_, index)
return rawget(_G, index)
end,
__newindex = function(_, index, value)
return rawset(_G, index, value)
end
})
-- Globals merged with modlib
modlib.G = setmetatable({}, {__index = function(self, module_name)
local module = load_module(self, module_name)
if module == nil then
return _G[module_name]
end
if _G[module_name] then
setmetatable(module, {__index = _G[module_name]})
end
return module
end})
-- "Imports" modlib by changing the environment of the calling function
--! This alters environments at the expense of performance. Use with caution.
--! Prefer localizing modlib library functions or API tables if possible.
function modlib.set_environment()
setfenv(2, setmetatable({}, {__index = modlib.G}))
end
-- Force load file module to pass dir_delim & to set concat_path
modlib.file = assert(loadfile(get_resource"file.lua"))(dir_delim)
modlib.file.concat_path = concat_path
if minetest then
-- Force-loading of the minetest & mod modules
-- Also sets modlib.mod -> modlib.minetest.mod alias.
local ml_mt = modlib.minetest
ml_mt.mod.get_resource = get_resource
modlib.mod = ml_mt.mod
-- HACK force load minetest/gametime.lua to ensure that the globalstep is registered earlier than globalsteps of mods depending on modlib
dofile(get_resource(modlib.modname, "minetest", "gametime.lua"))
local ie = minetest.request_insecure_environment()
if ie then
-- Force load persistence namespace to pass insecure require
-- TODO currently no need to set _G.require, lsqlite3 loads no dependencies that way
modlib.persistence = assert(loadfile(get_resource"persistence.lua"))(ie.require)
end
end
-- Run build scripts
-- dofile(modlib.mod.get_resource("modlib", "build", "html_entities.lua"))
-- TODO verify localizations suffice
return modlib

311
mods/modlib/iterator.lua Normal file
View file

@ -0,0 +1,311 @@
--[[
Iterators are always the *last* argument(s) to all functions here,
which differs from other modules which take what they operate on as first argument.
This is because iterators consist of three variables - iterator function, state & control variable -
and wrapping them (using a table, a closure or the like) would be rather inconvenient.
Having them as the last argument allows to just pass in the three variables returned by functions such as `[i]pairs`.
Additionally, putting functions first - although syntactically inconvenient - is consistent with Python and Lisp.
]]
local coroutine_create, coroutine_resume, coroutine_yield, coroutine_status, unpack, select
= coroutine.create, coroutine.resume, coroutine.yield, coroutine.status, unpack, select
local identity, not_, add = modlib.func.identity, modlib.func.not_, modlib.func.add
--+ For all functions which aggregate over single values, use modlib.table.ivalues - not ipairs - for lists!
--+ Otherwise they will be applied to the indices.
local iterator = {}
function iterator.wrap(iterator, state, control_var)
local function update_control_var(...)
control_var = ...
return ...
end
return function()
return update_control_var(iterator(state, control_var))
end
end
iterator.closure = iterator.wrap
iterator.make_stateful = iterator.wrap
function iterator.filter(predicate, iterator, state, control_var)
local function _filter(...)
local cvar = ...
if cvar == nil then
return
end
if predicate(...) then
return ...
end
return _filter(iterator(state, cvar))
end
return function(state, control_var)
return _filter(iterator(state, control_var))
end, state, control_var
end
function iterator.truthy(...)
return iterator.filter(identity, ...)
end
function iterator.falsy(...)
return iterator.filter(not_, ...)
end
function iterator.map(map_func, iterator, state, control_var)
local function _map(...)
control_var = ... -- update control var
if control_var == nil then return end
return map_func(...)
end
return function()
return _map(iterator(state, control_var))
end
end
function iterator.map_values(map_func, iterator, state, control_var)
local function _map_values(cvar, ...)
if cvar == nil then return end
return cvar, map_func(...)
end
return function(state, control_var)
return _map_values(iterator(state, control_var))
end, state, control_var
end
-- Iterator must be restartable
function iterator.rep(times, iterator, state, control_var)
times = times or 1
if times == 1 then
return iterator, state, control_var
end
local function _rep(cvar, ...)
if cvar == nil then
times = times - 1
if times == 0 then return end
return _rep(iterator(state, control_var))
end
return cvar, ...
end
return function(state, control_var)
return _rep(iterator(state, control_var))
end, state, control_var
end
-- Equivalent to `for x, y, z in iterator, state, ... do callback(x, y, z) end`
function iterator.foreach(callback, iterator, state, ...)
local function loop(...)
if ... == nil then return end
callback(...)
return loop(iterator(state, ...))
end
return loop(iterator(state, ...))
end
function iterator.for_generator(caller, ...)
local co = coroutine_create(function(...)
return caller(function(...)
return coroutine_yield(...)
end, ...)
end)
local args, n_args = {...}, select("#", ...)
return function()
if coroutine_status(co) == "dead" then
return
end
local function _iterate(status, ...)
if not status then
error((...))
end
return ...
end
return _iterate(coroutine_resume(co, unpack(args, 1, n_args)))
end
end
function iterator.range(from, to, step)
if not step then
if not to then
from, to = 1, from
end
step = 1
end
return function(_, current)
current = current + step
if current > to then
return
end
return current
end, nil, from - step
end
function iterator.aggregate(binary_func, total, ...)
for value in ... do
total = binary_func(total, value)
end
return total
end
-- Like `iterator.aggregate`, but does not expect a `total`
function iterator.reduce(binary_func, iterator, state, control_var)
local total = iterator(state, control_var)
if total == nil then
return -- nothing if the iterator is empty
end
for value in iterator, state, total do
total = binary_func(total, value)
end
return total
end
iterator.fold = iterator.reduce
-- TODO iterator.find(predicate, iterator, state, control_var)
function iterator.any(...)
for val in ... do
if val then return true end
end
return false
end
function iterator.all(...)
for val in ... do
if not val then return false end
end
return true
end
function iterator.min(less_than_func, ...)
local min
for value in ... do
if min == nil or less_than_func(value, min) then
min = value
end
end
return min
end
-- TODO iterator.max
function iterator.empty(iterator, state, control_var)
return iterator(state, control_var) == nil
end
function iterator.first(iterator, state, control_var)
return iterator(state, control_var)
end
function iterator.last(iterator, state, control_var)
-- Storing a vararg in a table seems to be necessary: https://stackoverflow.com/questions/73914273/
-- This could be optimized further for memory by keeping the same table across calls,
-- but that might cause issues with multiple coroutines calling this
local last, last_n = {}, 0
local function _last(...)
local cvar = ...
if cvar == nil then
return unpack(last, 1, last_n)
end
-- Write vararg to table: Avoid the creation of a garbage table every iteration by reusing the same table
last_n = select("#", ...)
for i = 1, last_n do
last[i] = select(i, ...)
end
return _last(iterator(state, cvar))
end
return _last(iterator(state, control_var))
end
-- Converts a vararg starting with `nil` (end of loop control variable) into nothing
local function nil_to_nothing(...)
if ... == nil then return end
return ...
end
function iterator.select(n, iterator, state, control_var)
for _ = 1, n - 1 do
control_var = iterator(state, control_var)
if control_var == nil then return end
end
-- Either all values returned by the n-th call iteration
-- or nothing if the iterator holds fewer than `n` values
return nil_to_nothing(iterator(state, control_var))
end
function iterator.limit(count, iterator, state, control_var)
return function(state, control_var)
count = count - 1
if count < 0 then return end
return iterator(state, control_var)
end, state, control_var
end
function iterator.count(...)
local count = 0
for _ in ... do
count = count + 1
end
return count
end
function iterator.sum(...)
return iterator.aggregate(add, 0, ...)
end
function iterator.average(...)
local count = 0
local sum = 0
for value in ... do
count = count + 1
sum = sum + value
end
return sum / count
end
--: ... **restartable** iterator
-- A single pass method for calculating the standard deviation exists but is highly inaccurate
function iterator.standard_deviation(...)
local avg = iterator.average(...)
local count = 0
local sum = 0
for value in ... do
count = count + 1
sum = sum + (value - avg)^2
end
return (sum / count)^.5
end
-- Comprehensions ("collectors")
-- Shorthand for `for k, v in ... do t[k] = v end`
function iterator.to_table(...)
local t = {}
for k, v in ... do
t[k] = v
end
return t
end
-- Shorthand for `for k in ... do t[#t + 1] = k end`
function iterator.to_list(...)
local t = {}
for k in ... do
t[#t + 1] = k
end
return t
end
-- Shorthand for `for k in ... do t[k] = true end`
function iterator.to_set(...)
local t = {}
for k in ... do
t[k] = true
end
return t
end
return iterator

402
mods/modlib/json.lua Normal file
View file

@ -0,0 +1,402 @@
local modlib, setmetatable, pairs, assert, error, table_insert, table_concat, tonumber, tostring, math_huge, string, type, next
= modlib, setmetatable, pairs, assert, error, table.insert, table.concat, tonumber, tostring, math.huge, string, type, next
local _ENV = {}
setfenv(1, _ENV)
-- See https://tools.ietf.org/id/draft-ietf-json-rfc4627bis-09.html#unichars and https://json.org
-- Null
-- TODO consider using userdata (for ex. by using newproxy)
do
local metatable = {}
-- eq is not among the metamethods, len won't work on 5.1
for _, metamethod in pairs{"add", "sub", "mul", "div", "mod", "pow", "unm", "concat", "len", "lt", "le", "index", "newindex", "call"} do
metatable["__" .. metamethod] = function() return error("attempt to " .. metamethod .. " a null value") end
end
null = setmetatable({}, metatable)
end
local metatable = {__index = _ENV}
_ENV.metatable = metatable
function new(self)
return setmetatable(self, metatable)
end
local whitespace = modlib.table.set{"\t", "\r", "\n", " "}
local decoding_escapes = {
['"'] = '"',
["\\"] = "\\",
["/"] = "/",
b = "\b",
f = "\f",
n = "\n",
r = "\r",
t = "\t"
-- TODO is this complete?
}
-- Set up a DFA for number syntax validations
local number_dfa
do -- as a RegEx: (0|(1-9)(0-9)*)[.(0-9)+[(e|E)[+|-](0-9)+]]; does not need to handle the first sign
-- TODO proper DFA utilities
local function set_transitions(state, transitions)
for chars, next_state in pairs(transitions) do
for char in chars:gmatch"." do
state[char] = next_state
end
end
end
local onenine = "123456789"
local digit = "0" .. onenine
local e = "eE"
local exponent = {final = true}
set_transitions(exponent, {
[digit] = exponent
})
local pre_exponent = {expected = "exponent"}
set_transitions(pre_exponent, {
[digit] = exponent
})
local exponent_sign = {expected = "exponent"}
set_transitions(exponent_sign, {
[digit] = exponent,
["+"] = exponent,
["-"] = exponent
})
local fraction_final = {final = true}
set_transitions(fraction_final, {
[digit] = fraction_final,
[e] = exponent_sign
})
local fraction = {expected = "fraction"}
set_transitions(fraction, {
[digit] = fraction_final
})
local integer = {final = true}
set_transitions(integer, {
[digit] = integer,
[e] = exponent_sign,
["."] = fraction
})
local zero = {final = true}
set_transitions(zero, {
["."] = fraction
})
number_dfa = {}
set_transitions(number_dfa, {
[onenine] = integer,
["0"] = zero
})
end
local hex_digit_values = {}
for i = 0, 9 do
hex_digit_values[tostring(i)] = i
end
for i = 0, 5 do
hex_digit_values[string.char(("a"):byte() + i)] = 10 + i
hex_digit_values[string.char(("A"):byte() + i)] = 10 + i
end
-- TODO SAX vs DOM
local utf8_char = modlib.utf8.char
function read(self, read_)
local index = 0
local char
-- TODO support read functions which provide additional debug output (such as row:column)
local function read()
index = index + 1
char = read_()
return char
end
local function syntax_error(errmsg)
-- TODO ensure the index isn't off
error("syntax error: " .. index .. ": " .. errmsg)
end
local function syntax_assert(value, errmsg)
if not value then
syntax_error(errmsg or "assertion failed!")
end
return value
end
local function skip_whitespace()
while whitespace[char] do
read()
end
end
-- Forward declaration
local value
local function number()
local state = number_dfa
local num = {}
while true do
-- Will work for nil too
local next_state = state[char]
if not next_state then
if not state.final then
if state == number_dfa then
syntax_error"expected a number"
end
syntax_error("invalid number: expected " .. state.expected)
end
return assert(tonumber(table_concat(num)))
end
table_insert(num, char)
state = next_state
read()
end
end
local function utf8_codepoint(codepoint)
return syntax_assert(utf8_char(codepoint), "invalid codepoint")
end
local function string()
local chars = {}
local high_surrogate
while true do
local string_char, next_high_surrogate
if char == '"' then
if high_surrogate then
table_insert(chars, utf8_codepoint(high_surrogate))
end
return table_concat(chars)
end
if char == "\\" then
read()
if char == "u" then
local codepoint = 0
for i = 3, 0, -1 do
codepoint = syntax_assert(hex_digit_values[read()], "expected a hex digit") * (16 ^ i) + codepoint
end
if high_surrogate and codepoint >= 0xDC00 and codepoint <= 0xDFFF then
-- TODO strict mode: throw an error for single surrogates
codepoint = 0x10000 + (high_surrogate - 0xD800) * 0x400 + codepoint - 0xDC00
-- Don't write the high surrogate
high_surrogate = nil
end
if codepoint >= 0xD800 and codepoint <= 0xDBFF then
next_high_surrogate = codepoint
else
string_char = utf8_codepoint(codepoint)
end
else
string_char = syntax_assert(decoding_escapes[char], "invalid escape sequence")
end
else
-- TODO check whether the character is one that must be escaped ("strict" mode)
string_char = syntax_assert(char, "unclosed string")
end
if high_surrogate then
table_insert(chars, utf8_codepoint(high_surrogate))
end
high_surrogate = next_high_surrogate
if string_char then
table_insert(chars, string_char)
end
read()
end
end
local element
local funcs = {
['-'] = function()
return -number()
end,
['"'] = string,
["{"] = function()
local dict = {}
skip_whitespace()
if char == "}" then return dict end
while true do
syntax_assert(char == '"', "key expected")
read()
local key = string()
read()
skip_whitespace()
syntax_assert(char == ":", "colon expected, got " .. char)
local val = element()
dict[key] = val
if char == "}" then return dict end
syntax_assert(char == ",", "comma expected")
read()
skip_whitespace()
end
end,
["["] = function()
local list = {}
skip_whitespace()
if char == "]" then return list end
while true do
table_insert(list, value())
skip_whitespace()
if char == "]" then return list end
syntax_assert(char == ",", "comma expected")
read()
skip_whitespace()
end
end,
}
local function expect_word(word, value)
local msg = word .. " expected"
funcs[word:sub(1, 1)] = function()
syntax_assert(char == word:sub(2, 2), msg)
for i = 3, #word do
read()
syntax_assert(char == word:sub(i, i), msg)
end
return value
end
end
expect_word("true", true)
expect_word("false", false)
expect_word("null", self.null)
function value()
syntax_assert(char, "value expected")
local func = funcs[char]
if func then
-- Advance after first char
read()
local val = func()
-- Advance after last char
read()
return val
end
if char >= "0" and char <= "9" then
return number()
end
syntax_error"value expected"
end
function element()
read()
skip_whitespace()
local val = value()
skip_whitespace()
return val
end
-- TODO consider asserting EOF as read() == nil, perhaps controlled by a parameter
return element()
end
local encoding_escapes = modlib.table.flip(decoding_escapes)
-- Solidus does not need to be escaped
encoding_escapes["/"] = nil
-- Control characters. Note: U+0080 to U+009F and U+007F are not considered control characters.
for byte = 0, 0x1F do
encoding_escapes[string.char(byte)] = string.format("u%04X", byte)
end
modlib.table.map(encoding_escapes, function(str) return "\\" .. str end)
local function escape(str)
return str:gsub(".", encoding_escapes)
end
function write(self, value, write)
local null = self.null
local written_strings = self.cache_escaped_strings and setmetatable({}, {__index = function(self, str)
local escaped_str = escape(str)
self[str] = escaped_str
return escaped_str
end})
local function string(str)
write'"'
write(written_strings and written_strings[str] or escape(str))
return write'"'
end
local dump
local function write_kv(key, value)
assert(type(key) == "string", "not a dictionary")
string(key)
write":"
dump(value)
end
function dump(value)
if value == null then
-- TODO improve null check (checking for equality doesn't allow using nan as null, for instance)
return write"null"
end
if value == true then
return write"true"
end
if value == false then
return write"false"
end
local type_ = type(value)
if type_ == "number" then
assert(value == value, "unsupported number value: nan")
assert(value ~= math_huge, "unsupported number value: inf")
assert(value ~= -math_huge, "unsupported number value: -inf")
return write(("%.17g"):format(value))
end
if type_ == "string" then
return string(value)
end
if type_ == "table" then
local table = value
local len = #table
if len == 0 then
local first, value = next(table)
write"{"
if first ~= nil then
write_kv(first, value)
end
for key, value in next, table, first do
write","
write_kv(key, value)
end
write"}"
else
assert(modlib.table.count(table) == len, "mixed list & hash part")
write"["
for i = 1, len - 1 do
dump(table[i])
write","
end
dump(table[len])
write"]"
end
return
end
error("unsupported type: " .. type_)
end
dump(value)
end
-- TODO get rid of this paste of write_file and write_string (see modlib.luon)
function write_file(self, value, file)
return self:write(value, function(text)
file:write(text)
end)
end
function write_string(self, value)
local rope = {}
self:write(value, function(text)
table_insert(rope, text)
end)
return table_concat(rope)
end
-- TODO read_path (for other serializers too)
function read_file(self, file)
local value = self:read(function()
return file:read(1)
end)
-- TODO consider file:close()
return value
end
function read_string(self, string)
-- TODO move the string -> one char read func pattern to modlib.text
local index = 0
local value = self:read(function()
index = index + 1
if index > #string then
return
end
return string:sub(index, index)
end)
-- We just expect EOF for strings
assert(index > #string, "EOF expected")
return value
end
return _ENV

64
mods/modlib/kdtree.lua Normal file
View file

@ -0,0 +1,64 @@
-- Localize globals
local assert, math, modlib, setmetatable, table, unpack = assert, math, modlib, setmetatable, table, unpack
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
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)
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)
local next_axis = ((axis + 1) % dimension) + 1
return setmetatable({
axis = axis,
pivot = vectors[median],
left = builder({ unpack(vectors, 1, median) }, next_axis),
right = builder({ unpack(vectors, median + 1) }, next_axis)
}, metatable)
end
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
end
-- TODO insertion & deletion + rebalancing
-- Export environment
return _ENV

53
mods/modlib/less_than.lua Normal file
View file

@ -0,0 +1,53 @@
-- Comparator utilities for "less than" functions returning whether a < b
local less_than = {}
setfenv(1, less_than)
default = {}
function default.less_than(a, b) return a < b end; default.lt = default.less_than
function default.less_or_equal(a, b) return a <= b end; default.leq = default.less_or_equal
function default.greater_than(a, b) return a > b end; default.gt = default.greater_than
function default.greater_or_equal(a, b) return a >= b end; default.geq = default.greater_or_equal
function less_or_equal(less_than)
return function(a, b) return not less_than(b, a) end
end
leq = less_or_equal
function greater_or_equal(less_than)
return function(a, b) return not less_than(a, b) end
end
geq = greater_or_equal
function greater_than(less_than)
return function(a, b) return less_than(b, a) end
end
gt = greater_than
function equal(less_than)
return function(a, b)
return not (less_than(a, b) or less_than(b, a))
end
end
function relation(less_than)
return function(a, b)
if less_than(a, b) then return "<"
elseif less_than(b, a) then return ">"
else return "=" end
end
end
function by_func(func)
return function(a, b)
return func(a) < func(b)
end
end
function by_field(key)
return function(a, b)
return a[key] < b[key]
end
end
return less_than

38
mods/modlib/logo.svg Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="svg2856"
height="48px"
width="48px">
<defs
id="defs2858" />
<metadata
id="metadata2861">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="fill:#030380;fill-opacity:1;stroke:#7f7f7f;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 11.75,30.567358 24,37.999999 36.25,31.287557 V 16.71244 L 24,10 11.75,17.432641 Z"
id="path3837" />
<path
style="fill:#030380;fill-opacity:1;stroke:#7f7f7f;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 30.318359,8.1694856 34.693,10.824001 39.068358,8.4266998 V 3.2213009 L 34.693,0.8240013 30.318359,3.4785156 Z"
id="path3837-6-3" />
<path
d="m 24,0.421875 -20.625,12.5 v 22.15625 L 3.625,35.222656 24,47.578125 44.625,36.261719 V 11.738281 l -7.202778,-4.4175761 -1,0.578125 L 43.625,12.316406 V 35.683594 L 24,46.421875 4.375,34.5 v -21 L 24,1.578125 30.318359,4.6347656 v -1.15625 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#7f7f7f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:6.5;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000"
id="path873" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

207
mods/modlib/luon.lua Normal file
View file

@ -0,0 +1,207 @@
-- Lua module to serialize values as Lua code
local assert, error, rawget, pairs, pcall, type, setfenv, setmetatable, select, loadstring, loadfile
= assert, error, rawget, pairs, pcall, type, setfenv, setmetatable, select, loadstring, loadfile
local table_concat, string_format, math_huge
= table.concat, string.format, math.huge
local count_objects = modlib.table.count_objects
local is_identifier = modlib.text.is_identifier
local function quote(string)
return string_format("%q", string)
end
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
_ENV.metatable = metatable
function new(self)
return setmetatable(self, metatable)
end
function aux_write(_self, _object)
-- returns reader, arguments
return
end
aux_read = {}
function write(self, value, write)
-- TODO evaluate custom aux. writers *before* writing for circular structs
local reference, refnum = "1", 1
-- [object] = reference
local references = {}
-- Circular tables that must be filled using `table[key] = value` statements
local to_fill = {}
-- TODO (?) sort objects by count, give frequently referenced objects shorter references
for object, count in pairs(count_objects(value)) do
local type_ = type(object)
-- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
if count >= 2 and (type_ ~= "string" or #reference + 5 < #object) then
if refnum == 1 then
write"local _={};" -- initialize reference table
end
write"_["
write(reference)
write"]="
if type_ == "table" then
write"{}"
elseif type_ == "string" then
write(quote(object))
end
write";"
references[object] = reference
if type_ == "table" then
to_fill[object] = reference
end
refnum = refnum + 1
reference = string_format("%d", refnum)
end
end
-- Used to decide whether we should do "key=..."
local function use_short_key(key)
return not references[key] and type(key) == "string" and is_identifier(key)
end
local function dump(value)
-- Primitive types
if value == nil then
return write"nil"
end if value == true then
return write"true"
end if value == false then
return write"false"
end
local type_ = type(value)
if type_ == "number" then
-- Explicit handling of special values for forwards compatibility
if value ~= value then -- nan
return write"0/0"
end if value == math_huge then
return write"1/0"
end if value == -math_huge then
return write"-1/0"
end
return write(string_format("%.17g", value))
end
-- Reference types: table and string
local ref = references[value]
if ref then
-- Referenced
write"_["
write(ref)
return write"]"
end if type_ == "string" then
return write(quote(value))
end if type_ == "table" then
write"{"
-- First write list keys:
-- Don't use the table length #value here as it may horribly fail
-- for tables which use large integers as keys in the hash part;
-- stop at the first "hole" (nil value) instead
local len = 0
local first = true -- whether this is the first entry, which may not have a leading comma
while true do
local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
if v == nil then break end
if first then first = false else write(",") end
dump(v)
len = len + 1
end
-- Now write map keys ([key] = value)
for k, v in pairs(value) do
-- We have written all non-float keys in [1, len] already
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
if first then first = false else write(",") end
if use_short_key(k) then
write(k)
else
write"["
dump(k)
write"]"
end
write"="
dump(v)
end
end
return write"}"
end
-- TODO move aux_write to start, to allow dealing with metatables etc.?
return (function(func, ...)
-- functions are the only way to deal with varargs
if not func then
return error("unsupported type: " .. type_)
end
write(func)
write"("
local n = select("#", ...)
for i = 1, n - 1 do
dump(select(i, ...))
write","
end
if n > 0 then
dump(select(n, ...))
end
write")"
end)(self:aux_write(value))
end
-- Write the statements to fill circular tables
for table, ref in pairs(to_fill) do
for k, v in pairs(table) do
write"_["
write(ref)
write"]"
if use_short_key(k) then
write"."
write(k)
else
write"["
dump(k)
write"]"
end
write"="
dump(v)
write";"
end
end
write"return "
dump(value)
end
function write_file(self, value, file)
return self:write(value, function(text)
file:write(text)
end)
end
function write_string(self, value)
local rope = {}
self:write(value, function(text)
rope[#rope + 1] = text
end)
return table_concat(rope)
end
function read(self, ...)
local read = assert(...)
-- math.huge was serialized to inf, 0/0 was serialized to -nan by `%.17g`
setfenv(read, setmetatable({inf = math_huge, nan = 0/0}, {__index = self.aux_read}))
local success, value_or_err = pcall(read)
if success then
return value_or_err
end
return nil, value_or_err
end
function read_file(self, path)
return self:read(loadfile(path))
end
function read_string(self, string)
return self:read(loadstring(string))
end
return _ENV

182
mods/modlib/math.lua Normal file
View file

@ -0,0 +1,182 @@
-- Localize globals
local assert, math, math_floor, minetest, modlib_table_reverse, os, string_char, select, setmetatable, table_insert, table_concat
= assert, math, math.floor, minetest, modlib.table.reverse, os, string.char, select, setmetatable, table.insert, table.concat
local inf = math.huge
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
-- TODO might be too invasive
-- Make random random
math.randomseed(minetest and minetest.get_us_time() or os.time() + os.clock())
for _ = 1, 100 do math.random() end
negative_nan = 0/0
positive_nan = negative_nan ^ 1
function sign(number)
if number ~= number then return number end -- nan
if number == 0 then return 0 end
if number < 0 then return -1 end
if number > 0 then return 1 end
end
function clamp(number, min, max)
return math.min(math.max(number, min), max)
end
-- Random integer from 0 to 2^53 - 1 (inclusive)
local function _randint()
return math.random(0, 2^27 - 1) * 2^26 + math.random(0, 2^26 - 1)
end
-- Random float from 0 to 1 (exclusive)
local function _randfloat()
return _randint() / (2^53)
end
--+ Increased randomness float random without overflows
--+ `random()`: Random number from `0` to `1` (exclusive)
--+ `random(max)`: Random number from `0` to `max` (exclusive)
--+ `random(min, max)`: Random number from `min` to `max` (exclusive)
function random(...)
local n = select("#", ...)
if n == 0 then
return _randfloat()
end if n == 1 then
local max = ...
return _randfloat() * max
end do assert(n == 2)
local min, max = ...
return min + (max - min) * _randfloat()
end
end
-- Increased randomness integer random
--+ `randint()`: Random integer from `0` to `2^53 - 1` (inclusive)
--+ `randint(max)`: Random integer from `0` to `max` (inclusive)
--+ `randint(min, max)`: Random integer from `min` to `max` (inclusive)
function randint(...)
local n = select("#", ...)
if n == 0 then
return _randint()
end if n == 1 then
local max = ...
return math.floor(_randfloat() * max + 0.5)
end do assert(n == 2)
local min, max = ...
return min + math.floor(_randfloat() * (max - min) + 0.5)
end
end
log = setmetatable({}, {
__index = function(self, base)
local div = math.log(base)
local function base_log(number)
return math.log(number) / div
end
self[base] = base_log
return base_log
end,
__call = function(_, number, base)
if not base then
return math.log(number)
end
return math.log(number) / math.log(base)
end
})
-- one-based mod
function onemod(number, modulus)
return ((number - 1) % modulus) + 1
end
function round(number, steps)
steps = steps or 1
return math_floor(number * steps + 0.5) / steps
end
local c0 = ("0"):byte()
local cA = ("A"):byte()
function default_digit_function(digit)
if digit <= 9 then return string_char(c0 + digit) end
return string_char(cA + digit - 10)
end
default_precision = 10
-- See https://github.com/appgurueu/Luon/blob/master/index.js#L724
function tostring(number, base, digit_function, precision)
if number ~= number then
return "nan"
end
if number == inf then
return "inf"
end
if number == -inf then
return "-inf"
end
digit_function = digit_function or default_digit_function
precision = precision or default_precision
local out = {}
if number < 0 then
table_insert(out, "-")
number = -number
end
-- Rounding
number = number + base ^ -precision / 2
local digit
while number >= base do
digit = math_floor(number % base)
table_insert(out, digit_function(digit))
number = (number - digit) / base
end
digit = math_floor(number)
table_insert(out, digit_function(digit))
modlib_table_reverse(out)
number = number % 1
if number ~= 0 and number >= base ^ -precision then
table_insert(out, ".")
while precision >= 0 and number >= base ^ -precision do
number = number * base
digit = math_floor(number % base)
table_insert(out, digit_function(digit))
number = number - digit
precision = precision - 1
end
end
return table_concat(out)
end
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround#polyfill
-- Rounds a 64-bit float to a 32-bit float;
-- if the closest 32-bit float is out of bounds,
-- the appropriate infinity is returned.
function fround(number)
if number == 0 or number ~= number then
return number
end
local sign = 1
if number < 0 then
sign = -1
number = -number
end
local _, exp = math.frexp(number)
exp = exp - 1 -- we want 2^exponent >= number > 2^(exponent-1)
local powexp = 2 ^ math.max(-126, math.min(exp, 127))
local leading = exp <= -127 and 0 or 1 -- subnormal number?
local mantissa = math.floor((number / powexp - leading) * 0x800000 + 0.5)
if
mantissa > 0x800000 -- doesn't fit in mantissa
or (exp >= 127 and mantissa == 0x800000) -- fits if the exponent can be increased
then
return sign * inf
end
return sign * powexp * (leading + mantissa / 0x800000)
end
-- Export environment
return _ENV

180
mods/modlib/matrix4.lua Normal file
View file

@ -0,0 +1,180 @@
-- Simple 4x4 matrix for 3d transformations (translation, rotation, scale);
-- provides exactly the methods needed to calculate inverse bind matrices (for b3d -> glTF conversion)
local mat4 = {}
local metatable = {__index = mat4}
function mat4.new(rows)
assert(#rows == 4)
for i = 1, 4 do
assert(#rows[i] == 4)
end
return setmetatable(rows, metatable)
end
function mat4.identity()
return mat4.new{
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1},
}
end
-- Matrices can't properly represent translation:
-- => work with 4d vectors, assume w = 1.
function mat4.translation(vec)
assert(#vec == 3)
local x, y, z = unpack(vec)
return mat4.new{
{1, 0, 0, x},
{0, 1, 0, y},
{0, 0, 1, z},
{0, 0, 0, 1},
}
end
-- See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
function mat4.rotation(unit_quat)
assert(#unit_quat == 4)
local x, y, z, w = unpack(unit_quat) -- TODO (?) assert unit quaternion
return mat4.new{
{1 - 2*(y^2 + z^2), 2*(x*y - z*w), 2*(x*z + y*w), 0},
{2*(x*y + z*w), 1 - 2*(x^2 + z^2), 2*(y*z - x*w), 0},
{2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x^2 + y^2), 0},
{0, 0, 0, 1},
}
end
function mat4.scale(vec)
assert(#vec == 3)
local x, y, z = unpack(vec)
return mat4.new{
{x, 0, 0, 0},
{0, y, 0, 0},
{0, 0, z, 0},
{0, 0, 0, 1},
}
end
-- Apply `self` to a 4d modlib vector `vec`
function mat4:apply(vec)
assert(#vec == 4)
local res = {}
for i = 1, 4 do
local sum = 0
for j = 1, 4 do
sum = sum + self[i][j] * vec[j]
end
res[i] = sum
end
return vec.new(res)
end
-- Multiplication: First apply other, then self
--> Matrix product `self * other`
function mat4:multiply(other)
local res = {}
for i = 1, 4 do
res[i] = {}
for j = 1, 4 do
local sum = 0 -- dot product of row & col vec
for k = 1, 4 do
sum = sum + self[i][k] * other[k][j]
end
res[i][j] = sum
end
end
return mat4.new(res)
end
-- Composition: First apply self, then other
function mat4:compose(other)
return other:multiply(self) -- equivalent to `other * self` in terms of matrix multiplication
end
-- Matrix inversion using Gauss-Jordan elimination
do
-- Fundamental operations
local function _swap_rows(mat, i, j)
mat[i], mat[j] = mat[j], mat[i]
end
local function _scale_row(mat, factor, row_idx)
for i = 1, 4 do
mat[row_idx][i] = factor * mat[row_idx][i]
end
end
local function _add_row_with_factor(mat, factor, src_row_idx, dst_row_idx)
assert(src_row_idx ~= dst_row_idx)
for i = 1, 4 do
mat[dst_row_idx][i] = mat[dst_row_idx][i] + factor * mat[src_row_idx][i]
end
end
local epsilon = 1e-6 -- small threshold; values below this are considered zero
function mat4:inverse()
local inv = mat4.identity() -- inverse matrix: all elimination operations will also be applied to this
local copy = {} -- copy of `self` the Gaussian elimination is being executed on
for i = 1, 4 do
copy[i] = {}
for j = 1, 4 do
copy[i][j] = self[i][j]
end
end
-- All operations must be mirrored to the inverse matrix
local function swap_rows(i, j)
_swap_rows(copy, i, j)
_swap_rows(inv, i, j)
end
local function scale_row(factor, row_idx)
_scale_row(copy, factor, row_idx)
_scale_row(inv, factor, row_idx)
end
local function add_with_factor(factor, src_row_idx, dst_row_idx)
_add_row_with_factor(copy, factor, src_row_idx, dst_row_idx)
_add_row_with_factor(inv, factor, src_row_idx, dst_row_idx)
end
-- Elimination phase
for col_idx = 1, 4 do
-- Find a pivot row: Choose the row with the largest absolute component
local max_row_idx = col_idx
local max_abs_comp = math.abs(copy[max_row_idx][col_idx])
for row_idx = col_idx, 4 do
local cand_comp = math.abs(copy[row_idx][col_idx])
if cand_comp > max_abs_comp then
max_row_idx, max_abs_comp = row_idx, cand_comp
end
end
-- Assert that there is a row that has this component "nonzero"
assert(max_abs_comp >= epsilon, "matrix not invertible!")
swap_rows(col_idx, max_row_idx) -- swap row to correct position
-- Eliminate the `col_idx`-th component in all rows *below* the pivot row
local pivot_value = copy[col_idx][col_idx]
for row_idx = col_idx + 1, 4 do
local factor = -copy[row_idx][col_idx] / pivot_value
add_with_factor(factor, col_idx, row_idx)
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
end
end
-- Resubstitution phase - pretty much the same but in reverse and without swapping
for col_idx = 4, 1, -1 do
local pivot_value = copy[col_idx][col_idx]
-- Eliminate the `col_idx`-th component in all rows *above* the pivot row
for row_idx = col_idx - 1, 1, -1 do
local factor = -copy[row_idx][col_idx] / pivot_value
add_with_factor(factor, col_idx, row_idx)
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
end
scale_row(1/pivot_value, col_idx) -- normalize row
end
-- Done: `copy` should now be the identity matrix <=> `inv` is the inverse.
return inv
end
end
return mat4

90
mods/modlib/minetest.lua Normal file
View file

@ -0,0 +1,90 @@
local _ENV = {}
local components = {}
for _, value in pairs{
"mod",
"luon",
"raycast",
"schematic",
"colorspec",
"media",
"obj",
"texmod",
} do
components[value] = value
end
-- These dirty files have to write to the modlib.minetest environment
local dirty_files = {}
for filename, comps in pairs{
-- get_gametime is missing from here as it is forceloaded in init.lua
misc = {
"max_wear",
"override",
"after",
"register_globalstep",
"form_listeners",
"register_form_listener",
"texture_modifier_inventorycube",
"get_node_inventory_image",
"check_player_privs",
"decode_base64",
"objects_inside_radius",
"objects_inside_area",
"nodename_matcher",
"playerdata",
"connected_players",
"set_privs",
"register_on_leaveplayer",
"get_mod_info",
"get_mod_load_order"
},
liquid = {
"liquid_level_max",
"get_liquid_corner_levels",
"flowing_downwards",
"get_liquid_flow_direction"
},
wielditem_change = {
"players",
"registered_on_wielditem_changes",
"register_on_wielditem_change"
},
colorspec = {
"named_colors",
"colorspec_to_colorstring"
},
boxes = {
"get_node_boxes",
"get_node_collisionboxes",
"get_node_selectionboxes",
},
png = {
"decode_png",
"convert_png_to_argb8",
"encode_png",
}
} do
for _, component in pairs(comps) do
components[component] = filename
end
dirty_files[filename] = true
end
local modpath, concat_path = minetest.get_modpath(modlib.modname), modlib.file.concat_path
setmetatable(_ENV, {__index = function(_ENV, name)
local filename = components[name]
if filename then
local loader = assert(loadfile(concat_path{modpath, "minetest", filename .. ".lua"}))
if dirty_files[filename] then
loader(_ENV)
return rawget(_ENV, name)
end
local module = loader()
_ENV[name] = module
return module
end
end})
return _ENV

View file

@ -0,0 +1,166 @@
-- Localize globals
local assert, ipairs, math, minetest, table, type, vector
= assert, ipairs, math, minetest, table, type, vector
-- Set environment
local _ENV = ...
setfenv(1, _ENV)
-- Minetest allows shorthand box = {...} instead of {{...}}
local function get_boxes(box_or_boxes)
return type(box_or_boxes[1]) == "number" and {box_or_boxes} or box_or_boxes
end
local has_boxes_prop = {collision_box = "walkable", selection_box = "pointable"}
-- Required for raycast box IDs to be accurate
local connect_sides_order = {"top", "bottom", "front", "left", "back", "right"}
local connect_sides_directions = {
top = vector.new(0, 1, 0),
bottom = vector.new(0, -1, 0),
front = vector.new(0, 0, -1),
left = vector.new(-1, 0, 0),
back = vector.new(0, 0, 1),
right = vector.new(1, 0, 0),
}
--> list of collisionboxes in Minetest format
local function get_node_boxes(pos, type)
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if not node_def or node_def[has_boxes_prop[type]] == false then
return {}
end
local boxes = {{-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}}
local def_node_box = node_def.drawtype == "nodebox" and node_def.node_box
local def_box = node_def[type] or def_node_box -- will evaluate to def_node_box for type = nil
if not def_box then
return boxes -- default to regular box
end
local box_type = def_box.type
if box_type == "regular" then
return boxes
end
local fixed = def_box.fixed
boxes = get_boxes(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 ipairs(boxes) do
box[5] = level
end
elseif box_type == "wallmounted" then
local dir = minetest.wallmounted_to_dir((paramtype2 == "colorwallmounted" and node.param2 % 8 or node.param2) or 0)
local box
-- The (undocumented!) node box defaults below are taken from `NodeBox::reset`
if dir.y > 0 then
box = def_box.wall_top or {-0.5, 0.5 - 1/16, -0.5, 0.5, 0.5, 0.5}
elseif dir.y < 0 then
box = def_box.wall_bottom or {-0.5, -0.5, -0.5, 0.5, -0.5 + 1/16, 0.5}
else
box = def_box.wall_side or {-0.5, -0.5, -0.5, -0.5 + 1/16, 0.5, 0.5}
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 = connect_sides_directions -- (ab)use directions as a "set" of sides
if node_def.connect_sides then -- build set of sides from given list
connect_sides = {}
for _, side in ipairs(node_def.connect_sides) do
connect_sides[side] = true
end
end
local function add_collisionbox(key)
for _, box in ipairs(get_boxes(def_box[key] or {})) do
table.insert(boxes, box)
end
end
local matchers = {}
for i, nodename_or_group in ipairs(node_def.connects_to or {}) do
matchers[i] = nodename_matcher(nodename_or_group)
end
local function connects_to(nodename)
for _, matcher in ipairs(matchers) do
if matcher(nodename) then
return true
end
end
end
local connected, connected_sides
for _, side in ipairs(connect_sides_order) do
if connect_sides[side] then
local direction = connect_sides_directions[side]
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
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 ipairs(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
return boxes
end
function _ENV.get_node_boxes(pos)
return get_node_boxes(pos, nil)
end
function get_node_selectionboxes(pos)
return get_node_boxes(pos, "selection_box")
end
function get_node_collisionboxes(pos)
return get_node_boxes(pos, "collision_box")
end

View file

@ -0,0 +1,325 @@
-- Localize globals
local assert, error, math, minetest, setmetatable, tonumber, type = assert, error, math, minetest, setmetatable, tonumber, type
local floor = math.floor
-- Set environment
local _ENV = ...
setfenv(1, _ENV)
-- As in src/util/string.cpp
named_colors = {
aliceblue = 0xf0f8ff,
antiquewhite = 0xfaebd7,
aqua = 0x00ffff,
aquamarine = 0x7fffd4,
azure = 0xf0ffff,
beige = 0xf5f5dc,
bisque = 0xffe4c4,
black = 0x000000,
blanchedalmond = 0xffebcd,
blue = 0x0000ff,
blueviolet = 0x8a2be2,
brown = 0xa52a2a,
burlywood = 0xdeb887,
cadetblue = 0x5f9ea0,
chartreuse = 0x7fff00,
chocolate = 0xd2691e,
coral = 0xff7f50,
cornflowerblue = 0x6495ed,
cornsilk = 0xfff8dc,
crimson = 0xdc143c,
cyan = 0x00ffff,
darkblue = 0x00008b,
darkcyan = 0x008b8b,
darkgoldenrod = 0xb8860b,
darkgray = 0xa9a9a9,
darkgreen = 0x006400,
darkgrey = 0xa9a9a9,
darkkhaki = 0xbdb76b,
darkmagenta = 0x8b008b,
darkolivegreen = 0x556b2f,
darkorange = 0xff8c00,
darkorchid = 0x9932cc,
darkred = 0x8b0000,
darksalmon = 0xe9967a,
darkseagreen = 0x8fbc8f,
darkslateblue = 0x483d8b,
darkslategray = 0x2f4f4f,
darkslategrey = 0x2f4f4f,
darkturquoise = 0x00ced1,
darkviolet = 0x9400d3,
deeppink = 0xff1493,
deepskyblue = 0x00bfff,
dimgray = 0x696969,
dimgrey = 0x696969,
dodgerblue = 0x1e90ff,
firebrick = 0xb22222,
floralwhite = 0xfffaf0,
forestgreen = 0x228b22,
fuchsia = 0xff00ff,
gainsboro = 0xdcdcdc,
ghostwhite = 0xf8f8ff,
gold = 0xffd700,
goldenrod = 0xdaa520,
gray = 0x808080,
green = 0x008000,
greenyellow = 0xadff2f,
grey = 0x808080,
honeydew = 0xf0fff0,
hotpink = 0xff69b4,
indianred = 0xcd5c5c,
indigo = 0x4b0082,
ivory = 0xfffff0,
khaki = 0xf0e68c,
lavender = 0xe6e6fa,
lavenderblush = 0xfff0f5,
lawngreen = 0x7cfc00,
lemonchiffon = 0xfffacd,
lightblue = 0xadd8e6,
lightcoral = 0xf08080,
lightcyan = 0xe0ffff,
lightgoldenrodyellow = 0xfafad2,
lightgray = 0xd3d3d3,
lightgreen = 0x90ee90,
lightgrey = 0xd3d3d3,
lightpink = 0xffb6c1,
lightsalmon = 0xffa07a,
lightseagreen = 0x20b2aa,
lightskyblue = 0x87cefa,
lightslategray = 0x778899,
lightslategrey = 0x778899,
lightsteelblue = 0xb0c4de,
lightyellow = 0xffffe0,
lime = 0x00ff00,
limegreen = 0x32cd32,
linen = 0xfaf0e6,
magenta = 0xff00ff,
maroon = 0x800000,
mediumaquamarine = 0x66cdaa,
mediumblue = 0x0000cd,
mediumorchid = 0xba55d3,
mediumpurple = 0x9370db,
mediumseagreen = 0x3cb371,
mediumslateblue = 0x7b68ee,
mediumspringgreen = 0x00fa9a,
mediumturquoise = 0x48d1cc,
mediumvioletred = 0xc71585,
midnightblue = 0x191970,
mintcream = 0xf5fffa,
mistyrose = 0xffe4e1,
moccasin = 0xffe4b5,
navajowhite = 0xffdead,
navy = 0x000080,
oldlace = 0xfdf5e6,
olive = 0x808000,
olivedrab = 0x6b8e23,
orange = 0xffa500,
orangered = 0xff4500,
orchid = 0xda70d6,
palegoldenrod = 0xeee8aa,
palegreen = 0x98fb98,
paleturquoise = 0xafeeee,
palevioletred = 0xdb7093,
papayawhip = 0xffefd5,
peachpuff = 0xffdab9,
peru = 0xcd853f,
pink = 0xffc0cb,
plum = 0xdda0dd,
powderblue = 0xb0e0e6,
purple = 0x800080,
rebeccapurple = 0x663399,
red = 0xff0000,
rosybrown = 0xbc8f8f,
royalblue = 0x4169e1,
saddlebrown = 0x8b4513,
salmon = 0xfa8072,
sandybrown = 0xf4a460,
seagreen = 0x2e8b57,
seashell = 0xfff5ee,
sienna = 0xa0522d,
silver = 0xc0c0c0,
skyblue = 0x87ceeb,
slateblue = 0x6a5acd,
slategray = 0x708090,
slategrey = 0x708090,
snow = 0xfffafa,
springgreen = 0x00ff7f,
steelblue = 0x4682b4,
tan = 0xd2b48c,
teal = 0x008080,
thistle = 0xd8bfd8,
tomato = 0xff6347,
turquoise = 0x40e0d0,
violet = 0xee82ee,
wheat = 0xf5deb3,
white = 0xffffff,
whitesmoke = 0xf5f5f5,
yellow = 0xffff00,
yellowgreen = 0x9acd32
}
colorspec = {}
local metatable = {__index = colorspec}
colorspec.metatable = metatable
function colorspec.new(table)
return setmetatable({
r = assert(table.r),
g = assert(table.g),
b = assert(table.b),
a = table.a or 255
}, metatable)
end
colorspec.from_table = colorspec.new
local c_comp = { "r", "g", "g", "b", "b", "r" }
local x_comp = { "g", "r", "b", "g", "r", "b" }
function colorspec.from_hsv(
-- 0 (inclusive) to 1 (exclusive)
hue,
-- 0 to 1 (both inclusive)
saturation,
-- 0 to 1 (both inclusive)
value
)
hue = hue * 6
local chroma = saturation * value
local m = value - chroma
local color = {r = m, g = m, b = m}
local idx = 1 + floor(hue)
color[c_comp[idx]] = color[c_comp[idx]] + chroma
local x = chroma * (1 - math.abs(hue % 2 - 1))
color[x_comp[idx]] = color[x_comp[idx]] + x
color.r = floor(color.r * 255 + 0.5)
color.g = floor(color.g * 255 + 0.5)
color.b = floor(color.b * 255 + 0.5)
return colorspec.from_table(color)
end
function colorspec.from_string(string)
string = string:lower() -- names and hex are case-insensitive
local number, alpha = named_colors[string], 0xFF
if not number then
local name, alpha_text = string:match("^([a-z]+)#(%x+)$")
if name then
if alpha_text:len() > 2 then
return
end
number = named_colors[name]
if not number then
return
end
alpha = tonumber(alpha_text, 0x10)
if alpha_text:len() == 1 then
alpha = alpha * 0x11
end
end
end
if number then
return colorspec.from_number_rgba(number * 0x100 + alpha)
end
local hex_text = string:match("^#(%x+)$")
if not hex_text then
return
end
local len, num = hex_text:len(), tonumber(hex_text, 0x10)
if len == 8 then
return colorspec.from_number_rgba(num)
end
if len == 6 then
return colorspec.from_number_rgba(num * 0x100 + 0xFF)
end
if len == 4 then
return colorspec.from_table{
a = (num % 0x10) * 0x11,
b = (floor(num / 0x10) % 0x10) * 0x11,
g = (floor(num / (0x100)) % 0x10) * 0x11,
r = (floor(num / (0x1000)) % 0x10) * 0x11
}
end
if len == 3 then
return colorspec.from_table{
b = (num % 0x10) * 0x11,
g = (floor(num / 0x10) % 0x10) * 0x11,
r = (floor(num / (0x100)) % 0x10) * 0x11
}
end
end
colorspec.from_text = colorspec.from_string
function colorspec.from_number_rgba(number)
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)
return colorspec.from_table{
a = 0xFF,
b = number % 0x100,
g = floor(number / 0x100) % 0x100,
r = floor(number / 0x10000)
}
end
function colorspec.from_number(number)
return colorspec.from_table{
b = number % 0x100,
g = floor(number / 0x100) % 0x100,
r = floor(number / 0x10000) % 0x100,
a = floor(number / 0x1000000)
}
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)
end
function colorspec:to_table()
return self
end
--> hex string, omits alpha if possible (if opaque)
function colorspec:to_string()
if self.a == 255 then
return ("#%02X%02X%02X"):format(self.r, self.g, self.b)
end
return ("#%02X%02X%02X%02X"):format(self.r, self.g, self.b, self.a)
end
metatable.__tostring = colorspec.to_string
function colorspec:to_number_rgba()
return self.r * 0x1000000 + self.g * 0x10000 + self.b * 0x100 + self.a
end
function colorspec:to_number_rgb()
return self.r * 0x10000 + self.g * 0x100 + self.b
end
function colorspec:to_number()
return self.a * 0x1000000 + self.r * 0x10000 + self.g * 0x100 + self.b
end
colorspec_to_colorstring = minetest.colorspec_to_colorstring or function(spec)
local color = colorspec.from_any(spec)
if not color then
return nil
end
return color:to_string()
end

View file

@ -0,0 +1,16 @@
local gametime
minetest.register_globalstep(function(dtime)
if gametime then
gametime = gametime + dtime
return
end
gametime = assert(minetest.get_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
end)

View file

@ -0,0 +1,122 @@
-- Localize globals
local math, minetest, modlib, pairs = math, minetest, modlib, pairs
-- Set environment
local _ENV = ...
setfenv(1, _ENV)
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
end
flowing_downwards = modlib.vector.new{0, -1, 0}
--+ Calculates the flow direction of a flowingliquid node
--> `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
end

View file

@ -0,0 +1,29 @@
-- Localize globals
local getmetatable, AreaStore, ItemStack
= getmetatable, AreaStore, ItemStack
-- Metatable lookup for classes specified in lua_api.txt, section "Class reference"
local AreaStoreMT = getmetatable(AreaStore())
local ItemStackMT = getmetatable(ItemStack"")
local metatables = {
[AreaStoreMT] = {name = "AreaStore", method = AreaStoreMT.to_string},
[ItemStackMT] = {name = "ItemStack", method = ItemStackMT.to_table},
-- TODO expand
}
return modlib.luon.new{
aux_write = function(_, value)
local type = metatables[getmetatable(value)]
if type then
return type.name, type.method(value)
end
end,
aux_read = {
AreaStore = function(...)
local store = AreaStore()
store:from_string(...)
return store
end,
ItemStack = ItemStack
}
}

View file

@ -0,0 +1,61 @@
local minetest, modlib, pairs, ipairs
= minetest, modlib, pairs, ipairs
-- TODO support for server texture packs (and possibly client TPs in singleplayer?)
local media_foldernames = {"textures", "sounds", "media", "models", "locale"}
local media_extensions = modlib.table.set{
-- Textures
"png", "jpg", "bmp", "tga", "pcx", "ppm", "psd", "wal", "rgb";
-- Sounds
"ogg";
-- Models
"x", "b3d", "md2", "obj";
-- Translations
"tr";
}
local function collect_media(modname)
local media = {}
local function traverse(folderpath)
-- Traverse files (collect media)
local filenames = minetest.get_dir_list(folderpath, false)
for _, filename in pairs(filenames) do
local _, ext = modlib.file.get_extension(filename)
if media_extensions[ext] then
media[filename] = modlib.file.concat_path{folderpath, filename}
end
end
-- Traverse subfolders
local foldernames = minetest.get_dir_list(folderpath, true)
for _, foldername in pairs(foldernames) do
if not foldername:match"^[_%.]" then -- ignore hidden subfolders / subfolders starting with `_`
traverse(modlib.file.concat_path{folderpath, foldername})
end
end
end
for _, foldername in ipairs(media_foldernames) do -- order matters!
traverse(modlib.mod.get_resource(modname, foldername))
end
return media
end
-- TODO clean this up eventually
local paths = {}
local mods = {}
local overridden_paths = {}
local overridden_mods = {}
for _, mod in ipairs(modlib.minetest.get_mod_load_order()) do
local mod_media = collect_media(mod.name)
for medianame, path in pairs(mod_media) do
if paths[medianame] then
overridden_paths[medianame] = overridden_paths[medianame] or {}
table.insert(overridden_paths[medianame], paths[medianame])
overridden_mods[medianame] = overridden_mods[medianame] or {}
table.insert(overridden_mods[medianame], mods[medianame])
end
paths[medianame] = path
mods[medianame] = mod.name
end
end
return {paths = paths, mods = mods, overridden_paths = overridden_paths, overridden_mods = overridden_mods}

View file

@ -0,0 +1,328 @@
-- Localize globals
local Settings, assert, minetest, modlib, next, pairs, ipairs, string, setmetatable, select, table, type, unpack
= Settings, assert, minetest, modlib, next, pairs, ipairs, string, setmetatable, select, table, type, unpack
-- Set environment
local _ENV = ...
setfenv(1, _ENV)
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)
end
local jobs = modlib.heap.new(function(a, b)
return a.time < b.time
end)
local job_metatable = {
__index = {
-- TODO (...) proper (instant rather than deferred) cancellation:
-- Keep index [job] = index, swap with last element and heapify
cancel = function(self)
self.cancelled = true
end
}
}
local time = 0
function after(seconds, func, ...)
local job = setmetatable({
time = time + seconds,
func = func,
["#"] = select("#", ...),
...
}, job_metatable)
jobs:push(job)
return job
end
minetest.register_globalstep(function(dtime)
time = time + dtime
local job = jobs[1]
while job and job.time <= time do
if not job.cancelled then
job.func(unpack(job, 1, job["#"]))
end
jobs:pop()
job = jobs[1]
end
end)
function register_globalstep(interval, callback)
if type(callback) ~= "function" then
return
end
local time = 0
minetest.register_globalstep(function(dtime)
time = time + dtime
if time >= interval then
callback(time)
-- TODO ensure this breaks nothing
time = time % interval
end
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
end
local icall = modlib.table.icall
minetest.register_on_player_receive_fields(function(player, formname, fields)
icall(form_listeners[formname] or {}, player, fields)
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, "%^", "&")
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])
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
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)
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
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
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
local groups = modlib.text.split(node_or_groupname:sub(("group:"):len() + 1), ",")
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
do
local default_create, default_free = function() return {} end, modlib.func.no_op
local metatable = {__index = function(self, player)
if type(player) == "userdata" then
return self[player:get_player_name()]
end
end}
function playerdata(create, free)
create = create or default_create
free = free or default_free
local data = {}
minetest.register_on_joinplayer(function(player)
data[player:get_player_name()] = create(player)
end)
minetest.register_on_leaveplayer(function(player)
data[player:get_player_name()] = free(player)
end)
setmetatable(data, metatable)
return data
end
end
function connected_players()
-- TODO cache connected players
local connected_players = minetest.get_connected_players()
local index = 0
return function()
index = index + 1
return connected_players[index]
end
end
function set_privs(name, priv_updates)
local privs = minetest.get_player_privs(name)
for priv, grant in pairs(priv_updates) do
if grant then
privs[priv] = true
else
-- May not be set to false; Minetest treats false as truthy in this instance
privs[priv] = nil
end
end
return minetest.set_player_privs(name, privs)
end
function register_on_leaveplayer(func)
return minetest["register_on_" .. (minetest.is_singleplayer() and "shutdown" or "leaveplayer")](func)
end
do local mod_info
function get_mod_info()
if mod_info then return mod_info end
mod_info = {}
-- TODO validate modnames
local modnames = minetest.get_modnames()
for _, mod in pairs(modnames) do
local info
local function read_file(filename)
return modlib.file.read(modlib.mod.get_resource(mod, filename))
end
local mod_conf = Settings(modlib.mod.get_resource(mod, "mod.conf"))
if mod_conf then
info = {}
mod_conf = mod_conf:to_table()
local function read_depends(field)
local depends = {}
for depend in (mod_conf[field] or ""):gmatch"[^,]+" do
depends[modlib.text.trim_spacing(depend)] = true
end
info[field] = depends
end
read_depends"depends"
read_depends"optional_depends"
else
info = {
description = read_file"description.txt",
depends = {},
optional_depends = {}
}
local depends_txt = read_file"depends.txt"
if depends_txt then
for _, dependency in ipairs(modlib.table.map(modlib.text.split(depends_txt or "", "\n"), modlib.text.trim_spacing)) do
local modname, is_optional = dependency:match"(.+)(%??)"
table.insert(is_optional == "" and info.depends or info.optional_depends, modname)
end
end
end
if info.name == nil then
info.name = mod
end
mod_info[mod] = info
end
return mod_info
end end
do local mod_load_order
function get_mod_load_order()
if mod_load_order then return mod_load_order end
mod_load_order = {}
local mod_info = get_mod_info()
-- If there are circular soft dependencies, it is possible that a mod is loaded, but not in the right order
-- TODO somehow maximize the number of soft dependencies fulfilled in case of circular soft dependencies
local function load(mod)
if mod.status == "loaded" then
return true
end
if mod.status == "loading" then
return false
end
-- TODO soft/vs hard loading status, reset?
mod.status = "loading"
-- Try hard dependencies first. These must be fulfilled.
for depend in pairs(mod.depends) do
if not load(mod_info[depend]) then
return false
end
end
-- Now, try soft dependencies.
for depend in pairs(mod.optional_depends) do
-- Mod may not exist
if mod_info[depend] then
load(mod_info[depend])
end
end
mod.status = "loaded"
table.insert(mod_load_order, mod)
return true
end
for _, mod in pairs(mod_info) do
assert(load(mod))
end
return mod_load_order
end end

View file

@ -0,0 +1,184 @@
-- Localize globals
local Settings, _G, assert, dofile, error, getmetatable, ipairs, loadfile, loadstring, minetest, modlib, pairs, rawget, rawset, setfenv, setmetatable, tonumber, type, table_concat, unpack
= Settings, _G, assert, dofile, error, getmetatable, ipairs, loadfile, loadstring, minetest, modlib, pairs, rawget, rawset, setfenv, setmetatable, tonumber, type, table.concat, unpack
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local loaded = {}
function require(filename)
local modname = minetest.get_current_modname()
loaded[modname] = loaded[modname] or {}
-- Minetest ensures that `/` works even on Windows (path normalization)
loaded[modname][filename] = loaded[modname][filename] -- already loaded?
or dofile(minetest.get_modpath(modname) .. "/" .. filename:gsub("%.", "/") .. ".lua")
return loaded[modname][filename]
end
function loadfile_exports(filename)
local env = setmetatable({}, {__index = _G})
local file = assert(loadfile(filename))
setfenv(file, env)
file()
return env
end
-- 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))
end
function include_env(file_or_string, env, is_string)
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
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))
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")
end
-- TODO `require` relative to current mod
local warn_parent_leaf = "modlib: setting %s used both as parent setting and as leaf, ignoring children"
local function build_tree(dict)
local tree = {}
for key, value in pairs(dict) do
local path = modlib.text.split_unlimited(key, ".", true)
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]
if type(subtree) ~= "table" then
minetest.log("warning", warn_parent_leaf:format(table_concat({unpack(path, 1, i)}, ".")))
break
end
end
if type(subtree) == "table" then
if type(subtree[path[#path]]) == "table" then
minetest.log("warning", warn_parent_leaf:format(key))
end
subtree[path[#path]] = value
end
end
return tree
end
settings = build_tree(minetest.settings:to_table())
--> conf, schema
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
local settingtypes = schema:generate_settingtypes()
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 build_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 = 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"), 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
-- FIXME mod security messes with this (disallows it if enabled)
assert(modlib.file.write(readme_path, readme))
end
end
if conf == nil then
return schema:load({}, {error_message = true}), schema
end
return conf, schema
end
-- Export environment
return _ENV

View file

@ -0,0 +1,190 @@
local assert, tonumber, type, setmetatable, ipairs, unpack
= assert, tonumber, type, setmetatable, ipairs, unpack
local math_floor, table_insert, table_concat
= math.floor, table.insert, table.concat
local obj = {}
local metatable = {__index = obj}
local function read_floats(next_word, n)
if n == 0 then return end
local num = next_word()
assert(num:find"^%-?%d+$" or num:find"^%-?%d+%.%d+$")
return tonumber(num), read_floats(next_word, n - 1)
end
local function read_index(list, index)
if not index then return end
index = tonumber(index)
if index < 0 then
index = index + #list + 1
end
assert(list[index])
return index
end
local function read_indices(self, next_word)
local word = next_word()
if not word then return end
-- TODO optimize this (ideally using a vararg-ish split by `/`)
local vertex, texcoord, normal
vertex = word:match"^%-?%d+$"
if not vertex then
vertex, texcoord = word:match"^(%-?%d+)/(%-?%d+)$"
if not vertex then
vertex, normal = word:match"^(%-?%d+)//(%-?%d+)$"
if not vertex then
vertex, texcoord, normal = word:match"^(%-?%d+)/(%-?%d+)/(%-?%d+)$"
end
end
end
return {
vertex = read_index(self.vertices, vertex),
texcoord = read_index(self.texcoords, texcoord),
normal = read_index(self.normals, normal)
}, read_indices(self, next_word)
end
function obj.read_lines(
... -- line iterator such as `modlib.text.lines"str"` or `io.lines"filename"`
)
local self = {
vertices = {},
texcoords = {},
normals = {},
groups = {}
}
local groups = {}
local active_group = {name = "default"}
groups[1] = active_group
groups.default = active_group
for line in ... do
if line:byte() ~= ("#"):byte() then
local next_word = line:gmatch"%S+"
local command = next_word()
if command == "v" or command == "vn" then
local x, y, z = read_floats(next_word, 3)
x = -x
table_insert(self[command == "v" and "vertices" or "normals"], {x, y, z})
elseif command == "vt" then
local x, y = read_floats(next_word, 2)
y = 1 - y
table_insert(self.texcoords, {x, y})
elseif command == "f" then
table_insert(active_group, {read_indices(self, next_word)})
elseif command == "g" or command == "usemtl" then
-- TODO consider distinguishing between materials & groups
local name = next_word() or "default"
if groups[name] then
active_group = groups[name]
else
active_group = {name = name}
table_insert(groups, active_group)
groups[name] = active_group
end
assert(not next_word(), "only a single group/material name is supported")
end
end
end
-- Keep only nonempty groups
for _, group in ipairs(groups) do
if group[1] ~= nil then
table_insert(self.groups, group)
end
end
return setmetatable(self, metatable) -- obj object
end
-- Does not close a file handle if passed
--> obj object
function obj.read_file(file_or_name)
if type(file_or_name) == "string" then
return obj.read_lines(io.lines(file_or_name))
end
local handle = file_or_name
-- `handle.read, handle` can be used as a line iterator
return obj.read_lines(assert(handle.read), handle)
end
--> obj object
function obj.read_string(str)
-- Empty lines can be ignored
return obj.read_lines(str:gmatch"[^\r\n]+")
end
local function write_float(float)
if math_floor(float) == float then
return ("%d"):format(float)
end
return ("%f"):format(float):match"^(.-)0*$" -- strip trailing zeros
end
local function write_index(index)
if index.texcoord then
if index.normal then
return("%d/%d/%d"):format(index.vertex, index.texcoord, index.normal)
end
return ("%d/%d"):format(index.vertex, index.texcoord)
end if index.normal then
return ("%d//%d"):format(index.vertex, index.normal)
end
return ("%d"):format(index.vertex)
end
-- Callback/"caller"-style iterator; use `iterator.for_generator` to turn this into a callee-style iterator
function obj:write_lines(
write_line -- function(line: string) to write a line
)
local function write_v3f(type, v3f)
local x, y, z = unpack(v3f)
x = -x
write_line(("%s %s %s %s"):format(type, write_float(x), write_float(y), write_float(z)))
end
for _, vertex in ipairs(self.vertices) do
write_v3f("v", vertex)
end
for _, normal in ipairs(self.normals) do
write_v3f("vn", normal)
end
for _, texcoord in ipairs(self.texcoords) do
local x, y = texcoord[1], texcoord[2]
y = 1 - y
write_line(("vt %s %s"):format(write_float(x), write_float(y)))
end
for _, group in ipairs(self.groups) do
write_line("g " .. group.name) -- this will convert `usemtl` into `g` but that shouldn't matter
for _, face in ipairs(group) do
local command = {"f"}
for i, index in ipairs(face) do
command[i + 1] = write_index(index)
end
write_line(table_concat(command, " "))
end
end
end
-- Write `self` to a file
-- Does not close or flush a file handle if passed
function obj:write_file(file_or_name)
if type(file_or_name) == "string" then
file_or_name = io.open(file_or_name)
end
self:write_lines(function(line)
file_or_name:write(line)
file_or_name:write"\n"
end)
end
-- Write `self` to a string
function obj:write_string()
local rope = {}
self:write_lines(function(line)
table_insert(rope, line)
end)
table_insert(rope, "") -- trailing newline for good measure
return table_concat(rope, "\n") -- string representation of `self`
end
return obj

View file

@ -0,0 +1,483 @@
local signature = "\137\80\78\71\13\10\26\10"
local assert, char, ipairs, insert, concat, abs, floor = assert, string.char, ipairs, table.insert, table.concat, math.abs, math.floor
-- TODO move to modlib.bit eventually
local function bit_xor(a, b)
local res = 0
local bit = 1
for _ = 1, 32 do
if a % 2 ~= b % 2 then
res = res + bit
end
a = floor(a / 2)
b = floor(b / 2)
bit = bit * 2
end
return res
end
-- Try to use `bit` library (if available) for a massive speed boost
local bit = rawget(_G, "bit")
if bit then
local bxor = bit.bxor
function bit_xor(a, b)
local res = bxor(a, b)
if res < 0 then -- convert signed to unsigned
return res + 2^32
end
return res
end
end
local crc_table = {}
for i = 0, 255 do
local c = i
for _ = 0, 7 do
if c % 2 > 0 then
c = bit_xor(0xEDB88320, floor(c / 2))
else
c = floor(c / 2)
end
end
crc_table[i] = c
end
local function update_crc(crc, text)
for i = 1, #text do
crc = bit_xor(crc_table[bit_xor(crc % 0x100, text:byte(i))], floor(crc / 0x100))
end
return crc
end
local color_types = {
[0] = {
color = "grayscale"
},
[2] = {
color = "truecolor"
},
[3] = {
color = "palette",
depth = 8
},
[4] = {
color = "grayscale",
alpha = true
},
[6] = {
color = "truecolor",
alpha = true
}
}
local set = modlib.table.set
local allowed_bit_depths = {
[0] = set{1, 2, 4, 8, 16},
[2] = set{8, 16},
[3] = set{1, 2, 4, 8},
[4] = set{8, 16},
[6] = set{8, 16}
}
local samples = {
grayscale = 1,
palette = 1,
truecolor = 3
}
local adam7_passes = {
x_min = { 0, 4, 0, 2, 0, 1, 0 },
y_min = { 0, 0, 4, 0, 2, 0, 1 },
x_step = { 8, 8, 4, 4, 2, 2, 1 },
y_step = { 8, 8, 8, 4, 4, 2, 2 },
};
(...).decode_png = function(stream)
local chunk_crc
local function read(n)
local text = stream:read(n)
assert(#text == n)
if chunk_crc then
chunk_crc = update_crc(chunk_crc, text)
end
return text
end
local function byte()
return read(1):byte()
end
local function _uint()
return 0x1000000 * byte() + 0x10000 * byte() + 0x100 * byte() + byte()
end
local function uint()
local val = _uint()
assert(val < 2^31, "uint out of range")
return val
end
local function check_crc()
local crc = chunk_crc
chunk_crc = nil
if _uint() ~= bit_xor(crc, 0xFFFFFFFF) then
error("CRC mismatch", 2)
end
end
assert(read(8) == signature, "PNG signature expected")
local IHDR_len = uint()
assert(IHDR_len == 13, "invalid IHDR length")
chunk_crc = 0xFFFFFFFF
assert(read(4) == "IHDR", "IHDR chunk expected")
local width = uint()
assert(width > 0)
local height = uint()
assert(height > 0)
local bit_depth = byte()
local color_type_number = byte()
local color_type = assert(color_types[color_type_number], "invalid color type")
if color_type.color ~= "palette" then
color_type.depth = bit_depth
end
assert(allowed_bit_depths[color_type_number][bit_depth], "disallowed bit depth for color type")
local compression_method = byte()
assert(compression_method == 0, "unsupported compression method")
local filter_method = byte()
assert(filter_method == 0, "unsupported filter method")
local interlace_method = byte()
assert(interlace_method <= 1, "unsupported interlace method")
local adam7 = interlace_method == 1
check_crc() -- IHDR CRC
local palette
local alpha
local source_gamma
local idat_content = {}
local idat_allowed = true
local iend
repeat
local chunk_length = uint()
chunk_crc = 0xFFFFFFFF
local chunk_type = read(4)
if chunk_type == "IDAT" then
assert(idat_allowed, "no chunks inbetween IDAT chunks allowed")
if color_type.color == "palette" then
assert(palette, "PLTE chunk expected")
end
insert(idat_content, read(chunk_length))
else
if next(idat_content) then
-- Non-IDAT chunk, no IDAT chunks allowed anymore
idat_allowed = false
end
if chunk_type == "PLTE" then
assert(color_type.color ~= "grayscale")
assert(not palette, "double PLTE chunk")
assert(idat_allowed, "PLTE after IDAT chunks")
palette = {}
local entries = chunk_length / 3
assert(entries % 1 == 0 and entries >= 1 and entries <= 2^bit_depth, "invalid PLTE chunk length")
for i = 1, entries do
palette[i] = 0x10000 * byte() + 0x100 * byte() + byte() -- RGB
end
elseif chunk_type == "tRNS" then
assert(not color_type.alpha, "unexpected tRNS chunk")
color_type.transparency = true
assert(idat_allowed, "tRNS after IDAT chunks")
if color_type.color == "palette" then
assert(palette, "PLTE chunk expected")
alpha = {}
for i = 1, chunk_length do
alpha[i] = byte()
end
elseif color_type.color == "grayscale" then
assert(chunk_length == 2)
alpha = 0x100 * byte() + byte()
else
assert(color_type.color == "truecolor")
assert(chunk_length == 6)
alpha = 0
-- Read 16-bit RGB (6 bytes)
for _ = 1, 6 do
alpha = alpha * 0x100 + byte()
end
end
elseif chunk_type == "gAMA" then
assert(not palette, "gAMA after PLTE chunk")
assert(idat_allowed, "gAMA after IDAT chunks")
assert(chunk_length == 4)
source_gamma = uint() / 1e5
elseif chunk_type == "IEND" then
iend = true
else
-- Check whether the fifth bit of the first byte is set (upper vs. lowercase ASCII)
local ancillary = floor(chunk_type:byte(1) % (2^6)) >= 2^5
if not ancillary then
error(("unsupported critical chunk: %q"):format(chunk_type))
end
read(chunk_length)
end
end
check_crc()
until iend
assert(next(idat_content), "no IDAT chunk")
idat_content = minetest.decompress(concat(idat_content), "deflate")
--[[
For memory efficiency, we try to pack everything in a single number:
Grayscale/lightness: AY
Palette: ARGB
Truecolor (8-bit): ARGB
Truecolor (16-bit): RGB + A
(64 bits required, packing non-mantissa bits isn't practical) => separate table with alpha values
]]
local data = {}
local alpha_data
if color_type.color == "truecolor" and bit_depth == 16 and (color_type.alpha or color_type.transparency) then
alpha_data = {}
end
if adam7 then
-- Allocate space in list part in order to not fill the hash part later
for i = 1, width * height do
data[i] = false
if alpha_data then
alpha_data[i] = false
end
end
end
local bits_per_pixel = (samples[color_type.color] + (color_type.alpha and 1 or 0)) * bit_depth
local bytes_per_pixel = math.ceil(bits_per_pixel / 8)
local previous_scanline
local idat_base_index = 1
local function read_scanline(x_min, x_step, y)
local scanline_width = math.ceil((width - x_min) / x_step)
local scanline_bytecount = math.ceil(scanline_width * bits_per_pixel / 8)
local filtering = idat_content:byte(idat_base_index)
local scanline = {}
for i = 1, scanline_bytecount do
local val = idat_content:byte(idat_base_index + i)
local left = scanline[i - bytes_per_pixel] or 0
local up = previous_scanline and previous_scanline[i] or 0
local left_up = previous_scanline and previous_scanline[i - bytes_per_pixel] or 0
-- Undo lossless filter
if filtering == 0 then -- None
scanline[i] = val
elseif filtering == 1 then -- Sub
scanline[i] = (left + val) % 0x100
elseif filtering == 2 then -- Up
scanline[i] = (up + val) % 0x100
elseif filtering == 3 then -- Average
scanline[i] = (floor((left + up) / 2) + val) % 0x100
elseif filtering == 4 then -- Paeth
local p = left + up - left_up
local p_left = abs(p - left)
local p_up = abs(p - up)
local p_left_up = abs(p - left_up)
local p_res
if p_left <= p_up and p_left <= p_left_up then
p_res = left
elseif p_up <= p_left_up then
p_res = up
else
p_res = left_up
end
scanline[i] = (p_res + val) % 0x100
else
error("invalid filtering method: " .. filtering)
end
assert(scanline[i] >= 0 and scanline[i] <= 255 and scanline[i] % 1 == 0)
end
local bit = 0
local function sample()
local byte_idx = 1 + floor(bit / 8)
bit = bit + bit_depth
local byte = scanline[byte_idx]
if bit_depth == 16 then
return byte * 0x100 + scanline[byte_idx + 1]
end
if bit_depth == 8 then
return byte
end
assert(bit_depth == 1 or bit_depth == 2 or bit_depth == 4)
local low = 2^(-bit % 8)
return floor(byte / low) % (2^bit_depth)
end
for x = x_min, width - 1, x_step do
local data_index = y * width + x + 1
if color_type.color == "palette" then
local palette_index = sample()
local rgb = assert(palette[palette_index + 1], "palette index out of range")
-- Index alpha table if available
local a = alpha and alpha[palette_index + 1] or 255
data[data_index] = a * 0x1000000 + rgb
elseif color_type.color == "grayscale" then
local Y = sample()
local a = 2^bit_depth - 1
if color_type.alpha then
a = sample()
elseif alpha == Y then
a = 0 -- Convert grayscale to transparency
end
data[data_index] = a * (2^bit_depth) + Y
else
assert(color_type.color == "truecolor")
local r, g, b = sample(), sample(), sample()
local rgb16 = r * 0x100000000 + g * 0x10000 + b
local a = 2^bit_depth - 1
if color_type.alpha then
a = sample()
elseif alpha == rgb16 then
a = 0 -- Convert color to transparency
end
if bit_depth == 8 then
data[data_index] = a * 0x1000000 + r * 0x10000 + g * 0x100 + b
else
assert(bit_depth == 16)
-- Pack only RGB in data, alpha goes in a different table
-- 3 * 16 = 48 bytes can still be held accurately by the double mantissa
data[data_index] = rgb16
if alpha_data then
alpha_data[data_index] = a
end
end
end
end
-- Each byte of the scanline must have been read from
assert(bit >= #scanline * 8 - 7)
previous_scanline = scanline
idat_base_index = idat_base_index + scanline_bytecount + 1
end
if adam7 then
for pass = 1, 7 do
local x_min, y_min = adam7_passes.x_min[pass], adam7_passes.y_min[pass]
if x_min < width and y_min < height then -- Non-empty pass
local x_step, y_step = adam7_passes.x_step[pass], adam7_passes.y_step[pass]
previous_scanline = nil -- Filtering doesn't use scanlines of previous passes
for y = y_min, height - 1, y_step do
read_scanline(x_min, x_step, y)
end
end
end
else
for y = 0, height - 1 do
read_scanline(0, 1, y)
end
end
return {
width = width,
height = height,
color_type = color_type,
source_gamma = source_gamma,
data = data,
alpha_data = alpha_data
}
end
local function rescale_depth(sample, source_depth, target_depth)
if source_depth == target_depth then
return sample
end
return floor((sample * (2^target_depth - 1) / (2^source_depth - 1)) + 0.5)
end
-- In-place lossy (if bit depth = 16) conversion to ARGB8
(...).convert_png_to_argb8 = function(png)
local color, transparency, depth = png.color_type.color, png.color_type.alpha or png.color_type.transparency, png.color_type.depth
if color == "palette" or (color == "truecolor" and depth == 8) then
return
end
for index, value in pairs(png.data) do
if color == "grayscale" then
local a, Y = rescale_depth(floor(value / (2^depth)), depth, 8), rescale_depth(value % (2^depth), depth, 8)
png.data[index] = a * 0x1000000 + Y * 0x10000 + Y * 0x100 + Y -- R = G = B = Y
else
assert(color == "truecolor" and depth == 16)
local r = rescale_depth(floor(value / 0x100000000), depth, 8)
local g = rescale_depth(floor(value / 0x10000) % 0x10000, depth, 8)
local b = rescale_depth(value % 0x10000, depth, 8)
local a = 0xFF
if transparency then
a = rescale_depth(png.alpha_data[index], depth, 8)
end
png.data[index] = a * 0x1000000 + r * 0x10000 + g * 0x100 + b
end
end
png.color_type = color_types[6]
png.bit_depth = 8
png.alpha_data = nil
end
local function encode_png(width, height, data, compression, raw_write)
local write = raw_write
local function byte(value)
write(char(value))
end
local function _uint(value)
local div = 0x1000000
for _ = 1, 4 do
byte(floor(value / div) % 0x100)
div = div / 0x100
end
end
local function uint(value)
assert(value < 2^31)
_uint(value)
end
local chunk_content
local function chunk_write(text)
insert(chunk_content, text)
end
local function chunk(type)
chunk_content = {}
write = chunk_write
write(type)
end
local function end_chunk()
write = raw_write
local chunk_len = 0
for i = 2, #chunk_content do
chunk_len = chunk_len + #chunk_content[i]
end
uint(chunk_len)
write(concat(chunk_content))
local chunk_crc = 0xFFFFFFFF
for _, text in ipairs(chunk_content) do
chunk_crc = update_crc(chunk_crc, text)
end
_uint(bit_xor(chunk_crc, 0xFFFFFFFF))
end
-- Signature
write(signature)
chunk"IHDR"
uint(width)
uint(height)
-- Always use bit depth 8
byte(8)
-- Always use color type "truecolor with alpha"
byte(6)
-- Compression method: deflate
byte(0)
-- Filter method: PNG filters
byte(0)
-- No interlace
byte(0)
end_chunk()
chunk"IDAT"
local data_rope = {}
for y = 0, height - 1 do
local base_index = y * width
insert(data_rope, "\0")
for x = 1, width do
local colorspec = modlib.minetest.colorspec.from_any(data[base_index + x])
insert(data_rope, char(colorspec.r, colorspec.g, colorspec.b, colorspec.a))
end
end
write(minetest.compress(type(data) == "string" and data or concat(data_rope), "deflate", compression))
end_chunk()
chunk"IEND"
end_chunk()
end
(...).encode_png = minetest.encode_png or function(width, height, data, compression)
local rope = {}
encode_png(width, height, data, compression or 9, function(text)
insert(rope, text)
end)
return concat(rope)
end

View file

@ -0,0 +1,137 @@
-- Localize globals
local assert, math, minetest, modlib, pairs, setmetatable, vector = assert, math, minetest, modlib, pairs, setmetatable, vector
--+ Raycast wrapper with proper flowingliquid intersections
return function(_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()
local pointed_thing = raycast:next()
if (not pointed_thing) or 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 = modlib.minetest.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
return next()
end
return setmetatable({next = next}, {__call = next})
end

View file

@ -0,0 +1,193 @@
-- Localize globals
local VoxelArea, ItemStack, assert, error, io, ipairs, math, minetest, modlib, next, pairs, setmetatable, string, table, type, vector
= VoxelArea, ItemStack, assert, error, io, ipairs, math, minetest, modlib, next, pairs, setmetatable, string, table, type, vector
local schematic = {}
local metatable = {__index = schematic}
function schematic.setmetatable(self)
return setmetatable(self, metatable)
end
function schematic.create(params, pos_min, pos_max)
pos_min, pos_max = vector.sort(pos_min, pos_max)
local size = vector.add(vector.subtract(pos_max, pos_min), 1)
local voxelmanip = minetest.get_voxel_manip(pos_min, pos_max)
local emin, emax = voxelmanip:read_from_map(pos_min, pos_max)
local voxelarea = VoxelArea:new{ MinEdge = emin, MaxEdge = emax }
local nodes, light_values, param2s = {}, params.light_values and {}, {}
local vm_nodes, vm_light_values, vm_param2s = voxelmanip:get_data(), light_values and voxelmanip:get_light_data(), voxelmanip:get_param2_data()
local node_names, node_ids = {}, {}
local i = 0
for index in voxelarea:iterp(pos_min, pos_max) do
if nodes[index] == minetest.CONTENT_UNKNOWN or nodes[index] == minetest.CONTENT_IGNORE then
error("unknown or ignore node at " .. minetest.pos_to_string(voxelarea:position(index)))
end
local name = minetest.get_name_from_content_id(vm_nodes[index])
local id = node_ids[name]
if not id then
table.insert(node_names, name)
id = #node_names
node_ids[name] = id
end
i = i + 1
nodes[i] = id
if params.light_values then
light_values[i] = vm_light_values[index]
end
param2s[i] = vm_param2s[index]
end
local metas
if params.metas or params.metas == nil then
metas = {}
for _, pos in ipairs(minetest.find_nodes_with_meta(pos_min, pos_max)) do
local meta = minetest.get_meta(pos):to_table()
if next(meta.fields) ~= nil or next(meta.inventory) ~= nil then
local relative = vector.subtract(pos, pos_min)
metas[((relative.z * size.y) + relative.y) * size.x + relative.x] = meta
end
end
end
return schematic.setmetatable({
size = size,
node_names = node_names,
nodes = nodes,
light_values = light_values,
param2s = param2s,
metas = metas,
})
end
function schematic:write_to_voxelmanip(voxelmanip, pos_min)
local size = self.size
local pos_max = vector.subtract(vector.add(pos_min, size), 1) -- `pos_max` is inclusive
local emin, emax = voxelmanip:read_from_map(pos_min, pos_max)
local voxelarea = VoxelArea:new{ MinEdge = emin, MaxEdge = emax }
local nodes, light_values, param2s, metas = self.nodes, self.light_values, self.param2s, self.metas
local vm_nodes, vm_lights, vm_param2s = voxelmanip:get_data(), light_values and voxelmanip:get_light_data(), voxelmanip:get_param2_data()
for _, pos in ipairs(minetest.find_nodes_with_meta(pos_min, pos_max)) do
-- Clear all metadata. Due to an engine bug, nodes will actually have empty metadata.
minetest.get_meta(pos):from_table{}
end
local content_ids = {}
for index, name in ipairs(self.node_names) do
content_ids[index] = assert(minetest.get_content_id(name), ("unknown node %q"):format(name))
end
local i = 0
for index in voxelarea:iterp(pos_min, pos_max) do
i = i + 1
vm_nodes[index] = content_ids[nodes[i]]
if light_values then
vm_lights[index] = light_values[i]
end
vm_param2s[index] = param2s[i]
end
voxelmanip:set_data(vm_nodes)
if light_values then
voxelmanip:set_light_data(vm_lights)
end
voxelmanip:set_param2_data(vm_param2s)
if metas then
for index, meta in pairs(metas) do
local floored = math.floor(index / size.x)
local relative = {
x = index % size.x,
y = floored % size.y,
z = math.floor(floored / size.y)
}
minetest.get_meta(vector.add(relative, pos_min)):from_table(meta)
end
end
end
function schematic:place(pos_min)
local pos_max = vector.subtract(vector.add(pos_min, self.size), 1) -- `pos_max` is inclusive
local voxelmanip = minetest.get_voxel_manip(pos_min, pos_max)
self:write_to_voxelmanip(voxelmanip, pos_min)
voxelmanip:write_to_map(not self.light_values)
return voxelmanip
end
local function table_to_byte_string(tab)
if not tab then return end
return table.concat(modlib.table.map(tab, string.char))
end
local function write_bluon(self, stream)
local metas = modlib.table.copy(self.metas)
for _, meta in pairs(metas) do
for _, list in pairs(meta.inventory) do
for index, stack in pairs(list) do
list[index] = stack:to_string()
end
end
end
modlib.bluon:write({
size = self.size,
node_names = self.node_names,
nodes = self.nodes,
light_values = table_to_byte_string(self.light_values),
param2s = table_to_byte_string(self.param2s),
metas = metas,
}, stream)
end
function schematic:write_bluon(path)
local file = io.open(path, "wb")
-- Header, short for "ModLib Bluon Schematic"
file:write"MLBS"
write_bluon(self, file)
file:close()
end
local function byte_string_to_table(self, field)
local byte_string = self[field]
if not byte_string then return end
local tab = {}
for i = 1, #byte_string do
tab[i] = byte_string:byte(i)
end
self[field] = tab
end
local function read_bluon(file)
local self = modlib.bluon:read(file)
assert(not file:read(1), "expected EOF")
for _, meta in pairs(self.metas) do
for _, list in pairs(meta.inventory) do
for index, itemstring in pairs(list) do
assert(type(itemstring) == "string")
list[index] = ItemStack(itemstring)
end
end
end
byte_string_to_table(self, "light_values")
byte_string_to_table(self, "param2s")
return self
end
function schematic.read_bluon(path)
local file = io.open(path, "rb")
assert(file:read(4) == "MLBS", "not a modlib bluon schematic")
return schematic.setmetatable(read_bluon(file))
end
function schematic:write_zlib_bluon(path, compression)
local file = io.open(path, "wb")
-- Header, short for "ModLib Zlib-compressed-bluon Schematic"
file:write"MLZS"
local rope = modlib.table.rope{}
write_bluon(self, rope)
local text = rope:to_text()
file:write(minetest.compress(text, "deflate", compression or 9))
file:close()
end
function schematic.read_zlib_bluon(path)
local file = io.open(path, "rb")
assert(file:read(4) == "MLZS", "not a modlib zlib compressed bluon schematic")
return schematic.setmetatable(read_bluon(modlib.text.inputstream(minetest.decompress(file:read"*a", "deflate"))))
end
return schematic

View file

@ -0,0 +1,30 @@
-- Texture Modifier representation for building, parsing and stringifying texture modifiers according to
-- https://github.com/minetest/minetest_docs/blob/master/doc/texture_modifiers.adoc
local function component(component_name, ...)
return assert(loadfile(modlib.mod.get_resource(modlib.modname, "minetest", "texmod", component_name .. ".lua")))(...)
end
local texmod, metatable = component"dsl"
local methods = metatable.__index
methods.write = component"write"
texmod.read = component("read", texmod)
methods.calc_dims = component"calc_dims"
methods.gen_tex = component"gen_tex"
function metatable:__tostring()
local rope = {}
self:write(function(str) rope[#rope+1] = str end)
return table.concat(rope)
end
function texmod.read_string(str, warn --[[function(warn_str)]])
local i = 0
return texmod.read(function()
i = i + 1
if i > #str then return end
return str:sub(i, i)
end, warn)
end
return texmod

View file

@ -0,0 +1,97 @@
local cd = {}
local function calc_dims(self, get_file_dims)
return assert(cd[self.type])(self, get_file_dims)
end
function cd:file(d)
return d(self.filename)
end
do
local function base_dim(self, get_dims) return calc_dims(self.base, get_dims) end
cd.opacity = base_dim
cd.invert = base_dim
cd.brighten = base_dim
cd.noalpha = base_dim
cd.makealpha = base_dim
cd.lowpart = base_dim
cd.mask = base_dim
cd.multiply = base_dim
cd.colorize = base_dim
cd.colorizehsl = base_dim
cd.hsl = base_dim
cd.screen = base_dim
cd.contrast = base_dim
end
do
local function wh(self) return self.w, self.h end
cd.resize = wh
cd.combine = wh
end
function cd:fill(get_dims)
if self.base then return calc_dims(self.base, get_dims) end
return self.w, self.h
end
do
local function upscale_to_higher_res(self, get_dims)
local base_w, base_h = calc_dims(self.base, get_dims)
local over_w, over_h = calc_dims(self.over, get_dims)
if base_w * base_h > over_w * over_h then
return base_w, base_h
end
return over_w, over_h
end
cd.blit = upscale_to_higher_res
cd.hardlight = upscale_to_higher_res
end
function cd:transform(get_dims)
if self.rotation_deg % 180 ~= 0 then
local base_w, base_h = calc_dims(self.base, get_dims)
return base_h, base_w
end
return calc_dims(self.base, get_dims)
end
do
local math_clamp = modlib.math.clamp
local function next_pow_of_2(x)
-- I don't want to use a naive 2^ceil(log(x)/log(2)) due to possible float precision issues.
local m, e = math.frexp(x) -- x = _*2^e, _ in [0.5, 1)
if m == 0.5 then e = e - 1 end -- x = 2^(e-1)
return math.ldexp(1, e) -- 2^e, premature optimization here we go
end
function cd:inventorycube(get_dims)
local top_w, top_h = calc_dims(self.top, get_dims)
local left_w, left_h = calc_dims(self.left, get_dims)
local right_w, right_h = calc_dims(self.right, get_dims)
local d = math_clamp(next_pow_of_2(math.max(top_w, top_h, left_w, left_h, right_w, right_h)), 2, 64)
return d, d
end
end
do
local function frame_dims(self, get_dims)
local base_w, base_h = calc_dims(self.base, get_dims)
return base_w, math.floor(base_h / self.framecount)
end
cd.verticalframe = frame_dims
cd.crack = frame_dims
cd.cracko = frame_dims
end
function cd:sheet(get_dims)
local base_w, base_h = calc_dims(self.base, get_dims)
return math.floor(base_w / self.w), math.floor(base_h / self.h)
end
function cd:png()
local png = modlib.minetest.decode_png(modlib.text.inputstream(self.data))
return png.width, png.height
end
return calc_dims

View file

@ -0,0 +1,422 @@
local colorspec = modlib.minetest.colorspec
local texmod = {}
local mod = {}
local metatable = {__index = mod}
local function new(self)
return setmetatable(self, metatable)
end
-- `texmod{...}` may be used to create texture modifiers, bypassing the checks
setmetatable(texmod, {__call = new})
-- Constructors / "generators"
function texmod.file(filename)
-- See `TEXTURENAME_ALLOWED_CHARS` in Minetest (`src/network/networkprotocol.h`)
assert(not filename:find"[^%w_.-]", "invalid characters in file name")
return new{
type = "file",
filename = filename
}
end
function texmod.png(data)
assert(type(data) == "string")
return new{
type = "png",
data = data
}
end
function texmod.combine(w, h, blits)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
for _, blit in ipairs(blits) do
assert(blit.x % 1 == 0)
assert(blit.y % 1 == 0)
assert(blit.texture)
end
return new{
type = "combine",
w = w,
h = h,
blits = blits
}
end
function texmod.inventorycube(top, left, right)
return new{
type = "inventorycube",
top = top,
left = left,
right = right
}
end
-- As a base generator, `fill` ignores `x` and `y`. Leave them as `nil`.
function texmod.fill(w, h, color)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
return new{
type = "fill",
w = w,
h = h,
color = colorspec.from_any(color)
}
end
-- Methods / "modifiers"
local function assert_int_range(num, min, max)
assert(num % 1 == 0 and num >= min and num <= max)
end
-- As a modifier, `fill` takes `x` and `y`
function mod:fill(w, h, x, y, color)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
assert(x % 1 == 0 and x >= 0)
assert(y % 1 == 0 and y >= 0)
return new{
type = "fill",
base = self,
w = w,
h = h,
x = x,
y = y,
color = colorspec.from_any(color)
}
end
-- This is the real "overlay", associated with `^`.
function mod:blit(overlay)
return new{
type = "blit",
base = self,
over = overlay
}
end
function mod:brighten()
return new{
type = "brighten",
base = self,
}
end
function mod:noalpha()
return new{
type = "noalpha",
base = self
}
end
function mod:resize(w, h)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
return new{
type = "resize",
base = self,
w = w,
h = h,
}
end
local function assert_uint8(num)
assert_int_range(num, 0, 0xFF)
end
function mod:makealpha(r, g, b)
assert_uint8(r); assert_uint8(g); assert_uint8(b)
return new{
type = "makealpha",
base = self,
r = r, g = g, b = b
}
end
function mod:opacity(ratio)
assert_uint8(ratio)
return new{
type = "opacity",
base = self,
ratio = ratio
}
end
local function tobool(val)
return not not val
end
function mod:invert(channels --[[set with keys "r", "g", "b", "a"]])
return new{
type = "invert",
base = self,
r = tobool(channels.r),
g = tobool(channels.g),
b = tobool(channels.b),
a = tobool(channels.a)
}
end
function mod:flip(flip_axis --[["x" or "y"]])
return self:transform(assert(
(flip_axis == "x" and "fx")
or (flip_axis == "y" and "fy")
or (not flip_axis and "i")))
end
function mod:rotate(deg)
assert(deg % 90 == 0)
deg = deg % 360
return self:transform(("r%d"):format(deg))
end
-- D4 group transformations (see https://proofwiki.org/wiki/Definition:Dihedral_Group_D4),
-- represented using indices into a table of matrices
-- TODO (...) try to come up with a more elegant solution
do
-- Matrix multiplication for composition: First applies a, then b <=> b * a
local function mat_2x2_compose(a, b)
local a_1_1, a_1_2, a_2_1, a_2_2 = unpack(a)
local b_1_1, b_1_2, b_2_1, b_2_2 = unpack(b)
return {
a_1_1 * b_1_1 + a_2_1 * b_1_2, a_1_2 * b_1_1 + a_2_2 * b_1_2;
a_1_1 * b_2_1 + a_2_1 * b_2_2, a_1_2 * b_2_1 + a_2_2 * b_2_2
}
end
local r90 ={
0, -1;
1, 0
}
local fx = {
-1, 0;
0, 1
}
local fy = {
1, 0;
0, -1
}
local r180 = mat_2x2_compose(r90, r90)
local r270 = mat_2x2_compose(r180, r90)
local fxr90 = mat_2x2_compose(fx, r90)
local fyr90 = mat_2x2_compose(fy, r90)
local transform_mats = {[0] = {1, 0; 0, 1}, r90, r180, r270, fx, fxr90, fy, fyr90}
local transform_idx_by_name = {i = 0, r90 = 1, r180 = 2, r270 = 3, fx = 4, fxr90 = 5, fy = 6, fyr90 = 7}
-- Lookup tables for getting the flipped axis / rotation angle
local flip_by_idx = {
[4] = "x",
[5] = "x",
[6] = "y",
[7] = "y",
}
local rot_by_idx = {
[1] = 90,
[2] = 180,
[3] = 270,
[5] = 90,
[7] = 90,
}
local idx_by_mat_2x2 = {}
local function transform_idx(mat)
-- note: assumes mat[i] in {-1, 0, 1}
return mat[1] + 3*(mat[2] + 3*(mat[3] + 3*mat[4]))
end
for i = 0, 7 do
idx_by_mat_2x2[transform_idx(transform_mats[i])] = i
end
-- Compute a multiplication table
local composition_idx = {}
local function ij_idx(i, j)
return i*8 + j
end
for i = 0, 7 do
for j = 0, 7 do
composition_idx[ij_idx(i, j)] = assert(idx_by_mat_2x2[
transform_idx(mat_2x2_compose(transform_mats[i], transform_mats[j]))])
end
end
function mod:transform(...)
if select("#", ...) == 0 then return self end
local idx = ...
if type(idx) == "string" then
idx = assert(transform_idx_by_name[idx:lower()])
end
local base = self
if self.type == "transform" then
-- Merge with a `^[transform` base image
assert(transform_mats[idx])
base = self.base
idx = composition_idx[ij_idx(self.idx, idx)]
end
assert(transform_mats[idx])
if idx == 0 then return base end -- identity
return new{
type = "transform",
base = base,
idx = idx,
-- Redundantly store this information for convenience. Do not modify!
flip_axis = flip_by_idx[idx],
rotation_deg = rot_by_idx[idx] or 0,
}:transform(select(2, ...))
end
end
function mod:verticalframe(framecount, frame)
assert(framecount >= 1)
assert(frame >= 0)
return new{
type = "verticalframe",
base = self,
framecount = framecount,
frame = frame
}
end
local function crack(self, name, ...)
local tilecount, framecount, frame
if select("#", ...) == 2 then
tilecount, framecount, frame = 1, ...
else
assert(select("#", ...) == 3, "invalid number of arguments")
tilecount, framecount, frame = ...
end
assert(tilecount >= 1)
assert(framecount >= 1)
assert(frame >= 0)
return new{
type = name,
base = self,
tilecount = tilecount,
framecount = framecount,
frame = frame
}
end
function mod:crack(...)
return crack(self, "crack", ...)
end
function mod:cracko(...)
return crack(self, "cracko", ...)
end
mod.crack_with_opacity = mod.cracko
function mod:sheet(w, h, x, y)
assert(w % 1 == 0 and w >= 1)
assert(h % 1 == 0 and h >= 1)
assert(x % 1 == 0 and x >= 0)
assert(y % 1 == 0 and y >= 0)
return new{
type = "sheet",
base = self,
w = w,
h = h,
x = x,
y = y
}
end
function mod:screen(color)
return new{
type = "screen",
base = self,
color = colorspec.from_any(color),
}
end
function mod:multiply(color)
return new{
type = "multiply",
base = self,
color = colorspec.from_any(color)
}
end
function mod:colorize(color, ratio)
color = colorspec.from_any(color)
if ratio == "alpha" then
assert(color.alpha or 0xFF == 0xFF)
else
ratio = ratio or color.alpha or 0xFF
assert_uint8(ratio)
if color.alpha == ratio then
ratio = nil
end
end
return new{
type = "colorize",
base = self,
color = color,
ratio = ratio
}
end
local function hsl(type, s_def, s_max, l_def)
return function(self, h, s, l)
s, l = s or s_def, l or l_def
assert_int_range(h, -180, 180)
assert_int_range(s, 0, s_max)
assert_int_range(l, -100, 100)
return new{
type = type,
base = self,
hue = h,
saturation = s,
lightness = l,
}
end
end
mod.colorizehsl = hsl("colorizehsl", 50, 100, 0)
mod.hsl = hsl("hsl", 0, math.huge, 0)
function mod:contrast(contrast, brightness)
brightness = brightness or 0
assert_int_range(contrast, -127, 127)
assert_int_range(brightness, -127, 127)
return new{
type = "contrast",
base = self,
contrast = contrast,
brightness = brightness,
}
end
function mod:mask(mask_texmod)
return new{
type = "mask",
base = self,
_mask = mask_texmod
}
end
function mod:hardlight(overlay)
return new{
type = "hardlight",
base = self,
over = overlay
}
end
-- Overlay *blend*.
-- This was unfortunately named `[overlay` in Minetest,
-- and so is named `:overlay` for consistency.
--! Do not confuse this with the simple `^` used for blitting
function mod:overlay(overlay)
return overlay:hardlight(self)
end
function mod:lowpart(percent, overlay)
assert(percent % 1 == 0 and percent >= 0 and percent <= 100)
return new{
type = "lowpart",
base = self,
percent = percent,
over = overlay
}
end
return texmod, metatable

View file

@ -0,0 +1,190 @@
local tex = modlib.tex
local paths = modlib.minetest.media.paths
local function read_png(fname)
if fname == "blank.png" then return tex.new{w=1,h=1,0} end
return tex.read_png(assert(paths[fname]))
end
local gt = {}
-- TODO colorizehsl, hsl, contrast
-- TODO (...) inventorycube; this is nontrivial.
function gt:file()
return read_png(self.filename)
end
function gt:opacity()
local t = self.base:gen_tex()
t:opacity(self.ratio / 255)
return t
end
function gt:invert()
local t = self.base:gen_tex()
t:invert(self.r, self.g, self.b, self.a)
return t
end
function gt:brighten()
local t = self.base:gen_tex()
t:brighten()
return t
end
function gt:noalpha()
local t = self.base:gen_tex()
t:noalpha()
return t
end
function gt:makealpha()
local t = self.base:gen_tex()
t:makealpha(self.r, self.g, self.b)
return t
end
function gt:multiply()
local c = self.color
local t = self.base:gen_tex()
t:multiply_rgb(c.r, c.g, c.b)
return t
end
function gt:screen()
local c = self.color
local t = self.base:gen_tex()
t:screen_blend_rgb(c.r, c.g, c.b)
return t
end
function gt:colorize()
local c = self.color
local t = self.base:gen_tex()
t:colorize(c.r, c.g, c.b, self.ratio)
return t
end
local function resized_to_larger(a, b)
if a.w * a.h > b.w * b.h then
b = b:resized(a.w, a.h)
else
a = a:resized(b.w, b.h)
end
return a, b
end
function gt:mask()
local a, b = resized_to_larger(self.base:gen_tex(), self._mask:gen_tex())
a:band(b)
return a
end
function gt:lowpart()
local t = self.base:gen_tex()
local over = self.over:gen_tex()
local lowpart_h = math.ceil(self.percent/100 * over.h) -- TODO (?) ceil or floor
if lowpart_h > 0 then
t, over = resized_to_larger(t, over)
local y = over.h - lowpart_h + 1
over:crop(1, y, over.w, over.h)
t:blit(1, y, over)
end
return t
end
function gt:resize()
return self.base:gen_tex():resized(self.w, self.h)
end
function gt:combine()
local t = tex.filled(self.w, self.h, 0)
for _, blt in ipairs(self.blits) do
t:blit(blt.x + 1, blt.y + 1, blt.texture:gen_tex())
end
return t
end
function gt:fill()
if self.base then
return self.base:gen_tex():fill(self.w, self.h, self.x, self.y, self.color:to_number())
end
return tex.filled(self.w, self.h, self.color:to_number())
end
function gt:blit()
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
t:blit(1, 1, o)
return t
end
function gt:hardlight()
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
t:hardlight_blend(o)
return t
end
-- TODO (...?) optimize this
function gt:transform()
local t = self.base:gen_tex()
if self.flip_axis == "x" then
t:flip_x()
elseif self.flip_axis == "y" then
t:flip_y()
end
-- TODO implement counterclockwise rotations to get rid of this hack
for _ = 1, 360 - self.rotation_deg / 90 do
t = t:rotated_90()
end
return t
end
local frame = function(t, frame, framecount)
local fh = math.floor(t.h / framecount)
t:crop(1, frame * fh + 1, t.w, (frame + 1) * fh)
end
local crack = function(self, o)
local crack = read_png"crack_anylength.png"
frame(crack, self.frame, math.floor(crack.h / crack.w))
local t = self.base:gen_tex()
local tile_w, tile_h = math.floor(t.w / self.tilecount), math.floor(t.h / self.framecount)
crack = crack:resized(tile_w, tile_h)
for ty = 1, t.h, tile_h do
for tx = 1, t.w, tile_w do
t[o and "blito" or "blit"](t, tx, ty, crack)
end
end
return t
end
function gt:crack()
return crack(self, false)
end
function gt:cracko()
return crack(self, true)
end
function gt:verticalframe()
local t = self.base:gen_tex()
frame(t, self.frame, self.framecount)
return t
end
function gt:sheet()
local t = self.base:gen_tex()
local tw, th = math.floor(t.w / self.w), math.floor(t.h / self.h)
local x, y = self.x, self.y
t:crop(x * tw + 1, y * th + 1, (x + 1) * tw, (y + 1) * th)
return t
end
function gt:png()
return tex.read_png_string(self.data)
end
return function(self)
return assert(gt[self.type])(self)
end

View file

@ -0,0 +1,429 @@
local texmod = ...
local colorspec = modlib.minetest.colorspec
-- Generator readers
local gr = {}
function gr.png(r)
r:expect":"
local base64 = r:match_str"[a-zA-Z0-9+/=]"
return assert(minetest.decode_base64(base64), "invalid base64")
end
function gr.inventorycube(r)
local top = r:invcubeside()
local left = r:invcubeside()
local right = r:invcubeside()
return top, left, right
end
function gr.combine(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
local blits = {}
while r:match":" do
if r.eof then break end -- we can just end with `:`, right?
local x = r:int()
r:expect","
local y = r:int()
r:expect"="
table.insert(blits, {x = x, y = y, texture = r:subtexp()})
end
return w, h, blits
end
function gr.fill(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
-- Be strict(er than Minetest): Do not accept x, y for a base
local color = r:colorspec()
return w, h, color
end
-- Parameter readers
local pr = {}
function pr.fill(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
if assert(r:peek(), "unexpected eof"):find"%d" then
local x = r:int()
r:expect","
local y = r:int()
r:expect":"
local color = r:colorspec()
return w, h, x, y, color
end
local color = r:colorspec()
return w, h, color
end
function pr.brighten() end
function pr.noalpha() end
function pr.resize(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
return w, h
end
function pr.makealpha(r)
r:expect":"
local red = r:int()
r:expect","
local green = r:int()
r:expect","
local blue = r:int()
return red, green, blue
end
function pr.opacity(r)
r:expect":"
local ratio = r:int()
return ratio
end
function pr.invert(r)
r:expect":"
local channels = {}
while true do
local c = r:match_charset"[rgba]"
if not c then break end
channels[c] = true
end
return channels
end
do
function pr.transform(r)
if r:match_charset"[iI]" then
return pr.transform(r)
end
local idx = r:match_charset"[0-7]"
if idx then
return tonumber(idx), pr.transform(r)
end
if r:match_charset"[fF]" then
local flip_axis = assert(r:match_charset"[xXyY]", "axis expected")
return "f" .. flip_axis, pr.transform(r)
end
if r:match_charset"[rR]" then
local deg = r:match_str"%d"
-- Be strict here: Minetest won't recognize other ways to write these numbers (or other numbers)
assert(deg == "90" or deg == "180" or deg == "270")
return ("r%d"):format(deg), pr.transform(r)
end
-- return nothing, we're done
end
end
function pr.verticalframe(r)
r:expect":"
local framecount = r:int()
r:expect":"
local frame = r:int()
return framecount, frame
end
function pr.crack(r)
r:expect":"
local framecount = r:int()
r:expect":"
local frame = r:int()
if r:match":" then
return framecount, frame, r:int()
end
return framecount, frame
end
pr.cracko = pr.crack
function pr.sheet(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
local x = r:int()
r:expect","
local y = r:int()
return w, h, x, y
end
function pr.multiply(r)
r:expect":"
return r:colorspec()
end
pr.screen = pr.multiply
function pr.colorize(r)
r:expect":"
local color = r:colorspec()
if not r:match":" then
return color
end
if not r:match"a" then
return color, r:int()
end
for c in ("lpha"):gmatch"." do
r:expect(c)
end
return color, "alpha"
end
function pr.colorizehsl(r)
r:expect":"
local hue = r:int()
if not r:match":" then
return hue
end
local saturation = r:int()
if not r:match":" then
return hue, saturation
end
local lightness = r:int()
return hue, saturation, lightness
end
pr.hsl = pr.colorizehsl
function pr.contrast(r)
r:expect":"
local contrast = r:int()
if not r:match":" then
return contrast
end
local brightness = r:int()
return contrast, brightness
end
function pr.overlay(r)
r:expect":"
return r:subtexp()
end
function pr.hardlight(r)
r:expect":"
return r:subtexp()
end
function pr.mask(r)
r:expect":"
return r:subtexp()
end
function pr.lowpart(r)
r:expect":"
local percent = r:int()
assert(percent)
r:expect":"
return percent, r:subtexp()
end
-- Build a prefix tree of parameter readers to greedily match the longest texture modifier prefix;
-- just matching `%a+` and looking it up in a table
-- doesn't work since `[transform` may be followed by a lowercase transform name
-- TODO (?...) consolidate with `modlib.trie`
local texmod_reader_trie = {}
for _, readers in pairs{pr, gr} do
for type in pairs(readers) do
local subtrie = texmod_reader_trie
for char in type:gmatch"." do
subtrie[char] = subtrie[char] or {}
subtrie = subtrie[char]
end
subtrie.type = type
end
end
-- Reader methods. We use `r` instead of the `self` "sugar" for consistency (and to save us some typing).
local rm = {}
function rm.peek(r, parenthesized)
if r.eof then return end
local expected_escapes = 0
if r.level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, r.level)
end
if r.character:match"[&^:]" then -- "special" characters - these need to be escaped
if r.escapes == expected_escapes then
return r.character
elseif parenthesized and r.character == "^" and r.escapes < expected_escapes then
-- Special handling for `^` inside `(...)`: This is undocumented behavior but works in Minetest
r.warn"parenthesized caret (`^`) with too few escapes"
return r.character
end
elseif r.escapes <= expected_escapes then
return r.character
end if r.escapes >= 2*expected_escapes then
return "\\"
end
end
function rm.popchar(r)
assert(not r.eof, "unexpected eof")
r.escapes = 0
while true do
r.character = r:read_char()
if r.character ~= "\\" then break end
r.escapes = r.escapes + 1
end
if r.character == nil then
assert(r.escapes == 0, "end of texmod expected")
r.eof = true
end
end
function rm.pop(r)
local expected_escapes = 0
if r.level > 0 then
-- Premature optimization my beloved (this is `2^(level-1)`)
expected_escapes = math.ldexp(0.5, r.level)
end
if r.escapes > 0 and r.escapes >= 2*expected_escapes then
r.escapes = r.escapes - 2*expected_escapes
return
end
return r:popchar()
end
function rm.match(r, char)
if r:peek() == char then
r:pop()
return true
end
end
function rm.expect(r, char)
if not r:match(char) then
error(("%q expected"):format(char))
end
end
function rm.hat(r, parenthesized)
if r:peek(parenthesized) == (r.invcube and "&" or "^") then
r:pop()
return true
end
end
function rm.match_charset(r, set)
local char = r:peek()
if char and char:match(set) then
r:pop()
return char
end
end
function rm.match_str(r, set)
local c = r:match_charset(set)
if not c then
error(("character in %s expected"):format(set))
end
local t = {c}
while true do
c = r:match_charset(set)
if not c then break end
table.insert(t, c)
end
return table.concat(t)
end
function rm.int(r)
local sign = 1
if r:match"-" then sign = -1 end
return sign * tonumber(r:match_str"%d")
end
function rm.fname(r)
-- This is overly permissive, as is Minetest;
-- we just allow arbitrary characters up until a character which may terminate the name.
-- Inside an inventorycube, `&` also terminates names.
-- Note that the constructor will however - unlike Minetest - perform validation.
-- We could leverage the knowledge of the allowed charset here already,
-- but that might lead to more confusing error messages.
return r:match_str(r.invcube and "[^:^&){]" or "[^:^){]")
end
function rm.subtexp(r)
r.level = r.level + 1
local res = r:texp()
r.level = r.level - 1
return res
end
function rm.invcubeside(r)
assert(not r.invcube, "can't nest inventorycube")
r.invcube = true
assert(r:match"{", "'{' expected")
local res = r:texp()
r.invcube = false
return res
end
function rm.basexp(r)
if r:match"(" then
local res = r:texp(true)
r:expect")"
return res
end
if r:match"[" then
local type = r:match_str"%a"
local gen_reader = gr[type]
if not gen_reader then
error("invalid texture modifier: " .. type)
end
return texmod[type](gen_reader(r))
end
return texmod.file(r:fname())
end
function rm.colorspec(r)
-- Leave exact validation up to colorspec, only do a rough greedy charset matching
return assert(colorspec.from_string(r:match_str"[#%x%a]"))
end
function rm.texp(r, parenthesized)
local base = r:basexp() -- TODO (?) make optional - warn about omitting the base
while r:hat(parenthesized) do
if r:match"[" then
local reader_subtrie = texmod_reader_trie
while true do
local next_subtrie = reader_subtrie[r:peek()]
if next_subtrie then
reader_subtrie = next_subtrie
r:pop()
else
break
end
end
local type = assert(reader_subtrie.type, "invalid texture modifier")
local param_reader, gen_reader = pr[type], gr[type]
assert(param_reader or gen_reader)
if param_reader then
-- Note: It is important that this takes precedence to properly handle `[fill`
base = base[type](base, param_reader(r))
elseif gen_reader then
base = base:blit(texmod[type](gen_reader(r)))
end
-- TODO (?...) we could consume leftover parameters here to be as lax as Minetest
else
base = base:blit(r:basexp())
end
end
return base
end
local mt = {__index = rm}
return function(read_char, warn --[[function(str)]])
local r = setmetatable({
level = 0,
invcube = false,
parenthesized = false,
eof = false,
read_char = read_char,
warn = warn or error,
}, mt)
r:popchar()
local res = r:texp(false)
assert(r.eof, "eof expected")
return res
end

View file

@ -0,0 +1,181 @@
local pw = {} -- parameter writers: `[type] = func(self, write)`
function pw:png(w)
w.colon(); w.str(minetest.encode_base64(self.data))
end
function pw:combine(w)
w.colon(); w.int(self.w); w.str"x"; w.str(self.h)
for _, blit in ipairs(self.blits) do
w.colon()
w.int(blit.x); w.str","; w.int(blit.y); w.str"="
w.esctex(blit.texture)
end
end
function pw:inventorycube(w)
assert(not w.inventorycube, "[inventorycube may not be nested")
local function write_side(side)
w.str"{"
w.inventorycube = true
w.tex(self[side])
w.inventorycube = false
end
write_side"top"
write_side"left"
write_side"right"
end
-- Handles both the generator & the modifier
function pw:fill(w)
w.colon(); w.int(self.w); w.str"x"; w.int(self.h)
if self.base then
w.colon(); w.int(self.x); w.str","; w.int(self.y)
end
w.colon(); w.str(self.color:to_string())
end
-- No parameters to write
pw.brighten = modlib.func.no_op
pw.noalpha = modlib.func.no_op
function pw:resize(w)
w.colon(); w.int(self.w); w.str"x"; w.int(self.h)
end
function pw:makealpha(w)
w.colon(); w.int(self.r); w.str","; w.int(self.g); w.str","; w.int(self.b)
end
function pw:opacity(w)
w.colon(); w.int(self.ratio)
end
function pw:invert(w)
w.colon()
if self.r then w.str"r" end
if self.g then w.str"g" end
if self.b then w.str"b" end
if self.a then w.str"a" end
end
function pw:transform(w)
w.int(self.idx)
end
function pw:verticalframe(w)
w.colon(); w.int(self.framecount); w.colon(); w.int(self.frame)
end
function pw:crack(w)
w.colon(); w.int(self.tilecount); w.colon(); w.int(self.framecount); w.colon(); w.int(self.frame)
end
pw.cracko = pw.crack
function pw:sheet(w)
w.colon(); w.int(self.w); w.str"x"; w.int(self.h); w.colon(); w.int(self.x); w.str","; w.int(self.y)
end
function pw:screen(w)
w.colon()
w.str(self.color:to_string())
end
function pw:multiply(w)
w.colon()
w.str(self.color:to_string())
end
function pw:colorize(w)
w.colon()
w.str(self.color:to_string())
if self.ratio then
w.colon()
if self.ratio == "alpha" then
w.str"alpha"
else
w.int(self.ratio)
end
end
end
function pw:colorizehsl(w)
w.colon(); w.int(self.hue); w.colon(); w.int(self.saturation); w.colon(); w.int(self.lightness)
end
pw.hsl = pw.colorizehsl
function pw:contrast(w)
w.colon(); w.int(self.contrast); w.colon(); w.int(self.brightness)
end
-- We don't have to handle `[overlay`; the DSL normalizes everything to `[hardlight`
function pw:hardlight(w)
w.colon(); w.esctex(self.over)
end
function pw:mask(w)
w.colon(); w.esctex(self._mask)
end
function pw:lowpart(w)
w.colon(); w.int(self.percent); w.colon(); w.esctex(self.over)
end
-- Set of "non-modifiers" which do not modify a base image
local non_modifiers = {file = true, png = true, combine = true, inventorycube = true}
return function(self, write_str)
-- We could use a metatable here, but it wouldn't really be worth it;
-- it would save us instantiating a handful of closures at the cost of constant `__index` events
-- and having to constantly pass `self`, increasing code complexity
local w = {}
w.inventorycube = false
w.level = 0
w.str = write_str
function w.esc()
if w.level == 0 then return end
w.str(("\\"):rep(math.ldexp(0.5, w.level)))
end
function w.hat()
-- Note: We technically do not need to escape `&` inside an [inventorycube which is nested inside [combine,
-- but we do it anyways for good practice and since we have to escape `&` inside [combine inside [inventorycube.
w.esc()
w.str(w.inventorycube and "&" or "^")
end
function w.colon()
w.esc(); w.str":"
end
function w.int(int)
w.str(("%d"):format(int))
end
function w.tex(tex)
if tex.type == "file" then
w.str(tex.filename)
return
end
if tex.base then
w.tex(tex.base)
w.hat()
end
if tex.type == "blit" then -- simply `^`
if non_modifiers[tex.over.type] then
w.tex(tex.over)
else
-- Make sure the modifier is first applied to its base image
-- and only after this overlaid on top of `tex.base`
w.str"("; w.tex(tex.over); w.str")"
end
else
w.str"["
w.str(tex.type)
pw[tex.type](tex, w)
end
end
function w.esctex(tex)
w.level = w.level + 1
w.tex(tex)
w.level = w.level - 1
end
w.tex(self)
end

View file

@ -0,0 +1,66 @@
-- Localize globals
local minetest, modlib, pairs, table = minetest, modlib, pairs, table
-- Set environment
local _ENV = ...
setfenv(1, _ENV)
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
end}
--+ Registers an on_wielditem_change callback: function(player, previous_item, previous_index, item)
--+ Will be called once with player, nil, index, item on join
register_on_wielditem_change = modlib.func.curry(table.insert, registered_on_wielditem_changes)
local function register_callbacks()
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
-- Other on_joinplayer / on_leaveplayer callbacks should execute first
if minetest.get_current_modname() then
-- Loaded during load time, register callbacks after load time
minetest.register_on_mods_loaded(register_callbacks)
else
-- Loaded after load time, register callbacks immediately
register_callbacks()
end
-- TODO export
local function itemstack_equals(a, b)
return a:get_name() == b:get_name() and a:get_count() == b:get_count() and a:get_wear() == b:get_wear() and a:get_meta():equals(b:get_meta())
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 not (itemstack_equals(item, previous_item) and 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)

6
mods/modlib/mod.conf Normal file
View file

@ -0,0 +1,6 @@
name = modlib
title = Modding Library
description = Multipurpose Minetest Modding Library
author = LMD
optional_depends = dbg, strictest
release = 29483

View file

@ -0,0 +1,23 @@
local require = ... or require
-- TODO consider moving serializers in this namespace
local function load(module_name)
return assert(loadfile(modlib.mod.get_resource(modlib.modname, "persistence", module_name .. ".lua")))
end
return setmetatable({}, {__index = function(self, module_name)
if module_name == "lua_log_file" then
local module = load(module_name)()
self[module_name] = module
return module
end
if module_name == "sqlite3" then
local func = load(module_name)
local module = function(sqlite3)
if sqlite3 then
return func(sqlite3)
end
return func(require"lsqlite3")
end
self[module_name] = module
return module
end
end})

View file

@ -0,0 +1,194 @@
-- Localize globals
local assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type
= assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
-- Default value
reference_strings = true
-- Note: keys may not be marked as weak references: garbage collected log files wouldn't close the file:
-- The `__gc` metamethod doesn't work for tables in Lua 5.1; a hack using `newproxy` would be needed
-- See https://stackoverflow.com/questions/27426704/lua-5-1-workaround-for-gc-metamethod-for-tables)
-- Therefore, :close() must be called on log files to remove them from the `files` table
local files = {}
local metatable = {__index = _ENV}
_ENV.metatable = metatable
function new(file_path, root, reference_strings)
local self = setmetatable({
file_path = assert(file_path),
root = root,
reference_strings = reference_strings
}, metatable)
if minetest then
files[self] = true
end
return self
end
local function set_references(self, table)
-- Weak table keys to allow the collection of dead reference tables
-- TODO garbage collect strings in the references table
self.references = setmetatable(table, {__mode = "k"})
end
function load(self)
-- Bytecode is blocked by the engine
local read = assert(loadfile(self.file_path))
-- math.huge is serialized to inf
local env = {inf = math.huge}
setfenv(read, env)
read()
env.R = env.R or {{}}
local reference_count = #env.R
for ref in pairs(env.R) do
if ref > reference_count then
-- Ensure reference count always has the value of the largest reference
-- in case of "holes" (nil values) in the reference list
reference_count = ref
end
end
self.reference_count = reference_count
self.root = env.R[1]
set_references(self, {})
end
function open(self)
self.file = io.open(self.file_path, "a+")
end
function init(self)
if modlib.file.exists(self.file_path) then
self:load()
self:_rewrite()
self:open()
return
end
self:open()
self:_write()
end
function log(self, statement)
self.file:write(statement)
self.file:write"\n"
end
function flush(self)
self.file:flush()
end
function close(self)
self.file:close()
self.file = nil
files[self] = nil
end
if minetest then
minetest.register_on_shutdown(function()
for self in pairs(files) do
self.file:close()
end
end)
end
local function _dump(self, value, is_key)
if value == nil then
return "nil"
end
if value == true then
return "true"
end
if value == false then
return "false"
end
if value ~= value then
-- nan
return "0/0"
end
local _type = type(value)
if _type == "number" then
return ("%.17g"):format(value)
end
local reference = self.references[value]
if reference then
return "R[" .. reference .."]"
end
reference = self.reference_count + 1
local key = "R[" .. reference .."]"
local function create_reference()
self.reference_count = reference
self.references[value] = reference
end
if _type == "string" then
local reference_strings = self.reference_strings
if is_key and ((not reference_strings) or value:len() <= key:len()) and modlib.text.is_identifier(value) then
-- Short key
return value, true
end
local formatted = ("%q"):format(value)
if (not reference_strings) or formatted:len() <= key:len() then
-- Short string
return formatted
end
-- Use reference
create_reference()
self:log(key .. "=" .. formatted)
elseif _type == "table" then
-- Tables always need a reference before they are traversed to prevent infinite recursion
create_reference()
-- TODO traverse tables to determine whether this is actually needed
self:log(key .. "={}")
for k, v in pairs(value) do
local dumped, short = _dump(self, k, true)
self:log(key .. (short and ("." .. dumped) or ("[" .. dumped .. "]")) .. "=" .. _dump(self, v))
end
else
error("unsupported type: " .. _type)
end
return key
end
function set(self, table, key, value)
if not self.references[table] then
error"orphan table"
end
if table[key] == value then
-- No change
return
end
table[key] = value
table = _dump(self, table)
local key, short_key = _dump(self, key, true)
self:log(table .. (short_key and ("." .. key) or ("[" .. key .. "]")) .. "=" .. _dump(self, value))
end
function set_root(self, key, value)
return self:set(self.root, key, value)
end
function _write(self)
set_references(self, {})
self.reference_count = 0
self:log"R={}"
_dump(self, self.root)
end
function _rewrite(self)
self.file = io.open(self.file_path, "w+")
self:_write()
self.file:close()
end
function rewrite(self)
if self.file then
self.file:close()
end
self:_rewrite()
self:open()
end
-- Export environment
return _ENV

View file

@ -0,0 +1,318 @@
local assert, error, math_huge, modlib, minetest, setmetatable, type, table_insert, table_sort, pairs, ipairs
= assert, error, math.huge, modlib, minetest, setmetatable, type, table.insert, table.sort, pairs, ipairs
local sqlite3 = ...
--[[
Currently uses reference counting to immediately delete tables which aren't reachable from the root table anymore, which has two issues:
1. Deletion might trigger a large deletion chain
TODO defer deletion, clean up unused tables on startup, delete & iterate tables partially
2. Reference counting is unable to handle cycles. `:collectgarbage()` implements a tracing "stop-the-world" garbage collector which handles cycles.
TODO take advantage of Lua's garbage collection by keeping a bunch of "twin" objects in a weak structure using proxies (Lua 5.1) or the __gc metamethod (Lua 5.2)
See https://wiki.c2.com/?ReferenceCountingCanHandleCycles, https://www.memorymanagement.org/mmref/recycle.html#mmref-recycle and https://wiki.c2.com/?GenerationalGarbageCollectio
Weak tables are of no use here, as we need to be notified when a reference is dropped
]]
local ptab = {} -- SQLite3-backed implementation for a persistent Lua table ("ptab")
local metatable = {__index = ptab}
ptab.metatable = metatable
-- Note: keys may not be marked as weak references: wouldn't close the database: see persistence/lua_log_file.lua
local databases = {}
local types = {
boolean = 1,
number = 2,
string = 3,
table = 4
}
local function increment_highest_table_id(self)
self.highest_table_id = self.highest_table_id + 1
if self.highest_table_id > 2^50 then
-- IDs are approaching double precision limit (52 bits mantissa), defragment them
self:defragment_ids()
end
return self.highest_table_id
end
function ptab.new(file_path, root)
return setmetatable({
database = sqlite3.open(file_path),
root = root
}, metatable)
end
function ptab.setmetatable(self)
assert(self.database and self.root)
return setmetatable(self, metatable)
end
local set
local function add_table(self, table)
if type(table) ~= "table" then return end
if self.counts[table] then
self.counts[table] = self.counts[table] + 1
return
end
self.table_ids[table] = increment_highest_table_id(self)
self.counts[table] = 1
for k, v in pairs(table) do
set(self, table, k, v)
end
end
local decrement_reference_count
local function delete_table(self, table)
local id = assert(self.table_ids[table])
self.table_ids[table] = nil
self.counts[table] = nil
for k, v in pairs(table) do
decrement_reference_count(self, k)
decrement_reference_count(self, v)
end
local statement = self._prepared.delete_table
statement:bind(1, id)
statement:step()
statement:reset()
end
function decrement_reference_count(self, table)
if type(table) ~= "table" then return end
local count = self.counts[table]
if not count then return end
count = count - 1
if count == 0 then return delete_table(self, table) end
self.counts[table] = count
end
function set(self, table, key, value)
local deletion = value == nil
if not deletion then
add_table(self, key)
add_table(self, value)
end
local previous_value = table[key]
if type(previous_value) == "table" then
decrement_reference_count(self, previous_value)
end
if deletion and type(key) == "table" then
decrement_reference_count(self, key)
end
local statement = self._prepared[deletion and "delete" or "insert"]
local function bind_type_and_content(n, value)
local type_ = type(value)
statement:bind(n, assert(types[type_]))
if type_ == "boolean" then
statement:bind(n + 1, value and 1 or 0)
elseif type_ == "number" then
if value ~= value then
statement:bind(n + 1, "nan")
elseif value == math_huge then
statement:bind(n + 1, "inf")
elseif value == -math_huge then
statement:bind(n + 1, "-inf")
else
statement:bind(n + 1, value)
end
elseif type_ == "string" then
-- Use bind_blob instead of bind as Lua strings are effectively byte strings
statement:bind_blob(n + 1, value)
elseif type_ == "table" then
statement:bind(n + 1, self.table_ids[value])
end
end
statement:bind(1, assert(self.table_ids[table]))
bind_type_and_content(2, key)
if not deletion then
bind_type_and_content(4, value)
end
statement:step()
statement:reset()
end
local function exec(self, sql)
if self.database:exec(sql) ~= sqlite3.OK then
error(self.database:errmsg())
end
end
function ptab:init()
local database = self.database
local function prepare(sql)
local stmt = database:prepare(sql)
if not stmt then error(database:errmsg()) end
return stmt
end
exec(self, [[
CREATE TABLE IF NOT EXISTS table_entries (
table_id INTEGER NOT NULL,
key_type INTEGER NOT NULL,
key BLOB NOT NULL,
value_type INTEGER NOT NULL,
value BLOB NOT NULL,
PRIMARY KEY (table_id, key_type, key)
)]])
self._prepared = {
insert = prepare"INSERT OR REPLACE INTO table_entries(table_id, key_type, key, value_type, value) VALUES (?, ?, ?, ?, ?)",
delete = prepare"DELETE FROM table_entries WHERE table_id = ? AND key_type = ? AND key = ?",
delete_table = prepare"DELETE FROM table_entries WHERE table_id = ?",
update = {
id = prepare"UPDATE table_entries SET table_id = ? WHERE table_id = ?",
keys = prepare("UPDATE table_entries SET key = ? WHERE key_type = " .. types.table .. " AND key = ?"),
values = prepare("UPDATE table_entries SET value = ? WHERE value_type = " .. types.table .. " AND value = ?")
}
}
-- Default value
self.highest_table_id = 0
for id in self.database:urows"SELECT MAX(table_id) FROM table_entries" do
-- Gets a single value
self.highest_table_id = id
end
increment_highest_table_id(self)
local tables = {}
local counts = {}
self.counts = counts
local function get_value(type_, content)
if type_ == types.boolean then
if content == 0 then return false end
if content == 1 then return true end
error("invalid boolean value: " .. content)
end
if type_ == types.number then
if content == "nan" then
return 0/0
end
if content == "inf" then
return math_huge
end
if content == "-inf" then
return -math_huge
end
assert(type(content) == "number")
return content
end
if type_ == types.string then
assert(type(content) == "string")
return content
end
if type_ == types.table then
-- Table reference
tables[content] = tables[content] or {}
counts[content] = counts[content] or 1
return tables[content]
end
-- Null is unused
error("unsupported type: " .. type_)
end
-- Order by key_content to retrieve list parts in the correct order, making it easier for Lua
for table_id, key_type, key, value_type, value in self.database:urows"SELECT * FROM table_entries ORDER BY table_id, key_type, key" do
local table = tables[table_id] or {}
counts[table] = counts[table] or 1
table[get_value(key_type, key)] = get_value(value_type, value)
tables[table_id] = table
end
if tables[1] then
self.root = tables[1]
counts[self.root] = counts[self.root] + 1
self.table_ids = modlib.table.flip(tables)
self:collectgarbage()
else
self.highest_table_id = 0
self.table_ids = {}
add_table(self, self.root)
end
databases[self] = true
end
function ptab:rewrite()
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
exec(self, "DELETE FROM table_entries")
self.highest_table_id = 0
self.table_ids = {}
self.counts = {}
add_table(self, self.root)
exec(self, "COMMIT TRANSACTION")
end
function ptab:set(table, key, value)
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
local previous_value = table[key]
if previous_value == value then
-- no change
return
end
set(self, table, key, value)
table[key] = value
exec(self, "COMMIT TRANSACTION")
end
function ptab:set_root(key, value)
return self:set(self.root, key, value)
end
function ptab:collectgarbage()
local marked = {}
local function mark(table)
if type(table) ~= "table" or marked[table] then return end
marked[table] = true
for k, v in pairs(table) do
mark(k)
mark(v)
end
end
mark(self.root)
for table in pairs(self.table_ids) do
if not marked[table] then
delete_table(self, table)
end
end
end
function ptab:defragment_ids()
local ids = {}
for _, id in pairs(self.table_ids) do
table_insert(ids, id)
end
table_sort(ids)
local update = self._prepared.update
local tables = modlib.table.flip(self.table_ids)
for new_id, old_id in ipairs(ids) do
for _, stmt in pairs(update) do
stmt:bind_values(new_id, old_id)
stmt:step()
stmt:reset()
end
self.table_ids[tables[old_id]] = new_id
end
self.highest_table_id = #ids
end
local function finalize_statements(table)
for _, stmt in pairs(table) do
if type(stmt) == "table" then
finalize_statements(stmt)
else
local errcode = stmt:finalize()
assert(errcode == sqlite3.OK, errcode)
end
end
end
function ptab:close()
finalize_statements(self._prepared)
self.database:close()
databases[self] = nil
end
if minetest then
minetest.register_on_shutdown(function()
for self in pairs(databases) do
self:close()
end
end)
end
return ptab

165
mods/modlib/quaternion.lua Normal file
View file

@ -0,0 +1,165 @@
-- Localize globals
local math, modlib, pairs, unpack, vector = math, modlib, pairs, unpack, vector
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
-- TODO OOP, extend vector
function from_euler_rotation(rotation)
rotation = vector.divide(rotation, 2)
local cos = vector.apply(rotation, math.cos)
local sin = vector.apply(rotation, math.sin)
return {
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
cos.z * cos.x * sin.y - sin.z * sin.x * cos.y,
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
cos.z * cos.x * cos.y + sin.z * sin.x * sin.y
}
end
function from_euler_rotation_deg(rotation)
return from_euler_rotation(vector.apply(rotation, math.rad))
end
function multiply(self, other)
local X, Y, Z, W = unpack(self)
return normalize{
(other[4] * X) + (other[1] * W) + (other[2] * Z) - (other[3] * Y);
(other[4] * Y) + (other[2] * W) + (other[3] * X) - (other[1] * Z);
(other[4] * Z) + (other[3] * W) + (other[1] * Y) - (other[2] * X);
(other[4] * W) - (other[1] * X) - (other[2] * Y) - (other[3] * Z);
}
end
function compose(self, other)
return multiply(other, self)
end
function len(self)
return (self[1] ^ 2 + self[2] ^ 2 + self[3] ^ 2 + self[4] ^ 2) ^ 0.5
end
function normalize(self)
local l = len(self)
local res = {}
for key, value in pairs(self) do
res[key] = value / l
end
return res
end
function conjugate(self)
return {
-self[1],
-self[2],
-self[3],
self[4]
}
end
function inverse(self)
-- TODO this is just a fancy normalization *of the conjungate*,
-- which for rotations is the inverse
return modlib.vector.divide_scalar(conjugate(self), self[1] ^ 2 + self[2] ^ 2 + self[3] ^ 2 + self[4] ^ 2)
end
function negate(self)
for key, value in pairs(self) do
self[key] = -value
end
end
function dot(self, other)
return self[1] * other[1] + self[2] * other[2] + self[3] * other[3] + self[4] * other[4]
end
--: self normalized quaternion
--: other normalized quaternion
function slerp(self, other, ratio)
local d = dot(self, other)
if d < 0 then
d = -d
negate(other)
end
-- Threshold beyond which linear interpolation is used
if d > 1 - 1e-10 then
return modlib.vector.interpolate(self, other, ratio)
end
local theta_0 = math.acos(d)
local theta = theta_0 * ratio
local sin_theta = math.sin(theta)
local sin_theta_0 = math.sin(theta_0)
local s_1 = sin_theta / sin_theta_0
local s_0 = math.cos(theta) - d * s_1
return modlib.vector.add(modlib.vector.multiply_scalar(self, s_0), modlib.vector.multiply_scalar(other, s_1))
end
--> axis, angle
function to_axis_angle(self)
local axis = modlib.vector.new{self[1], self[2], self[3]}
local len = axis:length()
-- HACK invert axis for correct rotation in Minetest
return len == 0 and axis or axis:divide_scalar(-len), 2 * math.atan2(len, self[4])
end
function to_euler_rotation_rad(self)
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 sinp = 2 * (self[4] * self[2] - self[3] * self[1])
if sinp <= -1 then
rotation.y = -math.pi/2
elseif sinp >= 1 then
rotation.y = math.pi/2
else
rotation.y = 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)
rotation.z = math.atan2(siny_cosp, cosy_cosp)
return rotation
end
-- TODO rename this to to_euler_rotation_deg eventually (breaking change)
--> {x = pitch, y = yaw, z = roll} euler rotation in degrees
function to_euler_rotation(self)
return vector.apply(to_euler_rotation_rad(self), math.deg)
end
-- See https://github.com/zaki/irrlicht/blob/master/include/quaternion.h#L652
function to_euler_rotation_irrlicht(self)
local x, y, z, w = unpack(self)
local test = 2 * (y * w - x * z)
local rot
if math.abs(test - 1) <= 1e-6 then
rot = {
z = -2 * math.atan2(x, w),
x = 0,
y = math.pi/2
}
elseif math.abs(test + 1) <= 1e-6 then
rot = {
z = 2 * math.atan2(x, w),
x = 0,
y = math.pi/-2
}
else
rot = {
z = math.atan2(2 * (x * y + z * w), x ^ 2 - y ^ 2 - z ^ 2 + w ^ 2),
x = math.atan2(2 * (y * z + x * w), -x ^ 2 - y ^ 2 + z ^ 2 + w ^ 2),
y = math.asin(math.min(math.max(test, -1), 1))
}
end
return vector.apply(rot, math.deg)
end
-- Export environment
return _ENV

331
mods/modlib/schema.lua Normal file
View file

@ -0,0 +1,331 @@
-- Localize globals
local assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type = assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
function new(def)
-- TODO type inference, sanity checking etc.
return setmetatable(def, metatable)
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, " ")
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"
if self.values then
local values = {}
for value in pairs(self.values) do
if value:find"," then
values = nil
break
end
table.insert(values, value)
end
if values then
settingtype = "enum"
type_args = table.concat(values, ",")
end
end
elseif typ == "number" then
settingtype = self.int and "int" or "float"
if self.range and (self.range.min or self.range.max) then
-- TODO handle exclusive min/max
type_args = (self.int and "%d %d" or "%f %f"):format(self.range.min or (2 ^ -30), self.range.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 = {"[" .. ("*"):rep(self._level - 1) .. self.name .. "]"}
end
local function setting(key, value_scheme)
key = tostring(key)
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, function(key, other_key)
-- Force leaves before subtrees to prevent them from being accidentally graphically treated as part of the subtree
local is_subtree = self.entries[key].type == "table"
local other_is_subtree = self.entries[other_key].type == "table"
if is_subtree ~= other_is_subtree then
return not is_subtree
end
return key < other_key
end)
for _, key in ipairs(keys) do
setting(key, self.entries[key])
end
return table.concat(settings, "\n\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 function description(lines)
local description = self.description
if description then
if type(description) ~= "table" then
table.insert(lines, description)
else
modlib.table.append(lines, description)
end
end
end
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 = {}
description(settings)
-- TODO generate Markdown for key/value-checks
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 = {}
description(lines)
local function line(text)
table.insert(lines, "* " .. text)
end
table.insert(lines, "")
line("Type: " .. self.type)
if self.default ~= nil then
line("Default: `" .. tostring(self.default) .. "`")
end
if self.int then
line"Integer"
elseif self.list then
line"List"
end
if self.infinity then
line"Infinities allowed"
end
if self.nan then
line"Not-a-Number (NaN) allowed"
end
if self.range then
if self.range.min then
line("&gt;= `" .. self.range.min .. "`")
elseif self.range.min_exclusive then
line("&gt; `" .. self.range.min_exclusive .. "`")
end
if self.range.max then
line("&lt;= `" .. self.range.max .. "`")
elseif self.range.max_exclusive then
line("&lt; `" .. self.range.max_exclusive .. "`")
end
end
if self.values then
line("Possible values:")
for value in pairs(self.values) do
table.insert(lines, " * " .. value)
end
end
return table.concat(lines, "\n")
end
function settingtypes(self)
self.settingtypes = self.settingtypes or generate_settingtypes(self)
return self.settingtypes
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.type == "table" and self.default == nil then
override = {}
else
return self.default
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, ...)
_error("Invalid value: " .. (self.name and (self.name .. ": ") or "") .. 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.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
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
assert((not self.list) or modlib.table.count(override) == #override, "list")
end
-- Apply the values check only for primitive types where table indexing is by value;
-- the `values` field has a different meaning for tables (constraint all values must fulfill)
if self.type ~= "table" then
assert((not self.values) or self.values[override], "values")
end
if self.func then self.func(override) end
return override
end
-- Export environment
return _ENV

889
mods/modlib/table.lua Normal file
View file

@ -0,0 +1,889 @@
-- Localize globals
local assert, ipairs, math, next, pairs, rawget, rawset, getmetatable, setmetatable, select, string, table, type
= assert, ipairs, math, next, pairs, rawget, rawset, getmetatable, setmetatable, select, string, table, type
local lt = modlib.func.lt
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
-- Empty table
empty = {}
-- Table helpers
function from_iterator(...)
local table = {}
for key, value in ... do
table[key] = value
end
return table
end
function default(table, value)
return setmetatable(table, {
__index = function()
return value
end,
})
end
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)
end
function set_case_insensitive_index(table)
return map_index(table, string.lower)
end
--+ nilget(a, "b", "c") == a?.b?.c
function nilget(value, ...)
local n = select("#", ...)
for i = 1, n do
if value == nil then return nil end
value = value[select(i, ...)]
end
return value
end
deepget = nilget
--+ `deepset(a, "b", "c", d)` is the same as `a.b = a.b ?? {}; a.b.c = d`
function deepset(table, ...)
local n = select("#", ...)
for i = 1, n - 2 do
local key = select(i, ...)
local parent = table
table = parent[key]
if table == nil then
table = {}
parent[key] = table
end
end
table[select(n - 1, ...)] = select(n, ...)
end
-- Fisher-Yates
function shuffle(table)
for index = 1, #table - 1 do
local index_2 = math.random(index, #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
}}
--> rope with simple metatable (:write(text) and :to_text())
function rope(table)
return setmetatable(table or {}, rope_metatable)
end
local rope_len_metatable = {__index = {
write = function(self, text)
self.len = self.len + text:len()
end
}}
--> rope for determining length of text supporting `:write(text)` and `.len` to get the length of written text
function rope_len(len)
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)
end
--+ Simple table equality check. Stack overflow if tables are too deep or circular.
--+ Use `is_circular(table)` to check whether a table is circular.
--> 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
end
equals = equals_noncircular
--+ Table equality check properly handling circular tables - tables are equal as long as they provide equal key/value-pairs
--> 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)
end
--+ Table equality check: content has to be equal, relations between tables as well
--+ The only difference may be in the memory addresses ("identities") of the (sub)tables
--+ Performance may suffer if the tables contain table keys
--+ equals(table, copy(table)) is true
--> 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, {})
end
-- Supports circular tables; does not support table keys
--> `true` if a mapping of references exists, `false` otherwise
function same(a, b)
local same = {}
local function is_same(a, b)
if type(a) ~= "table" or type(b) ~= "table" then
return a == b
end
if same[a] or same[b] then
return same[a] == b and same[b] == a
end
if a == b then
return true
end
same[a], same[b] = b, a
local count = 0
for k, v in pairs(a) do
count = count + 1
assert(type(k) ~= "table", "table keys not supported")
if not is_same(v, b[k], same) then
return false
end
end
for _ in pairs(b) do
count = count - 1
if count < 0 then
return false
end
end
return true
end
return is_same(a, b)
end
function shallowcopy(
table -- table to copy
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
)
if type(table) ~= "table" then
return table
end
local copy = {}
if not strip_metatables then
setmetatable(copy, getmetatable(table))
end
for key, value in pairs(table) do
copy[key] = value
end
return copy
end
function deepcopy_tree(
table -- table; may not contain circular references; cross references will be copied multiple times
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
)
if type(table) ~= "table" then
return table
end
local copy = {}
if not strip_metatables then
setmetatable(copy, getmetatable(table))
end
for key, value in pairs(table) do
copy[deepcopy_tree(key)] = deepcopy_tree(value)
end
return copy
end
deepcopy_noncircular = deepcopy_tree
function deepcopy(
table -- table to copy; reference equality will be preserved
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
)
local copies = {}
local function _deepcopy(table)
local copy = copies[table]
if copy then
return copy
end
copy = {}
if not strip_metatables then
setmetatable(copy, getmetatable(table))
end
copies[table] = copy
local function _copy(value)
if type(value) ~= "table" then
return value
end
if copies[value] then
return copies[value]
end
return _deepcopy(value)
end
for key, value in pairs(table) do
copy[_copy(key)] = _copy(value)
end
return copy
end
return _deepcopy(table)
end
copy = deepcopy
function count(table)
local count = 0
for _ in pairs(table) do
count = count + 1
end
return count
end
function count_equals(table, count)
local k
for _ = 1, count do
k = next(table, k)
if k == nil then return false end -- less than n keys
end
return next(table, k) == nil -- no (n + 1)th entry
end
function is_empty(table)
return next(table) == nil
end
function clear(table)
for k in pairs(table) do
table[k] = nil
end
end
function foreach(table, func)
for k, v in pairs(table) do
func(k, v)
end
end
function deep_foreach_any(table, func)
local seen = {}
local function visit(value)
func(value)
if type(value) == "table" then
if seen[value] then return end
seen[value] = true
for k, v in pairs(value) do
visit(k)
visit(v)
end
end
end
visit(table)
end
-- Recursively counts occurences of objects (non-primitives including strings) in a table.
function count_objects(value)
local counts = {}
if value == nil then
-- Early return for nil
return counts
end
local function count_values(value)
local type_ = type(value)
if type_ == "boolean" or type_ == "number" then return end
local count = counts[value]
counts[value] = (count or 0) + 1
if not count and type_ == "table" then
for k, v in pairs(value) do
count_values(k)
count_values(v)
end
end
end
count_values(value)
return counts
end
function foreach_value(table, func)
for _, v in pairs(table) do
func(v)
end
end
function call(table, ...)
for _, func in pairs(table) do
func(...)
end
end
function icall(table, ...)
for _, func in ipairs(table) do
func(...)
end
end
function foreach_key(table, func)
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
end
map_values = map
function map_keys(table, func)
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
end
function call(funcs, ...)
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
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
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
local sub_additions = deep_to_add(table[key], value)
if next(sub_additions) ~= nil then
additions[key] = sub_additions
end
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
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
end
function complete(table, completions)
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
end
function merge(table, other_table, merge_func)
merge_func = merge_func or merge
local res = {}
for key, value in pairs(table) do
local other_value = other_table[key]
if other_value == nil then
res[key] = value
else
res[key] = merge_func(value, other_value)
end
end
for key, value in pairs(other_table) do
if table[key] == nil then
res[key] = value
end
end
return res
end
function merge_tables(table, other_table)
return add_all(shallowcopy(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
end
function append(table, other_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
end
function values(table)
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
end
function set(table)
local flipped = {}
for _, value in pairs(table) do
flipped[value] = true
end
return flipped
end
function unique(table)
return keys(set(table))
end
function ivalues(table)
local index = 0
return function()
index = index + 1
return table[index]
end
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
end
-- Iterates the hash (= non-list) part of the table. The list part may not be modified while iterating.
function hpairs(table)
local len = #table -- length only has to be determined once as hnext is a closure
local function hnext(key)
local value
key, value = next(table, key)
if type(key) == "number" and key % 1 == 0 and key >= 1 and key <= len then -- list entry, skip
return hnext(key)
end
return key, value
end
return hnext
end
function min_key(table, less_than)
less_than = less_than or lt
local min_key = next(table)
if min_key == nil then
return -- empty table
end
for candidate_key in next, table, min_key do
if less_than(candidate_key, min_key) then
min_key = candidate_key
end
end
return min_key
end
function min_value(table, less_than)
less_than = less_than or lt
local min_key, min_value = next(table)
if min_key == nil then
return -- empty table
end
for candidate_key, candidate_value in next, table, min_key do
if less_than(candidate_value, min_value) then
min_key, min_value = candidate_key, candidate_value
end
end
return min_value, min_key
end
-- TODO move all of the below functions to modlib.list eventually
--! deprecated
function default_comparator(value, other_value)
if value == other_value then
return 0
end
if value > other_value then
return 1
end
return -1
end
--! deprecated, use `binary_search(list, value, less_than)` instead
--> 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
end
function binary_search(
list -- sorted list
, value -- value to be be searched for
, less_than -- function(a, b) return a < b end
)
less_than = less_than or lt
local min, max = 1, #list
while min <= max do
local mid = math.floor((min + max) / 2)
local element = list[mid]
if less_than(value, element) then
max = mid - 1
elseif less_than(element, value) then
min = mid + 1
else -- neither smaller nor larger => must be equal
return mid -- index if found
end
end
return nil, min -- nil, insertion index if not found
end
--> whether the list is sorted in ascending order
function is_sorted(list, less_than --[[function(a, b) return a < b end]])
less_than = less_than or function(a, b) return a < b end
for index = 2, #list do
if less_than(list[index], list[index - 1]) then
return false
end
end
return true
end
function reverse(table)
local len = #table
for index = 1, len / 2 do
local index_from_end = len + 1 - index
table[index_from_end], table[index] = table[index], table[index_from_end]
end
return table
end
function repetition(value, count)
local table = {}
for index = 1, count do
table[index] = value
end
return table
end
function slice(list, from, to)
from, to = from or 1, to or #list
local res = {}
for i = from, to do
res[#res + 1] = list[i]
end
return res
end
-- JS-ish array splice
function splice(
list, -- to modify
start, -- index (inclusive) for where to start modifying the array (defaults to after the last element)
delete_count, -- how many elements to remove (defaults to `0`)
... -- elements to insert after `start`
)
start, delete_count = start or (#list + 1), delete_count or 0
if start < 0 then
start = start + #list + 1
end
local add_count = select("#", ...)
local shift = add_count - delete_count
if shift > 0 then -- shift up
for i = #list, start + delete_count, -1 do
list[i + shift] = list[i]
end
elseif shift < 0 then -- shift down
for i = start, #list do
list[i] = list[i - shift]
end
end
-- Add elements
for i = 1, add_count do
list[start + i - 1] = select(i, ...)
end
return list
end
-- Equivalent to to_list[to], ..., to_list[to + count] = from_list[from], ..., from_list[from + count]
function move(from_list, from, to, count, to_list)
from, to, count, to_list = from or 1, to or 1, count or #from_list, to_list or from_list
if to_list ~= from_list or to < from then
for i = 0, count do
to_list[to + i] = from_list[from + i]
end
else -- iterate in reverse order
for i = count, 0, -1 do
to_list[to + i] = from_list[from + i]
end
end
end
-- Export environment
return _ENV

386
mods/modlib/tex.lua Normal file
View file

@ -0,0 +1,386 @@
--[[
This file does not follow the usual conventions;
it duplicates some code for performance reasons.
In particular, use of `modlib.minetest.colorspec` is avoided.
Most methods operate *in-place* (imperative method names)
rather than returning a modified copy (past participle method names).
Outside-facing methods consistently use 1-based indexing; indices are inclusive.
]]
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
local function round(x) return floor(x + 0.5) end
local function clamp(x, mn, mx) return max(min(x, mx), mn) end
-- ARGB handling utilities
local function unpack_argb(argb)
return floor(argb / 0x1000000),
floor(argb / 0x10000) % 0x100,
floor(argb / 0x100) % 0x100,
argb % 0x100
end
local function pack_argb(a, r, g, b)
local argb = (((a * 0x100 + r) * 0x100) + g) * 0x100 + b
return argb
end
local function round_argb(a, r, g, b)
return round(a), round(r), round(g), round(b)
end
local function scale_0_1_argb(a, r, g, b)
return a / 255, r / 255, g / 255, b / 255
end
local function scale_0_255_argb(a, r, g, b)
return a * 255, r * 255, g * 255, b * 255
end
local tex = {}
local metatable = {__index = tex}
function metatable:__eq(other)
if self.w ~= other.w or self.h ~= other.h then return false end
for i = 1, #self do if self[i] ~= other[i] then return false end end
return true
end
function tex:new()
return setmetatable(self, metatable)
end
function tex.filled(w, h, argb)
local self = {w = w, h = h}
for i = 1, w*h do
self[i] = argb
end
return tex.new(self)
end
function tex:copy()
local copy = {w = self.w, h = self.h}
for i = 1, #self do
copy[i] = self[i]
end
return tex.new(copy)
end
-- Reading & writing
function tex.read_png_string(str)
local stream = modlib.text.inputstream(str)
local png = modlib.minetest.decode_png(stream)
assert(stream:read(1) == nil, "eof expected")
modlib.minetest.convert_png_to_argb8(png)
png.data.w, png.data.h = png.width, png.height
return tex.new(png.data)
end
function tex.read_png(path)
local png
modlib.file.with_open(path, "rb", function(f)
png = modlib.minetest.decode_png(f)
assert(f:read(1) == nil, "eof expected")
end)
modlib.minetest.convert_png_to_argb8(png)
png.data.w, png.data.h = png.width, png.height
return tex.new(png.data)
end
function tex:write_png_string()
return modlib.minetest.encode_png(self.w, self.h, self)
end
function tex:write_png(path)
modlib.file.write_binary(path, self:write_png_string())
end
function tex:fill(sx, sy, argb)
local w, h = self.w, self.h
for y = sy, h do
local i = (y - 1) * w + sx
for _ = sx, w do
self[i] = argb
i = i + 1
end
end
end
function tex:in_bounds(x, y)
return x >= 1 and y >= 1 and x <= self.w and y <= self.h
end
function tex:get_argb_packed(x, y)
return self[(y - 1) * self.w + x]
end
function tex:get_argb(x, y)
return unpack_argb(self[(y - 1) * self.w + x])
end
function tex:set_argb_packed(x, y, argb)
self[(y - 1) * self.w + x] = argb
end
function tex:set_argb(x, y, a, r, g, b)
self[(y - 1) * self.w + x] = pack_argb(a, r, g, b)
end
function tex:map_argb(func)
for i = 1, #self do
self[i] = pack_argb(func(unpack_argb(self[i])))
end
end
local function blit(s, x, y, t, o)
local sw, sh = s.w, s.h
local tw, th = t.w, t.h
-- Restrict to overlapping region
x, y = clamp(x, 1, sw), clamp(y, 1, sh)
local min_tx, min_ty = max(1, 2 - x), max(1, 2 - y)
local max_tx, max_ty = min(tw, sw - x + 1), min(th, sh - y + 1)
for ty = min_ty, max_ty do
local ti, si = (ty - 1) * tw, (y + ty - 2) * sw + x - 1
for _ = min_tx, max_tx do
ti, si = ti + 1, si + 1
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(s[si]))
if sa == 1 or not o then -- HACK because of dirty `[cracko`
local ta, tr, tg, tb = scale_0_1_argb(unpack_argb(t[ti]))
-- "`t` over `s`" (Porter-Duff-Algorithm)
local sata = sa * (1 - ta)
local ra = ta + sata
assert(ra > 0 or (sa == 0 and ta == 0))
if ra > 0 then
s[si] = pack_argb(round_argb(scale_0_255_argb(
ra,
(ta * tr + sata * sr) / ra,
(ta * tg + sata * sg) / ra,
(ta * tb + sata * sb) / ra
)))
end
end
end
end
end
-- Blitting with proper alpha blending.
function tex.blit(s, x, y, t)
return blit(s, x, y, t, false)
end
-- Blit, but only on fully opaque base pixels. Only `[cracko` uses this.
function tex.blito(s, x, y, t)
return blit(s, x, y, t, true)
end
function tex.combine_argb(s, t, cf)
assert(#s == #t)
for i = 1, #s do
s[i] = cf(s[i], t[i])
end
end
-- See https://github.com/TheAlgorithms/Lua/blob/162c4c59f5514c6115e0add8a2b4d56afd6d3204/src/bit/uint53/and.lua
-- TODO (?) optimize fallback band using caching, move somewhere else
local band = bit and bit.band or function(n, m)
local res = 0
local bit = 1
while n * m ~= 0 do -- while both are nonzero
local n_bit, m_bit = n % 2, m % 2 -- extract LSB
res = res + (n_bit * m_bit) * bit -- add AND of LSBs
n, m = (n - n_bit) / 2, (m - m_bit) / 2 -- remove LSB from n & m
bit = bit * 2 -- next bit
end
return res
end
function tex.band(s, t)
return s:combine_argb(t, band)
end
function tex.hardlight_blend(s, t)
return s:combine_argb(t, function(sargb, targb)
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(sargb))
local _, tr, tg, tb = scale_0_1_argb(unpack_argb(targb))
return pack_argb(round_argb(scale_0_255_argb(
sa,
sr < 0.5 and 2*sr*tr or 1 - 2*(1-sr)*(1-tr),
sr < 0.5 and 2*sg*tg or 1 - 2*(1-sg)*(1-tg),
sr < 0.5 and 2*sb*tb or 1 - 2*(1-sb)*(1-tb)
)))
end)
end
function tex:brighten()
return self:map_argb(function(a, r, g, b)
return round_argb((255 + a) / 2, (255 + r) / 2, (255 + g) / 2, (255 + b) / 2)
end)
end
function tex:noalpha()
for i = 1, #self do
self[i] = 0xFF000000 + self[i] % 0x1000000
end
end
function tex:makealpha(r, g, b)
local mrgb = r * 0x10000 + g * 0x100 + b
for i = 1, #self do
local rgb = self[i] % 0x1000000
if rgb == mrgb then
self[i] = rgb
end
end
end
function tex:opacity(factor)
for i = 1, #self do
self[i] = round(floor(self[i] / 0x1000000) * factor) * 0x1000000 + self[i] % 0x1000000
end
end
function tex:invert(ir, ig, ib, ia)
return self:map_argb(function(a, r, g, b)
if ia then a = 255 - a end
if ir then r = 255 - r end
if ig then g = 255 - g end
if ib then b = 255 - b end
return a, r, g, b
end)
end
function tex:multiply_rgb(r, g, b)
return self:map_argb(function(sa, sr, sg, sb)
return round_argb(sa, r * sr, g * sg, b * sb)
end)
end
function tex:screen_blend_rgb(r, g, b)
return self:map_argb(function(sa, sr, sg, sb)
return round_argb(sa,
255 - ((255 - sr) * (255 - r)) / 255,
255 - ((255 - sg) * (255 - g)) / 255,
255 - ((255 - sb) * (255 - b)) / 255)
end)
end
function tex:colorize(cr, cg, cb, ratio)
return self:map_argb(function(a, r, g, b)
local rat = ratio == "alpha" and a or ratio
return round_argb(
a,
rat * r + (1 - rat) * cr,
rat * g + (1 - rat) * cg,
rat * b + (1 - rat) * cb
)
end)
end
function tex:crop(from_x, from_y, to_x, to_y)
local w = self.w
local i = 1
for y = from_y, to_y do
local j = (y - 1) * w + from_x
for _ = from_x, to_x do
self[i] = self[j]
i, j = i + 1, j + 1
end
end
-- Remove remaining pixels
for j = i, #self do self[j] = nil end
self.w, self.h = to_x - from_x + 1, to_y - from_y + 1
end
function tex:flip_x()
for y = 1, self.h do
local i = (y - 1) * self.w
local j = i + self.w + 1
while i < j do
i, j = i + 1, j - 1
self[i], self[j] = self[j], self[i]
end
end
end
function tex:flip_y()
for x = 1, self.w do
local i, j = x, (self.h - 1) * self.w + x
while i < j do
i, j = i + self.w, j - self.w
self[i], self[j] = self[j], self[i]
end
end
end
--> copy of the texture, rotated 90 degrees clockwise
function tex:rotated_90()
local w, h = self.w, self.h
local t = {w = h, h = w}
local i = 0
for y = 1, w do
for x = 1, h do
i = i + 1
t[i] = self[(h-x)*w + y]
end
end
t = tex.new(t)
return t
end
-- Uses box sampling. Hard to optimize.
-- TODO (...) interpolate box samples; match what Minetest does
--> copy of `self` resized to `w` x `h`
function tex:resized(w, h)
--! This function works with 0-based indices.
local sw, sh = self.w, self.h
local fx, fy = sw / w, sh / h
local t = {w = w, h = h}
local i = 0
for y = 0, h - 1 do
for x = 0, w - 1 do
-- Sample the area
local vy_from = y * fy
local vy_to = vy_from + fy
local vx_from = x * fx
local vx_to = vx_from + fx
local a, r, g, b = 0, 0, 0, 0
local pf_sum = 0
local function blend(sx, sy, pf)
if pf <= 0 then return end
local sa, sr, sg, sb = unpack_argb(self[sy * sw + sx + 1])
pf_sum = pf_sum + pf -- TODO (?) eliminate `pf_sum`
sa = sa * pf
a = a + sa
r, g, b = r + sa * sr, g + sa * sg, b + sa * sb
end
local function srow(sy, pf)
if pf <= 0 then return end
local sx_from, sx_to = ceil(vx_from), floor(vx_to)
for sx = sx_from, sx_to - 1 do blend(sx, sy, pf) end -- whole pixels
-- Pixels at edges
blend(floor(vx_from), sy, pf * (sx_from - vx_from))
blend(floor(vx_to), sy, pf * (vx_to - sx_to))
end
local sy_from, sy_to = ceil(vy_from), floor(vy_to)
for sy = sy_from, sy_to - 1 do srow(sy, 1) end -- whole pixels
-- Pixels at edges
srow(floor(vy_from), sy_from - vy_from)
srow(floor(vy_to), vy_to - sy_to)
if a > 0 then r, g, b = r / a, g / a, b / a end
assert(pf_sum > 0)
i = i + 1
t[i] = pack_argb(round_argb(a / pf_sum, r, g, b))
end
end
return tex.new(t)
end
return tex

189
mods/modlib/text.lua Normal file
View file

@ -0,0 +1,189 @@
-- Localize globals
local assert, math, modlib, setmetatable, string, table
= assert, math, modlib, setmetatable, string, table
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
function upper_first(text) return text:sub(1, 1):upper() .. text:sub(2) end
function lower_first(text) return text:sub(1, 1):lower() .. text:sub(2) end
function starts_with(text, prefix) return text:sub(1, #prefix) == prefix end
function ends_with(text, suffix) return text:sub(-#suffix) == suffix end
function contains(str, substr, plain)
return not not str:find(substr, 1, plain == nil and true or plain)
end
function trim_spacing(text)
return text:match"^%s*(.-)%s*$"
end
local inputstream_metatable = {
__index = {
read = function(self, count)
local cursor = self.cursor + 1
self.cursor = self.cursor + count
local text = self.text:sub(cursor, self.cursor)
return text ~= "" and text or nil
end,
seek = function(self) return self.cursor end
}
}
--> inputstream "handle"; only allows reading characters (given a count), seeking does not accept any arguments
function inputstream(text)
return setmetatable({text = text, cursor = 0}, inputstream_metatable)
end
function hexdump(text)
local dump = {}
for index = 1, text:len() do
dump[index] = ("%02X"):format(text:byte(index))
end
return table.concat(dump)
end
function spliterator(str, delim, plain)
assert(delim ~= "")
local last_delim_end = 0
-- Iterator of possibly empty substrings between two matches of the delimiter
-- To exclude empty strings, filter the iterator or use `:gmatch"[...]+"` instead
return function()
if not last_delim_end then
return
end
local delim_start, delim_end = str:find(delim, last_delim_end + 1, plain)
local substr
if delim_start then
substr = str:sub(last_delim_end + 1, delim_start - 1)
else
substr = str:sub(last_delim_end + 1)
end
last_delim_end = delim_end
return substr
end
end
function split(text, delimiter, limit, plain)
limit = limit or math.huge
local parts = {}
local occurences = 1
local last_index = 1
local index = string.find(text, delimiter, 1, plain)
while index and occurences < limit do
table.insert(parts, string.sub(text, last_index, index - 1))
last_index = index + string.len(delimiter)
index = string.find(text, delimiter, index + string.len(delimiter), plain)
occurences = occurences + 1
end
table.insert(parts, string.sub(text, last_index))
return parts
end
function split_without_limit(text, delimiter, plain)
return split(text, delimiter, nil, plain)
end
split_unlimited = split_without_limit
--! Does not support Macintosh pre-OSX CR-only line endings
--! Deprecated in favor of the `lines` iterator below
function split_lines(text, limit)
return modlib.text.split(text, "\r?\n", limit, true)
end
-- When reading from a file, directly use `io.lines` instead
-- Lines are possibly empty substrings separated by CR, LF or CRLF
-- A trailing linefeed is ignored
function lines(str)
local line_start = 1
-- Line iterator
return function()
if line_start > #str then
return
end
local linefeed_start, _, linefeed = str:find("([\r\n][\r\n]?)", line_start)
local line
if linefeed_start then
line = str:sub(line_start, linefeed_start - 1)
line_start = linefeed_start + (linefeed == "\r\n" and 2 or 1)
else
line = str:sub(line_start)
line_start = #str + 1
end
return line
end
end
local zero = string.byte"0"
local nine = string.byte"9"
local letter_a = string.byte"A"
local letter_f = string.byte"F"
function is_hexadecimal(byte)
return byte >= zero and byte <= nine or byte >= letter_a and byte <= letter_f
end
magic_charset = "[" .. ("%^$+-*?.[]()"):gsub(".", "%%%1") .. "]"
function escape_pattern(text)
return text:gsub(magic_charset, "%%%1")
end
escape_magic_chars = escape_pattern
local keywords = modlib.table.set{"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}
keywords["goto"] = true -- Lua 5.2 (LuaJIT) support
function is_keyword(text)
return keywords[text]
end
function is_identifier(text)
return (not keywords[text]) and text:match"^[A-Za-z_][A-Za-z%d_]*$"
end
local function inextchar(text, i)
if i >= #text then return end
i = i + 1
return i, text:sub(i, i)
end
function ichars(text, start)
-- Iterator over `index, character`
return inextchar, text, (start or 1) - 1
end
local function inextbyte(text, i)
if i >= #text then return end
i = i + 1
return i, text:byte(i, i)
end
function ibytes(text, start)
-- Iterator over `index, byte`
return inextbyte, text, (start or 1) - 1
end
local function _random_bytes(count)
if count == 0 then return end
return math.random(0, 0xFF), _random_bytes(count - 1)
end
function random_bytes(
-- number, how many random bytes the string should have, defaults to 1
-- limited by stack size
count
)
count = count or 1
return string.char(_random_bytes(count))
end
-- Export environment
return _ENV

133
mods/modlib/trie.lua Normal file
View file

@ -0,0 +1,133 @@
-- Localize globals
local math, next, pairs, setmetatable, string, table, unpack = math, next, pairs, setmetatable, string, table, unpack
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
-- Setting the metatable is fine as it does not contain single-character keys.
-- TODO (?) encapsulate in "root" field for better code quality?
function new(table) return setmetatable(table or {}, metatable) end
function insert(self, word, value, overwrite)
for i = 1, word:len() do
local char = word:sub(i, i)
self[char] = self[char] or {}
self = self[char]
end
local previous_value = self.value
if not previous_value or overwrite then self.value = value or true end
return previous_value
end
function remove(self, word)
local branch, character = self, word:sub(1, 1)
for i = 1, word:len() - 1 do
local char = word:sub(i, i)
if not self[char] then return end
if self[char].value or next(self, next(self)) then
branch = self
character = char
end
self = self[char]
end
local char = word:sub(word:len())
if not self[char] then return end
self = self[char]
local previous_value = self.value
self.value = nil
if branch and not next(self) then branch[character] = nil end
return previous_value
end
--> value if found
--> nil else
function get(self, word)
for i = 1, word:len() do
local char = word:sub(i, i)
self = self[char]
if not self then return end
end
return self.value
end
function suggestion(self, remainder)
local until_now = {}
local subtries = { [self] = until_now }
local suggestion, value
while next(subtries) do
local new_subtries = {}
local leaves = {}
for trie, word in pairs(subtries) do
if trie.value then table.insert(leaves, { word = word, value = trie.value }) end
end
if #leaves > 0 then
if remainder then
local best_leaves = {}
local best_score = 0
for _, leaf in pairs(leaves) do
local score = 0
for i = 1, math.min(#leaf.word, string.len(remainder)) do
-- calculate intersection
if remainder:sub(i, i) == leaf.word[i] then score = score + 1 end
end
if score == best_score then table.insert(best_leaves, leaf)
elseif score > best_score then best_leaves = { leaf } end
end
leaves = best_leaves
end
-- TODO select best instead of random
local leaf = leaves[math.random(1, #leaves)]
suggestion, value = table.concat(leaf.word), leaf.value
break
end
for trie, word in pairs(subtries) do
for char, subtrie in pairs(trie) do
local word = { unpack(word) }
table.insert(word, char)
new_subtries[subtrie] = word
end
end
subtries = new_subtries
end
return suggestion, value
end
--> value if found
--> nil, suggestion, value of suggestion else
function search(self, word)
for i = 1, word:len() do
local char = word:sub(i, i)
if not self[char] then
local until_now = word:sub(1, i - 1)
local suggestion, value = suggestion(self, word:sub(i))
return nil, until_now .. suggestion, value
end
self = self[char]
end
local value = self.value
if value then return value end
local until_now = word
local suggestion, value = suggestion(self)
return nil, until_now .. suggestion, value
end
function find_longest(self, query, query_offset)
local leaf_pos = query_offset
local last_leaf
for i = query_offset, query:len() do
local char = query:sub(i, i)
self = self[char]
if not self then break
elseif self.value then
last_leaf = self.value
leaf_pos = i
end
end
return last_leaf, leaf_pos
end
-- Export environment
return _ENV

102
mods/modlib/utf8.lua Normal file
View file

@ -0,0 +1,102 @@
local assert, error, select, string_char, table_concat
= assert, error, select, string.char, table.concat
local utf8 = {}
-- Overly permissive pattern that greedily matches a single UTF-8 codepoint
utf8.charpattern = "[%z-\127\194-\253][\128-\191]*"
function utf8.is_valid_codepoint(codepoint)
-- Must be in bounds & must not be a surrogate
return codepoint <= 0x10FFFF and (codepoint < 0xD800 or codepoint > 0xDFFF)
end
local function utf8_bytes(codepoint)
if codepoint <= 0x007F then
return codepoint
end if codepoint <= 0x7FF then
local payload_2 = codepoint % 0x40
codepoint = (codepoint - payload_2) / 0x40
return 0xC0 + codepoint, 0x80 + payload_2
end if codepoint <= 0xFFFF then
local payload_3 = codepoint % 0x40
codepoint = (codepoint - payload_3) / 0x40
local payload_2 = codepoint % 0x40
codepoint = (codepoint - payload_2) / 0x40
return 0xE0 + codepoint, 0x80 + payload_2, 0x80 + payload_3
end if codepoint <= 0x10FFFF then
local payload_4 = codepoint % 0x40
codepoint = (codepoint - payload_4) / 0x40
local payload_3 = codepoint % 0x40
codepoint = (codepoint - payload_3) / 0x40
local payload_2 = codepoint % 0x40
codepoint = (codepoint - payload_2) / 0x40
return 0xF0 + codepoint, 0x80 + payload_2, 0x80 + payload_3, 0x80 + payload_4
end error"codepoint out of range"
end
function utf8.char(...)
local n_args = select("#", ...)
if n_args == 0 then
return
end if n_args == 1 then
return string_char(utf8_bytes(...))
end
local chars = {}
for i = 1, n_args do
chars[i] = string_char(utf8_bytes(select(i, ...)))
end
return table_concat(chars)
end
local function utf8_next_codepoint(str, i)
local first_byte = str:byte(i)
if first_byte < 0x80 then
return i + 1, first_byte
end
local len, head_bits
if first_byte >= 0xC0 and first_byte <= 0xDF then -- 110_00000 to 110_11111
len, head_bits = 2, first_byte % 0x20 -- last 5 bits
elseif first_byte >= 0xE0 and first_byte <= 0xEF then -- 1110_0000 to 1110_1111
len, head_bits = 3, first_byte % 0x10 -- last 4 bits
elseif first_byte >= 0xF0 and first_byte <= 0xF7 then -- 11110_000 to 11110_111
len, head_bits = 4, first_byte % 0x8 -- last 3 bits
else error"invalid UTF-8" end
local codepoint = 0
local pow = 1
for j = i + len - 1, i + 1, -1 do
local byte = assert(str:byte(j), "invalid UTF-8")
local val_bits = byte % 0x40 -- extract last 6 bits xxxxxx from 10xxxxxx
assert(byte - val_bits == 0x80) -- assert that first two bits are 10
codepoint = codepoint + val_bits * pow
pow = pow * 0x40
end
return i + len, codepoint + head_bits * pow
end
function utf8.codepoint(str, i, j)
i, j = i or 1, j or #str
if i > j then return end
local codepoint
i, codepoint = utf8_next_codepoint(str, i)
assert(i - j <= 1, "invalid UTF-8")
return codepoint, utf8.codepoint(str, i)
end
-- Iterator to loop over the UTF-8 characters as `index, codepoint`
function utf8.codes(text, i)
i = i or 1
return function()
if i > #text then
return
end
local prev_index = i
local codepoint
i, codepoint = utf8_next_codepoint(text, i)
return prev_index, codepoint
end
end
return utf8

64
mods/modlib/vararg.lua Normal file
View file

@ -0,0 +1,64 @@
local select, setmetatable, unpack = select, setmetatable, unpack
local vararg = {}
function vararg.aggregate(binary_func, initial, ...)
local total = initial
for i = 1, select("#", ...) do
total = binary_func(total, select(i, ...))
end
return total
end
local metatable = {__index = {}}
function vararg.pack(...)
return setmetatable({["#"] = select("#", ...); ...}, metatable)
end
local va = metatable.__index
function va:unpack()
return unpack(self, 1, self["#"])
end
function va:select(n)
if n > self["#"] then return end
return self[n]
end
local function inext(self, i)
i = i + 1
if i > self["#"] then return end
return i, self[i]
end
function va:ipairs()
return inext, self, 0
end
function va:concat(other)
local self_len, other_len = self["#"], other["#"]
local res = {["#"] = self_len + other_len}
for i = 1, self_len do
res[i] = self[i]
end
for i = 1, other_len do
res[self_len + i] = other[i]
end
return setmetatable(res, metatable)
end
metatable.__concat = va.concat
function va:equals(other)
if self["#"] ~= other["#"] then return false end
for i = 1, self["#"] do if self[i] ~= other[i] then return false end end
return true
end
metatable.__eq = va.equals
function va:aggregate(binary_func, initial)
return vararg.aggregate(binary_func, initial, self:unpack())
end
return vararg

274
mods/modlib/vector.lua Normal file
View file

@ -0,0 +1,274 @@
-- Localize globals
local assert, math, pairs, rawget, rawset, setmetatable, unpack, vector = assert, math, pairs, rawget, rawset, setmetatable, unpack, vector
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local mt_vector = vector
index_aliases = {
x = 1,
y = 2,
z = 3,
w = 4;
"x", "y", "z", "w";
}
metatable = {
__index = function(table, key)
local index = index_aliases[key]
if index ~= nil then
return rawget(table, index)
end
return _ENV[key]
end,
__newindex = function(table, key, value)
-- TODO
local index = index_aliases[key]
if index ~= nil then
return rawset(table, index, value)
end
return rawset(table, key, value)
end
}
function new(v)
return setmetatable(v, metatable)
end
function zeros(n)
local v = {}
for i = 1, n do
v[i] = 0
end
return new(v)
end
function from_xyzw(v)
return new{v.x, v.y, v.z, v.w}
end
function from_minetest(v)
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]}
end
--+ not necessarily required, as Minetest respects the metatable
function to_minetest(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
end
metatable.__eq = equals
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)
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)
end
function combinator(f)
return function(v, w)
return combine(v, w, f)
end, function(v, ...)
return apply(v, f, ...)
end
end
function invert(v)
local res = {}
for key, value in pairs(v) do
res[key] = -value
end
return new(res)
end
add, add_scalar = combinator(function(v, w) return v + w end)
subtract, subtract_scalar = combinator(function(v, w) return v - w end)
multiply, multiply_scalar = combinator(function(v, w) return v * w end)
divide, divide_scalar = combinator(function(v, w) return v / w end)
pow, pow_scalar = combinator(function(v, w) return v ^ w end)
metatable.__add = add
metatable.__unm = invert
metatable.__sub = subtract
metatable.__mul = multiply
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_scalar(v, 1 - ratio), multiply_scalar(w, ratio))
end
function norm(v)
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))
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)
end
function normalize(v)
return divide_scalar(v, length(v))
end
function normalize_zero(v)
local len = length(v)
if len == 0 then
-- Return a zeroed vector with the same keys
local zeroed = {}
for k in pairs(v) do
zeroed[k] = 0
end
return new(zeroed)
end
return divide_scalar(v, len)
end
function floor(v)
return apply(v, math.floor)
end
function ceil(v)
return apply(v, math.ceil)
end
function clamp(v, 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]
}
end
function dot(v, w)
local sum = 0
for i, c in pairs(v) do
sum = sum + c * w[i]
end
return sum
end
function reflect(v, normal --[[**normalized** plane normal vector]])
return subtract(v, multiply_scalar(normal, 2 * dot(v, normal))) -- reflection of v at the plane
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))
end
-- See https://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToAngle/
function axis_angle3(euler_rotation)
assert(#euler_rotation == 3)
euler_rotation = divide_scalar(euler_rotation, 2)
local cos = apply(euler_rotation, math.cos)
local sin = apply(euler_rotation, math.sin)
return normalize_zero{
sin[1] * sin[2] * cos[3] + cos[1] * cos[2] * sin[3],
sin[1] * cos[2] * cos[3] + cos[1] * sin[2] * sin[3],
cos[1] * sin[2] * cos[3] - sin[1] * cos[2] * sin[3],
}, 2 * math.acos(cos[1] * cos[2] * cos[3] - sin[1] * sin[2] * sin[3])
end
-- Uses Rodrigues' rotation formula
-- axis must be normalized
function rotate3(v, axis, angle)
assert(#v == 3 and #axis == 3)
local cos = math.cos(angle)
return multiply_scalar(v, cos)
-- Minetest's coordinate system is *left-handed*, so `v` and `axis` must be swapped here
+ multiply_scalar(cross3(v, axis), math.sin(angle))
+ multiply_scalar(axis, dot(axis, v) * (1 - cos))
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
end
local function moeller_trumbore(origin, direction, triangle, is_tri)
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 (is_tri and u or 0) + v > 1 then
return
end
local pos_on_line = f * dot(edge_2, q)
if pos_on_line >= 0 then
return pos_on_line, u, v
end
end
function ray_triangle_intersection(origin, direction, triangle)
return moeller_trumbore(origin, direction, triangle, true)
end
function ray_parallelogram_intersection(origin, direction, parallelogram)
return moeller_trumbore(origin, direction, parallelogram)
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))
end
-- Export environment
return _ENV

7
mods/modlib/web.lua Normal file
View file

@ -0,0 +1,7 @@
return setmetatable({}, {__index = function(self, module_name)
if module_name == "uri" or module_name == "html" then
local module = assert(loadfile(modlib.mod.get_resource(modlib.modname, "web", module_name .. ".lua")))()
self[module_name] = module
return module
end
end})

27
mods/modlib/web/html.lua Normal file
View file

@ -0,0 +1,27 @@
local html = setmetatable({}, {__index = function(self, key)
if key == "unescape" then
local func = assert(loadfile(modlib.mod.get_resource("modlib", "web", "html", "entities.lua")))
setfenv(func, {})
local named_entities = assert(func())
local function unescape(text)
return text
:gsub("&([A-Za-z]+);", named_entities) -- named
:gsub("&#(%d+);", function(digits) return modlib.utf8.char(tonumber(digits)) end) -- decimal
:gsub("&#x(%x+);", function(digits) return modlib.utf8.char(tonumber(digits, 16)) end) -- hex
end
self.unescape = unescape
return unescape
end
end})
function html.escape(text)
return text:gsub(".", {
["<"] = "&lt;",
[">"] = "&gt;",
["&"] = "&amp;",
["'"] = "&apos;",
['"'] = "&quot;",
})
end
return html

File diff suppressed because one or more lines are too long

42
mods/modlib/web/uri.lua Normal file
View file

@ -0,0 +1,42 @@
-- URI escaping utilities
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
local uri_unescaped_chars = {}
for char in ("-_.!~*'()"):gmatch(".") do
uri_unescaped_chars[char] = true
end
local function add_unescaped_range(from, to)
for byte = from:byte(), to:byte() do
uri_unescaped_chars[string.char(byte)] = true
end
end
add_unescaped_range("0", "9")
add_unescaped_range("a", "z")
add_unescaped_range("A", "Z")
local uri_allowed_chars = table.copy(uri_unescaped_chars)
for char in (";,/?:@&=+$#"):gmatch(".") do
-- Reserved characters are allowed
uri_allowed_chars[char] = true
end
local function encode(text, allowed_chars)
return text:gsub(".", function(char)
if allowed_chars[char] then
return char
end
return ("%%%02X"):format(char:byte())
end)
end
local uri = {}
function uri.encode_component(text)
return encode(text, uri_unescaped_chars)
end
function uri.encode(text)
return encode(text, uri_allowed_chars)
end
return uri