Körperbewegung
This commit is contained in:
parent
b16b24e4f7
commit
95945c0306
78 changed files with 12503 additions and 0 deletions
166
mods/modlib/minetest/boxes.lua
Normal file
166
mods/modlib/minetest/boxes.lua
Normal 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
|
325
mods/modlib/minetest/colorspec.lua
Normal file
325
mods/modlib/minetest/colorspec.lua
Normal 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
|
16
mods/modlib/minetest/gametime.lua
Normal file
16
mods/modlib/minetest/gametime.lua
Normal 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)
|
122
mods/modlib/minetest/liquid.lua
Normal file
122
mods/modlib/minetest/liquid.lua
Normal 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
|
29
mods/modlib/minetest/luon.lua
Normal file
29
mods/modlib/minetest/luon.lua
Normal 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
|
||||
}
|
||||
}
|
61
mods/modlib/minetest/media.lua
Normal file
61
mods/modlib/minetest/media.lua
Normal 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}
|
328
mods/modlib/minetest/misc.lua
Normal file
328
mods/modlib/minetest/misc.lua
Normal 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
|
184
mods/modlib/minetest/mod.lua
Normal file
184
mods/modlib/minetest/mod.lua
Normal 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
|
190
mods/modlib/minetest/obj.lua
Normal file
190
mods/modlib/minetest/obj.lua
Normal 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
|
483
mods/modlib/minetest/png.lua
Normal file
483
mods/modlib/minetest/png.lua
Normal 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
|
137
mods/modlib/minetest/raycast.lua
Normal file
137
mods/modlib/minetest/raycast.lua
Normal 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
|
193
mods/modlib/minetest/schematic.lua
Normal file
193
mods/modlib/minetest/schematic.lua
Normal 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
|
30
mods/modlib/minetest/texmod.lua
Normal file
30
mods/modlib/minetest/texmod.lua
Normal 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
|
97
mods/modlib/minetest/texmod/calc_dims.lua
Normal file
97
mods/modlib/minetest/texmod/calc_dims.lua
Normal 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
|
422
mods/modlib/minetest/texmod/dsl.lua
Normal file
422
mods/modlib/minetest/texmod/dsl.lua
Normal 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
|
190
mods/modlib/minetest/texmod/gen_tex.lua
Normal file
190
mods/modlib/minetest/texmod/gen_tex.lua
Normal 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
|
429
mods/modlib/minetest/texmod/read.lua
Normal file
429
mods/modlib/minetest/texmod/read.lua
Normal 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
|
181
mods/modlib/minetest/texmod/write.lua
Normal file
181
mods/modlib/minetest/texmod/write.lua
Normal 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
|
66
mods/modlib/minetest/wielditem_change.lua
Normal file
66
mods/modlib/minetest/wielditem_change.lua
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue