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

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)