Körperbewegung
This commit is contained in:
parent
b16b24e4f7
commit
95945c0306
78 changed files with 12503 additions and 0 deletions
7
mods/character_anim/.luacheckrc
Normal file
7
mods/character_anim/.luacheckrc
Normal file
|
@ -0,0 +1,7 @@
|
|||
globals = {"character_anim"}
|
||||
read_globals = {
|
||||
"modlib",
|
||||
-- Minetest
|
||||
math = {fields = {"sign"}},
|
||||
"vector", "minetest"
|
||||
}
|
184
mods/character_anim/Readme.md
Normal file
184
mods/character_anim/Readme.md
Normal file
|
@ -0,0 +1,184 @@
|
|||
# Character Animations (`character_anim`)
|
||||
|
||||
Animates the character. Resembles [`playeranim`](https://github.com/minetest-mods/playeranim) and [`headanim`](https://github.com/LoneWolfHT/headanim).
|
||||
|
||||
## About
|
||||
|
||||
Depends on [`modlib`](https://github.com/appgurueu/modlib). Code written by Lars Mueller aka LMD or appguru(eu) and licensed under the MIT license.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## Links
|
||||
|
||||
* [GitHub](https://github.com/appgurueu/character_anim) - sources, issue tracking, contributing
|
||||
* [Discord](https://discordapp.com/invite/ysP74by) - discussion, chatting
|
||||
* [Minetest Forum](https://forum.minetest.net/viewtopic.php?f=9&t=25385) - (more organized) discussion
|
||||
* [ContentDB](https://content.minetest.net/packages/LMD/character_anim) - releases (cloning from GitHub is recommended)
|
||||
|
||||
## Features
|
||||
|
||||
* Animates head, right arm & body
|
||||
* Support for arbitrary player models, as long as `Head`, `Arm_Right` & `Body` bones exist
|
||||
* If any of these bones do not exist, it will still try to animate the remaining bones
|
||||
* Advantages over `playeranim`:
|
||||
* Extracts exact animations and bone positions from b3d models at runtime (no complex installation)
|
||||
* Also animates attached players (with restrictions on angles)
|
||||
* Advantages over `headanim`:
|
||||
* Provides compatibility back until Minetest 0.4.x (as opposed to `headanim` supporting only 5.3+)
|
||||
* Head angles are clamped, head can tilt sideways
|
||||
* Animates right arm & body as well
|
||||
|
||||
## Configuration
|
||||
|
||||
<!--modlib:conf:2-->
|
||||
### `default`
|
||||
|
||||
#### `arm_right`
|
||||
|
||||
##### `radius`
|
||||
|
||||
Right arm spin radius
|
||||
|
||||
* Type: number
|
||||
* Default: `10`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
##### `speed`
|
||||
|
||||
Right arm spin speed
|
||||
|
||||
* Type: number
|
||||
* Default: `1000`
|
||||
* > `0`
|
||||
* <= `10000`
|
||||
|
||||
##### `yaw`
|
||||
|
||||
###### `max`
|
||||
|
||||
Right arm yaw (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `160`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
Right arm yaw (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-30`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
|
||||
#### `body`
|
||||
|
||||
##### `turn_speed`
|
||||
|
||||
Body turn speed
|
||||
|
||||
* Type: number
|
||||
* Default: `0.2`
|
||||
* > `0`
|
||||
* <= `1000`
|
||||
|
||||
|
||||
#### `head`
|
||||
|
||||
##### `pitch`
|
||||
|
||||
###### `max`
|
||||
|
||||
Head pitch (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `80`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
Head pitch (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-60`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw`
|
||||
|
||||
###### `max`
|
||||
|
||||
Head yaw (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `90`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
Head yaw (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-90`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw_restricted`
|
||||
|
||||
###### `max`
|
||||
|
||||
Head yaw restricted (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `45`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
Head yaw restricted (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `0`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw_restriction`
|
||||
|
||||
Head yaw restriction
|
||||
|
||||
* Type: number
|
||||
* Default: `60`
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
|
||||
### `models`
|
||||
|
||||
Other models, same format as `default` model
|
||||
<!--modlib:conf-->
|
||||
|
||||
## API
|
||||
|
||||
Minetest's `player:set_bone_position` is overridden so that it still works as expected.
|
||||
|
||||
### `character_anim.set_bone_override(player, bonename, position, rotation)`
|
||||
|
||||
The signature resembles that of `set_bone_position`. `bonename` must be a string. The following additional features are provided:
|
||||
|
||||
* Using it like `set_bone_position` by setting `rotation` and `position` to non-`nil` values and using `""` to set the root bone
|
||||
* *Setting only the bone position* by setting `rotation` to `nil` - bone rotation will then be model-animation-determined
|
||||
* *Setting only the bone rotation* by setting `position` to `nil` - bone position will then be model-animation-determined
|
||||
* *Clearing the override* by setting both `rotation` and `position` to `nil` ("unset_bone_position")
|
1
mods/character_anim/depends.txt
Normal file
1
mods/character_anim/depends.txt
Normal file
|
@ -0,0 +1 @@
|
|||
modlib
|
379
mods/character_anim/init.lua
Normal file
379
mods/character_anim/init.lua
Normal file
|
@ -0,0 +1,379 @@
|
|||
assert(modlib.version >= 103, "character_anim requires at least version rolling-103 of modlib")
|
||||
local workaround_model = modlib.mod.require"workaround"
|
||||
|
||||
character_anim = {}
|
||||
|
||||
character_anim.conf = modlib.mod.configuration()
|
||||
|
||||
local quaternion = modlib.quaternion
|
||||
-- TODO deduplicate code: move to modlib (see ghosts mod)
|
||||
local media_paths = modlib.minetest.media.paths
|
||||
|
||||
local static_model_names = {}
|
||||
local animated_model_names = {}
|
||||
for name in pairs(media_paths) do
|
||||
if (name:find"character" or name:find"player") and name:match"%.b3d$" then
|
||||
local fixed, data = pcall(workaround_model, name)
|
||||
if fixed then
|
||||
local static_name = "_character_anim_" .. name
|
||||
minetest.dynamic_add_media({
|
||||
filename = static_name,
|
||||
filedata = data,
|
||||
})
|
||||
static_model_names[name] = static_name
|
||||
animated_model_names[static_name] = name
|
||||
else
|
||||
minetest.log("warning", "character_anim: failed to workaround model " .. name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function find_node(root, name)
|
||||
if root.name == name then return root end
|
||||
for _, child in ipairs(root.children) do
|
||||
local node = find_node(child, name)
|
||||
if node then return node end
|
||||
end
|
||||
end
|
||||
|
||||
local models = setmetatable({}, {__index = function(self, filename)
|
||||
if animated_model_names[filename] then
|
||||
return self[animated_model_names[filename]]
|
||||
end
|
||||
local _, ext = modlib.file.get_extension(filename)
|
||||
if not ext or ext:lower() ~= "b3d" then
|
||||
-- Only B3D support currently
|
||||
return
|
||||
end
|
||||
local path = assert(media_paths[filename], filename)
|
||||
local stream = io.open(path, "rb")
|
||||
local model = assert(modlib.b3d.read(stream))
|
||||
assert(stream:read(1) == nil, "EOF expected")
|
||||
stream:close()
|
||||
self[filename] = model
|
||||
return model
|
||||
end})
|
||||
|
||||
function character_anim.is_interacting(player)
|
||||
local control = player:get_player_control()
|
||||
return minetest.check_player_privs(player, "interact") and (control.RMB or control.LMB)
|
||||
end
|
||||
|
||||
local function get_look_horizontal(player)
|
||||
return -math.deg(player:get_look_horizontal())
|
||||
end
|
||||
|
||||
local players = {}
|
||||
character_anim.players = players
|
||||
|
||||
local function get_playerdata(player)
|
||||
local name = player:get_player_name()
|
||||
local data = players[name]
|
||||
if data then return data end
|
||||
-- Initialize playerdata if it doesn't already exist
|
||||
data = {
|
||||
interaction_time = 0,
|
||||
animation_time = 0,
|
||||
look_horizontal = get_look_horizontal(player),
|
||||
bone_positions = {}
|
||||
}
|
||||
players[name] = data
|
||||
return data
|
||||
end
|
||||
|
||||
function character_anim.set_bone_override(player, bonename, position, rotation)
|
||||
local value = {
|
||||
position = position,
|
||||
euler_rotation = rotation
|
||||
}
|
||||
get_playerdata(player).bone_positions[bonename] = next(value) and value
|
||||
end
|
||||
|
||||
local function nil_default(value, default)
|
||||
if value == nil then return default end
|
||||
return value
|
||||
end
|
||||
|
||||
-- Forward declaration
|
||||
local handle_player_animations
|
||||
-- Raw PlayerRef methods
|
||||
local set_bone_position, set_animation, set_local_animation
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
get_playerdata(player) -- Initalizes playerdata if it isn't already initialized
|
||||
if not set_bone_position then
|
||||
local PlayerRef = getmetatable(player)
|
||||
|
||||
-- Keep our model hack completely opaque to the outside world
|
||||
|
||||
local set_properties = PlayerRef.set_properties
|
||||
function PlayerRef:set_properties(props)
|
||||
if not self:is_player() then
|
||||
return set_properties(self, props)
|
||||
end
|
||||
local old_mesh = props.mesh
|
||||
props.mesh = static_model_names[old_mesh] or old_mesh
|
||||
set_properties(self, props)
|
||||
props.mesh = old_mesh
|
||||
end
|
||||
|
||||
local get_properties = PlayerRef.get_properties
|
||||
function PlayerRef:get_properties()
|
||||
if not self:is_player() then
|
||||
return get_properties(self)
|
||||
end
|
||||
local props = get_properties(self)
|
||||
if not props then return nil end
|
||||
props.mesh = animated_model_names[props.mesh] or props.mesh
|
||||
return props
|
||||
end
|
||||
|
||||
set_bone_position = PlayerRef.set_bone_position
|
||||
function PlayerRef:set_bone_position(bonename, position, rotation)
|
||||
if self:is_player() then
|
||||
character_anim.set_bone_override(self, bonename or "",
|
||||
position or {x = 0, y = 0, z = 0},
|
||||
rotation or {x = 0, y = 0, z = 0})
|
||||
end
|
||||
return set_bone_position(self, bonename, position, rotation)
|
||||
end
|
||||
|
||||
set_animation = PlayerRef.set_animation
|
||||
function PlayerRef:set_animation(frame_range, frame_speed, frame_blend, frame_loop)
|
||||
if not self:is_player() then
|
||||
return set_animation(self, frame_range, frame_speed, frame_blend, frame_loop)
|
||||
end
|
||||
local player_animation = get_playerdata(self)
|
||||
if not player_animation then
|
||||
return
|
||||
end
|
||||
local prev_anim = player_animation.animation
|
||||
local new_anim = {
|
||||
nil_default(frame_range, {x = 1, y = 1}),
|
||||
nil_default(frame_speed, 15),
|
||||
nil_default(frame_blend, 0),
|
||||
nil_default(frame_loop, true)
|
||||
}
|
||||
player_animation.animation = new_anim
|
||||
if not prev_anim or (prev_anim[1].x ~= new_anim[1].x or prev_anim[1].y ~= new_anim[1].y) then
|
||||
-- Reset animation only if the animation changed
|
||||
player_animation.animation_time = 0
|
||||
handle_player_animations(0, player)
|
||||
elseif prev_anim[2] ~= new_anim[2] then
|
||||
-- Adapt time to new speed
|
||||
player_animation.animation_time = player_animation.animation_time * prev_anim[2] / new_anim[2]
|
||||
end
|
||||
end
|
||||
local set_animation_frame_speed = PlayerRef.set_animation_frame_speed
|
||||
function PlayerRef:set_animation_frame_speed(frame_speed)
|
||||
if not self:is_player() then
|
||||
return set_animation_frame_speed(self, frame_speed)
|
||||
end
|
||||
frame_speed = nil_default(frame_speed, 15)
|
||||
local player_animation = get_playerdata(self)
|
||||
if not player_animation then
|
||||
return
|
||||
end
|
||||
local prev_speed = player_animation.animation[2]
|
||||
player_animation.animation[2] = frame_speed
|
||||
-- Adapt time to new speed
|
||||
player_animation.animation_time = player_animation.animation_time * prev_speed / frame_speed
|
||||
end
|
||||
|
||||
local get_animation = PlayerRef.get_animation
|
||||
function PlayerRef:get_animation()
|
||||
if not self:is_player() then
|
||||
return get_animation(self)
|
||||
end
|
||||
local anim = get_playerdata(self).animation
|
||||
if anim then
|
||||
return unpack(anim, 1, 4)
|
||||
end
|
||||
return get_animation(self)
|
||||
end
|
||||
|
||||
set_local_animation = PlayerRef.set_local_animation
|
||||
function PlayerRef:set_local_animation(idle, walk, dig, walk_while_dig, frame_speed)
|
||||
if not self:is_player() then return set_local_animation(self) end
|
||||
frame_speed = frame_speed or 30
|
||||
get_playerdata(self).local_animation = {idle, walk, dig, walk_while_dig, frame_speed}
|
||||
end
|
||||
local get_local_animation = PlayerRef.get_local_animation
|
||||
function PlayerRef:get_local_animation()
|
||||
if not self:is_player() then return get_local_animation(self) end
|
||||
local local_anim = get_playerdata(self).local_animation
|
||||
if local_anim then
|
||||
return unpack(local_anim, 1, 5)
|
||||
end
|
||||
return get_local_animation(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- First update `character_anim` with the current animation
|
||||
-- which mods like `player_api` might have already set
|
||||
-- (note: these two methods are already hooked)
|
||||
player:set_animation(player:get_animation())
|
||||
-- Then disable animation & local animation
|
||||
local no_anim = {x = 0, y = 0}
|
||||
set_animation(player, no_anim, 0, 0, false)
|
||||
set_local_animation(player, no_anim, no_anim, no_anim, no_anim, 1)
|
||||
end)
|
||||
|
||||
minetest.register_on_leaveplayer(function(player) players[player:get_player_name()] = nil end)
|
||||
|
||||
local function clamp(value, range)
|
||||
if value > range.max then
|
||||
return range.max
|
||||
end
|
||||
if value < range.min then
|
||||
return range.min
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function normalize_angle(angle)
|
||||
return ((angle + 180) % 360) - 180
|
||||
end
|
||||
|
||||
local function normalize_rotation(euler_rotation)
|
||||
return vector.apply(euler_rotation, normalize_angle)
|
||||
end
|
||||
|
||||
function handle_player_animations(dtime, player)
|
||||
local mesh
|
||||
do
|
||||
local props = player:get_properties()
|
||||
if not props then
|
||||
-- HACK inside on_joinplayer, the player object may be invalid
|
||||
-- causing get_properties() to return nothing - just ignore this
|
||||
return
|
||||
end
|
||||
mesh = props.mesh
|
||||
end
|
||||
if static_model_names[mesh] then
|
||||
player:set_properties{mesh = mesh}
|
||||
elseif animated_model_names[mesh] then
|
||||
mesh = animated_model_names[mesh]
|
||||
end
|
||||
local model = models[mesh]
|
||||
if not model then
|
||||
return
|
||||
end
|
||||
local conf = character_anim.conf.models[mesh] or character_anim.conf.default
|
||||
local player_animation = get_playerdata(player)
|
||||
local anim = player_animation.animation
|
||||
if not anim then
|
||||
return
|
||||
end
|
||||
local range, frame_speed, _, frame_loop = unpack(anim, 1, 4)
|
||||
local animation_time = player_animation.animation_time
|
||||
animation_time = animation_time + dtime
|
||||
player_animation.animation_time = animation_time
|
||||
local range_min, range_max = range.x + 1, range.y + 1
|
||||
local keyframe
|
||||
if range_min == range_max then
|
||||
keyframe = range_min
|
||||
elseif frame_loop then
|
||||
keyframe = range_min + ((animation_time * frame_speed) % (range_max - range_min))
|
||||
else
|
||||
keyframe = math.min(range_max, range_min + animation_time * frame_speed)
|
||||
end
|
||||
local bones = {}
|
||||
local animated_bone_props = model:get_animated_bone_properties(keyframe, true)
|
||||
local body_quaternion
|
||||
for _, props in ipairs(animated_bone_props) do
|
||||
local bone = props.bone_name
|
||||
if bone == "Body" then
|
||||
body_quaternion = props.rotation
|
||||
end
|
||||
local position, rotation = modlib.vector.to_minetest(props.position), props.rotation
|
||||
-- Invert quaternion to match Minetest's coordinate system
|
||||
rotation = {-rotation[1], -rotation[2], -rotation[3], rotation[4]}
|
||||
local euler_rotation = quaternion.to_euler_rotation(rotation)
|
||||
bones[bone] = {position = position, rotation = rotation, euler_rotation = euler_rotation}
|
||||
end
|
||||
local Body = (bones.Body or {}).euler_rotation
|
||||
local Head = (bones.Head or {}).euler_rotation
|
||||
local Arm_Right = (bones.Arm_Right or {}).euler_rotation
|
||||
local look_vertical = math.deg(player:get_look_vertical())
|
||||
if Head then Head.x = -look_vertical end
|
||||
local interacting = character_anim.is_interacting(player)
|
||||
if interacting and Arm_Right then
|
||||
local interaction_time = player_animation.interaction_time
|
||||
-- Note: -90 instead of -Arm_Right.x because it looks better
|
||||
Arm_Right.x = -90 - look_vertical - math.sin(-interaction_time) * conf.arm_right.radius
|
||||
Arm_Right.y = Arm_Right.y + math.cos(-interaction_time) * conf.arm_right.radius
|
||||
player_animation.interaction_time = interaction_time + dtime * math.rad(conf.arm_right.speed)
|
||||
else
|
||||
player_animation.interaction_time = 0
|
||||
end
|
||||
local look_horizontal = get_look_horizontal(player)
|
||||
local diff = look_horizontal - player_animation.look_horizontal
|
||||
if math.abs(diff) > 180 then
|
||||
diff = math.sign(-diff) * 360 + diff
|
||||
end
|
||||
local moving_diff = math.sign(diff) * math.abs(diff) * math.min(1, dtime / conf.body.turn_speed)
|
||||
player_animation.look_horizontal = player_animation.look_horizontal + moving_diff
|
||||
if math.abs(moving_diff) < 1e-6 then
|
||||
player_animation.look_horizontal = look_horizontal
|
||||
end
|
||||
local lag_behind = diff - moving_diff
|
||||
local attach_parent, _, _, attach_rotation = player:get_attach()
|
||||
if attach_parent then
|
||||
local parent_rotation
|
||||
if attach_parent.get_rotation then
|
||||
parent_rotation = attach_parent:get_rotation()
|
||||
else -- 0.4.x doesn't have get_rotation(), only yaw
|
||||
parent_rotation = {x = 0, y = attach_parent:get_yaw(), z = 0}
|
||||
end
|
||||
if attach_rotation and parent_rotation then
|
||||
parent_rotation = vector.apply(parent_rotation, math.deg)
|
||||
local total_rotation = normalize_rotation(vector.subtract(attach_rotation, parent_rotation))
|
||||
local function rotate_relative(euler_rotation)
|
||||
if not euler_rotation then return end
|
||||
euler_rotation.y = euler_rotation.y + look_horizontal
|
||||
local new_rotation = normalize_rotation(vector.subtract(euler_rotation, total_rotation))
|
||||
modlib.table.add_all(euler_rotation, new_rotation)
|
||||
end
|
||||
|
||||
rotate_relative(Head)
|
||||
if interacting then rotate_relative(Arm_Right) end
|
||||
end
|
||||
elseif Body and not modlib.table.nilget(rawget(_G, "player_api"), "player_attached", player:get_player_name()) then
|
||||
Body.y = Body.y + lag_behind
|
||||
if Head then Head.y = Head.y + lag_behind end
|
||||
if interacting and Arm_Right then Arm_Right.y = Arm_Right.y + lag_behind end
|
||||
end
|
||||
|
||||
-- HACK this essentially only works for very character.b3d-like models;
|
||||
-- it tries to find the (sole) X-rotation of the body relative to a subsequent (180°) Y-rotation.
|
||||
if body_quaternion then
|
||||
local body_rotation = assert(assert(find_node(model.node, "Body")).rotation)
|
||||
local body_x = quaternion.to_euler_rotation(modlib.quaternion.compose(body_rotation, body_quaternion)).x
|
||||
if Head then Head.x = normalize_angle(Head.x - body_x) end
|
||||
if interacting and Arm_Right then Arm_Right.x = normalize_angle(Arm_Right.x - body_x) end
|
||||
end
|
||||
|
||||
if Head then
|
||||
Head.x = clamp(Head.x, conf.head.pitch)
|
||||
Head.y = clamp(Head.y, conf.head.yaw)
|
||||
if math.abs(Head.y) > conf.head.yaw_restriction then
|
||||
Head.x = clamp(Head.x, conf.head.yaw_restricted)
|
||||
end
|
||||
end
|
||||
if Arm_Right then Arm_Right.y = clamp(Arm_Right.y, conf.arm_right.yaw) end
|
||||
|
||||
-- Replace animation with serverside bone animation
|
||||
for bone, values in pairs(bones) do
|
||||
local overridden_values = player_animation.bone_positions[bone]
|
||||
overridden_values = overridden_values or {}
|
||||
set_bone_position(player, bone,
|
||||
overridden_values.position or values.position,
|
||||
overridden_values.euler_rotation or values.euler_rotation)
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_globalstep(function(dtime)
|
||||
for _, player in pairs(minetest.get_connected_players()) do
|
||||
handle_player_animations(dtime, player)
|
||||
end
|
||||
end)
|
8
mods/character_anim/mod.conf
Normal file
8
mods/character_anim/mod.conf
Normal file
|
@ -0,0 +1,8 @@
|
|||
name = character_anim
|
||||
title = Character Animations
|
||||
description = Animates the character
|
||||
author = LMD
|
||||
depends = modlib
|
||||
# these mods are supported, but don't necessarily need to load before character_anim
|
||||
optional_depends = player_api, 3d_armor, skinsdb
|
||||
release = 29484
|
65
mods/character_anim/schema.lua
Normal file
65
mods/character_anim/schema.lua
Normal file
|
@ -0,0 +1,65 @@
|
|||
local function angle(description, default)
|
||||
return { type = "number", range = { min = -180, max = 180 }, description = description, default = default }
|
||||
end
|
||||
local range = function(description, default_min, default_max)
|
||||
return {
|
||||
type = "table",
|
||||
entries = {
|
||||
min = angle(description .. " (min)", default_min),
|
||||
max = angle(description .. " (max)", default_max)
|
||||
},
|
||||
func = function(range)
|
||||
if range.max < range.min then return "Minimum range value is not <= maximum range value" end
|
||||
end
|
||||
}
|
||||
end
|
||||
local model = {
|
||||
type = "table",
|
||||
entries = {
|
||||
body = {
|
||||
type = "table",
|
||||
entries = {
|
||||
turn_speed = {
|
||||
type = "number",
|
||||
range = { min_exclusive = 0, max = 1e3 },
|
||||
description = "Body turn speed",
|
||||
default = 0.2
|
||||
}
|
||||
}
|
||||
},
|
||||
head = {
|
||||
type = "table",
|
||||
entries = {
|
||||
pitch = range("Head pitch", -60, 80),
|
||||
yaw = range("Head yaw", -90, 90),
|
||||
yaw_restricted = range("Head yaw restricted", 0, 45),
|
||||
yaw_restriction = angle("Head yaw restriction", 60)
|
||||
}
|
||||
},
|
||||
arm_right = {
|
||||
type = "table",
|
||||
entries = {
|
||||
radius = angle("Right arm spin radius", 10),
|
||||
speed = {
|
||||
type = "number",
|
||||
range = { min_exclusive = 0, max = 1e4 },
|
||||
description = "Right arm spin speed",
|
||||
default = 1e3
|
||||
},
|
||||
yaw = range("Right arm yaw", -30, 160)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type = "table",
|
||||
entries = {
|
||||
default = model,
|
||||
models = {
|
||||
type = "table",
|
||||
keys = { type = "string" },
|
||||
description = "Other models, same format as `default` model"
|
||||
}
|
||||
}
|
||||
}
|
BIN
mods/character_anim/screenshot.png
Normal file
BIN
mods/character_anim/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
53
mods/character_anim/settingtypes.txt
Normal file
53
mods/character_anim/settingtypes.txt
Normal file
|
@ -0,0 +1,53 @@
|
|||
[character_anim.default]
|
||||
|
||||
[*character_anim.default.arm_right]
|
||||
|
||||
# Right arm spin radius
|
||||
character_anim.default.arm_right.radius (Character anim Default Arm right Radius) float 10 -180.000000 180.000000
|
||||
|
||||
# Right arm spin speed
|
||||
character_anim.default.arm_right.speed (Character anim Default Arm right Speed) float 1000 0.000000 10000.000000
|
||||
|
||||
[**character_anim.default.arm_right.yaw]
|
||||
|
||||
# Right arm yaw (max)
|
||||
character_anim.default.arm_right.yaw.max (Character anim Default Arm right Yaw Max) float 160 -180.000000 180.000000
|
||||
|
||||
# Right arm yaw (min)
|
||||
character_anim.default.arm_right.yaw.min (Character anim Default Arm right Yaw Min) float -30 -180.000000 180.000000
|
||||
|
||||
[*character_anim.default.body]
|
||||
|
||||
# Body turn speed
|
||||
character_anim.default.body.turn_speed (Character anim Default Body Turn speed) float 0.2 0.000000 1000.000000
|
||||
|
||||
[*character_anim.default.head]
|
||||
|
||||
# Head yaw restriction
|
||||
character_anim.default.head.yaw_restriction (Character anim Default Head Yaw restriction) float 60 -180.000000 180.000000
|
||||
|
||||
[**character_anim.default.head.pitch]
|
||||
|
||||
# Head pitch (max)
|
||||
character_anim.default.head.pitch.max (Character anim Default Head Pitch Max) float 80 -180.000000 180.000000
|
||||
|
||||
# Head pitch (min)
|
||||
character_anim.default.head.pitch.min (Character anim Default Head Pitch Min) float -60 -180.000000 180.000000
|
||||
|
||||
[**character_anim.default.head.yaw]
|
||||
|
||||
# Head yaw (max)
|
||||
character_anim.default.head.yaw.max (Character anim Default Head Yaw Max) float 90 -180.000000 180.000000
|
||||
|
||||
# Head yaw (min)
|
||||
character_anim.default.head.yaw.min (Character anim Default Head Yaw Min) float -90 -180.000000 180.000000
|
||||
|
||||
[**character_anim.default.head.yaw_restricted]
|
||||
|
||||
# Head yaw restricted (max)
|
||||
character_anim.default.head.yaw_restricted.max (Character anim Default Head Yaw restricted Max) float 45 -180.000000 180.000000
|
||||
|
||||
# Head yaw restricted (min)
|
||||
character_anim.default.head.yaw_restricted.min (Character anim Default Head Yaw restricted Min) float 0 -180.000000 180.000000
|
||||
|
||||
[character_anim.models]
|
54
mods/character_anim/workaround.lua
Normal file
54
mods/character_anim/workaround.lua
Normal file
|
@ -0,0 +1,54 @@
|
|||
-- See https://github.com/luanti-org/luanti/issues/15692
|
||||
|
||||
local mod = modlib.mod
|
||||
local b3d = modlib.b3d
|
||||
|
||||
local media_paths = modlib.minetest.media.paths
|
||||
|
||||
local function is_perfect(quat)
|
||||
local mat = modlib.matrix4.rotation(quat)
|
||||
local diag_abs_sum = 0
|
||||
for i = 1, 3 do
|
||||
diag_abs_sum = diag_abs_sum + math.abs(mat[i][i])
|
||||
end
|
||||
return math.abs(diag_abs_sum - 3) < 1e-5
|
||||
end
|
||||
|
||||
return function(name)
|
||||
local stream = assert(io.open(media_paths[name], "rb"))
|
||||
local character = assert(b3d.read(stream))
|
||||
stream:close()
|
||||
|
||||
local function wiggle_rotation(quat)
|
||||
if math.abs(quat[1]) + math.abs(quat[2]) + math.abs(quat[3]) < 1e-5 then return quat end -- identity
|
||||
if not is_perfect(quat) then return quat end
|
||||
local wiggled = {}
|
||||
for i = 1, 4 do
|
||||
wiggled[i] = quat[i] + 1e-3
|
||||
end
|
||||
wiggled = modlib.quaternion.normalize(wiggled)
|
||||
if not is_perfect(wiggled) then return wiggled end
|
||||
for i = 1, 4 do
|
||||
wiggled[i] = quat[i] - 1e-3
|
||||
end
|
||||
wiggled = modlib.quaternion.normalize(wiggled)
|
||||
if not is_perfect(wiggled) then return wiggled end
|
||||
return quat -- this shouldn't happen
|
||||
end
|
||||
|
||||
local function wiggle_rotations(node)
|
||||
node.rotation = wiggle_rotation(node.rotation)
|
||||
node.keys = {}
|
||||
for _, child in ipairs(node.children) do
|
||||
wiggle_rotations(child)
|
||||
end
|
||||
end
|
||||
|
||||
wiggle_rotations(character.node)
|
||||
|
||||
local rope = {}
|
||||
character:write({write = function(_, str)
|
||||
table.insert(rope, str)
|
||||
end})
|
||||
return table.concat(rope)
|
||||
end
|
7
mods/modlib/License.txt
Normal file
7
mods/modlib/License.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
Copyright 2019 - 2021 Lars Mueller alias LMD or appguru(eu)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
36
mods/modlib/Readme.md
Normal file
36
mods/modlib/Readme.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
#  Modding Library (`modlib`)
|
||||
|
||||
Multipurpose Minetest Modding Library
|
||||
|
||||
## At a glance
|
||||
|
||||
No dependencies. Licensed under the MIT License. Written by Lars Mueller aka LMD or appguru(eu). Requires Lua 5.1 / LuaJIT.
|
||||
|
||||
### Acknowledgement
|
||||
|
||||
* [luk3yx](https://github.com/luk3yx): Various suggestions, bug reports and fixes
|
||||
* [grorp](https://github.com/grorp) (Gregor Parzefall): [Bug reports & proposed fixes for node box code](https://github.com/appgurueu/modlib/pull/8)
|
||||
* [NobWow](https://github.com/NobWow/): [Another bugfix](https://github.com/appgurueu/modlib/pull/7)
|
||||
|
||||
### Principles
|
||||
|
||||
* Game-agnostic: Modlib aims to provide nothing game-specific;
|
||||
* Minimal invasiveness: Modlib should not disrupt other mods;
|
||||
even at the expense of syntactic sugar, changes to the global
|
||||
environment - apart from the addition of the modlib scope - are forbidden
|
||||
* Architecture: Modlib is organized hierarchically
|
||||
* Performance: Modlib tries to not compromise performance for convenience; modlib loads lazily
|
||||
|
||||
## Tests
|
||||
|
||||
The tests are located in a different repo, [`modlib_test`](https://github.com/appgurueu/modlib_test), as they are quite heavy due to testing the PNG reader using PngSuite. Reading the tests for examples of API usage is recommended.
|
||||
|
||||
## API
|
||||
|
||||
(Incomplete) documentation resides in the `doc` folder; you'll have to dive into the code for everything else.
|
||||
|
||||
The mod namespace is `modlib`, containing all modules which in turn contain variables & functions.
|
||||
|
||||
Modules are lazily loaded by indexing the `modlib` table. Do `_ = modlib.<module>` to avoid file load spikes at run time.
|
||||
|
||||
Localizing modules (`local <module> = modlib.<module>`) is recommended.
|
1137
mods/modlib/b3d.lua
Normal file
1137
mods/modlib/b3d.lua
Normal file
File diff suppressed because it is too large
Load diff
132
mods/modlib/base64.lua
Normal file
132
mods/modlib/base64.lua
Normal file
|
@ -0,0 +1,132 @@
|
|||
local assert, floor, char, insert, concat = assert, math.floor, string.char, table.insert, table.concat
|
||||
|
||||
local base64 = {}
|
||||
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
--! This is currently 5 - 10x slower than a C(++) implementation like Minetest's `minetest.encode_base64`
|
||||
function base64.encode(
|
||||
str, -- byte string to encode
|
||||
padding -- whether to add padding, defaults to `true`
|
||||
)
|
||||
local res = {}
|
||||
for i = 1, #str - 2, 3 do
|
||||
-- Convert 3 bytes to 4 sextets
|
||||
local b1, b2, b3 = str:byte(i, i + 2)
|
||||
insert(res, char(
|
||||
alphabet:byte(floor(b1 / 4) + 1), -- high 6 bits of first byte
|
||||
alphabet:byte(16 * (b1 % 4) + floor(b2 / 16) + 1), -- low 2 bits of first byte & high 4 bits of second byte
|
||||
alphabet:byte(4 * (b2 % 16) + floor(b3 / 64) + 1), -- low 4 bits of second byte & high 2 bits of third byte
|
||||
alphabet:byte((b3 % 64) + 1) -- low 6 bits of third byte
|
||||
))
|
||||
end
|
||||
-- Handle remaining 1 or 2 bytes:
|
||||
-- Treat "missing" bytes to a multiple of 3 as "0" bytes, add appropriate padding.
|
||||
local bytes_left = #str % 3
|
||||
if bytes_left == 1 then
|
||||
local b = str:byte(#str) -- b2 and b3 are missing ("= 0")
|
||||
insert(res, char(
|
||||
alphabet:byte(floor(b / 4) + 1),
|
||||
alphabet:byte(16 * (b % 4) + 1)
|
||||
))
|
||||
-- Last two sextets depend only on missing bytes => padding
|
||||
if padding ~= false then
|
||||
insert(res, "==")
|
||||
end
|
||||
elseif bytes_left == 2 then
|
||||
local b1, b2 = str:byte(#str - 1, #str) -- b3 is missing ("= 0")
|
||||
insert(res, char(
|
||||
alphabet:byte(floor(b1 / 4) + 1),
|
||||
alphabet:byte(16 * (b1 % 4) + floor(b2 / 16) + 1),
|
||||
alphabet:byte(4 * (b2 % 16) + 1)
|
||||
))
|
||||
-- Last sextet depends only on missing byte => padding
|
||||
if padding ~= false then
|
||||
insert(res, "=")
|
||||
end
|
||||
end
|
||||
|
||||
return concat(res)
|
||||
end
|
||||
|
||||
-- Build reverse lookup table
|
||||
local values = {}
|
||||
for i = 1, #alphabet do
|
||||
values[alphabet:byte(i)] = i - 1
|
||||
end
|
||||
|
||||
local function decode_sextets_2(b1, b2)
|
||||
local v1, v2 = values[b1], values[b2]
|
||||
assert(v1 and v2)
|
||||
assert(v2 % 16 == 0) -- 4 low bits from second sextet must be 0
|
||||
return char(4 * v1 + floor(v2 / 16)) -- first sextet + 2 high bits from second sextet
|
||||
end
|
||||
|
||||
local function decode_sextets_3(b1, b2, b3)
|
||||
local v1, v2, v3 = values[b1], values[b2], values[b3]
|
||||
assert(v1 and v2 and v3)
|
||||
assert(v3 % 4 == 0) -- 2 low bits from third sextet must be 0
|
||||
return char(
|
||||
4 * v1 + floor(v2 / 16), -- first sextet + 2 high bits from second sextet
|
||||
16 * (v2 % 16) + floor(v3 / 4) -- 4 low bits from second sextet + 4 high bits from third sextet
|
||||
)
|
||||
end
|
||||
|
||||
local function decode_sextets_4(b1, b2, b3, b4)
|
||||
local v1, v2, v3, v4 = values[b1], values[b2], values[b3], values[b4]
|
||||
assert(v1 and v2 and v3 and v4)
|
||||
return char(
|
||||
4 * v1 + floor(v2 / 16), -- first sextet + 2 high bits from second sextet
|
||||
16 * (v2 % 16) + floor(v3 / 4), -- 4 low bits from second sextet + 4 high bits from third sextet
|
||||
64 * (v3 % 4) + v4 -- 2 low bits from third sextet + fourth sextet
|
||||
)
|
||||
end
|
||||
|
||||
--! This is also about 10x slower than a C(++) implementation like Minetest's `minetest.decode_base64`
|
||||
function base64.decode(
|
||||
-- base64-encoded string to decode
|
||||
str,
|
||||
-- Whether to expect padding:
|
||||
-- * `nil` (default) - may (or may not) be padded,
|
||||
-- * `false` - must not be padded,
|
||||
-- * `true` - must be padded
|
||||
padding
|
||||
)
|
||||
-- Handle the empty string - the below code expects a nonempty string
|
||||
if str == "" then return "" end
|
||||
|
||||
local res = {}
|
||||
-- Note: the last (up to) 4 sextets are deliberately excluded, since they may contain padding
|
||||
for i = 1, #str - 4, 4 do
|
||||
-- Convert 4 sextets to 3 bytes
|
||||
insert(res, decode_sextets_4(str:byte(i, i + 3)))
|
||||
end
|
||||
local sextets_left = #str % 4
|
||||
if sextets_left == 0 then -- possibly padded
|
||||
-- Convert 4 sextets to 3 bytes, taking padding into account
|
||||
local b3, b4 = str:byte(#str - 1, #str)
|
||||
if b3 == ("="):byte() then
|
||||
assert(b4 == ("="):byte())
|
||||
assert(padding ~= false, "got padding")
|
||||
insert(res, decode_sextets_2(str:byte(#str - 3, #str - 2)))
|
||||
elseif b4 == ("="):byte() then
|
||||
assert(padding ~= false, "got padding")
|
||||
insert(res, decode_sextets_3(str:byte(#str - 3, #str - 1)))
|
||||
else -- no padding necessary
|
||||
assert(#str >= 4)
|
||||
assert(#({str:byte(#str - 3, #str)}) == 4)
|
||||
insert(res, decode_sextets_4(str:byte(#str - 3, #str)))
|
||||
end
|
||||
else -- no padding and length not divisible by 4
|
||||
assert(padding ~= true, "missing/invalid padding")
|
||||
assert(sextets_left ~= 1)
|
||||
if sextets_left == 2 then
|
||||
insert(res, decode_sextets_2(str:byte(#str - 1, #str)))
|
||||
elseif sextets_left == 3 then
|
||||
insert(res, decode_sextets_3(str:byte(#str - 2, #str)))
|
||||
end
|
||||
end
|
||||
return concat(res)
|
||||
end
|
||||
|
||||
return base64
|
236
mods/modlib/binary.lua
Normal file
236
mods/modlib/binary.lua
Normal file
|
@ -0,0 +1,236 @@
|
|||
-- Localize globals
|
||||
local assert, math_huge, math_frexp, math_floor
|
||||
= assert, math.huge, math.frexp, math.floor
|
||||
|
||||
local positive_nan, negative_nan = modlib.math.positive_nan, modlib.math.negative_nan
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- All little endian
|
||||
|
||||
--+ Reads an IEEE 754 single-precision floating point number (f32)
|
||||
function read_single(read_byte)
|
||||
-- First read the mantissa
|
||||
local mantissa = read_byte() / 0x100
|
||||
mantissa = (mantissa + read_byte()) / 0x100
|
||||
|
||||
-- Second and first byte in big endian: last bit of exponent + 7 bits of mantissa, sign bit + 7 bits of exponent
|
||||
local exponent_byte = read_byte()
|
||||
local sign_byte = read_byte()
|
||||
local sign = 1
|
||||
if sign_byte >= 0x80 then
|
||||
sign = -1
|
||||
sign_byte = sign_byte - 0x80
|
||||
end
|
||||
local exponent = sign_byte * 2
|
||||
if exponent_byte >= 0x80 then
|
||||
exponent = exponent + 1
|
||||
exponent_byte = exponent_byte - 0x80
|
||||
end
|
||||
mantissa = (mantissa + exponent_byte) / 0x80
|
||||
if exponent == 0xFF then
|
||||
if mantissa == 0 then
|
||||
return sign * math_huge
|
||||
end
|
||||
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
|
||||
return sign == 1 and positive_nan or negative_nan
|
||||
end
|
||||
assert(mantissa < 1)
|
||||
if exponent == 0 then
|
||||
-- subnormal value
|
||||
return sign * 2^-126 * mantissa
|
||||
end
|
||||
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
|
||||
end
|
||||
|
||||
--+ Reads an IEEE 754 double-precision floating point number (f64)
|
||||
function read_double(read_byte)
|
||||
-- First read the mantissa
|
||||
local mantissa = 0
|
||||
for _ = 1, 6 do
|
||||
mantissa = (mantissa + read_byte()) / 0x100
|
||||
end
|
||||
-- Second and first byte in big endian: last 4 bits of exponent + 4 bits of mantissa; sign bit + 7 bits of exponent
|
||||
local exponent_byte = read_byte()
|
||||
local sign_byte = read_byte()
|
||||
local sign = 1
|
||||
if sign_byte >= 0x80 then
|
||||
sign = -1
|
||||
sign_byte = sign_byte - 0x80
|
||||
end
|
||||
local exponent = sign_byte * 0x10
|
||||
local mantissa_bits = exponent_byte % 0x10
|
||||
exponent = exponent + (exponent_byte - mantissa_bits) / 0x10
|
||||
mantissa = (mantissa + mantissa_bits) / 0x10
|
||||
if exponent == 0x7FF then
|
||||
if mantissa == 0 then
|
||||
return sign * math_huge
|
||||
end
|
||||
-- Differentiating quiet and signalling nan is not possible in Lua, hence we don't have to do it
|
||||
return sign == 1 and positive_nan or negative_nan
|
||||
end
|
||||
assert(mantissa < 1)
|
||||
if exponent == 0 then
|
||||
-- subnormal value
|
||||
return sign * 2^-1022 * mantissa
|
||||
end
|
||||
return sign * 2 ^ (exponent - 1023) * (1 + mantissa)
|
||||
end
|
||||
|
||||
--+ Reads doubles (f64) or floats (f32)
|
||||
--: double reads an f64 if true, f32 otherwise
|
||||
function read_float(read_byte, double)
|
||||
return (double and read_double or read_single)(read_byte)
|
||||
end
|
||||
|
||||
function read_uint(read_byte, bytes)
|
||||
local factor = 1
|
||||
local uint = 0
|
||||
for _ = 1, bytes do
|
||||
uint = uint + read_byte() * factor
|
||||
factor = factor * 0x100
|
||||
end
|
||||
return uint
|
||||
end
|
||||
|
||||
function read_int(read_byte, bytes)
|
||||
local uint = read_uint(read_byte, bytes)
|
||||
local max = 0x100 ^ bytes
|
||||
if uint >= max / 2 then
|
||||
return uint - max
|
||||
end
|
||||
return uint
|
||||
end
|
||||
|
||||
function write_uint(write_byte, uint, bytes)
|
||||
for _ = 1, bytes do
|
||||
write_byte(uint % 0x100)
|
||||
uint = math_floor(uint / 0x100)
|
||||
end
|
||||
assert(uint == 0)
|
||||
end
|
||||
|
||||
function write_int(write_byte, int, bytes)
|
||||
local max = 0x100 ^ bytes
|
||||
if int < 0 then
|
||||
assert(-int <= max / 2)
|
||||
int = max + int
|
||||
else
|
||||
assert(int < max / 2)
|
||||
end
|
||||
return write_uint(write_byte, int, bytes)
|
||||
end
|
||||
|
||||
function write_single(write_byte, number)
|
||||
if number ~= number then -- nan: all ones
|
||||
for _ = 1, 4 do write_byte(0xFF) end
|
||||
return
|
||||
end
|
||||
|
||||
local sign_byte, exponent_byte, mantissa_byte_1, mantissa_byte_2
|
||||
|
||||
local sign_bit = 0
|
||||
if number < 0 then
|
||||
number = -number
|
||||
sign_bit = 0x80
|
||||
end
|
||||
|
||||
if number == math_huge then -- inf: exponent = all 1, mantissa = all 0
|
||||
sign_byte, exponent_byte, mantissa_byte_1, mantissa_byte_2 = sign_bit + 0x7F, 0x80, 0, 0
|
||||
else -- real number
|
||||
local mantissa, exponent = math_frexp(number)
|
||||
if exponent <= -126 or number == 0 then -- must write a subnormal number
|
||||
mantissa = mantissa * 2 ^ (exponent + 126)
|
||||
exponent = 0
|
||||
else -- normal numbers are stored as 1.<mantissa>
|
||||
mantissa = mantissa * 2 - 1
|
||||
exponent = exponent - 1 + 127 -- mantissa << 1 <=> exponent--
|
||||
assert(exponent < 0xFF)
|
||||
end
|
||||
|
||||
local exp_lowest_bit = exponent % 2
|
||||
|
||||
sign_byte = sign_bit + (exponent - exp_lowest_bit) / 2
|
||||
|
||||
mantissa = mantissa * 0x80
|
||||
exponent_byte = exp_lowest_bit * 0x80 + math_floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
|
||||
mantissa = mantissa * 0x100
|
||||
mantissa_byte_1 = math_floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
|
||||
mantissa = mantissa * 0x100
|
||||
mantissa_byte_2 = math_floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
|
||||
assert(mantissa == 0) -- no truncation allowed: round numbers properly using modlib.math.fround
|
||||
end
|
||||
|
||||
write_byte(mantissa_byte_2)
|
||||
write_byte(mantissa_byte_1)
|
||||
write_byte(exponent_byte)
|
||||
write_byte(sign_byte)
|
||||
end
|
||||
|
||||
function write_double(write_byte, number)
|
||||
if number ~= number then -- nan: all ones
|
||||
for _ = 1, 8 do write_byte(0xFF) end
|
||||
return
|
||||
end
|
||||
|
||||
local sign_byte, exponent_byte, mantissa_bytes
|
||||
|
||||
local sign_bit = 0
|
||||
if number < 0 then
|
||||
number = -number
|
||||
sign_bit = 0x80
|
||||
end
|
||||
|
||||
if number == math_huge then -- inf: exponent = all 1, mantissa = all 0
|
||||
sign_byte, exponent_byte, mantissa_bytes = sign_bit + 0x7F, 0xF0, {0, 0, 0, 0, 0, 0}
|
||||
else -- real number
|
||||
local mantissa, exponent = math_frexp(number)
|
||||
if exponent <= -1022 or number == 0 then -- must write a subnormal number
|
||||
mantissa = mantissa * 2 ^ (exponent + 1022)
|
||||
exponent = 0
|
||||
else -- normal numbers are stored as 1.<mantissa>
|
||||
mantissa = mantissa * 2 - 1
|
||||
exponent = exponent - 1 + 1023 -- mantissa << 1 <=> exponent--
|
||||
assert(exponent < 0x7FF)
|
||||
end
|
||||
|
||||
local exp_low_nibble = exponent % 0x10
|
||||
|
||||
sign_byte = sign_bit + (exponent - exp_low_nibble) / 0x10
|
||||
|
||||
mantissa = mantissa * 0x10
|
||||
exponent_byte = exp_low_nibble * 0x10 + math_floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
|
||||
mantissa_bytes = {}
|
||||
for i = 1, 6 do
|
||||
mantissa = mantissa * 0x100
|
||||
mantissa_bytes[i] = math_floor(mantissa)
|
||||
mantissa = mantissa % 1
|
||||
end
|
||||
assert(mantissa == 0)
|
||||
end
|
||||
|
||||
for i = 6, 1, -1 do
|
||||
write_byte(mantissa_bytes[i])
|
||||
end
|
||||
write_byte(exponent_byte)
|
||||
write_byte(sign_byte)
|
||||
end
|
||||
|
||||
--: on_write function(double)
|
||||
--: double true - f64, false - f32
|
||||
function write_float(write_byte, number, double)
|
||||
(double and write_double or write_single)(write_byte, number)
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
333
mods/modlib/bluon.lua
Normal file
333
mods/modlib/bluon.lua
Normal file
|
@ -0,0 +1,333 @@
|
|||
-- Localize globals
|
||||
local assert, error, ipairs, math_floor, math_abs, math_huge, modlib, next, pairs, setmetatable, string, table_insert, type, unpack
|
||||
= assert, error, ipairs, math.floor, math.abs, math.huge, modlib, next, pairs, setmetatable, string, table.insert, type, unpack
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local fround = modlib.math.fround
|
||||
local write_single, write_double = modlib.binary.write_single, modlib.binary.write_double
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
|
||||
function new(self)
|
||||
return setmetatable(self or {}, metatable)
|
||||
end
|
||||
|
||||
function aux_is_valid()
|
||||
return false
|
||||
end
|
||||
|
||||
function aux_len(object)
|
||||
error("unsupported type: " .. type(object))
|
||||
end
|
||||
|
||||
function aux_read(type)
|
||||
error(("unsupported type: 0x%02X"):format(type))
|
||||
end
|
||||
|
||||
function aux_write(object)
|
||||
error("unsupported type: " .. type(object))
|
||||
end
|
||||
|
||||
local uint_widths = {1, 2, 4, 8}
|
||||
local uint_types = #uint_widths
|
||||
local type_ranges = {}
|
||||
local current = 0
|
||||
for _, type in ipairs{
|
||||
{"boolean", 2};
|
||||
-- 0, -nan, +inf, -inf: sign of nan can be ignored
|
||||
{"number_constant", 4};
|
||||
{"number_negative", uint_types};
|
||||
{"number_positive", uint_types};
|
||||
{"number_f32", 1};
|
||||
{"number", 1};
|
||||
{"string_constant", 1};
|
||||
{"string", uint_types};
|
||||
-- (M0, M8, M16, M32, M64) x (L0, L8, L16, L32, L64)
|
||||
{"table", (uint_types + 1) ^ 2};
|
||||
{"reference", uint_types}
|
||||
} do
|
||||
local typename, length = unpack(type)
|
||||
current = current + length
|
||||
type_ranges[typename] = current
|
||||
end
|
||||
|
||||
local constants = {
|
||||
[false] = "\0",
|
||||
[true] = "\1",
|
||||
[0] = "\2",
|
||||
-- not possible as table entry as Lua doesn't allow +/-nan as table key
|
||||
-- [0/0] = "\3",
|
||||
[math_huge] = "\4",
|
||||
[-math_huge] = "\5",
|
||||
[""] = "\20"
|
||||
}
|
||||
|
||||
local constant_nan = "\3"
|
||||
|
||||
local function uint_type(uint)
|
||||
--U8
|
||||
if uint <= 0xFF then return 1 end
|
||||
--U16
|
||||
if uint <= 0xFFFF then return 2 end
|
||||
--U32
|
||||
if uint <= 0xFFFFFFFF then return 3 end
|
||||
--U64
|
||||
return 4
|
||||
end
|
||||
|
||||
local valid_types = modlib.table.set{"nil", "boolean", "number", "string"}
|
||||
function is_valid(self, value)
|
||||
local _type = type(value)
|
||||
if valid_types[_type] then
|
||||
return true
|
||||
end
|
||||
if _type == "table" then
|
||||
for key, value in pairs(value) do
|
||||
if not (is_valid(self, key) and is_valid(self, value)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return self.aux_is_valid(value)
|
||||
end
|
||||
|
||||
local function uint_len(uint)
|
||||
return uint_widths[uint_type(uint)]
|
||||
end
|
||||
|
||||
local function is_map_key(key, list_len)
|
||||
return type(key) ~= "number" or (key < 1 or key > list_len or key % 1 ~= 0)
|
||||
end
|
||||
|
||||
function len(self, value)
|
||||
if value == nil then
|
||||
return 0
|
||||
end
|
||||
if constants[value] then
|
||||
return 1
|
||||
end
|
||||
local object_ids = {}
|
||||
local current_id = 0
|
||||
local _type = type(value)
|
||||
if _type == "number" then
|
||||
if value ~= value then
|
||||
return 1
|
||||
end
|
||||
if value % 1 == 0 then
|
||||
return 1 + uint_len(value > 0 and value or -value)
|
||||
end
|
||||
local bytes = 4
|
||||
if fround(value) ~= value then bytes = 8 end
|
||||
return 1 + bytes
|
||||
end
|
||||
local id = object_ids[value]
|
||||
if id then
|
||||
return 1 + uint_len(id)
|
||||
end
|
||||
current_id = current_id + 1
|
||||
object_ids[value] = current_id
|
||||
if _type == "string" then
|
||||
local object_len = value:len()
|
||||
return 1 + uint_len(object_len) + object_len
|
||||
end
|
||||
if _type == "table" then
|
||||
if next(value) == nil then
|
||||
-- empty {} table
|
||||
return 1
|
||||
end
|
||||
local list_len = #value
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(value) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local table_len = 1 + uint_len(list_len) + uint_len(kv_len)
|
||||
for index = 1, list_len do
|
||||
table_len = table_len + self:len(value[index])
|
||||
end
|
||||
for key, value in pairs(value) do
|
||||
if is_map_key(key, list_len) then
|
||||
table_len = table_len + self:len(key) + self:len(value)
|
||||
end
|
||||
end
|
||||
return kv_len + table_len
|
||||
end
|
||||
return self.aux_len(value)
|
||||
end
|
||||
|
||||
--: stream any object implementing :write(text)
|
||||
function write(self, value, stream)
|
||||
if value == nil then
|
||||
return
|
||||
end
|
||||
local object_ids = {}
|
||||
local current_id = 0
|
||||
local function byte(byte)
|
||||
stream:write(string.char(byte))
|
||||
end
|
||||
local write_uint = modlib.binary.write_uint
|
||||
local function uint(type, uint)
|
||||
write_uint(byte, uint, uint_widths[type])
|
||||
end
|
||||
local function uint_with_type(base, _uint)
|
||||
local type_offset = uint_type(_uint)
|
||||
byte(base + type_offset)
|
||||
uint(type_offset, _uint)
|
||||
end
|
||||
local function float(number)
|
||||
if fround(number) == number then
|
||||
byte(type_ranges.number_f32)
|
||||
write_single(byte, number)
|
||||
else
|
||||
byte(type_ranges.number)
|
||||
write_double(byte, number)
|
||||
end
|
||||
end
|
||||
local aux_write = self.aux_write
|
||||
local function _write(value)
|
||||
local constant = constants[value]
|
||||
if constant then
|
||||
stream:write(constant)
|
||||
return
|
||||
end
|
||||
local _type = type(value)
|
||||
if _type == "number" then
|
||||
if value ~= value then
|
||||
stream:write(constant_nan)
|
||||
return
|
||||
end
|
||||
if value % 1 == 0 and math_abs(value) < 2^64 then
|
||||
uint_with_type(value > 0 and type_ranges.number_constant or type_ranges.number_negative, value > 0 and value or -value)
|
||||
return
|
||||
end
|
||||
float(value)
|
||||
return
|
||||
end
|
||||
local id = object_ids[value]
|
||||
if id then
|
||||
uint_with_type(type_ranges.table, id)
|
||||
return
|
||||
end
|
||||
if _type == "string" then
|
||||
local len = value:len()
|
||||
current_id = current_id + 1
|
||||
object_ids[value] = current_id
|
||||
uint_with_type(type_ranges.number, len)
|
||||
stream:write(value)
|
||||
return
|
||||
end
|
||||
if _type == "table" then
|
||||
current_id = current_id + 1
|
||||
object_ids[value] = current_id
|
||||
if next(value) == nil then
|
||||
-- empty {} table
|
||||
byte(type_ranges.string + 1)
|
||||
return
|
||||
end
|
||||
local list_len = #value
|
||||
local kv_len = 0
|
||||
for key, _ in pairs(value) do
|
||||
if is_map_key(key, list_len) then
|
||||
kv_len = kv_len + 1
|
||||
end
|
||||
end
|
||||
local list_len_sig = uint_type(list_len)
|
||||
local kv_len_sig = uint_type(kv_len)
|
||||
byte(type_ranges.string + list_len_sig + kv_len_sig * 5 + 1)
|
||||
uint(list_len_sig, list_len)
|
||||
uint(kv_len_sig, kv_len)
|
||||
for index = 1, list_len do
|
||||
_write(value[index])
|
||||
end
|
||||
for key, value in pairs(value) do
|
||||
if is_map_key(key, list_len) then
|
||||
_write(key)
|
||||
_write(value)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
aux_write(value, object_ids)
|
||||
end
|
||||
_write(value)
|
||||
end
|
||||
|
||||
local constants_flipped = modlib.table.flip(constants)
|
||||
constants_flipped[constant_nan] = 0/0
|
||||
|
||||
-- See https://www.lua.org/manual/5.1/manual.html#2.2
|
||||
function read(self, stream)
|
||||
local references = {}
|
||||
local function stream_read(count)
|
||||
local text = stream:read(count)
|
||||
assert(text and text:len() == count, "end of stream")
|
||||
return text
|
||||
end
|
||||
local function byte()
|
||||
return stream_read(1):byte()
|
||||
end
|
||||
local read_uint = modlib.binary.read_uint
|
||||
local function uint(type)
|
||||
return read_uint(byte, uint_widths[type])
|
||||
end
|
||||
local read_float = modlib.binary.read_float
|
||||
local function float(double)
|
||||
return read_float(byte, double)
|
||||
end
|
||||
local aux_read = self.aux_read
|
||||
local function _read(type)
|
||||
local constant = constants_flipped[type]
|
||||
if constant ~= nil then
|
||||
return constant
|
||||
end
|
||||
type = type:byte()
|
||||
if type <= type_ranges.number then
|
||||
if type <= type_ranges.number_negative then
|
||||
return uint(type - type_ranges.number_constant)
|
||||
end
|
||||
if type <= type_ranges.number_positive then
|
||||
return -uint(type - type_ranges.number_negative)
|
||||
end
|
||||
return float(type == type_ranges.number)
|
||||
end
|
||||
if type <= type_ranges.string then
|
||||
local string = stream_read(uint(type - type_ranges.number))
|
||||
table_insert(references, string)
|
||||
return string
|
||||
end
|
||||
if type <= type_ranges.table then
|
||||
type = type - type_ranges.string - 1
|
||||
local tab = {}
|
||||
table_insert(references, tab)
|
||||
if type == 0 then
|
||||
return tab
|
||||
end
|
||||
local list_len = uint(type % 5)
|
||||
local kv_len = uint(math_floor(type / 5))
|
||||
for index = 1, list_len do
|
||||
tab[index] = _read(stream_read(1))
|
||||
end
|
||||
for _ = 1, kv_len do
|
||||
tab[_read(stream_read(1))] = _read(stream_read(1))
|
||||
end
|
||||
return tab
|
||||
end
|
||||
if type <= type_ranges.reference then
|
||||
return references[uint(type - type_ranges.table)]
|
||||
end
|
||||
return aux_read(type, stream, references)
|
||||
end
|
||||
local type = stream:read(1)
|
||||
if type == nil then
|
||||
return
|
||||
end
|
||||
return _read(type)
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
19
mods/modlib/build/html_entities.lua
Normal file
19
mods/modlib/build/html_entities.lua
Normal file
|
@ -0,0 +1,19 @@
|
|||
-- Generate lookup table for HTML entities out of https://html.spec.whatwg.org/entities.json
|
||||
-- Requires https://github.com/brunoos/luasec to fetch the JSON
|
||||
local https = require 'ssl.https'
|
||||
local res, code = https.request"https://html.spec.whatwg.org/entities.json"
|
||||
assert(code == 200)
|
||||
local entity_map = {}
|
||||
for entity, chars in pairs(assert(modlib.json:read_string(res))) do
|
||||
entity_map[entity:sub(2, #entity - 1)] = table.concat(modlib.table.map(chars.codepoints, modlib.utf8.char))
|
||||
end
|
||||
local entries = {}
|
||||
for entity, chars in pairs(entity_map) do
|
||||
table.insert(entries, ("[%q] = %q"):format(entity, chars))
|
||||
end
|
||||
local serialized = [[-- HTML entity lookup table generated by build/html_entities.lua. Do not edit.
|
||||
return {]] .. table.concat(entries, ", ") .. "}"
|
||||
local loaded = assert(loadstring(serialized))
|
||||
setfenv(loaded, {})
|
||||
assert(modlib.table.equals(entity_map, loaded()))
|
||||
modlib.file.write(modlib.mod.get_resource("modlib", "web", "html", "entities.lua"), serialized)
|
79
mods/modlib/doc/b3d.md
Normal file
79
mods/modlib/doc/b3d.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# B3D Reader & Writer
|
||||
|
||||
## `b3d.read(file)`
|
||||
|
||||
Reads from `file`, which is expected to provide `file:read(nbytes)`. `file` is not closed.
|
||||
|
||||
Returns a B3D model object.
|
||||
|
||||
## `:write(file)`
|
||||
|
||||
Writes the B3D model object `self` to `file`.
|
||||
|
||||
`file` must provide `file:write(bytestr)`. It should be in binary mode.
|
||||
It is not closed after writing.
|
||||
|
||||
## `:write_string()`
|
||||
|
||||
Writes the B3D model object to a bytestring, which is returned.
|
||||
|
||||
## `:to_gltf()`
|
||||
|
||||
Returns a glTF JSON representation of `self` in Lua table format.
|
||||
|
||||
## `:write_gltf(file)`
|
||||
|
||||
Convenience function to write the glTF representation to `file` using modlib's `json` writer.
|
||||
|
||||
`file` must provide `file:write(str)`. It is not closed afterwards.
|
||||
|
||||
## Examples
|
||||
|
||||
### Converting B3D to glTF
|
||||
|
||||
This example loops over all files in `dir_path`, converting them to glTFs which are stored in `out_dir_path`.
|
||||
|
||||
```lua
|
||||
local modpath = minetest.get_modpath(minetest.get_current_modname())
|
||||
local dir_path = modpath .. "/b3d"
|
||||
local out_dir_path = modpath .. "/gltf"
|
||||
for _, filename in ipairs(minetest.get_dir_list(dir_path, false --[[only files]])) do
|
||||
-- First read the B3D
|
||||
local in_file = assert(io.open(dir_path .. "/" .. filename, "rb"))
|
||||
local model = assert(b3d.read(in_file))
|
||||
in_file:close()
|
||||
-- Then write the glTF
|
||||
local out_file = io.open(out_dir_path .. "/" .. filename .. ".gltf", "wb")
|
||||
model:write_gltf(out_file)
|
||||
out_file:close()
|
||||
end
|
||||
```
|
||||
|
||||
### [Round-trip (minifying B3Ds)](https://github.com/appgurueu/modlib_test/blob/f11c8e580e90454bc1adaa11a58e0c0217217d90/b3d.lua)
|
||||
|
||||
This example from [`modlib_test`](https://github.com/appgurueu/modlib_test) reads, writes, and then reads again,
|
||||
in order to verify that no data is lost through writing.
|
||||
|
||||
Simply re-writing a model using modlib's B3D writer often reduces model sizes,
|
||||
since for example modlib does not write `0` weights for bones.
|
||||
|
||||
Keep in mind to use the `rb` and `wb` modes for I/O operations
|
||||
to force Windows systems to not perform a line feed normalization.
|
||||
|
||||
### [Extracting triangle sets](https://github.com/appgurueu/ghosts/blob/42a9eb9ee81fc6760a0278d23e4c47bc68bb4919/init.lua#L41-L79)
|
||||
|
||||
The [Ghosts](https://github.com/appgurueu/ghosts/) mod extracts triangle sets using the B3D module
|
||||
to then approximate the player shape using particles picked from these triangles.
|
||||
|
||||
### [Animating the player](https://github.com/appgurueu/character_anim/blob/c48b282c0b42b32294ec2fddc03aa93141cbd894/init.lua#L213)
|
||||
|
||||
[`character_anim`](https://github.com/appgurueu/character_anim/) uses the B3D module to determine the bone overrides required
|
||||
for animating the player entirely Lua-side using bone overrides.
|
||||
|
||||
### [Generating a Go board](https://github.com/appgurueu/go/blob/997ce85260d232a05dd668c32c6854bf34e3d5be/build/generate_models.lua)
|
||||
|
||||
This example from the [Go](https://github.com/appgurueu/go) mod generates a Go board
|
||||
where for each spot on the board there are two pieces (black and white),
|
||||
both of which can be moved out of the board using a bone.
|
||||
|
||||
It demonstrates how to use the writer (and how the table structure roughly looks like).
|
260
mods/modlib/doc/b3d_specification.txt
Normal file
260
mods/modlib/doc/b3d_specification.txt
Normal file
|
@ -0,0 +1,260 @@
|
|||
************************************************************************************
|
||||
* Blitz3d file format V0.01 *
|
||||
************************************************************************************
|
||||
|
||||
This document and the information contained within is placed in the Public Domain.
|
||||
|
||||
Please visit http://www.blitzbasic.co.nz for the latest version of this document.
|
||||
|
||||
Please contact marksibly@blitzbasic.co.nz for more information and general inquiries.
|
||||
|
||||
|
||||
|
||||
************************************************************************************
|
||||
* Introduction *
|
||||
************************************************************************************
|
||||
|
||||
The Blitz3D file format specifies a format for storing texture, brush and entity descriptions for
|
||||
use with the Blitz3D programming language.
|
||||
|
||||
The rationale behind the creation of this format is to allow for the generation of much richer and
|
||||
more complex Blitz3D scenes than is possible using established file formats - many of which do not
|
||||
support key features of Blitz3D, and all of which miss out on at least some features!
|
||||
|
||||
A Blitz3D (.b3d) file is split up into a sequence of 'chunks', each of which can contain data
|
||||
and/or other chunks.
|
||||
|
||||
Each chunk is preceded by an eight byte header:
|
||||
|
||||
char tag[4] ;4 byte chunk 'tag'
|
||||
int length ;4 byte chunk length (not including *this* header!)
|
||||
|
||||
If a chunk contains both data and other chunks, the data always appears first and is of a fixed
|
||||
length.
|
||||
|
||||
A file parser should ignore unrecognized chunks.
|
||||
|
||||
Blitz3D files are stored little endian (intel) style.
|
||||
|
||||
Many aspects of the file format are not quite a 'perfect fit' for the way Blitz3D works. This has
|
||||
been done mainly to keep the file format simple, and to make life easier for the authors of third
|
||||
party importers/exporters.
|
||||
|
||||
|
||||
|
||||
************************************************************************************
|
||||
* Chunk Types *
|
||||
************************************************************************************
|
||||
|
||||
This lists the types of chunks that can appear in a b3d file, and the data they contain.
|
||||
|
||||
Color values are always in the range 0 to 1.
|
||||
|
||||
string (char[]) values are 'C' style null terminated strings.
|
||||
|
||||
Quaternions are used to specify general orientations. The first value is the quaternion 'w' value,
|
||||
the next 3 are the quaternion 'vector'. A 'null' rotation should be specified as 1,0,0,0.
|
||||
|
||||
Anything that is referenced 'by index' always appears EARLIER in the file than anything that
|
||||
references it.
|
||||
|
||||
brush_id references can be -1: no brush.
|
||||
|
||||
In the following descriptions, {} is used to signify 'repeating until end of chunk'. Also, a chunk
|
||||
name enclosed in '[]' signifies the chunk is optional.
|
||||
|
||||
Here we go!
|
||||
|
||||
|
||||
BB3D
|
||||
int version ;file format version: default=1
|
||||
[TEXS] ;optional textures chunk
|
||||
[BRUS] ;optional brushes chunk
|
||||
[NODE] ;optional node chunk
|
||||
|
||||
The BB3D chunk appears first in a b3d file, and its length contains the rest of the file.
|
||||
|
||||
Version is in major*100+minor format. To check the version, just divide by 100 and compare it with
|
||||
the major version your software supports, eg:
|
||||
|
||||
if file_version/100>my_version/100
|
||||
RuntimeError "Can't handle this file version!"
|
||||
EndIf
|
||||
|
||||
if file_version Mod 100>my_version Mod 100
|
||||
;file is a more recent version, but should still be backwardly compatbile with what we can
|
||||
handle!
|
||||
EndIf
|
||||
|
||||
|
||||
TEXS
|
||||
{
|
||||
char file[] ;texture file name
|
||||
int flags,blend ;blitz3D TextureFLags and TextureBlend: default=1,2
|
||||
float x_pos,y_pos ;x and y position of texture: default=0,0
|
||||
float x_scale,y_scale ;x and y scale of texture: default=1,1
|
||||
float rotation ;rotation of texture (in radians): default=0
|
||||
}
|
||||
|
||||
The TEXS chunk contains a list of all textures used in the file.
|
||||
|
||||
The flags field value can conditional an additional flag value of '65536'. This is used to indicate that the texture uses secondary UV values, ala the TextureCoords command. Yes, I forgot about this one.
|
||||
|
||||
|
||||
BRUS
|
||||
int n_texs
|
||||
{
|
||||
char name[] ;eg "WATER" - just use texture name by default
|
||||
float red,green,blue,alpha ;Blitz3D Brushcolor and Brushalpha: default=1,1,1,1
|
||||
float shininess ;Blitz3D BrushShininess: default=0
|
||||
int blend,fx ;Blitz3D Brushblend and BrushFX: default=1,0
|
||||
int texture_id[n_texs] ;textures used in brush
|
||||
}
|
||||
|
||||
The BRUS chunk contains a list of all brushes used in the file.
|
||||
|
||||
|
||||
VRTS:
|
||||
int flags ;1=normal values present, 2=rgba values present
|
||||
int tex_coord_sets ;texture coords per vertex (eg: 1 for simple U/V) max=8
|
||||
int tex_coord_set_size ;components per set (eg: 2 for simple U/V) max=4
|
||||
{
|
||||
float x,y,z ;always present
|
||||
float nx,ny,nz ;vertex normal: present if (flags&1)
|
||||
float red,green,blue,alpha ;vertex color: present if (flags&2)
|
||||
float tex_coords[tex_coord_sets][tex_coord_set_size] ;tex coords
|
||||
}
|
||||
|
||||
The VRTS chunk contains a list of vertices. The 'flags' value is used to indicate how much extra
|
||||
data (normal/color) is stored with each vertex, and the tex_coord_sets and tex_coord_set_size
|
||||
values describe texture coordinate information stored with each vertex.
|
||||
|
||||
|
||||
TRIS:
|
||||
int brush_id ;brush applied to these TRIs: default=-1
|
||||
{
|
||||
int vertex_id[3] ;vertex indices
|
||||
}
|
||||
|
||||
The TRIS chunk contains a list of triangles that all share a common brush.
|
||||
|
||||
|
||||
MESH:
|
||||
int brush_id ;'master' brush: default=-1
|
||||
VRTS ;vertices
|
||||
TRIS[,TRIS...] ;1 or more sets of triangles
|
||||
|
||||
The MESH chunk describes a mesh. A mesh only has one VRTS chunk, but potentially many TRIS chunks.
|
||||
|
||||
|
||||
BONE:
|
||||
{
|
||||
int vertex_id ;vertex affected by this bone
|
||||
float weight ;how much the vertex is affected
|
||||
}
|
||||
|
||||
The BONE chunk describes a bone. Weights are applied to the mesh described in the enclosing ANIM -
|
||||
in 99% of cases, this will simply be the MESH contained in the root NODE chunk.
|
||||
|
||||
|
||||
KEYS:
|
||||
int flags ;1=position, 2=scale, 4=rotation
|
||||
{
|
||||
int frame ;where key occurs
|
||||
float position[3] ;present if (flags&1)
|
||||
float scale[3] ;present if (flags&2)
|
||||
float rotation[4] ;present if (flags&4)
|
||||
}
|
||||
|
||||
The KEYS chunk is a list of animation keys. The 'flags' value describes what kind of animation
|
||||
info is stored in the chunk - position, scale, rotation, or any combination of.
|
||||
|
||||
|
||||
ANIM:
|
||||
int flags ;unused: default=0
|
||||
int frames ;how many frames in anim
|
||||
float fps ;default=60
|
||||
|
||||
The ANIM chunk describes an animation.
|
||||
|
||||
|
||||
NODE:
|
||||
char name[] ;name of node
|
||||
float position[3] ;local...
|
||||
float scale[3] ;coord...
|
||||
float rotation[4] ;system...
|
||||
[MESH|BONE] ;what 'kind' of node this is - if unrecognized, just use a Blitz3D
|
||||
pivot.
|
||||
[KEYS[,KEYS...]] ;optional animation keys
|
||||
[NODE[,NODE...]] ;optional child nodes
|
||||
[ANIM] ;optional animation
|
||||
|
||||
The NODE chunk describes a Blitz3D Entity. The scene hierarchy is expressed by the nesting of NODE
|
||||
chunks.
|
||||
|
||||
NODE kinds are currently mutually exclusive - ie: a node can be a MESH, or a BONE, but not both!
|
||||
However, it can be neither...if no kind is specified, the node is just a 'null' node - in Blitz3D
|
||||
speak, a pivot.
|
||||
|
||||
The presence of an ANIM chunk in a NODE indicates that an animation starts here in the hierarchy.
|
||||
This allows animations of differing speeds/lengths to be potentially nested.
|
||||
|
||||
There are many more 'kind' chunks coming, including camera, light, sprite, plane etc. For now, the
|
||||
use of a Pivot in cases where the node kind is unknown will allow for backward compatibility.
|
||||
|
||||
|
||||
|
||||
************************************************************************************
|
||||
* Examples *
|
||||
************************************************************************************
|
||||
|
||||
A typical b3d file will contain 1 TEXS chunk, 1 BRUS chunk and 1 NODE chunk, like this:
|
||||
|
||||
BB3D
|
||||
1
|
||||
TEXS
|
||||
...list of textures...
|
||||
BRUS
|
||||
...list of brushes...
|
||||
NODE
|
||||
...stuff in the node...
|
||||
|
||||
A simple, non-animating, non-textured etc mesh might look like this:
|
||||
|
||||
BB3D
|
||||
1 ;version
|
||||
NODE
|
||||
"root_node" ;node name
|
||||
0,0,0 ;position
|
||||
1,1,1 ;scale
|
||||
1,0,0,0 ;rotation
|
||||
MESH ;the mesh
|
||||
-1 ;brush: no brush
|
||||
VRTS ;vertices in the mesh
|
||||
0 ;no normal/color info in verts
|
||||
0,0 ;no texture coords in verts
|
||||
{x,y,z...} ;vertex coordinates
|
||||
TRIS ;triangles in the mesh
|
||||
-1 ;no brush for this triangle
|
||||
{v0,v1,v2...} ;vertices
|
||||
|
||||
|
||||
A more complex 'skinned mesh' might look like this (only chunks shown):
|
||||
|
||||
BB3D
|
||||
TEXS ;texture list
|
||||
BRUS ;brush list
|
||||
NODE ;root node
|
||||
MESH ;mesh - the 'skin'
|
||||
ANIM ;anim
|
||||
NODE ;first child of root node - eg: "pelvis"
|
||||
BONE ;vertex weights for pelvis
|
||||
KEYS ;anim keys for pelvis
|
||||
NODE ;first child of pelvis - eg: "left-thigh"
|
||||
BONE ;bone
|
||||
KEYS ;anim keys for left-thigh
|
||||
NODE ;second child of pelvis - eg: "right-thigh"
|
||||
BONE ;vertex weights for right-thigh
|
||||
KEYS ;anim keys for right-thigh
|
||||
|
||||
...and so on.
|
132
mods/modlib/doc/bluon.md
Normal file
132
mods/modlib/doc/bluon.md
Normal file
|
@ -0,0 +1,132 @@
|
|||
# Bluon
|
||||
|
||||
Binary Lua object notation.
|
||||
|
||||
## `new(def)`
|
||||
|
||||
```lua
|
||||
def = {
|
||||
aux_is_valid = function(object)
|
||||
return is_valid
|
||||
end,
|
||||
aux_len = function(object)
|
||||
return length_in_bytes
|
||||
end,
|
||||
-- read type byte, stream providing :read(count), map of references -> id
|
||||
aux_read = function(type, stream, references)
|
||||
... = stream:read(...)
|
||||
return object
|
||||
end,
|
||||
-- object to be written, stream providing :write(text), list of references
|
||||
aux_write = function(object, stream, references)
|
||||
stream:write(...)
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
## `:is_valid(object)`
|
||||
|
||||
Returns whether the given object can be represented by the instance as boolean.
|
||||
|
||||
## `:len(object)`
|
||||
|
||||
Returns the expected length of the object if serialized by the current instance in bytes.
|
||||
|
||||
## `:write(object, stream)`
|
||||
|
||||
Writes the object to a stream supporting `:write(text)`. Throws an error if invalid.
|
||||
|
||||
## `:read(stream)`
|
||||
|
||||
Reads a single bluon object from a stream supporting `:read(count)`. Throws an error if invalid bluon.
|
||||
|
||||
Checking whether the stream has been fully consumed by doing `assert(not stream:read(1))` is left up to the user.
|
||||
|
||||
## Format
|
||||
|
||||
Bluon uses a "tagged union" binary format:
|
||||
Values are stored as a one-byte tag followed by the contents of the union.
|
||||
For frequently used "constants", only a tag is used.
|
||||
|
||||
`nil` is an exception; since it can't appear in tables, it gets no tag.
|
||||
If the value to be written by Bluon is `nil`, Bluon simply writes *nothing*.
|
||||
|
||||
The following is an enumeration of tag numbers, which are assigned *in this order*.
|
||||
|
||||
* `false`: 0
|
||||
* `true`: 1
|
||||
* Numbers:
|
||||
* Constants: 0, nan, +inf, -inf
|
||||
* Integers: Little endian:
|
||||
* Unsigned: `U8`, `U16`, `U32`, `U64`
|
||||
* Negative: `-U8`, `-U16`, `-U32`, `-U64`
|
||||
* Floats: Little endian `F32`, `F64`
|
||||
* Strings:
|
||||
* Constant: `""`
|
||||
* Length is written as unsigned integer according to the tag: `S8`, `S16`, `S32`, `S64`
|
||||
* followed by the raw bytes
|
||||
* Tables:
|
||||
* Tags: `M0`, `M8`, `M16`, `M32`, `M64` times `L0`, `L8`, `L16`, `L32`, `L64`
|
||||
* `M` is more significant than `L`: The order of the cartesian product is `M0L0`, `M0L1`, ...
|
||||
* List and map part count encoded as unsigned integers according to the tag,
|
||||
list part count comes first
|
||||
* followed by all values in the list part written as Bluon
|
||||
* followed by all key-value pairs in the map part written as Bluon
|
||||
(first the key is written as Bluon, then the value)
|
||||
* Reference:
|
||||
* Reference ID as unsigned integer: `R8`, `R16`, `R32`, `R64`
|
||||
* References a previously encountered table or string by an index:
|
||||
All tables and strings are numbered in the order they occur in the Bluon
|
||||
* Reserved tags:
|
||||
* All tags <= 55 are reserved. This gives 200 free tags.
|
||||
|
||||
## Features
|
||||
|
||||
* Embeddable: Written in pure Lua
|
||||
* Storage efficient: No duplication of strings or reference-equal tables
|
||||
* Flexible: Can serialize circular references and strings containing null
|
||||
|
||||
## Simple example
|
||||
|
||||
```lua
|
||||
local object = ...
|
||||
-- Write to file
|
||||
local file = io.open(..., "wb")
|
||||
modlib.bluon:write(object, file)
|
||||
file:close()
|
||||
-- Write to text
|
||||
local rope = modlib.table.rope{}
|
||||
modlib.bluon:write(object, rope)
|
||||
text = rope:to_text()
|
||||
-- Read from text
|
||||
local inputstream = modlib.text.inputstream"\1"
|
||||
assert(modlib.bluon:read(object, rope) == true)
|
||||
```
|
||||
|
||||
## Advanced example
|
||||
|
||||
```lua
|
||||
-- Serializes all userdata to a constant string:
|
||||
local custom_bluon = bluon.new{
|
||||
aux_is_valid = function(object)
|
||||
return type(object) == "userdata"
|
||||
end,
|
||||
aux_len = function(object)
|
||||
return 1 + ("userdata"):len())
|
||||
end,
|
||||
aux_read = function(type, stream, references)
|
||||
assert(type == 100, "unsupported type")
|
||||
assert(stream:read(("userdata"):len()) == "userdata")
|
||||
return userdata()
|
||||
end,
|
||||
-- object to be written, stream providing :write(text), list of references
|
||||
aux_write = function(object, stream, references)
|
||||
assert(type(object) == "userdata")
|
||||
stream:write"\100userdata"
|
||||
end
|
||||
}
|
||||
-- Write to text
|
||||
local rope = modlib.table.rope{}
|
||||
custom_bluon:write(userdata(), rope)
|
||||
assert(rope:to_text() == "\100userdata")
|
||||
```
|
76
mods/modlib/doc/irr_obj_spec.md
Normal file
76
mods/modlib/doc/irr_obj_spec.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Minetest Wavefront `.obj` file format specification
|
||||
|
||||
Minetest Wavefront `.obj` is a subset of [Wavefront `.obj`](http://paulbourke.net/dataformats/obj/).
|
||||
|
||||
It is inferred from the [Minetest Irrlicht `.obj` reader](https://github.com/minetest/irrlicht/blob/master/source/Irrlicht/COBJMeshFileLoader.cpp).
|
||||
|
||||
`.mtl` files are not supported since Minetest's media loading process ignores them due to the extension.
|
||||
|
||||
## Lines / "Commands"
|
||||
|
||||
Irrlicht only looks at the first characters needed to tell commands apart (imagine a prefix tree of commands).
|
||||
|
||||
Superfluous parameters are ignored.
|
||||
|
||||
Numbers are formatted as either:
|
||||
|
||||
* Float: An optional minus sign (`-`), one or more decimal digits, followed by the decimal dot (`.`) then again one or more digits
|
||||
* Integer: An optional minus sign (`-`) followed by one or more decimal digits
|
||||
|
||||
Indexing starts at one. Indices are formatted as integers. Negative indices relative to the end of a buffer are supported.
|
||||
|
||||
* Comments: `# ...`; unsupported commands are silently ignored as well
|
||||
* Groups: `g <name>` or `usemtl <name>`
|
||||
* Subsequent faces belong to a new group / material, no matter the supplied names
|
||||
* Each group gets their own material (texture); indices are determined by order of appearance
|
||||
* Empty groups (groups without faces) are ignored
|
||||
* Vertices (all numbers): `v <x> <y> <z>`, global to the model
|
||||
* Texture Coordinates (all numbers): `vt <x> <y>`, global to the model
|
||||
* Normals (all numbers): `vn <x> <y> <z>`, global to the model
|
||||
* Faces (all vertex/texcoord/normal indices); always local to the current group:
|
||||
* `f <v1> <v2> <v3>`
|
||||
* `f <v1>/<t1> <v2>/<t2> ... <vn>/<tn>`
|
||||
* `f <v1>//<n1> <v2>/<n2> ... <vn>/<nn>`
|
||||
* `f <v1>/<t1>/<n1> <v2>/<t2>/<n2> ... <vn>/<tn>/<nn>`
|
||||
|
||||
## Coordinate system orientation ("handedness")
|
||||
|
||||
Vertex & normal X-coordinates are inverted ($x' = -x$);
|
||||
texture Y-coordinates are inverted as well ($y' = 1 - y$).
|
||||
|
||||
## Example
|
||||
|
||||
```obj
|
||||
# A simple 2³ cube centered at the origin; each face receives a separate texture / tile
|
||||
# no care was taken to ensure "proper" texture orientation
|
||||
v -1 -1 -1
|
||||
v -1 -1 1
|
||||
v -1 1 -1
|
||||
v -1 1 1
|
||||
v 1 -1 -1
|
||||
v 1 -1 1
|
||||
v 1 1 -1
|
||||
v 1 1 1
|
||||
vn -1 0 0
|
||||
vn 0 -1 0
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 0 1
|
||||
vt 0 0
|
||||
vt 1 0
|
||||
vt 0 1
|
||||
vt 1 1
|
||||
g negative_x
|
||||
f 1/1/1 3/3/1 2/2/1 4/4/1
|
||||
g negative_y
|
||||
f 1/1/2 5/3/2 2/2/2 6/4/2
|
||||
g negative_z
|
||||
f 1/1/3 5/3/3 3/2/3 7/4/3
|
||||
g positive_x
|
||||
f 5/1/4 7/3/4 2/2/4 8/4/4
|
||||
g positive_y
|
||||
f 3/1/5 7/3/5 4/2/5 8/4/5
|
||||
g positive_z
|
||||
f 2/1/6 6/3/6 4/2/6 8/4/6
|
||||
```
|
9
mods/modlib/doc/json.md
Normal file
9
mods/modlib/doc/json.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# JSON
|
||||
|
||||
Advantages over `minetest.write_json`/`minetest.parse_json`:
|
||||
|
||||
* Twice as fast in most benchmarks (for pre-5.6 at least)
|
||||
* Uses streams instead of strings
|
||||
* Customizable
|
||||
* Useful error messages
|
||||
* Pure Lua
|
31
mods/modlib/doc/minetest/conf.md
Normal file
31
mods/modlib/doc/minetest/conf.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Configuration
|
||||
|
||||
## Legacy
|
||||
|
||||
1. Configuration is loaded from `<worldpath>/config/<modname>.<extension>`, the following extensions are supported and loaded (in the given order), with loaded configurations overriding properties of previous ones:
|
||||
1. [`json`](https://json.org)
|
||||
2. [`lua`](https://lua.org)
|
||||
3. [`luon`](https://github.com/appgurueu/luon), Lua but without the `return`
|
||||
4. [`conf`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)
|
||||
2. Settings are loaded from `minetest.conf` and override configuration values
|
||||
|
||||
## Locations
|
||||
|
||||
0. Default configuration: `<modfolder>/conf.lua`
|
||||
1. World configuration: `config/<modname>.<format>`
|
||||
2. Mod configuration: `<modfolder>/conf.<format>`
|
||||
3. Minetest configuration: `minetest.conf`
|
||||
|
||||
## Formats
|
||||
|
||||
1. [`lua`](https://lua.org)
|
||||
* Lua, with the environment being the configuration object
|
||||
* `field = value` works
|
||||
* Return new configuration object to replace
|
||||
2. [`luon`](https://github.com/appgurueu/luon)
|
||||
* Single Lua literal
|
||||
* Booleans, numbers, strings and tables
|
||||
3. [`conf`](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)
|
||||
* Minetest-like configuration files
|
||||
4. [`json`](https://json.org)
|
||||
* Not recommended
|
79
mods/modlib/doc/minetest/schematic.md
Normal file
79
mods/modlib/doc/minetest/schematic.md
Normal file
|
@ -0,0 +1,79 @@
|
|||
# Schematic
|
||||
|
||||
A schematic format with support for metadata and baked light data.
|
||||
|
||||
## Table Format
|
||||
|
||||
The table format uses a table with the following mandatory fields:
|
||||
|
||||
* `size`: Size of the schematic in nodes, vector
|
||||
* `node_names`: List of node names
|
||||
* `nodes`: List of node indices (into the `node_names` table)
|
||||
* `param2s`: List of node `param2` values (numbers)
|
||||
|
||||
and the following optional fields:
|
||||
|
||||
* `light_values`: List of node `param1` (light) values (numbers)
|
||||
* `metas`: Map from indices in the cuboid to metadata tables as produced by `minetest.get_meta(pos):to_table()`
|
||||
|
||||
A "vector" is a table with fields `x`, `y`, `z` for the 3 coordinates.
|
||||
|
||||
The `nodes`, `param2s` and `light_values` lists are in the order dictated by `VoxelArea:iterp` (Z-Y-X).
|
||||
|
||||
The cuboid indices for the `metas` table are calculated as `(z * size.y) + y * size.x + x` where `x`, `y`, `z` are relative to the min pos of the cuboid.
|
||||
|
||||
## Binary Format
|
||||
|
||||
The binary format uses modlib's Bluon to write the table format.
|
||||
|
||||
Since `param2s` (and optionally `light_values`) are all bytes, they are converted from lists of numbers to (byte)strings before writing.
|
||||
|
||||
For uncompressed files, it uses `MLBS` (short for "ModLib Bluon Schematic") for the magic bytes,
|
||||
followed by the raw Bluon binary data.
|
||||
|
||||
For compressed files, it uses `MLZS` (short for "ModLib Zlib-compressed Schematic") for the magic bytes,
|
||||
followed by the zlib-compressed Bluon binary data.
|
||||
|
||||
## API
|
||||
|
||||
### `schematic.setmetatable(obj)`
|
||||
|
||||
Sets the metatable of a table `obj` to the schematic metatable.
|
||||
Useful if you've deserialized a schematic or want to create a schematic from the table format.
|
||||
|
||||
### `schematic.create(params, pos_min, pos_max)`
|
||||
|
||||
Creates a schematic from a map cuboid
|
||||
|
||||
* `params`: Table with fields
|
||||
* `metas` (default `true`): Whether to store metadata
|
||||
* `light_values`: Whether to bake light values (`param1`).
|
||||
Usually not recommended, default `false`.
|
||||
* `pos_min`: Minimum position of the cuboid, inclusive
|
||||
* `pos_max`: Maximum position of the cuboid, inclusive
|
||||
|
||||
### `schematic:place(pos_min)`
|
||||
|
||||
"Inverse" to `schematic.create`: Places the schematic `self` starting at `pos_min`.
|
||||
|
||||
Content IDs (nodes), param1s, param2s, and metadata in the area will be completely erased and replaced; if light data is present, param1s will simply be set, otherwise they will be recalculated.
|
||||
|
||||
### `schematic:write_zlib_bluon(path)`
|
||||
|
||||
Write a binary file containing the schematic in *zlib-compressed* binary format to `path`.
|
||||
**You should generally prefer this over `schematic:write_bluon`: zlib compression comes with massive size reductions.**
|
||||
|
||||
### `schematic.read_zlib_bluon(path)`
|
||||
|
||||
"Inverse": Read a binary file containing a schematic in *zlib-compressed* binary format from `path`, returning a `schematic` instance.
|
||||
**You should generally prefer this over `schematic.read_bluon`: zlib compression comes with massive size reductions.**
|
||||
|
||||
### `schematic:write_bluon(path)`
|
||||
|
||||
Write a binary file containing the schematic in uncompressed binary format to `path`.
|
||||
Useful only if you want to eliminate the time spent compressing.
|
||||
|
||||
### `schematic.read_bluon(path)`
|
||||
|
||||
"Inverse": Read a binary file containing a schematic in uncompressed binary format from `path`, returning a `schematic` instance.
|
||||
Useful only if you want to eliminate the time spent decompressing.
|
39
mods/modlib/doc/minetest/texmod.md
Normal file
39
mods/modlib/doc/minetest/texmod.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Texture Modifiers
|
||||
|
||||
## Specification
|
||||
|
||||
Refer to the following "specifications", in this order of precedence:
|
||||
|
||||
1. [Minetest Docs](https://github.com/minetest/minetest_docs/blob/master/doc/texture_modifiers.adoc)
|
||||
2. [Minetest Lua API](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt), section "texture modifiers"
|
||||
3. [Minetest Sources](https://github.com/minetest/minetest/blob/master/src/client/tile.cpp)
|
||||
|
||||
## Implementation
|
||||
|
||||
### Constructors ("DSL")
|
||||
|
||||
Constructors are kept close to the original forms and perform basic validation. Additionally, texture modifiers can directly be created using `texmod{type = "...", ...}`, bypassing the checks.
|
||||
|
||||
### Writing
|
||||
|
||||
The naive way to implement string building would be to have a
|
||||
`tostring` function recursively `tostring`ing the sub-modifiers of the current modifier;
|
||||
each writer would only need a stream (often passed in the form of a `write` function).
|
||||
|
||||
The problem with this is that applying escaping quickly makes this run in quadratic time.
|
||||
|
||||
A more efficient approach passes the escaping along with the `write` function. Thus a "writer" object `w` encapsulating this state is passed around.
|
||||
|
||||
The writer won't necessarily produce the *shortest* or most readable texture modifier possible; for example, colors will be converted to hexadecimal representation, and texture modifiers with optional parameters may have the default values be written.
|
||||
You should not rely on the writer to produce any particular of the various valid outputs.
|
||||
|
||||
### Reading
|
||||
|
||||
**The reader does not attempt to precisely match the behavior of Minetest's shotgun "parser".** It *may* be more strict in some instances, rejecting insane constructs Minetest's parser allows.
|
||||
It *may* however sometimes also be more lenient (though I haven't encountered an instance of this yet), accepting sane constructs which Minetest's parser rejects due to shortcomings in its implementation.
|
||||
|
||||
The parser is written *to spec*, in the given order of precedence.
|
||||
If a documented construct is not working, that's a bug. If a construct which is incorrect according to the docs is accepted, that's a bug too.
|
||||
Compatibility with Minetest's parser for all reasonable inputs is greatly valued. If an invalid input is notably used in the wild (or it is reasonable that it may occur in the wild) and supported by Minetest, this parser ought to support it too.
|
||||
|
||||
Recursive descent parsing is complicated by the two forms of escaping texture modifiers support: Reading each character needs to handle escaping. The current depth and whether the parser is inside an inventorycube need to be saved in state variables. These could be passed on the stack, but it's more comfortable (and possibly more efficient) to just share them across all functions and restore them after leaving an inventorycube / moving to a lower level.
|
23
mods/modlib/doc/persistence/lua_log_file.md
Normal file
23
mods/modlib/doc/persistence/lua_log_file.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Lua Log Files
|
||||
|
||||
A data log file based on Lua statements. High performance. Example from `test.lua`:
|
||||
|
||||
```lua
|
||||
local logfile = persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {})
|
||||
logfile:init()
|
||||
logfile.root = {}
|
||||
logfile:rewrite()
|
||||
logfile:set_root({a = 1}, {b = 2, c = 3})
|
||||
logfile:close()
|
||||
logfile:init()
|
||||
assert(table.equals(logfile.root, {[{a = 1}] = {b = 2, c = 3}}))
|
||||
```
|
||||
|
||||
Both strings and tables are stored in a reference table. Unused strings won't be garbage collected as Lua doesn't allow marking them as weak references.
|
||||
This means that setting lots of temporary strings will waste memory until you call `:rewrite()` on the log file. An alternative is to set the third parameter, `reference_strings`, to `false` (default value is `true`):
|
||||
|
||||
```lua
|
||||
persistence.lua_log_file.new(mod.get_resource"logfile.test.lua", {}, false)
|
||||
```
|
||||
|
||||
This will prevent strings from being referenced, possibly bloating file size, but saving memory.
|
41
mods/modlib/doc/persistence/sqlite3.md
Normal file
41
mods/modlib/doc/persistence/sqlite3.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# SQLite3 Database Persistence
|
||||
|
||||
Uses a SQLite3 database to persistently store a Lua table. Obtaining it is a bit trickier, as it requires access to the `lsqlite3` library, which may be passed:
|
||||
|
||||
```lua
|
||||
local modlib_sqlite3 = persistence.sqlite3(require"lsqlite3")
|
||||
```
|
||||
|
||||
(assuming `require` is that of an insecure environment if Minetest is used)
|
||||
|
||||
Alternatively, if you are not running Minetest, mod security is disabled, you have (temporarily) provided `require` globally, or added `modlib` to `secure.trusted_mods`, you can simply do the following:
|
||||
|
||||
```lua
|
||||
local modlib_sqlite3 = persistence.sqlite3()
|
||||
```
|
||||
|
||||
Modlib will then simply call `require"lsqlite3"` for you.
|
||||
|
||||
Then, you can proceed to create a new database:
|
||||
|
||||
```lua
|
||||
local database = persistence.modlib_sqlite3.new(mod.get_resource"database.test.sqlite3", {})
|
||||
-- Create or load
|
||||
database:init()
|
||||
-- Use it
|
||||
database:set_root("key", {nested = true})
|
||||
database:close()
|
||||
```
|
||||
|
||||
It uses a similar API to Lua log files:
|
||||
|
||||
* `new(filename, root)` - without `reference_strings` however (strings aren't referenced currently)
|
||||
* `init`
|
||||
* `set`
|
||||
* `set_root`
|
||||
* `rewrite`
|
||||
* `close`
|
||||
|
||||
The advantage over Lua log files is that the SQlite3 database keeps disk usage minimal. Unused tables are dropped from the database immediately through reference counting. The downside of this is that this, combined with the overhead of using SQLite3, of course takes time, making updates on the SQLite3 database slower than Lua log file updates (which just append to an append-only file).
|
||||
As simple and fast reference counting doesn't handle cycles, an additional `collectgarbage` stop-the-world method performing a full garbage collection on the database is provided which is called during `init`.
|
||||
The method `defragment_ids` should not have to be used in practice (if it has to be, it happens automatically) and should be used solely for debugging purposes (neater IDs).
|
47
mods/modlib/doc/schema.md
Normal file
47
mods/modlib/doc/schema.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Schema
|
||||
|
||||
Place a file `schema.lua` in your mod, returning a schema table.
|
||||
|
||||
## Non-string entries and `minetest.conf`
|
||||
|
||||
Suppose you have the following schema:
|
||||
|
||||
```lua
|
||||
return {
|
||||
type = "table",
|
||||
entries = {
|
||||
[42] = {
|
||||
type = "boolean",
|
||||
description = "The Answer"
|
||||
default = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And a user sets the following config:
|
||||
|
||||
```conf
|
||||
mod.42 = false
|
||||
```
|
||||
|
||||
It won't work, as the resulting table will be `{["42"] = false}` instead of `{[42] = false}`. In order to make this work, you have to convert the keys yourself:
|
||||
|
||||
```lua
|
||||
return {
|
||||
type = "table",
|
||||
keys = {
|
||||
-- this will convert all keys to numbers
|
||||
type = "number"
|
||||
},
|
||||
entries = {
|
||||
[42] = {
|
||||
type = "boolean",
|
||||
description = "The Answer"
|
||||
default = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is best left explicit. First, you shouldn't be using numbered field keys if you want decent `minetest.conf` support, and second, `modlib`'s schema module could only guess in this case, attempting conversion to number / boolean. What if both number and string field were set as possible entries? Should the string field be deleted? And so on.
|
164
mods/modlib/file.lua
Normal file
164
mods/modlib/file.lua
Normal file
|
@ -0,0 +1,164 @@
|
|||
local dir_delim = ...
|
||||
-- Localize globals
|
||||
local assert, io, minetest, modlib, string, pcall = assert, io, minetest, modlib, string, pcall
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
_ENV.dir_delim = dir_delim
|
||||
|
||||
function get_name(filepath)
|
||||
return filepath:match("([^%" .. dir_delim .. "]+)$") or filepath
|
||||
end
|
||||
|
||||
function split_extension(filename)
|
||||
return filename:match"^(.*)%.(.*)$"
|
||||
end
|
||||
--! deprecated
|
||||
get_extension = split_extension
|
||||
|
||||
function split_path(filepath)
|
||||
return modlib.text.split_unlimited(filepath, dir_delim, true)
|
||||
end
|
||||
|
||||
-- concat_path is set by init.lua to avoid code duplication
|
||||
|
||||
-- Lua 5.4 has `<close>` for this, but we're restricted to 5.1,
|
||||
-- so we need to roll our own `try f = io.open(...); return func(f) finally f:close() end`.
|
||||
function with_open(filename, mode, func --[[function(file), called with `file = io.open(filename, mode)`]])
|
||||
local file = assert(io.open(filename, mode or "r"))
|
||||
-- Throw away the stacktrace. The alternative would be to use `xpcall`
|
||||
-- to bake the stack trace into the error string using `debug.traceback`.
|
||||
-- Lua will have prepended `<source>:<line>:` already however.
|
||||
return (function(status, ...)
|
||||
file:close()
|
||||
assert(status, ...)
|
||||
return ...
|
||||
end)(pcall(func, file))
|
||||
end
|
||||
|
||||
function read(filename)
|
||||
local file, err = io.open(filename, "r")
|
||||
if file == nil then return nil, err end
|
||||
local content = file:read"*a"
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
function read_binary(filename)
|
||||
local file, err = io.open(filename, "rb")
|
||||
if file == nil then return nil, err end
|
||||
local content = file:read"*a"
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
function write_unsafe(filename, new_content)
|
||||
local file, err = io.open(filename, "w")
|
||||
if file == nil then return false, err end
|
||||
file:write(new_content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
write = minetest and minetest.safe_file_write or write_unsafe
|
||||
|
||||
function write_binary_unsafe(filename, new_content)
|
||||
local file, err = io.open(filename, "wb")
|
||||
if file == nil then return false, err end
|
||||
file:write(new_content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
write_binary = minetest and minetest.safe_file_write or write_binary_unsafe
|
||||
|
||||
function ensure_content(filename, ensured_content)
|
||||
local content = read(filename)
|
||||
if content ~= ensured_content then
|
||||
return write(filename, ensured_content)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function append(filename, new_content)
|
||||
local file, err = io.open(filename, "a")
|
||||
if file == nil then return false, err end
|
||||
file:write(new_content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
function exists(filename)
|
||||
local file, err = io.open(filename, "r")
|
||||
if file == nil then return false, err end
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
function create_if_not_exists(filename, content)
|
||||
if not exists(filename) then
|
||||
return write(filename, content or "")
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function create_if_not_exists_from_file(filename, src_filename) return create_if_not_exists(filename, read(src_filename)) end
|
||||
|
||||
if not minetest then return end
|
||||
|
||||
-- Process Bridge Helpers
|
||||
process_bridges = {}
|
||||
|
||||
function process_bridge_build(name, input, output, logs)
|
||||
if not input or not output or not logs then
|
||||
minetest.mkdir(minetest.get_worldpath() .. "/bridges/" .. name)
|
||||
end
|
||||
input = input or minetest.get_worldpath() .. "/bridges/" .. name .. "/input.txt"
|
||||
output = output or minetest.get_worldpath() .. "/bridges/" .. name .. "/output.txt"
|
||||
logs = logs or minetest.get_worldpath() .. "/bridges/" .. name .. "/logs.txt"
|
||||
-- Clear input
|
||||
write(input, "")
|
||||
-- Clear output
|
||||
write(output, "")
|
||||
-- Create logs if not exists
|
||||
create_if_not_exists(logs, "")
|
||||
process_bridges[name] = {
|
||||
input = input,
|
||||
output = output,
|
||||
logs = logs,
|
||||
output_file = io.open(output, "a")
|
||||
}
|
||||
end
|
||||
|
||||
function process_bridge_listen(name, line_consumer, step)
|
||||
local bridge = process_bridges[name]
|
||||
modlib.minetest.register_globalstep(step or 0.1, function()
|
||||
for line in io.lines(bridge.input) do
|
||||
line_consumer(line)
|
||||
end
|
||||
write(bridge.input, "")
|
||||
end)
|
||||
end
|
||||
|
||||
function process_bridge_serve(name, step)
|
||||
local bridge = process_bridges[name]
|
||||
modlib.minetest.register_globalstep(step or 0.1, function()
|
||||
bridge.output_file:close()
|
||||
process_bridges[name].output_file = io.open(bridge.output, "a")
|
||||
end)
|
||||
end
|
||||
|
||||
function process_bridge_write(name, message)
|
||||
local bridge = process_bridges[name]
|
||||
bridge.output_file:write(message .. "\n")
|
||||
end
|
||||
|
||||
function process_bridge_start(name, command, os_execute)
|
||||
local bridge = process_bridges[name]
|
||||
os_execute(string.format(command, bridge.output, bridge.input, bridge.logs))
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
126
mods/modlib/func.lua
Normal file
126
mods/modlib/func.lua
Normal file
|
@ -0,0 +1,126 @@
|
|||
-- Localize globals
|
||||
local modlib, unpack, select, setmetatable
|
||||
= modlib, unpack, select, setmetatable
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
function no_op() end
|
||||
|
||||
function identity(...) return ... end
|
||||
|
||||
-- TODO switch all of these to proper vargs
|
||||
|
||||
function curry(func, ...)
|
||||
local args = { ... }
|
||||
return function(...) return func(unpack(args), ...) end
|
||||
end
|
||||
|
||||
function curry_tail(func, ...)
|
||||
local args = { ... }
|
||||
return function(...) return func(unpack(modlib.table.concat({...}, args))) end
|
||||
end
|
||||
|
||||
function curry_full(func, ...)
|
||||
local args = { ... }
|
||||
return function() return func(unpack(args)) end
|
||||
end
|
||||
|
||||
function args(...)
|
||||
local args = { ... }
|
||||
return function(func) return func(unpack(args)) end
|
||||
end
|
||||
|
||||
function value(val) return function() return val end end
|
||||
|
||||
function values(...)
|
||||
local args = { ... }
|
||||
return function() return unpack(args) end
|
||||
end
|
||||
|
||||
function memoize(func)
|
||||
return setmetatable({}, {
|
||||
__index = function(self, key)
|
||||
local value = func(key)
|
||||
self[key] = value
|
||||
return value
|
||||
end,
|
||||
__call = function(self, arg)
|
||||
return self[arg]
|
||||
end,
|
||||
__mode = "k"
|
||||
})
|
||||
end
|
||||
|
||||
function compose(func, other_func)
|
||||
return function(...)
|
||||
return func(other_func(...))
|
||||
end
|
||||
end
|
||||
|
||||
function override_chain(func, override)
|
||||
return function(...)
|
||||
func(...)
|
||||
return override(...)
|
||||
end
|
||||
end
|
||||
|
||||
--+ Calls func using the provided arguments, deepcopies all arguments
|
||||
function call_by_value(func, ...)
|
||||
return func(unpack(modlib.table.deepcopy{...}, 1, select("#", ...)))
|
||||
end
|
||||
|
||||
-- Functional wrappers for Lua's builtin metatable operators (arithmetic, concatenation, length, comparison, indexing, call)
|
||||
|
||||
-- TODO (?) add operator table `["+"] = add, ...`
|
||||
|
||||
function add(a, b) return a + b end
|
||||
|
||||
function sub(a, b) return a - b end
|
||||
|
||||
function mul(a, b) return a * b end
|
||||
|
||||
function div(a, b) return a / b end
|
||||
|
||||
function mod(a, b) return a % b end
|
||||
|
||||
function pow(a, b) return a ^ b end
|
||||
|
||||
function unm(a) return -a end
|
||||
|
||||
function concat(a, b) return a .. b end
|
||||
|
||||
function len(a) return #a end
|
||||
|
||||
function eq(a, b) return a == b end
|
||||
|
||||
function neq(a, b) return a ~= b end
|
||||
|
||||
function lt(a, b) return a < b end
|
||||
|
||||
function gt(a, b) return a > b end
|
||||
|
||||
function le(a, b) return a <= b end
|
||||
|
||||
function ge(a, b) return a >= b end
|
||||
|
||||
function index(object, key) return object[key] end
|
||||
|
||||
function newindex(object, key, value) object[key] = value end
|
||||
|
||||
function call(object, ...) object(...) end
|
||||
|
||||
-- Functional wrappers for logical operators, suffixed with _ for syntactical convenience
|
||||
|
||||
function not_(a) return not a end
|
||||
_ENV["not"] = not_
|
||||
|
||||
function and_(a, b) return a and b end
|
||||
_ENV["and"] = and_
|
||||
|
||||
function or_(a, b) return a or b end
|
||||
_ENV["or"] = or_
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
117
mods/modlib/hashheap.lua
Normal file
117
mods/modlib/hashheap.lua
Normal file
|
@ -0,0 +1,117 @@
|
|||
-- Localize globals
|
||||
local assert, math_floor, setmetatable, table_insert = assert, math.floor, setmetatable, table.insert
|
||||
|
||||
-- Set environment
|
||||
-- Min. heap + Lua hash table to allow updating the stored values
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local metatable = { __index = _ENV }
|
||||
|
||||
function less_than(a, b)
|
||||
return a < b
|
||||
end
|
||||
|
||||
--> empty, duplicate-free min heap with priority queue functionality
|
||||
function new(less_than)
|
||||
return setmetatable({ less_than = less_than, indices = {} }, metatable)
|
||||
end
|
||||
|
||||
local function swap(self, child_index, parent_index)
|
||||
local child_value, parent_value = self[child_index], self[parent_index]
|
||||
self.indices[parent_value], self.indices[child_value] = child_index, parent_index
|
||||
self[parent_index], self[child_index] = child_value, parent_value
|
||||
end
|
||||
|
||||
local function heapify_up(self, index)
|
||||
if index == 1 then
|
||||
return
|
||||
end
|
||||
local parent_index = math_floor(index / 2)
|
||||
if self.less_than(self[index], self[parent_index]) then
|
||||
swap(self, index, parent_index)
|
||||
heapify_up(self, parent_index)
|
||||
end
|
||||
end
|
||||
|
||||
local function heapify_down(self, index)
|
||||
local left_child = index * 2
|
||||
if left_child > #self then
|
||||
return
|
||||
end
|
||||
local smallest_child = left_child + 1
|
||||
if smallest_child > #self or self.less_than(self[left_child], self[smallest_child]) then
|
||||
smallest_child = left_child
|
||||
end
|
||||
if self.less_than(self[smallest_child], self[index]) then
|
||||
swap(self, smallest_child, index)
|
||||
heapify_down(self, smallest_child)
|
||||
end
|
||||
end
|
||||
|
||||
function push(self, value)
|
||||
table_insert(self, value)
|
||||
local last = #self
|
||||
self.indices[value] = last
|
||||
heapify_up(self, last)
|
||||
end
|
||||
|
||||
function top(self)
|
||||
return self[1]
|
||||
end
|
||||
|
||||
-- TODO what if empty?
|
||||
function pop(self)
|
||||
local value = self[1]
|
||||
self.indices[value] = nil
|
||||
local last = #self
|
||||
if last == 1 then
|
||||
self[1] = nil
|
||||
return value
|
||||
end
|
||||
self[1], self[last] = self[last], nil
|
||||
heapify_down(self, 1)
|
||||
return value
|
||||
end
|
||||
|
||||
function find_index(self, element)
|
||||
return self.indices[element]
|
||||
end
|
||||
|
||||
-- Notify heap that the element has been decreased
|
||||
function decrease(self, element)
|
||||
heapify_up(self, assert(self:find_index(element)))
|
||||
end
|
||||
|
||||
-- Notify heap that the element has been increased
|
||||
function increase(self, element)
|
||||
heapify_down(self, assert(self:find_index(element)))
|
||||
end
|
||||
|
||||
-- Replaces the specified element - by identity - with the new element
|
||||
function replace(self, element, new_element)
|
||||
local index = assert(self:find_index(element))
|
||||
assert(self:find_index(new_element) == nil, "no duplicates allowed")
|
||||
self[index] = new_element
|
||||
self.indices[element] = nil
|
||||
self.indices[new_element] = index;
|
||||
(self.less_than(new_element, element) and heapify_up or heapify_down)(self, index)
|
||||
end
|
||||
|
||||
function remove(self, element)
|
||||
local index = assert(self:find_index(element), "element not found")
|
||||
self.indices[element] = nil
|
||||
if index == #self then
|
||||
self[index] = nil
|
||||
else
|
||||
local last_index = #self
|
||||
local last_element = self[last_index]
|
||||
self[last_index] = nil
|
||||
self[index] = last_element
|
||||
self.indices[last_element] = index;
|
||||
(self.less_than(last_element, element) and heapify_up or heapify_down)(self, index)
|
||||
end
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
93
mods/modlib/hashlist.lua
Normal file
93
mods/modlib/hashlist.lua
Normal file
|
@ -0,0 +1,93 @@
|
|||
-- Localize globals
|
||||
local setmetatable = setmetatable
|
||||
|
||||
-- Table based list, can handle at most 2^52 pushes
|
||||
local list = {}
|
||||
-- TODO use __len for Lua version > 5.1
|
||||
local metatable = {__index = list}
|
||||
list.metatable = metatable
|
||||
|
||||
-- Takes a list
|
||||
function list:new()
|
||||
self.head = 0
|
||||
self.length = #self
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
function list:in_bounds(index)
|
||||
return index >= 1 and index <= self.length
|
||||
end
|
||||
|
||||
function list:get(index)
|
||||
return self[self.head + index]
|
||||
end
|
||||
|
||||
function list:set(index, value)
|
||||
assert(value ~= nil)
|
||||
self[self.head + index] = value
|
||||
end
|
||||
|
||||
function list:len()
|
||||
return self.length
|
||||
end
|
||||
|
||||
function list:ipairs()
|
||||
local index = 0
|
||||
return function()
|
||||
index = index + 1
|
||||
if index > self.length then
|
||||
return
|
||||
end
|
||||
return index, self[self.head + index]
|
||||
end
|
||||
end
|
||||
|
||||
function list:rpairs()
|
||||
local index = self.length + 1
|
||||
return function()
|
||||
index = index - 1
|
||||
if index < 1 then
|
||||
return
|
||||
end
|
||||
return index, self[self.head + index]
|
||||
end
|
||||
end
|
||||
|
||||
function list:push_tail(value)
|
||||
assert(value ~= nil)
|
||||
self.length = self.length + 1
|
||||
self[self.head + self.length] = value
|
||||
end
|
||||
|
||||
function list:get_tail()
|
||||
return self[self.head + self.length]
|
||||
end
|
||||
|
||||
function list:pop_tail()
|
||||
if self.length == 0 then return end
|
||||
local value = self:get_tail()
|
||||
self[self.head + self.length] = nil
|
||||
self.length = self.length - 1
|
||||
return value
|
||||
end
|
||||
|
||||
function list:push_head(value)
|
||||
self[self.head] = value
|
||||
self.head = self.head - 1
|
||||
self.length = self.length + 1
|
||||
end
|
||||
|
||||
function list:get_head()
|
||||
return self[self.head + 1]
|
||||
end
|
||||
|
||||
function list:pop_head()
|
||||
if self.length == 0 then return end
|
||||
local value = self:get_head()
|
||||
self.length = self.length - 1
|
||||
self.head = self.head + 1
|
||||
self[self.head] = nil
|
||||
return value
|
||||
end
|
||||
|
||||
return list
|
60
mods/modlib/heap.lua
Normal file
60
mods/modlib/heap.lua
Normal file
|
@ -0,0 +1,60 @@
|
|||
-- Localize globals
|
||||
local math_floor, setmetatable, table_insert = math.floor, setmetatable, table.insert
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
|
||||
function less_than(a, b) return a < b end
|
||||
|
||||
--> empty min heap
|
||||
function new(less_than)
|
||||
return setmetatable({less_than = less_than}, metatable)
|
||||
end
|
||||
|
||||
function push(self, value)
|
||||
table_insert(self, value)
|
||||
local function heapify(index)
|
||||
if index == 1 then
|
||||
return
|
||||
end
|
||||
local parent = math_floor(index / 2)
|
||||
if self.less_than(self[index], self[parent]) then
|
||||
self[parent], self[index] = self[index], self[parent]
|
||||
heapify(parent)
|
||||
end
|
||||
end
|
||||
heapify(#self)
|
||||
end
|
||||
|
||||
function pop(self)
|
||||
local value = self[1]
|
||||
local last = #self
|
||||
if last == 1 then
|
||||
self[1] = nil
|
||||
return value
|
||||
end
|
||||
self[1], self[last] = self[last], nil
|
||||
last = last - 1
|
||||
local function heapify(index)
|
||||
local left_child = index * 2
|
||||
if left_child > last then
|
||||
return
|
||||
end
|
||||
local smallest_child = left_child + 1
|
||||
if smallest_child > last or self.less_than(self[left_child], self[smallest_child]) then
|
||||
smallest_child = left_child
|
||||
end
|
||||
if self.less_than(self[smallest_child], self[index]) then
|
||||
self[index], self[smallest_child] = self[smallest_child], self[index]
|
||||
heapify(smallest_child)
|
||||
end
|
||||
end
|
||||
heapify(1)
|
||||
return value
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
147
mods/modlib/init.lua
Normal file
147
mods/modlib/init.lua
Normal file
|
@ -0,0 +1,147 @@
|
|||
local modules = {}
|
||||
for _, file in pairs{
|
||||
"schema",
|
||||
"file",
|
||||
"func",
|
||||
"less_than",
|
||||
"iterator",
|
||||
"math",
|
||||
"table",
|
||||
"vararg",
|
||||
"text",
|
||||
"utf8",
|
||||
"vector",
|
||||
"matrix4",
|
||||
"quaternion",
|
||||
"trie",
|
||||
"kdtree",
|
||||
"hashlist",
|
||||
"hashheap",
|
||||
"heap",
|
||||
"binary",
|
||||
"b3d",
|
||||
"json",
|
||||
"luon",
|
||||
"bluon",
|
||||
"base64",
|
||||
"persistence",
|
||||
"debug",
|
||||
"web",
|
||||
"tex"
|
||||
} do
|
||||
modules[file] = file
|
||||
end
|
||||
if minetest then
|
||||
modules.minetest = "minetest"
|
||||
end
|
||||
|
||||
-- modlib.mod is an alias for modlib.minetest.mod
|
||||
modules.string = "text"
|
||||
modules.number = "math"
|
||||
|
||||
local parent_dir
|
||||
if not minetest then
|
||||
-- TOFIX
|
||||
local init_path = arg and arg[0]
|
||||
parent_dir = init_path and init_path:match"^.[/\\]" or ""
|
||||
end
|
||||
|
||||
local dir_delim = rawget(_G, "DIR_DELIM") -- Minetest
|
||||
or (rawget(_G, "package") and package.config and assert(package.config:match("^(.-)[\r\n]"))) or "/"
|
||||
|
||||
local function concat_path(path)
|
||||
return table.concat(path, dir_delim)
|
||||
end
|
||||
|
||||
-- only used if Minetest is available
|
||||
local function get_resource(modname, resource, ...)
|
||||
if not resource then
|
||||
resource = modname
|
||||
modname = minetest.get_current_modname()
|
||||
end
|
||||
return concat_path{minetest.get_modpath(modname), resource, ...}
|
||||
end
|
||||
|
||||
local function load_module(self, module_name_or_alias)
|
||||
local module_name = modules[module_name_or_alias]
|
||||
if not module_name then
|
||||
-- no such module
|
||||
return
|
||||
end
|
||||
local environment
|
||||
if module_name ~= module_name_or_alias then
|
||||
-- alias handling
|
||||
environment = self[module_name]
|
||||
else
|
||||
environment = dofile(minetest
|
||||
and get_resource(self.modname, module_name .. ".lua")
|
||||
or (parent_dir .. module_name .. ".lua"))
|
||||
end
|
||||
self[module_name_or_alias] = environment
|
||||
return environment
|
||||
end
|
||||
|
||||
local rawget, rawset = rawget, rawset
|
||||
modlib = setmetatable({}, { __index = load_module })
|
||||
|
||||
-- TODO bump on release
|
||||
modlib.version = 103
|
||||
|
||||
if minetest then
|
||||
modlib.modname = minetest.get_current_modname()
|
||||
end
|
||||
|
||||
-- Raw globals
|
||||
modlib._RG = setmetatable({}, {
|
||||
__index = function(_, index)
|
||||
return rawget(_G, index)
|
||||
end,
|
||||
__newindex = function(_, index, value)
|
||||
return rawset(_G, index, value)
|
||||
end
|
||||
})
|
||||
|
||||
-- Globals merged with modlib
|
||||
modlib.G = setmetatable({}, {__index = function(self, module_name)
|
||||
local module = load_module(self, module_name)
|
||||
if module == nil then
|
||||
return _G[module_name]
|
||||
end
|
||||
if _G[module_name] then
|
||||
setmetatable(module, {__index = _G[module_name]})
|
||||
end
|
||||
return module
|
||||
end})
|
||||
|
||||
-- "Imports" modlib by changing the environment of the calling function
|
||||
--! This alters environments at the expense of performance. Use with caution.
|
||||
--! Prefer localizing modlib library functions or API tables if possible.
|
||||
function modlib.set_environment()
|
||||
setfenv(2, setmetatable({}, {__index = modlib.G}))
|
||||
end
|
||||
|
||||
-- Force load file module to pass dir_delim & to set concat_path
|
||||
modlib.file = assert(loadfile(get_resource"file.lua"))(dir_delim)
|
||||
modlib.file.concat_path = concat_path
|
||||
|
||||
if minetest then
|
||||
-- Force-loading of the minetest & mod modules
|
||||
-- Also sets modlib.mod -> modlib.minetest.mod alias.
|
||||
local ml_mt = modlib.minetest
|
||||
ml_mt.mod.get_resource = get_resource
|
||||
modlib.mod = ml_mt.mod
|
||||
-- HACK force load minetest/gametime.lua to ensure that the globalstep is registered earlier than globalsteps of mods depending on modlib
|
||||
dofile(get_resource(modlib.modname, "minetest", "gametime.lua"))
|
||||
local ie = minetest.request_insecure_environment()
|
||||
if ie then
|
||||
-- Force load persistence namespace to pass insecure require
|
||||
-- TODO currently no need to set _G.require, lsqlite3 loads no dependencies that way
|
||||
modlib.persistence = assert(loadfile(get_resource"persistence.lua"))(ie.require)
|
||||
end
|
||||
end
|
||||
|
||||
-- Run build scripts
|
||||
-- dofile(modlib.mod.get_resource("modlib", "build", "html_entities.lua"))
|
||||
|
||||
-- TODO verify localizations suffice
|
||||
return modlib
|
311
mods/modlib/iterator.lua
Normal file
311
mods/modlib/iterator.lua
Normal file
|
@ -0,0 +1,311 @@
|
|||
--[[
|
||||
Iterators are always the *last* argument(s) to all functions here,
|
||||
which differs from other modules which take what they operate on as first argument.
|
||||
This is because iterators consist of three variables - iterator function, state & control variable -
|
||||
and wrapping them (using a table, a closure or the like) would be rather inconvenient.
|
||||
Having them as the last argument allows to just pass in the three variables returned by functions such as `[i]pairs`.
|
||||
Additionally, putting functions first - although syntactically inconvenient - is consistent with Python and Lisp.
|
||||
]]
|
||||
|
||||
local coroutine_create, coroutine_resume, coroutine_yield, coroutine_status, unpack, select
|
||||
= coroutine.create, coroutine.resume, coroutine.yield, coroutine.status, unpack, select
|
||||
|
||||
local identity, not_, add = modlib.func.identity, modlib.func.not_, modlib.func.add
|
||||
|
||||
--+ For all functions which aggregate over single values, use modlib.table.ivalues - not ipairs - for lists!
|
||||
--+ Otherwise they will be applied to the indices.
|
||||
local iterator = {}
|
||||
|
||||
function iterator.wrap(iterator, state, control_var)
|
||||
local function update_control_var(...)
|
||||
control_var = ...
|
||||
return ...
|
||||
end
|
||||
return function()
|
||||
return update_control_var(iterator(state, control_var))
|
||||
end
|
||||
end
|
||||
iterator.closure = iterator.wrap
|
||||
iterator.make_stateful = iterator.wrap
|
||||
|
||||
function iterator.filter(predicate, iterator, state, control_var)
|
||||
local function _filter(...)
|
||||
local cvar = ...
|
||||
if cvar == nil then
|
||||
return
|
||||
end
|
||||
if predicate(...) then
|
||||
return ...
|
||||
end
|
||||
return _filter(iterator(state, cvar))
|
||||
end
|
||||
return function(state, control_var)
|
||||
return _filter(iterator(state, control_var))
|
||||
end, state, control_var
|
||||
end
|
||||
|
||||
function iterator.truthy(...)
|
||||
return iterator.filter(identity, ...)
|
||||
end
|
||||
|
||||
function iterator.falsy(...)
|
||||
return iterator.filter(not_, ...)
|
||||
end
|
||||
|
||||
function iterator.map(map_func, iterator, state, control_var)
|
||||
local function _map(...)
|
||||
control_var = ... -- update control var
|
||||
if control_var == nil then return end
|
||||
return map_func(...)
|
||||
end
|
||||
return function()
|
||||
return _map(iterator(state, control_var))
|
||||
end
|
||||
end
|
||||
|
||||
function iterator.map_values(map_func, iterator, state, control_var)
|
||||
local function _map_values(cvar, ...)
|
||||
if cvar == nil then return end
|
||||
return cvar, map_func(...)
|
||||
end
|
||||
return function(state, control_var)
|
||||
return _map_values(iterator(state, control_var))
|
||||
end, state, control_var
|
||||
end
|
||||
|
||||
-- Iterator must be restartable
|
||||
function iterator.rep(times, iterator, state, control_var)
|
||||
times = times or 1
|
||||
if times == 1 then
|
||||
return iterator, state, control_var
|
||||
end
|
||||
local function _rep(cvar, ...)
|
||||
if cvar == nil then
|
||||
times = times - 1
|
||||
if times == 0 then return end
|
||||
return _rep(iterator(state, control_var))
|
||||
end
|
||||
return cvar, ...
|
||||
end
|
||||
return function(state, control_var)
|
||||
return _rep(iterator(state, control_var))
|
||||
end, state, control_var
|
||||
end
|
||||
|
||||
-- Equivalent to `for x, y, z in iterator, state, ... do callback(x, y, z) end`
|
||||
function iterator.foreach(callback, iterator, state, ...)
|
||||
local function loop(...)
|
||||
if ... == nil then return end
|
||||
callback(...)
|
||||
return loop(iterator(state, ...))
|
||||
end
|
||||
return loop(iterator(state, ...))
|
||||
end
|
||||
|
||||
function iterator.for_generator(caller, ...)
|
||||
local co = coroutine_create(function(...)
|
||||
return caller(function(...)
|
||||
return coroutine_yield(...)
|
||||
end, ...)
|
||||
end)
|
||||
local args, n_args = {...}, select("#", ...)
|
||||
return function()
|
||||
if coroutine_status(co) == "dead" then
|
||||
return
|
||||
end
|
||||
local function _iterate(status, ...)
|
||||
if not status then
|
||||
error((...))
|
||||
end
|
||||
return ...
|
||||
end
|
||||
return _iterate(coroutine_resume(co, unpack(args, 1, n_args)))
|
||||
end
|
||||
end
|
||||
|
||||
function iterator.range(from, to, step)
|
||||
if not step then
|
||||
if not to then
|
||||
from, to = 1, from
|
||||
end
|
||||
step = 1
|
||||
end
|
||||
|
||||
return function(_, current)
|
||||
current = current + step
|
||||
if current > to then
|
||||
return
|
||||
end
|
||||
return current
|
||||
end, nil, from - step
|
||||
end
|
||||
|
||||
function iterator.aggregate(binary_func, total, ...)
|
||||
for value in ... do
|
||||
total = binary_func(total, value)
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
-- Like `iterator.aggregate`, but does not expect a `total`
|
||||
function iterator.reduce(binary_func, iterator, state, control_var)
|
||||
local total = iterator(state, control_var)
|
||||
if total == nil then
|
||||
return -- nothing if the iterator is empty
|
||||
end
|
||||
for value in iterator, state, total do
|
||||
total = binary_func(total, value)
|
||||
end
|
||||
return total
|
||||
end
|
||||
iterator.fold = iterator.reduce
|
||||
|
||||
-- TODO iterator.find(predicate, iterator, state, control_var)
|
||||
|
||||
function iterator.any(...)
|
||||
for val in ... do
|
||||
if val then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function iterator.all(...)
|
||||
for val in ... do
|
||||
if not val then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function iterator.min(less_than_func, ...)
|
||||
local min
|
||||
for value in ... do
|
||||
if min == nil or less_than_func(value, min) then
|
||||
min = value
|
||||
end
|
||||
end
|
||||
return min
|
||||
end
|
||||
|
||||
-- TODO iterator.max
|
||||
|
||||
function iterator.empty(iterator, state, control_var)
|
||||
return iterator(state, control_var) == nil
|
||||
end
|
||||
|
||||
function iterator.first(iterator, state, control_var)
|
||||
return iterator(state, control_var)
|
||||
end
|
||||
|
||||
function iterator.last(iterator, state, control_var)
|
||||
-- Storing a vararg in a table seems to be necessary: https://stackoverflow.com/questions/73914273/
|
||||
-- This could be optimized further for memory by keeping the same table across calls,
|
||||
-- but that might cause issues with multiple coroutines calling this
|
||||
local last, last_n = {}, 0
|
||||
|
||||
local function _last(...)
|
||||
local cvar = ...
|
||||
if cvar == nil then
|
||||
return unpack(last, 1, last_n)
|
||||
end
|
||||
|
||||
-- Write vararg to table: Avoid the creation of a garbage table every iteration by reusing the same table
|
||||
last_n = select("#", ...)
|
||||
for i = 1, last_n do
|
||||
last[i] = select(i, ...)
|
||||
end
|
||||
|
||||
return _last(iterator(state, cvar))
|
||||
end
|
||||
|
||||
return _last(iterator(state, control_var))
|
||||
end
|
||||
|
||||
-- Converts a vararg starting with `nil` (end of loop control variable) into nothing
|
||||
local function nil_to_nothing(...)
|
||||
if ... == nil then return end
|
||||
return ...
|
||||
end
|
||||
|
||||
function iterator.select(n, iterator, state, control_var)
|
||||
for _ = 1, n - 1 do
|
||||
control_var = iterator(state, control_var)
|
||||
if control_var == nil then return end
|
||||
end
|
||||
-- Either all values returned by the n-th call iteration
|
||||
-- or nothing if the iterator holds fewer than `n` values
|
||||
return nil_to_nothing(iterator(state, control_var))
|
||||
end
|
||||
|
||||
function iterator.limit(count, iterator, state, control_var)
|
||||
return function(state, control_var)
|
||||
count = count - 1
|
||||
if count < 0 then return end
|
||||
return iterator(state, control_var)
|
||||
end, state, control_var
|
||||
end
|
||||
|
||||
function iterator.count(...)
|
||||
local count = 0
|
||||
for _ in ... do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function iterator.sum(...)
|
||||
return iterator.aggregate(add, 0, ...)
|
||||
end
|
||||
|
||||
function iterator.average(...)
|
||||
local count = 0
|
||||
local sum = 0
|
||||
for value in ... do
|
||||
count = count + 1
|
||||
sum = sum + value
|
||||
end
|
||||
return sum / count
|
||||
end
|
||||
|
||||
--: ... **restartable** iterator
|
||||
-- A single pass method for calculating the standard deviation exists but is highly inaccurate
|
||||
function iterator.standard_deviation(...)
|
||||
local avg = iterator.average(...)
|
||||
local count = 0
|
||||
local sum = 0
|
||||
for value in ... do
|
||||
count = count + 1
|
||||
sum = sum + (value - avg)^2
|
||||
end
|
||||
return (sum / count)^.5
|
||||
end
|
||||
|
||||
-- Comprehensions ("collectors")
|
||||
|
||||
-- Shorthand for `for k, v in ... do t[k] = v end`
|
||||
function iterator.to_table(...)
|
||||
local t = {}
|
||||
for k, v in ... do
|
||||
t[k] = v
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Shorthand for `for k in ... do t[#t + 1] = k end`
|
||||
function iterator.to_list(...)
|
||||
local t = {}
|
||||
for k in ... do
|
||||
t[#t + 1] = k
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Shorthand for `for k in ... do t[k] = true end`
|
||||
function iterator.to_set(...)
|
||||
local t = {}
|
||||
for k in ... do
|
||||
t[k] = true
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
return iterator
|
402
mods/modlib/json.lua
Normal file
402
mods/modlib/json.lua
Normal file
|
@ -0,0 +1,402 @@
|
|||
local modlib, setmetatable, pairs, assert, error, table_insert, table_concat, tonumber, tostring, math_huge, string, type, next
|
||||
= modlib, setmetatable, pairs, assert, error, table.insert, table.concat, tonumber, tostring, math.huge, string, type, next
|
||||
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- See https://tools.ietf.org/id/draft-ietf-json-rfc4627bis-09.html#unichars and https://json.org
|
||||
|
||||
-- Null
|
||||
-- TODO consider using userdata (for ex. by using newproxy)
|
||||
do
|
||||
local metatable = {}
|
||||
-- eq is not among the metamethods, len won't work on 5.1
|
||||
for _, metamethod in pairs{"add", "sub", "mul", "div", "mod", "pow", "unm", "concat", "len", "lt", "le", "index", "newindex", "call"} do
|
||||
metatable["__" .. metamethod] = function() return error("attempt to " .. metamethod .. " a null value") end
|
||||
end
|
||||
null = setmetatable({}, metatable)
|
||||
end
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
_ENV.metatable = metatable
|
||||
function new(self)
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
local whitespace = modlib.table.set{"\t", "\r", "\n", " "}
|
||||
local decoding_escapes = {
|
||||
['"'] = '"',
|
||||
["\\"] = "\\",
|
||||
["/"] = "/",
|
||||
b = "\b",
|
||||
f = "\f",
|
||||
n = "\n",
|
||||
r = "\r",
|
||||
t = "\t"
|
||||
-- TODO is this complete?
|
||||
}
|
||||
|
||||
-- Set up a DFA for number syntax validations
|
||||
local number_dfa
|
||||
do -- as a RegEx: (0|(1-9)(0-9)*)[.(0-9)+[(e|E)[+|-](0-9)+]]; does not need to handle the first sign
|
||||
-- TODO proper DFA utilities
|
||||
local function set_transitions(state, transitions)
|
||||
for chars, next_state in pairs(transitions) do
|
||||
for char in chars:gmatch"." do
|
||||
state[char] = next_state
|
||||
end
|
||||
end
|
||||
end
|
||||
local onenine = "123456789"
|
||||
local digit = "0" .. onenine
|
||||
local e = "eE"
|
||||
local exponent = {final = true}
|
||||
set_transitions(exponent, {
|
||||
[digit] = exponent
|
||||
})
|
||||
local pre_exponent = {expected = "exponent"}
|
||||
set_transitions(pre_exponent, {
|
||||
[digit] = exponent
|
||||
})
|
||||
local exponent_sign = {expected = "exponent"}
|
||||
set_transitions(exponent_sign, {
|
||||
[digit] = exponent,
|
||||
["+"] = exponent,
|
||||
["-"] = exponent
|
||||
})
|
||||
local fraction_final = {final = true}
|
||||
set_transitions(fraction_final, {
|
||||
[digit] = fraction_final,
|
||||
[e] = exponent_sign
|
||||
})
|
||||
local fraction = {expected = "fraction"}
|
||||
set_transitions(fraction, {
|
||||
[digit] = fraction_final
|
||||
})
|
||||
local integer = {final = true}
|
||||
set_transitions(integer, {
|
||||
[digit] = integer,
|
||||
[e] = exponent_sign,
|
||||
["."] = fraction
|
||||
})
|
||||
local zero = {final = true}
|
||||
set_transitions(zero, {
|
||||
["."] = fraction
|
||||
})
|
||||
number_dfa = {}
|
||||
set_transitions(number_dfa, {
|
||||
[onenine] = integer,
|
||||
["0"] = zero
|
||||
})
|
||||
end
|
||||
|
||||
local hex_digit_values = {}
|
||||
for i = 0, 9 do
|
||||
hex_digit_values[tostring(i)] = i
|
||||
end
|
||||
for i = 0, 5 do
|
||||
hex_digit_values[string.char(("a"):byte() + i)] = 10 + i
|
||||
hex_digit_values[string.char(("A"):byte() + i)] = 10 + i
|
||||
end
|
||||
|
||||
-- TODO SAX vs DOM
|
||||
local utf8_char = modlib.utf8.char
|
||||
function read(self, read_)
|
||||
local index = 0
|
||||
local char
|
||||
-- TODO support read functions which provide additional debug output (such as row:column)
|
||||
local function read()
|
||||
index = index + 1
|
||||
char = read_()
|
||||
return char
|
||||
end
|
||||
local function syntax_error(errmsg)
|
||||
-- TODO ensure the index isn't off
|
||||
error("syntax error: " .. index .. ": " .. errmsg)
|
||||
end
|
||||
local function syntax_assert(value, errmsg)
|
||||
if not value then
|
||||
syntax_error(errmsg or "assertion failed!")
|
||||
end
|
||||
return value
|
||||
end
|
||||
local function skip_whitespace()
|
||||
while whitespace[char] do
|
||||
read()
|
||||
end
|
||||
end
|
||||
-- Forward declaration
|
||||
local value
|
||||
local function number()
|
||||
local state = number_dfa
|
||||
local num = {}
|
||||
while true do
|
||||
-- Will work for nil too
|
||||
local next_state = state[char]
|
||||
if not next_state then
|
||||
if not state.final then
|
||||
if state == number_dfa then
|
||||
syntax_error"expected a number"
|
||||
end
|
||||
syntax_error("invalid number: expected " .. state.expected)
|
||||
end
|
||||
return assert(tonumber(table_concat(num)))
|
||||
end
|
||||
table_insert(num, char)
|
||||
state = next_state
|
||||
read()
|
||||
end
|
||||
end
|
||||
local function utf8_codepoint(codepoint)
|
||||
return syntax_assert(utf8_char(codepoint), "invalid codepoint")
|
||||
end
|
||||
local function string()
|
||||
local chars = {}
|
||||
local high_surrogate
|
||||
while true do
|
||||
local string_char, next_high_surrogate
|
||||
if char == '"' then
|
||||
if high_surrogate then
|
||||
table_insert(chars, utf8_codepoint(high_surrogate))
|
||||
end
|
||||
return table_concat(chars)
|
||||
end
|
||||
if char == "\\" then
|
||||
read()
|
||||
if char == "u" then
|
||||
local codepoint = 0
|
||||
for i = 3, 0, -1 do
|
||||
codepoint = syntax_assert(hex_digit_values[read()], "expected a hex digit") * (16 ^ i) + codepoint
|
||||
end
|
||||
if high_surrogate and codepoint >= 0xDC00 and codepoint <= 0xDFFF then
|
||||
-- TODO strict mode: throw an error for single surrogates
|
||||
codepoint = 0x10000 + (high_surrogate - 0xD800) * 0x400 + codepoint - 0xDC00
|
||||
-- Don't write the high surrogate
|
||||
high_surrogate = nil
|
||||
end
|
||||
if codepoint >= 0xD800 and codepoint <= 0xDBFF then
|
||||
next_high_surrogate = codepoint
|
||||
else
|
||||
string_char = utf8_codepoint(codepoint)
|
||||
end
|
||||
else
|
||||
string_char = syntax_assert(decoding_escapes[char], "invalid escape sequence")
|
||||
end
|
||||
else
|
||||
-- TODO check whether the character is one that must be escaped ("strict" mode)
|
||||
string_char = syntax_assert(char, "unclosed string")
|
||||
end
|
||||
if high_surrogate then
|
||||
table_insert(chars, utf8_codepoint(high_surrogate))
|
||||
end
|
||||
high_surrogate = next_high_surrogate
|
||||
if string_char then
|
||||
table_insert(chars, string_char)
|
||||
end
|
||||
read()
|
||||
end
|
||||
end
|
||||
local element
|
||||
local funcs = {
|
||||
['-'] = function()
|
||||
return -number()
|
||||
end,
|
||||
['"'] = string,
|
||||
["{"] = function()
|
||||
local dict = {}
|
||||
skip_whitespace()
|
||||
if char == "}" then return dict end
|
||||
while true do
|
||||
syntax_assert(char == '"', "key expected")
|
||||
read()
|
||||
local key = string()
|
||||
read()
|
||||
skip_whitespace()
|
||||
syntax_assert(char == ":", "colon expected, got " .. char)
|
||||
local val = element()
|
||||
dict[key] = val
|
||||
if char == "}" then return dict end
|
||||
syntax_assert(char == ",", "comma expected")
|
||||
read()
|
||||
skip_whitespace()
|
||||
end
|
||||
end,
|
||||
["["] = function()
|
||||
local list = {}
|
||||
skip_whitespace()
|
||||
if char == "]" then return list end
|
||||
while true do
|
||||
table_insert(list, value())
|
||||
skip_whitespace()
|
||||
if char == "]" then return list end
|
||||
syntax_assert(char == ",", "comma expected")
|
||||
read()
|
||||
skip_whitespace()
|
||||
end
|
||||
end,
|
||||
}
|
||||
local function expect_word(word, value)
|
||||
local msg = word .. " expected"
|
||||
funcs[word:sub(1, 1)] = function()
|
||||
syntax_assert(char == word:sub(2, 2), msg)
|
||||
for i = 3, #word do
|
||||
read()
|
||||
syntax_assert(char == word:sub(i, i), msg)
|
||||
end
|
||||
return value
|
||||
end
|
||||
end
|
||||
expect_word("true", true)
|
||||
expect_word("false", false)
|
||||
expect_word("null", self.null)
|
||||
function value()
|
||||
syntax_assert(char, "value expected")
|
||||
local func = funcs[char]
|
||||
if func then
|
||||
-- Advance after first char
|
||||
read()
|
||||
local val = func()
|
||||
-- Advance after last char
|
||||
read()
|
||||
return val
|
||||
end
|
||||
if char >= "0" and char <= "9" then
|
||||
return number()
|
||||
end
|
||||
syntax_error"value expected"
|
||||
end
|
||||
function element()
|
||||
read()
|
||||
skip_whitespace()
|
||||
local val = value()
|
||||
skip_whitespace()
|
||||
return val
|
||||
end
|
||||
-- TODO consider asserting EOF as read() == nil, perhaps controlled by a parameter
|
||||
return element()
|
||||
end
|
||||
|
||||
local encoding_escapes = modlib.table.flip(decoding_escapes)
|
||||
-- Solidus does not need to be escaped
|
||||
encoding_escapes["/"] = nil
|
||||
-- Control characters. Note: U+0080 to U+009F and U+007F are not considered control characters.
|
||||
for byte = 0, 0x1F do
|
||||
encoding_escapes[string.char(byte)] = string.format("u%04X", byte)
|
||||
end
|
||||
modlib.table.map(encoding_escapes, function(str) return "\\" .. str end)
|
||||
local function escape(str)
|
||||
return str:gsub(".", encoding_escapes)
|
||||
end
|
||||
function write(self, value, write)
|
||||
local null = self.null
|
||||
local written_strings = self.cache_escaped_strings and setmetatable({}, {__index = function(self, str)
|
||||
local escaped_str = escape(str)
|
||||
self[str] = escaped_str
|
||||
return escaped_str
|
||||
end})
|
||||
local function string(str)
|
||||
write'"'
|
||||
write(written_strings and written_strings[str] or escape(str))
|
||||
return write'"'
|
||||
end
|
||||
local dump
|
||||
local function write_kv(key, value)
|
||||
assert(type(key) == "string", "not a dictionary")
|
||||
string(key)
|
||||
write":"
|
||||
dump(value)
|
||||
end
|
||||
function dump(value)
|
||||
if value == null then
|
||||
-- TODO improve null check (checking for equality doesn't allow using nan as null, for instance)
|
||||
return write"null"
|
||||
end
|
||||
if value == true then
|
||||
return write"true"
|
||||
end
|
||||
if value == false then
|
||||
return write"false"
|
||||
end
|
||||
local type_ = type(value)
|
||||
if type_ == "number" then
|
||||
assert(value == value, "unsupported number value: nan")
|
||||
assert(value ~= math_huge, "unsupported number value: inf")
|
||||
assert(value ~= -math_huge, "unsupported number value: -inf")
|
||||
return write(("%.17g"):format(value))
|
||||
end
|
||||
if type_ == "string" then
|
||||
return string(value)
|
||||
end
|
||||
if type_ == "table" then
|
||||
local table = value
|
||||
local len = #table
|
||||
if len == 0 then
|
||||
local first, value = next(table)
|
||||
write"{"
|
||||
if first ~= nil then
|
||||
write_kv(first, value)
|
||||
end
|
||||
for key, value in next, table, first do
|
||||
write","
|
||||
write_kv(key, value)
|
||||
end
|
||||
write"}"
|
||||
else
|
||||
assert(modlib.table.count(table) == len, "mixed list & hash part")
|
||||
write"["
|
||||
for i = 1, len - 1 do
|
||||
dump(table[i])
|
||||
write","
|
||||
end
|
||||
dump(table[len])
|
||||
write"]"
|
||||
end
|
||||
return
|
||||
end
|
||||
error("unsupported type: " .. type_)
|
||||
end
|
||||
dump(value)
|
||||
end
|
||||
|
||||
-- TODO get rid of this paste of write_file and write_string (see modlib.luon)
|
||||
|
||||
function write_file(self, value, file)
|
||||
return self:write(value, function(text)
|
||||
file:write(text)
|
||||
end)
|
||||
end
|
||||
|
||||
function write_string(self, value)
|
||||
local rope = {}
|
||||
self:write(value, function(text)
|
||||
table_insert(rope, text)
|
||||
end)
|
||||
return table_concat(rope)
|
||||
end
|
||||
|
||||
-- TODO read_path (for other serializers too)
|
||||
|
||||
function read_file(self, file)
|
||||
local value = self:read(function()
|
||||
return file:read(1)
|
||||
end)
|
||||
-- TODO consider file:close()
|
||||
return value
|
||||
end
|
||||
|
||||
function read_string(self, string)
|
||||
-- TODO move the string -> one char read func pattern to modlib.text
|
||||
local index = 0
|
||||
local value = self:read(function()
|
||||
index = index + 1
|
||||
if index > #string then
|
||||
return
|
||||
end
|
||||
return string:sub(index, index)
|
||||
end)
|
||||
-- We just expect EOF for strings
|
||||
assert(index > #string, "EOF expected")
|
||||
return value
|
||||
end
|
||||
|
||||
return _ENV
|
64
mods/modlib/kdtree.lua
Normal file
64
mods/modlib/kdtree.lua
Normal file
|
@ -0,0 +1,64 @@
|
|||
-- Localize globals
|
||||
local assert, math, modlib, setmetatable, table, unpack = assert, math, modlib, setmetatable, table, unpack
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
|
||||
distance = modlib.vector.distance
|
||||
|
||||
--: vectors first vector is used to infer the dimension
|
||||
--: distance (vector, other_vector) -> number, default: modlib.vector.distance
|
||||
function new(vectors, distance)
|
||||
assert(#vectors > 0, "vector list must not be empty")
|
||||
local dimension = #vectors[1]
|
||||
local function builder(vectors, axis)
|
||||
if #vectors == 1 then return { value = vectors[1] } end
|
||||
table.sort(vectors, function(a, b) return a[axis] > b[axis] end)
|
||||
local median = math.floor(#vectors / 2)
|
||||
local next_axis = ((axis + 1) % dimension) + 1
|
||||
return setmetatable({
|
||||
axis = axis,
|
||||
pivot = vectors[median],
|
||||
left = builder({ unpack(vectors, 1, median) }, next_axis),
|
||||
right = builder({ unpack(vectors, median + 1) }, next_axis)
|
||||
}, metatable)
|
||||
end
|
||||
local self = builder(vectors, 1)
|
||||
self.distance = distance
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
function get_nearest_neighbor(self, vector)
|
||||
local min_distance = math.huge
|
||||
local nearest_neighbor
|
||||
local distance_func = self.distance
|
||||
local function visit(tree)
|
||||
local axis = tree.axis
|
||||
if tree.value ~= nil then
|
||||
local distance = distance_func(tree.value, vector)
|
||||
if distance < min_distance then
|
||||
min_distance = distance
|
||||
nearest_neighbor = tree.value
|
||||
end
|
||||
return
|
||||
else
|
||||
local this_side, other_side = tree.left, tree.right
|
||||
if vector[axis] < tree.pivot[axis] then this_side, other_side = other_side, this_side end
|
||||
visit(this_side)
|
||||
if tree.pivot then
|
||||
local dist = math.abs(tree.pivot[axis] - vector[axis])
|
||||
if dist <= min_distance then visit(other_side) end
|
||||
end
|
||||
end
|
||||
end
|
||||
visit(self)
|
||||
return nearest_neighbor, min_distance
|
||||
end
|
||||
|
||||
-- TODO insertion & deletion + rebalancing
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
53
mods/modlib/less_than.lua
Normal file
53
mods/modlib/less_than.lua
Normal file
|
@ -0,0 +1,53 @@
|
|||
-- Comparator utilities for "less than" functions returning whether a < b
|
||||
local less_than = {}
|
||||
setfenv(1, less_than)
|
||||
|
||||
default = {}
|
||||
|
||||
function default.less_than(a, b) return a < b end; default.lt = default.less_than
|
||||
function default.less_or_equal(a, b) return a <= b end; default.leq = default.less_or_equal
|
||||
function default.greater_than(a, b) return a > b end; default.gt = default.greater_than
|
||||
function default.greater_or_equal(a, b) return a >= b end; default.geq = default.greater_or_equal
|
||||
|
||||
function less_or_equal(less_than)
|
||||
return function(a, b) return not less_than(b, a) end
|
||||
end
|
||||
leq = less_or_equal
|
||||
|
||||
function greater_or_equal(less_than)
|
||||
return function(a, b) return not less_than(a, b) end
|
||||
end
|
||||
geq = greater_or_equal
|
||||
|
||||
function greater_than(less_than)
|
||||
return function(a, b) return less_than(b, a) end
|
||||
end
|
||||
gt = greater_than
|
||||
|
||||
function equal(less_than)
|
||||
return function(a, b)
|
||||
return not (less_than(a, b) or less_than(b, a))
|
||||
end
|
||||
end
|
||||
|
||||
function relation(less_than)
|
||||
return function(a, b)
|
||||
if less_than(a, b) then return "<"
|
||||
elseif less_than(b, a) then return ">"
|
||||
else return "=" end
|
||||
end
|
||||
end
|
||||
|
||||
function by_func(func)
|
||||
return function(a, b)
|
||||
return func(a) < func(b)
|
||||
end
|
||||
end
|
||||
|
||||
function by_field(key)
|
||||
return function(a, b)
|
||||
return a[key] < b[key]
|
||||
end
|
||||
end
|
||||
|
||||
return less_than
|
38
mods/modlib/logo.svg
Normal file
38
mods/modlib/logo.svg
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg2856"
|
||||
height="48px"
|
||||
width="48px">
|
||||
<defs
|
||||
id="defs2858" />
|
||||
<metadata
|
||||
id="metadata2861">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path
|
||||
style="fill:#030380;fill-opacity:1;stroke:#7f7f7f;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 11.75,30.567358 24,37.999999 36.25,31.287557 V 16.71244 L 24,10 11.75,17.432641 Z"
|
||||
id="path3837" />
|
||||
<path
|
||||
style="fill:#030380;fill-opacity:1;stroke:#7f7f7f;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 30.318359,8.1694856 34.693,10.824001 39.068358,8.4266998 V 3.2213009 L 34.693,0.8240013 30.318359,3.4785156 Z"
|
||||
id="path3837-6-3" />
|
||||
<path
|
||||
d="m 24,0.421875 -20.625,12.5 v 22.15625 L 3.625,35.222656 24,47.578125 44.625,36.261719 V 11.738281 l -7.202778,-4.4175761 -1,0.578125 L 43.625,12.316406 V 35.683594 L 24,46.421875 4.375,34.5 v -21 L 24,1.578125 30.318359,4.6347656 v -1.15625 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#7f7f7f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:6.5;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000"
|
||||
id="path873" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
207
mods/modlib/luon.lua
Normal file
207
mods/modlib/luon.lua
Normal file
|
@ -0,0 +1,207 @@
|
|||
-- Lua module to serialize values as Lua code
|
||||
|
||||
local assert, error, rawget, pairs, pcall, type, setfenv, setmetatable, select, loadstring, loadfile
|
||||
= assert, error, rawget, pairs, pcall, type, setfenv, setmetatable, select, loadstring, loadfile
|
||||
|
||||
local table_concat, string_format, math_huge
|
||||
= table.concat, string.format, math.huge
|
||||
|
||||
local count_objects = modlib.table.count_objects
|
||||
local is_identifier = modlib.text.is_identifier
|
||||
|
||||
local function quote(string)
|
||||
return string_format("%q", string)
|
||||
end
|
||||
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
local metatable = {__index = _ENV}
|
||||
_ENV.metatable = metatable
|
||||
|
||||
function new(self)
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
function aux_write(_self, _object)
|
||||
-- returns reader, arguments
|
||||
return
|
||||
end
|
||||
|
||||
aux_read = {}
|
||||
|
||||
function write(self, value, write)
|
||||
-- TODO evaluate custom aux. writers *before* writing for circular structs
|
||||
local reference, refnum = "1", 1
|
||||
-- [object] = reference
|
||||
local references = {}
|
||||
-- Circular tables that must be filled using `table[key] = value` statements
|
||||
local to_fill = {}
|
||||
|
||||
-- TODO (?) sort objects by count, give frequently referenced objects shorter references
|
||||
for object, count in pairs(count_objects(value)) do
|
||||
local type_ = type(object)
|
||||
-- Object must appear more than once. If it is a string, the reference has to be shorter than the string.
|
||||
if count >= 2 and (type_ ~= "string" or #reference + 5 < #object) then
|
||||
if refnum == 1 then
|
||||
write"local _={};" -- initialize reference table
|
||||
end
|
||||
write"_["
|
||||
write(reference)
|
||||
write"]="
|
||||
if type_ == "table" then
|
||||
write"{}"
|
||||
elseif type_ == "string" then
|
||||
write(quote(object))
|
||||
end
|
||||
write";"
|
||||
references[object] = reference
|
||||
if type_ == "table" then
|
||||
to_fill[object] = reference
|
||||
end
|
||||
refnum = refnum + 1
|
||||
reference = string_format("%d", refnum)
|
||||
end
|
||||
end
|
||||
-- Used to decide whether we should do "key=..."
|
||||
local function use_short_key(key)
|
||||
return not references[key] and type(key) == "string" and is_identifier(key)
|
||||
end
|
||||
local function dump(value)
|
||||
-- Primitive types
|
||||
if value == nil then
|
||||
return write"nil"
|
||||
end if value == true then
|
||||
return write"true"
|
||||
end if value == false then
|
||||
return write"false"
|
||||
end
|
||||
local type_ = type(value)
|
||||
if type_ == "number" then
|
||||
-- Explicit handling of special values for forwards compatibility
|
||||
if value ~= value then -- nan
|
||||
return write"0/0"
|
||||
end if value == math_huge then
|
||||
return write"1/0"
|
||||
end if value == -math_huge then
|
||||
return write"-1/0"
|
||||
end
|
||||
return write(string_format("%.17g", value))
|
||||
end
|
||||
-- Reference types: table and string
|
||||
local ref = references[value]
|
||||
if ref then
|
||||
-- Referenced
|
||||
write"_["
|
||||
write(ref)
|
||||
return write"]"
|
||||
end if type_ == "string" then
|
||||
return write(quote(value))
|
||||
end if type_ == "table" then
|
||||
write"{"
|
||||
-- First write list keys:
|
||||
-- Don't use the table length #value here as it may horribly fail
|
||||
-- for tables which use large integers as keys in the hash part;
|
||||
-- stop at the first "hole" (nil value) instead
|
||||
local len = 0
|
||||
local first = true -- whether this is the first entry, which may not have a leading comma
|
||||
while true do
|
||||
local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable
|
||||
if v == nil then break end
|
||||
if first then first = false else write(",") end
|
||||
dump(v)
|
||||
len = len + 1
|
||||
end
|
||||
-- Now write map keys ([key] = value)
|
||||
for k, v in pairs(value) do
|
||||
-- We have written all non-float keys in [1, len] already
|
||||
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then
|
||||
if first then first = false else write(",") end
|
||||
if use_short_key(k) then
|
||||
write(k)
|
||||
else
|
||||
write"["
|
||||
dump(k)
|
||||
write"]"
|
||||
end
|
||||
write"="
|
||||
dump(v)
|
||||
end
|
||||
end
|
||||
return write"}"
|
||||
end
|
||||
-- TODO move aux_write to start, to allow dealing with metatables etc.?
|
||||
return (function(func, ...)
|
||||
-- functions are the only way to deal with varargs
|
||||
if not func then
|
||||
return error("unsupported type: " .. type_)
|
||||
end
|
||||
write(func)
|
||||
write"("
|
||||
local n = select("#", ...)
|
||||
for i = 1, n - 1 do
|
||||
dump(select(i, ...))
|
||||
write","
|
||||
end
|
||||
if n > 0 then
|
||||
dump(select(n, ...))
|
||||
end
|
||||
write")"
|
||||
end)(self:aux_write(value))
|
||||
end
|
||||
-- Write the statements to fill circular tables
|
||||
for table, ref in pairs(to_fill) do
|
||||
for k, v in pairs(table) do
|
||||
write"_["
|
||||
write(ref)
|
||||
write"]"
|
||||
if use_short_key(k) then
|
||||
write"."
|
||||
write(k)
|
||||
else
|
||||
write"["
|
||||
dump(k)
|
||||
write"]"
|
||||
end
|
||||
write"="
|
||||
dump(v)
|
||||
write";"
|
||||
end
|
||||
end
|
||||
write"return "
|
||||
dump(value)
|
||||
end
|
||||
|
||||
function write_file(self, value, file)
|
||||
return self:write(value, function(text)
|
||||
file:write(text)
|
||||
end)
|
||||
end
|
||||
|
||||
function write_string(self, value)
|
||||
local rope = {}
|
||||
self:write(value, function(text)
|
||||
rope[#rope + 1] = text
|
||||
end)
|
||||
return table_concat(rope)
|
||||
end
|
||||
|
||||
function read(self, ...)
|
||||
local read = assert(...)
|
||||
-- math.huge was serialized to inf, 0/0 was serialized to -nan by `%.17g`
|
||||
setfenv(read, setmetatable({inf = math_huge, nan = 0/0}, {__index = self.aux_read}))
|
||||
local success, value_or_err = pcall(read)
|
||||
if success then
|
||||
return value_or_err
|
||||
end
|
||||
return nil, value_or_err
|
||||
end
|
||||
|
||||
function read_file(self, path)
|
||||
return self:read(loadfile(path))
|
||||
end
|
||||
|
||||
function read_string(self, string)
|
||||
return self:read(loadstring(string))
|
||||
end
|
||||
|
||||
return _ENV
|
182
mods/modlib/math.lua
Normal file
182
mods/modlib/math.lua
Normal file
|
@ -0,0 +1,182 @@
|
|||
-- Localize globals
|
||||
local assert, math, math_floor, minetest, modlib_table_reverse, os, string_char, select, setmetatable, table_insert, table_concat
|
||||
= assert, math, math.floor, minetest, modlib.table.reverse, os, string.char, select, setmetatable, table.insert, table.concat
|
||||
|
||||
local inf = math.huge
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- TODO might be too invasive
|
||||
-- Make random random
|
||||
math.randomseed(minetest and minetest.get_us_time() or os.time() + os.clock())
|
||||
for _ = 1, 100 do math.random() end
|
||||
|
||||
negative_nan = 0/0
|
||||
positive_nan = negative_nan ^ 1
|
||||
|
||||
function sign(number)
|
||||
if number ~= number then return number end -- nan
|
||||
if number == 0 then return 0 end
|
||||
if number < 0 then return -1 end
|
||||
if number > 0 then return 1 end
|
||||
end
|
||||
|
||||
function clamp(number, min, max)
|
||||
return math.min(math.max(number, min), max)
|
||||
end
|
||||
|
||||
-- Random integer from 0 to 2^53 - 1 (inclusive)
|
||||
local function _randint()
|
||||
return math.random(0, 2^27 - 1) * 2^26 + math.random(0, 2^26 - 1)
|
||||
end
|
||||
|
||||
-- Random float from 0 to 1 (exclusive)
|
||||
local function _randfloat()
|
||||
return _randint() / (2^53)
|
||||
end
|
||||
|
||||
--+ Increased randomness float random without overflows
|
||||
--+ `random()`: Random number from `0` to `1` (exclusive)
|
||||
--+ `random(max)`: Random number from `0` to `max` (exclusive)
|
||||
--+ `random(min, max)`: Random number from `min` to `max` (exclusive)
|
||||
function random(...)
|
||||
local n = select("#", ...)
|
||||
if n == 0 then
|
||||
return _randfloat()
|
||||
end if n == 1 then
|
||||
local max = ...
|
||||
return _randfloat() * max
|
||||
end do assert(n == 2)
|
||||
local min, max = ...
|
||||
return min + (max - min) * _randfloat()
|
||||
end
|
||||
end
|
||||
|
||||
-- Increased randomness integer random
|
||||
--+ `randint()`: Random integer from `0` to `2^53 - 1` (inclusive)
|
||||
--+ `randint(max)`: Random integer from `0` to `max` (inclusive)
|
||||
--+ `randint(min, max)`: Random integer from `min` to `max` (inclusive)
|
||||
function randint(...)
|
||||
local n = select("#", ...)
|
||||
if n == 0 then
|
||||
return _randint()
|
||||
end if n == 1 then
|
||||
local max = ...
|
||||
return math.floor(_randfloat() * max + 0.5)
|
||||
end do assert(n == 2)
|
||||
local min, max = ...
|
||||
return min + math.floor(_randfloat() * (max - min) + 0.5)
|
||||
end
|
||||
end
|
||||
|
||||
log = setmetatable({}, {
|
||||
__index = function(self, base)
|
||||
local div = math.log(base)
|
||||
local function base_log(number)
|
||||
return math.log(number) / div
|
||||
end
|
||||
self[base] = base_log
|
||||
return base_log
|
||||
end,
|
||||
__call = function(_, number, base)
|
||||
if not base then
|
||||
return math.log(number)
|
||||
end
|
||||
return math.log(number) / math.log(base)
|
||||
end
|
||||
})
|
||||
|
||||
-- one-based mod
|
||||
function onemod(number, modulus)
|
||||
return ((number - 1) % modulus) + 1
|
||||
end
|
||||
|
||||
function round(number, steps)
|
||||
steps = steps or 1
|
||||
return math_floor(number * steps + 0.5) / steps
|
||||
end
|
||||
|
||||
local c0 = ("0"):byte()
|
||||
local cA = ("A"):byte()
|
||||
|
||||
function default_digit_function(digit)
|
||||
if digit <= 9 then return string_char(c0 + digit) end
|
||||
return string_char(cA + digit - 10)
|
||||
end
|
||||
|
||||
default_precision = 10
|
||||
|
||||
-- See https://github.com/appgurueu/Luon/blob/master/index.js#L724
|
||||
function tostring(number, base, digit_function, precision)
|
||||
if number ~= number then
|
||||
return "nan"
|
||||
end
|
||||
if number == inf then
|
||||
return "inf"
|
||||
end
|
||||
if number == -inf then
|
||||
return "-inf"
|
||||
end
|
||||
digit_function = digit_function or default_digit_function
|
||||
precision = precision or default_precision
|
||||
local out = {}
|
||||
if number < 0 then
|
||||
table_insert(out, "-")
|
||||
number = -number
|
||||
end
|
||||
-- Rounding
|
||||
number = number + base ^ -precision / 2
|
||||
local digit
|
||||
while number >= base do
|
||||
digit = math_floor(number % base)
|
||||
table_insert(out, digit_function(digit))
|
||||
number = (number - digit) / base
|
||||
end
|
||||
digit = math_floor(number)
|
||||
table_insert(out, digit_function(digit))
|
||||
modlib_table_reverse(out)
|
||||
number = number % 1
|
||||
if number ~= 0 and number >= base ^ -precision then
|
||||
table_insert(out, ".")
|
||||
while precision >= 0 and number >= base ^ -precision do
|
||||
number = number * base
|
||||
digit = math_floor(number % base)
|
||||
table_insert(out, digit_function(digit))
|
||||
number = number - digit
|
||||
precision = precision - 1
|
||||
end
|
||||
end
|
||||
return table_concat(out)
|
||||
end
|
||||
|
||||
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround#polyfill
|
||||
-- Rounds a 64-bit float to a 32-bit float;
|
||||
-- if the closest 32-bit float is out of bounds,
|
||||
-- the appropriate infinity is returned.
|
||||
function fround(number)
|
||||
if number == 0 or number ~= number then
|
||||
return number
|
||||
end
|
||||
local sign = 1
|
||||
if number < 0 then
|
||||
sign = -1
|
||||
number = -number
|
||||
end
|
||||
local _, exp = math.frexp(number)
|
||||
exp = exp - 1 -- we want 2^exponent >= number > 2^(exponent-1)
|
||||
local powexp = 2 ^ math.max(-126, math.min(exp, 127))
|
||||
local leading = exp <= -127 and 0 or 1 -- subnormal number?
|
||||
local mantissa = math.floor((number / powexp - leading) * 0x800000 + 0.5)
|
||||
if
|
||||
mantissa > 0x800000 -- doesn't fit in mantissa
|
||||
or (exp >= 127 and mantissa == 0x800000) -- fits if the exponent can be increased
|
||||
then
|
||||
return sign * inf
|
||||
end
|
||||
return sign * powexp * (leading + mantissa / 0x800000)
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
180
mods/modlib/matrix4.lua
Normal file
180
mods/modlib/matrix4.lua
Normal file
|
@ -0,0 +1,180 @@
|
|||
-- Simple 4x4 matrix for 3d transformations (translation, rotation, scale);
|
||||
-- provides exactly the methods needed to calculate inverse bind matrices (for b3d -> glTF conversion)
|
||||
local mat4 = {}
|
||||
local metatable = {__index = mat4}
|
||||
|
||||
function mat4.new(rows)
|
||||
assert(#rows == 4)
|
||||
for i = 1, 4 do
|
||||
assert(#rows[i] == 4)
|
||||
end
|
||||
return setmetatable(rows, metatable)
|
||||
end
|
||||
|
||||
function mat4.identity()
|
||||
return mat4.new{
|
||||
{1, 0, 0, 0},
|
||||
{0, 1, 0, 0},
|
||||
{0, 0, 1, 0},
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
end
|
||||
|
||||
-- Matrices can't properly represent translation:
|
||||
-- => work with 4d vectors, assume w = 1.
|
||||
function mat4.translation(vec)
|
||||
assert(#vec == 3)
|
||||
local x, y, z = unpack(vec)
|
||||
return mat4.new{
|
||||
{1, 0, 0, x},
|
||||
{0, 1, 0, y},
|
||||
{0, 0, 1, z},
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
end
|
||||
|
||||
-- See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
|
||||
function mat4.rotation(unit_quat)
|
||||
assert(#unit_quat == 4)
|
||||
local x, y, z, w = unpack(unit_quat) -- TODO (?) assert unit quaternion
|
||||
return mat4.new{
|
||||
{1 - 2*(y^2 + z^2), 2*(x*y - z*w), 2*(x*z + y*w), 0},
|
||||
{2*(x*y + z*w), 1 - 2*(x^2 + z^2), 2*(y*z - x*w), 0},
|
||||
{2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x^2 + y^2), 0},
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
end
|
||||
|
||||
function mat4.scale(vec)
|
||||
assert(#vec == 3)
|
||||
local x, y, z = unpack(vec)
|
||||
return mat4.new{
|
||||
{x, 0, 0, 0},
|
||||
{0, y, 0, 0},
|
||||
{0, 0, z, 0},
|
||||
{0, 0, 0, 1},
|
||||
}
|
||||
end
|
||||
|
||||
-- Apply `self` to a 4d modlib vector `vec`
|
||||
function mat4:apply(vec)
|
||||
assert(#vec == 4)
|
||||
local res = {}
|
||||
for i = 1, 4 do
|
||||
local sum = 0
|
||||
for j = 1, 4 do
|
||||
sum = sum + self[i][j] * vec[j]
|
||||
end
|
||||
res[i] = sum
|
||||
end
|
||||
return vec.new(res)
|
||||
end
|
||||
|
||||
-- Multiplication: First apply other, then self
|
||||
--> Matrix product `self * other`
|
||||
function mat4:multiply(other)
|
||||
local res = {}
|
||||
for i = 1, 4 do
|
||||
res[i] = {}
|
||||
for j = 1, 4 do
|
||||
local sum = 0 -- dot product of row & col vec
|
||||
for k = 1, 4 do
|
||||
sum = sum + self[i][k] * other[k][j]
|
||||
end
|
||||
res[i][j] = sum
|
||||
end
|
||||
end
|
||||
return mat4.new(res)
|
||||
end
|
||||
|
||||
-- Composition: First apply self, then other
|
||||
function mat4:compose(other)
|
||||
return other:multiply(self) -- equivalent to `other * self` in terms of matrix multiplication
|
||||
end
|
||||
|
||||
-- Matrix inversion using Gauss-Jordan elimination
|
||||
do
|
||||
-- Fundamental operations
|
||||
local function _swap_rows(mat, i, j)
|
||||
mat[i], mat[j] = mat[j], mat[i]
|
||||
end
|
||||
local function _scale_row(mat, factor, row_idx)
|
||||
for i = 1, 4 do
|
||||
mat[row_idx][i] = factor * mat[row_idx][i]
|
||||
end
|
||||
end
|
||||
local function _add_row_with_factor(mat, factor, src_row_idx, dst_row_idx)
|
||||
assert(src_row_idx ~= dst_row_idx)
|
||||
for i = 1, 4 do
|
||||
mat[dst_row_idx][i] = mat[dst_row_idx][i] + factor * mat[src_row_idx][i]
|
||||
end
|
||||
end
|
||||
|
||||
local epsilon = 1e-6 -- small threshold; values below this are considered zero
|
||||
function mat4:inverse()
|
||||
local inv = mat4.identity() -- inverse matrix: all elimination operations will also be applied to this
|
||||
local copy = {} -- copy of `self` the Gaussian elimination is being executed on
|
||||
for i = 1, 4 do
|
||||
copy[i] = {}
|
||||
for j = 1, 4 do
|
||||
copy[i][j] = self[i][j]
|
||||
end
|
||||
end
|
||||
|
||||
-- All operations must be mirrored to the inverse matrix
|
||||
local function swap_rows(i, j)
|
||||
_swap_rows(copy, i, j)
|
||||
_swap_rows(inv, i, j)
|
||||
end
|
||||
local function scale_row(factor, row_idx)
|
||||
_scale_row(copy, factor, row_idx)
|
||||
_scale_row(inv, factor, row_idx)
|
||||
end
|
||||
local function add_with_factor(factor, src_row_idx, dst_row_idx)
|
||||
_add_row_with_factor(copy, factor, src_row_idx, dst_row_idx)
|
||||
_add_row_with_factor(inv, factor, src_row_idx, dst_row_idx)
|
||||
end
|
||||
|
||||
-- Elimination phase
|
||||
for col_idx = 1, 4 do
|
||||
-- Find a pivot row: Choose the row with the largest absolute component
|
||||
local max_row_idx = col_idx
|
||||
local max_abs_comp = math.abs(copy[max_row_idx][col_idx])
|
||||
for row_idx = col_idx, 4 do
|
||||
local cand_comp = math.abs(copy[row_idx][col_idx])
|
||||
if cand_comp > max_abs_comp then
|
||||
max_row_idx, max_abs_comp = row_idx, cand_comp
|
||||
end
|
||||
end
|
||||
|
||||
-- Assert that there is a row that has this component "nonzero"
|
||||
assert(max_abs_comp >= epsilon, "matrix not invertible!")
|
||||
|
||||
swap_rows(col_idx, max_row_idx) -- swap row to correct position
|
||||
-- Eliminate the `col_idx`-th component in all rows *below* the pivot row
|
||||
local pivot_value = copy[col_idx][col_idx]
|
||||
for row_idx = col_idx + 1, 4 do
|
||||
local factor = -copy[row_idx][col_idx] / pivot_value
|
||||
add_with_factor(factor, col_idx, row_idx)
|
||||
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
|
||||
end
|
||||
end
|
||||
|
||||
-- Resubstitution phase - pretty much the same but in reverse and without swapping
|
||||
for col_idx = 4, 1, -1 do
|
||||
local pivot_value = copy[col_idx][col_idx]
|
||||
-- Eliminate the `col_idx`-th component in all rows *above* the pivot row
|
||||
for row_idx = col_idx - 1, 1, -1 do
|
||||
local factor = -copy[row_idx][col_idx] / pivot_value
|
||||
add_with_factor(factor, col_idx, row_idx)
|
||||
assert(math.abs(copy[row_idx][col_idx]) < epsilon) -- should be eliminated now
|
||||
end
|
||||
scale_row(1/pivot_value, col_idx) -- normalize row
|
||||
end
|
||||
|
||||
-- Done: `copy` should now be the identity matrix <=> `inv` is the inverse.
|
||||
return inv
|
||||
end
|
||||
end
|
||||
|
||||
return mat4
|
90
mods/modlib/minetest.lua
Normal file
90
mods/modlib/minetest.lua
Normal file
|
@ -0,0 +1,90 @@
|
|||
local _ENV = {}
|
||||
|
||||
local components = {}
|
||||
for _, value in pairs{
|
||||
"mod",
|
||||
"luon",
|
||||
"raycast",
|
||||
"schematic",
|
||||
"colorspec",
|
||||
"media",
|
||||
"obj",
|
||||
"texmod",
|
||||
} do
|
||||
components[value] = value
|
||||
end
|
||||
|
||||
-- These dirty files have to write to the modlib.minetest environment
|
||||
local dirty_files = {}
|
||||
for filename, comps in pairs{
|
||||
-- get_gametime is missing from here as it is forceloaded in init.lua
|
||||
misc = {
|
||||
"max_wear",
|
||||
"override",
|
||||
"after",
|
||||
"register_globalstep",
|
||||
"form_listeners",
|
||||
"register_form_listener",
|
||||
"texture_modifier_inventorycube",
|
||||
"get_node_inventory_image",
|
||||
"check_player_privs",
|
||||
"decode_base64",
|
||||
"objects_inside_radius",
|
||||
"objects_inside_area",
|
||||
"nodename_matcher",
|
||||
"playerdata",
|
||||
"connected_players",
|
||||
"set_privs",
|
||||
"register_on_leaveplayer",
|
||||
"get_mod_info",
|
||||
"get_mod_load_order"
|
||||
},
|
||||
liquid = {
|
||||
"liquid_level_max",
|
||||
"get_liquid_corner_levels",
|
||||
"flowing_downwards",
|
||||
"get_liquid_flow_direction"
|
||||
},
|
||||
wielditem_change = {
|
||||
"players",
|
||||
"registered_on_wielditem_changes",
|
||||
"register_on_wielditem_change"
|
||||
},
|
||||
colorspec = {
|
||||
"named_colors",
|
||||
"colorspec_to_colorstring"
|
||||
},
|
||||
boxes = {
|
||||
"get_node_boxes",
|
||||
"get_node_collisionboxes",
|
||||
"get_node_selectionboxes",
|
||||
},
|
||||
png = {
|
||||
"decode_png",
|
||||
"convert_png_to_argb8",
|
||||
"encode_png",
|
||||
}
|
||||
} do
|
||||
for _, component in pairs(comps) do
|
||||
components[component] = filename
|
||||
end
|
||||
dirty_files[filename] = true
|
||||
end
|
||||
|
||||
local modpath, concat_path = minetest.get_modpath(modlib.modname), modlib.file.concat_path
|
||||
|
||||
setmetatable(_ENV, {__index = function(_ENV, name)
|
||||
local filename = components[name]
|
||||
if filename then
|
||||
local loader = assert(loadfile(concat_path{modpath, "minetest", filename .. ".lua"}))
|
||||
if dirty_files[filename] then
|
||||
loader(_ENV)
|
||||
return rawget(_ENV, name)
|
||||
end
|
||||
local module = loader()
|
||||
_ENV[name] = module
|
||||
return module
|
||||
end
|
||||
end})
|
||||
|
||||
return _ENV
|
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)
|
6
mods/modlib/mod.conf
Normal file
6
mods/modlib/mod.conf
Normal file
|
@ -0,0 +1,6 @@
|
|||
name = modlib
|
||||
title = Modding Library
|
||||
description = Multipurpose Minetest Modding Library
|
||||
author = LMD
|
||||
optional_depends = dbg, strictest
|
||||
release = 29483
|
23
mods/modlib/persistence.lua
Normal file
23
mods/modlib/persistence.lua
Normal file
|
@ -0,0 +1,23 @@
|
|||
local require = ... or require
|
||||
-- TODO consider moving serializers in this namespace
|
||||
local function load(module_name)
|
||||
return assert(loadfile(modlib.mod.get_resource(modlib.modname, "persistence", module_name .. ".lua")))
|
||||
end
|
||||
return setmetatable({}, {__index = function(self, module_name)
|
||||
if module_name == "lua_log_file" then
|
||||
local module = load(module_name)()
|
||||
self[module_name] = module
|
||||
return module
|
||||
end
|
||||
if module_name == "sqlite3" then
|
||||
local func = load(module_name)
|
||||
local module = function(sqlite3)
|
||||
if sqlite3 then
|
||||
return func(sqlite3)
|
||||
end
|
||||
return func(require"lsqlite3")
|
||||
end
|
||||
self[module_name] = module
|
||||
return module
|
||||
end
|
||||
end})
|
194
mods/modlib/persistence/lua_log_file.lua
Normal file
194
mods/modlib/persistence/lua_log_file.lua
Normal file
|
@ -0,0 +1,194 @@
|
|||
-- Localize globals
|
||||
local assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type
|
||||
= assert, error, io, loadfile, math, minetest, modlib, pairs, setfenv, setmetatable, type
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- Default value
|
||||
reference_strings = true
|
||||
|
||||
-- Note: keys may not be marked as weak references: garbage collected log files wouldn't close the file:
|
||||
-- The `__gc` metamethod doesn't work for tables in Lua 5.1; a hack using `newproxy` would be needed
|
||||
-- See https://stackoverflow.com/questions/27426704/lua-5-1-workaround-for-gc-metamethod-for-tables)
|
||||
-- Therefore, :close() must be called on log files to remove them from the `files` table
|
||||
local files = {}
|
||||
local metatable = {__index = _ENV}
|
||||
_ENV.metatable = metatable
|
||||
|
||||
function new(file_path, root, reference_strings)
|
||||
local self = setmetatable({
|
||||
file_path = assert(file_path),
|
||||
root = root,
|
||||
reference_strings = reference_strings
|
||||
}, metatable)
|
||||
if minetest then
|
||||
files[self] = true
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
local function set_references(self, table)
|
||||
-- Weak table keys to allow the collection of dead reference tables
|
||||
-- TODO garbage collect strings in the references table
|
||||
self.references = setmetatable(table, {__mode = "k"})
|
||||
end
|
||||
|
||||
function load(self)
|
||||
-- Bytecode is blocked by the engine
|
||||
local read = assert(loadfile(self.file_path))
|
||||
-- math.huge is serialized to inf
|
||||
local env = {inf = math.huge}
|
||||
setfenv(read, env)
|
||||
read()
|
||||
env.R = env.R or {{}}
|
||||
local reference_count = #env.R
|
||||
for ref in pairs(env.R) do
|
||||
if ref > reference_count then
|
||||
-- Ensure reference count always has the value of the largest reference
|
||||
-- in case of "holes" (nil values) in the reference list
|
||||
reference_count = ref
|
||||
end
|
||||
end
|
||||
self.reference_count = reference_count
|
||||
self.root = env.R[1]
|
||||
set_references(self, {})
|
||||
end
|
||||
|
||||
function open(self)
|
||||
self.file = io.open(self.file_path, "a+")
|
||||
end
|
||||
|
||||
function init(self)
|
||||
if modlib.file.exists(self.file_path) then
|
||||
self:load()
|
||||
self:_rewrite()
|
||||
self:open()
|
||||
return
|
||||
end
|
||||
self:open()
|
||||
self:_write()
|
||||
end
|
||||
|
||||
function log(self, statement)
|
||||
self.file:write(statement)
|
||||
self.file:write"\n"
|
||||
end
|
||||
|
||||
function flush(self)
|
||||
self.file:flush()
|
||||
end
|
||||
|
||||
function close(self)
|
||||
self.file:close()
|
||||
self.file = nil
|
||||
files[self] = nil
|
||||
end
|
||||
|
||||
if minetest then
|
||||
minetest.register_on_shutdown(function()
|
||||
for self in pairs(files) do
|
||||
self.file:close()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function _dump(self, value, is_key)
|
||||
if value == nil then
|
||||
return "nil"
|
||||
end
|
||||
if value == true then
|
||||
return "true"
|
||||
end
|
||||
if value == false then
|
||||
return "false"
|
||||
end
|
||||
if value ~= value then
|
||||
-- nan
|
||||
return "0/0"
|
||||
end
|
||||
local _type = type(value)
|
||||
if _type == "number" then
|
||||
return ("%.17g"):format(value)
|
||||
end
|
||||
local reference = self.references[value]
|
||||
if reference then
|
||||
return "R[" .. reference .."]"
|
||||
end
|
||||
reference = self.reference_count + 1
|
||||
local key = "R[" .. reference .."]"
|
||||
local function create_reference()
|
||||
self.reference_count = reference
|
||||
self.references[value] = reference
|
||||
end
|
||||
if _type == "string" then
|
||||
local reference_strings = self.reference_strings
|
||||
if is_key and ((not reference_strings) or value:len() <= key:len()) and modlib.text.is_identifier(value) then
|
||||
-- Short key
|
||||
return value, true
|
||||
end
|
||||
local formatted = ("%q"):format(value)
|
||||
if (not reference_strings) or formatted:len() <= key:len() then
|
||||
-- Short string
|
||||
return formatted
|
||||
end
|
||||
-- Use reference
|
||||
create_reference()
|
||||
self:log(key .. "=" .. formatted)
|
||||
elseif _type == "table" then
|
||||
-- Tables always need a reference before they are traversed to prevent infinite recursion
|
||||
create_reference()
|
||||
-- TODO traverse tables to determine whether this is actually needed
|
||||
self:log(key .. "={}")
|
||||
for k, v in pairs(value) do
|
||||
local dumped, short = _dump(self, k, true)
|
||||
self:log(key .. (short and ("." .. dumped) or ("[" .. dumped .. "]")) .. "=" .. _dump(self, v))
|
||||
end
|
||||
else
|
||||
error("unsupported type: " .. _type)
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
function set(self, table, key, value)
|
||||
if not self.references[table] then
|
||||
error"orphan table"
|
||||
end
|
||||
if table[key] == value then
|
||||
-- No change
|
||||
return
|
||||
end
|
||||
table[key] = value
|
||||
table = _dump(self, table)
|
||||
local key, short_key = _dump(self, key, true)
|
||||
self:log(table .. (short_key and ("." .. key) or ("[" .. key .. "]")) .. "=" .. _dump(self, value))
|
||||
end
|
||||
|
||||
function set_root(self, key, value)
|
||||
return self:set(self.root, key, value)
|
||||
end
|
||||
|
||||
function _write(self)
|
||||
set_references(self, {})
|
||||
self.reference_count = 0
|
||||
self:log"R={}"
|
||||
_dump(self, self.root)
|
||||
end
|
||||
|
||||
function _rewrite(self)
|
||||
self.file = io.open(self.file_path, "w+")
|
||||
self:_write()
|
||||
self.file:close()
|
||||
end
|
||||
|
||||
function rewrite(self)
|
||||
if self.file then
|
||||
self.file:close()
|
||||
end
|
||||
self:_rewrite()
|
||||
self:open()
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
318
mods/modlib/persistence/sqlite3.lua
Normal file
318
mods/modlib/persistence/sqlite3.lua
Normal file
|
@ -0,0 +1,318 @@
|
|||
local assert, error, math_huge, modlib, minetest, setmetatable, type, table_insert, table_sort, pairs, ipairs
|
||||
= assert, error, math.huge, modlib, minetest, setmetatable, type, table.insert, table.sort, pairs, ipairs
|
||||
|
||||
local sqlite3 = ...
|
||||
|
||||
--[[
|
||||
Currently uses reference counting to immediately delete tables which aren't reachable from the root table anymore, which has two issues:
|
||||
1. Deletion might trigger a large deletion chain
|
||||
TODO defer deletion, clean up unused tables on startup, delete & iterate tables partially
|
||||
2. Reference counting is unable to handle cycles. `:collectgarbage()` implements a tracing "stop-the-world" garbage collector which handles cycles.
|
||||
TODO take advantage of Lua's garbage collection by keeping a bunch of "twin" objects in a weak structure using proxies (Lua 5.1) or the __gc metamethod (Lua 5.2)
|
||||
See https://wiki.c2.com/?ReferenceCountingCanHandleCycles, https://www.memorymanagement.org/mmref/recycle.html#mmref-recycle and https://wiki.c2.com/?GenerationalGarbageCollectio
|
||||
Weak tables are of no use here, as we need to be notified when a reference is dropped
|
||||
]]
|
||||
|
||||
local ptab = {} -- SQLite3-backed implementation for a persistent Lua table ("ptab")
|
||||
local metatable = {__index = ptab}
|
||||
ptab.metatable = metatable
|
||||
|
||||
-- Note: keys may not be marked as weak references: wouldn't close the database: see persistence/lua_log_file.lua
|
||||
local databases = {}
|
||||
|
||||
local types = {
|
||||
boolean = 1,
|
||||
number = 2,
|
||||
string = 3,
|
||||
table = 4
|
||||
}
|
||||
|
||||
local function increment_highest_table_id(self)
|
||||
self.highest_table_id = self.highest_table_id + 1
|
||||
if self.highest_table_id > 2^50 then
|
||||
-- IDs are approaching double precision limit (52 bits mantissa), defragment them
|
||||
self:defragment_ids()
|
||||
end
|
||||
return self.highest_table_id
|
||||
end
|
||||
|
||||
function ptab.new(file_path, root)
|
||||
return setmetatable({
|
||||
database = sqlite3.open(file_path),
|
||||
root = root
|
||||
}, metatable)
|
||||
end
|
||||
|
||||
function ptab.setmetatable(self)
|
||||
assert(self.database and self.root)
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
local set
|
||||
|
||||
local function add_table(self, table)
|
||||
if type(table) ~= "table" then return end
|
||||
if self.counts[table] then
|
||||
self.counts[table] = self.counts[table] + 1
|
||||
return
|
||||
end
|
||||
self.table_ids[table] = increment_highest_table_id(self)
|
||||
self.counts[table] = 1
|
||||
for k, v in pairs(table) do
|
||||
set(self, table, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
local decrement_reference_count
|
||||
|
||||
local function delete_table(self, table)
|
||||
local id = assert(self.table_ids[table])
|
||||
self.table_ids[table] = nil
|
||||
self.counts[table] = nil
|
||||
for k, v in pairs(table) do
|
||||
decrement_reference_count(self, k)
|
||||
decrement_reference_count(self, v)
|
||||
end
|
||||
local statement = self._prepared.delete_table
|
||||
statement:bind(1, id)
|
||||
statement:step()
|
||||
statement:reset()
|
||||
end
|
||||
|
||||
function decrement_reference_count(self, table)
|
||||
if type(table) ~= "table" then return end
|
||||
local count = self.counts[table]
|
||||
if not count then return end
|
||||
count = count - 1
|
||||
if count == 0 then return delete_table(self, table) end
|
||||
self.counts[table] = count
|
||||
end
|
||||
|
||||
function set(self, table, key, value)
|
||||
local deletion = value == nil
|
||||
if not deletion then
|
||||
add_table(self, key)
|
||||
add_table(self, value)
|
||||
end
|
||||
local previous_value = table[key]
|
||||
if type(previous_value) == "table" then
|
||||
decrement_reference_count(self, previous_value)
|
||||
end
|
||||
if deletion and type(key) == "table" then
|
||||
decrement_reference_count(self, key)
|
||||
end
|
||||
local statement = self._prepared[deletion and "delete" or "insert"]
|
||||
local function bind_type_and_content(n, value)
|
||||
local type_ = type(value)
|
||||
statement:bind(n, assert(types[type_]))
|
||||
if type_ == "boolean" then
|
||||
statement:bind(n + 1, value and 1 or 0)
|
||||
elseif type_ == "number" then
|
||||
if value ~= value then
|
||||
statement:bind(n + 1, "nan")
|
||||
elseif value == math_huge then
|
||||
statement:bind(n + 1, "inf")
|
||||
elseif value == -math_huge then
|
||||
statement:bind(n + 1, "-inf")
|
||||
else
|
||||
statement:bind(n + 1, value)
|
||||
end
|
||||
elseif type_ == "string" then
|
||||
-- Use bind_blob instead of bind as Lua strings are effectively byte strings
|
||||
statement:bind_blob(n + 1, value)
|
||||
elseif type_ == "table" then
|
||||
statement:bind(n + 1, self.table_ids[value])
|
||||
end
|
||||
end
|
||||
statement:bind(1, assert(self.table_ids[table]))
|
||||
bind_type_and_content(2, key)
|
||||
if not deletion then
|
||||
bind_type_and_content(4, value)
|
||||
end
|
||||
statement:step()
|
||||
statement:reset()
|
||||
end
|
||||
|
||||
local function exec(self, sql)
|
||||
if self.database:exec(sql) ~= sqlite3.OK then
|
||||
error(self.database:errmsg())
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:init()
|
||||
local database = self.database
|
||||
local function prepare(sql)
|
||||
local stmt = database:prepare(sql)
|
||||
if not stmt then error(database:errmsg()) end
|
||||
return stmt
|
||||
end
|
||||
exec(self, [[
|
||||
CREATE TABLE IF NOT EXISTS table_entries (
|
||||
table_id INTEGER NOT NULL,
|
||||
key_type INTEGER NOT NULL,
|
||||
key BLOB NOT NULL,
|
||||
value_type INTEGER NOT NULL,
|
||||
value BLOB NOT NULL,
|
||||
PRIMARY KEY (table_id, key_type, key)
|
||||
)]])
|
||||
self._prepared = {
|
||||
insert = prepare"INSERT OR REPLACE INTO table_entries(table_id, key_type, key, value_type, value) VALUES (?, ?, ?, ?, ?)",
|
||||
delete = prepare"DELETE FROM table_entries WHERE table_id = ? AND key_type = ? AND key = ?",
|
||||
delete_table = prepare"DELETE FROM table_entries WHERE table_id = ?",
|
||||
update = {
|
||||
id = prepare"UPDATE table_entries SET table_id = ? WHERE table_id = ?",
|
||||
keys = prepare("UPDATE table_entries SET key = ? WHERE key_type = " .. types.table .. " AND key = ?"),
|
||||
values = prepare("UPDATE table_entries SET value = ? WHERE value_type = " .. types.table .. " AND value = ?")
|
||||
}
|
||||
}
|
||||
-- Default value
|
||||
self.highest_table_id = 0
|
||||
for id in self.database:urows"SELECT MAX(table_id) FROM table_entries" do
|
||||
-- Gets a single value
|
||||
self.highest_table_id = id
|
||||
end
|
||||
increment_highest_table_id(self)
|
||||
local tables = {}
|
||||
local counts = {}
|
||||
self.counts = counts
|
||||
local function get_value(type_, content)
|
||||
if type_ == types.boolean then
|
||||
if content == 0 then return false end
|
||||
if content == 1 then return true end
|
||||
error("invalid boolean value: " .. content)
|
||||
end
|
||||
if type_ == types.number then
|
||||
if content == "nan" then
|
||||
return 0/0
|
||||
end
|
||||
if content == "inf" then
|
||||
return math_huge
|
||||
end
|
||||
if content == "-inf" then
|
||||
return -math_huge
|
||||
end
|
||||
assert(type(content) == "number")
|
||||
return content
|
||||
end
|
||||
if type_ == types.string then
|
||||
assert(type(content) == "string")
|
||||
return content
|
||||
end
|
||||
if type_ == types.table then
|
||||
-- Table reference
|
||||
tables[content] = tables[content] or {}
|
||||
counts[content] = counts[content] or 1
|
||||
return tables[content]
|
||||
end
|
||||
-- Null is unused
|
||||
error("unsupported type: " .. type_)
|
||||
end
|
||||
-- Order by key_content to retrieve list parts in the correct order, making it easier for Lua
|
||||
for table_id, key_type, key, value_type, value in self.database:urows"SELECT * FROM table_entries ORDER BY table_id, key_type, key" do
|
||||
local table = tables[table_id] or {}
|
||||
counts[table] = counts[table] or 1
|
||||
table[get_value(key_type, key)] = get_value(value_type, value)
|
||||
tables[table_id] = table
|
||||
end
|
||||
if tables[1] then
|
||||
self.root = tables[1]
|
||||
counts[self.root] = counts[self.root] + 1
|
||||
self.table_ids = modlib.table.flip(tables)
|
||||
self:collectgarbage()
|
||||
else
|
||||
self.highest_table_id = 0
|
||||
self.table_ids = {}
|
||||
add_table(self, self.root)
|
||||
end
|
||||
databases[self] = true
|
||||
end
|
||||
|
||||
function ptab:rewrite()
|
||||
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
|
||||
exec(self, "DELETE FROM table_entries")
|
||||
self.highest_table_id = 0
|
||||
self.table_ids = {}
|
||||
self.counts = {}
|
||||
add_table(self, self.root)
|
||||
exec(self, "COMMIT TRANSACTION")
|
||||
end
|
||||
|
||||
function ptab:set(table, key, value)
|
||||
exec(self, "BEGIN EXCLUSIVE TRANSACTION")
|
||||
local previous_value = table[key]
|
||||
if previous_value == value then
|
||||
-- no change
|
||||
return
|
||||
end
|
||||
set(self, table, key, value)
|
||||
table[key] = value
|
||||
exec(self, "COMMIT TRANSACTION")
|
||||
end
|
||||
|
||||
function ptab:set_root(key, value)
|
||||
return self:set(self.root, key, value)
|
||||
end
|
||||
|
||||
function ptab:collectgarbage()
|
||||
local marked = {}
|
||||
local function mark(table)
|
||||
if type(table) ~= "table" or marked[table] then return end
|
||||
marked[table] = true
|
||||
for k, v in pairs(table) do
|
||||
mark(k)
|
||||
mark(v)
|
||||
end
|
||||
end
|
||||
mark(self.root)
|
||||
for table in pairs(self.table_ids) do
|
||||
if not marked[table] then
|
||||
delete_table(self, table)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:defragment_ids()
|
||||
local ids = {}
|
||||
for _, id in pairs(self.table_ids) do
|
||||
table_insert(ids, id)
|
||||
end
|
||||
table_sort(ids)
|
||||
local update = self._prepared.update
|
||||
local tables = modlib.table.flip(self.table_ids)
|
||||
for new_id, old_id in ipairs(ids) do
|
||||
for _, stmt in pairs(update) do
|
||||
stmt:bind_values(new_id, old_id)
|
||||
stmt:step()
|
||||
stmt:reset()
|
||||
end
|
||||
self.table_ids[tables[old_id]] = new_id
|
||||
end
|
||||
self.highest_table_id = #ids
|
||||
end
|
||||
|
||||
local function finalize_statements(table)
|
||||
for _, stmt in pairs(table) do
|
||||
if type(stmt) == "table" then
|
||||
finalize_statements(stmt)
|
||||
else
|
||||
local errcode = stmt:finalize()
|
||||
assert(errcode == sqlite3.OK, errcode)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ptab:close()
|
||||
finalize_statements(self._prepared)
|
||||
self.database:close()
|
||||
databases[self] = nil
|
||||
end
|
||||
|
||||
if minetest then
|
||||
minetest.register_on_shutdown(function()
|
||||
for self in pairs(databases) do
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return ptab
|
165
mods/modlib/quaternion.lua
Normal file
165
mods/modlib/quaternion.lua
Normal file
|
@ -0,0 +1,165 @@
|
|||
-- Localize globals
|
||||
local math, modlib, pairs, unpack, vector = math, modlib, pairs, unpack, vector
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- TODO OOP, extend vector
|
||||
|
||||
function from_euler_rotation(rotation)
|
||||
rotation = vector.divide(rotation, 2)
|
||||
local cos = vector.apply(rotation, math.cos)
|
||||
local sin = vector.apply(rotation, math.sin)
|
||||
return {
|
||||
cos.z * sin.x * cos.y + sin.z * cos.x * sin.y,
|
||||
cos.z * cos.x * sin.y - sin.z * sin.x * cos.y,
|
||||
sin.z * cos.x * cos.y - cos.z * sin.x * sin.y,
|
||||
cos.z * cos.x * cos.y + sin.z * sin.x * sin.y
|
||||
}
|
||||
end
|
||||
|
||||
function from_euler_rotation_deg(rotation)
|
||||
return from_euler_rotation(vector.apply(rotation, math.rad))
|
||||
end
|
||||
|
||||
function multiply(self, other)
|
||||
local X, Y, Z, W = unpack(self)
|
||||
return normalize{
|
||||
(other[4] * X) + (other[1] * W) + (other[2] * Z) - (other[3] * Y);
|
||||
(other[4] * Y) + (other[2] * W) + (other[3] * X) - (other[1] * Z);
|
||||
(other[4] * Z) + (other[3] * W) + (other[1] * Y) - (other[2] * X);
|
||||
(other[4] * W) - (other[1] * X) - (other[2] * Y) - (other[3] * Z);
|
||||
}
|
||||
end
|
||||
|
||||
function compose(self, other)
|
||||
return multiply(other, self)
|
||||
end
|
||||
|
||||
function len(self)
|
||||
return (self[1] ^ 2 + self[2] ^ 2 + self[3] ^ 2 + self[4] ^ 2) ^ 0.5
|
||||
end
|
||||
|
||||
function normalize(self)
|
||||
local l = len(self)
|
||||
local res = {}
|
||||
for key, value in pairs(self) do
|
||||
res[key] = value / l
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function conjugate(self)
|
||||
return {
|
||||
-self[1],
|
||||
-self[2],
|
||||
-self[3],
|
||||
self[4]
|
||||
}
|
||||
end
|
||||
|
||||
function inverse(self)
|
||||
-- TODO this is just a fancy normalization *of the conjungate*,
|
||||
-- which for rotations is the inverse
|
||||
return modlib.vector.divide_scalar(conjugate(self), self[1] ^ 2 + self[2] ^ 2 + self[3] ^ 2 + self[4] ^ 2)
|
||||
end
|
||||
|
||||
function negate(self)
|
||||
for key, value in pairs(self) do
|
||||
self[key] = -value
|
||||
end
|
||||
end
|
||||
|
||||
function dot(self, other)
|
||||
return self[1] * other[1] + self[2] * other[2] + self[3] * other[3] + self[4] * other[4]
|
||||
end
|
||||
|
||||
--: self normalized quaternion
|
||||
--: other normalized quaternion
|
||||
function slerp(self, other, ratio)
|
||||
local d = dot(self, other)
|
||||
if d < 0 then
|
||||
d = -d
|
||||
negate(other)
|
||||
end
|
||||
-- Threshold beyond which linear interpolation is used
|
||||
if d > 1 - 1e-10 then
|
||||
return modlib.vector.interpolate(self, other, ratio)
|
||||
end
|
||||
local theta_0 = math.acos(d)
|
||||
local theta = theta_0 * ratio
|
||||
local sin_theta = math.sin(theta)
|
||||
local sin_theta_0 = math.sin(theta_0)
|
||||
local s_1 = sin_theta / sin_theta_0
|
||||
local s_0 = math.cos(theta) - d * s_1
|
||||
return modlib.vector.add(modlib.vector.multiply_scalar(self, s_0), modlib.vector.multiply_scalar(other, s_1))
|
||||
end
|
||||
|
||||
--> axis, angle
|
||||
function to_axis_angle(self)
|
||||
local axis = modlib.vector.new{self[1], self[2], self[3]}
|
||||
local len = axis:length()
|
||||
-- HACK invert axis for correct rotation in Minetest
|
||||
return len == 0 and axis or axis:divide_scalar(-len), 2 * math.atan2(len, self[4])
|
||||
end
|
||||
|
||||
function to_euler_rotation_rad(self)
|
||||
local rotation = {}
|
||||
|
||||
local sinr_cosp = 2 * (self[4] * self[1] + self[2] * self[3])
|
||||
local cosr_cosp = 1 - 2 * (self[1] ^ 2 + self[2] ^ 2)
|
||||
rotation.x = math.atan2(sinr_cosp, cosr_cosp)
|
||||
|
||||
local sinp = 2 * (self[4] * self[2] - self[3] * self[1])
|
||||
if sinp <= -1 then
|
||||
rotation.y = -math.pi/2
|
||||
elseif sinp >= 1 then
|
||||
rotation.y = math.pi/2
|
||||
else
|
||||
rotation.y = math.asin(sinp)
|
||||
end
|
||||
|
||||
local siny_cosp = 2 * (self[4] * self[3] + self[1] * self[2])
|
||||
local cosy_cosp = 1 - 2 * (self[2] ^ 2 + self[3] ^ 2)
|
||||
rotation.z = math.atan2(siny_cosp, cosy_cosp)
|
||||
|
||||
return rotation
|
||||
end
|
||||
|
||||
-- TODO rename this to to_euler_rotation_deg eventually (breaking change)
|
||||
--> {x = pitch, y = yaw, z = roll} euler rotation in degrees
|
||||
function to_euler_rotation(self)
|
||||
return vector.apply(to_euler_rotation_rad(self), math.deg)
|
||||
end
|
||||
|
||||
-- See https://github.com/zaki/irrlicht/blob/master/include/quaternion.h#L652
|
||||
function to_euler_rotation_irrlicht(self)
|
||||
local x, y, z, w = unpack(self)
|
||||
local test = 2 * (y * w - x * z)
|
||||
|
||||
local rot
|
||||
if math.abs(test - 1) <= 1e-6 then
|
||||
rot = {
|
||||
z = -2 * math.atan2(x, w),
|
||||
x = 0,
|
||||
y = math.pi/2
|
||||
}
|
||||
elseif math.abs(test + 1) <= 1e-6 then
|
||||
rot = {
|
||||
z = 2 * math.atan2(x, w),
|
||||
x = 0,
|
||||
y = math.pi/-2
|
||||
}
|
||||
else
|
||||
rot = {
|
||||
z = math.atan2(2 * (x * y + z * w), x ^ 2 - y ^ 2 - z ^ 2 + w ^ 2),
|
||||
x = math.atan2(2 * (y * z + x * w), -x ^ 2 - y ^ 2 + z ^ 2 + w ^ 2),
|
||||
y = math.asin(math.min(math.max(test, -1), 1))
|
||||
}
|
||||
end
|
||||
return vector.apply(rot, math.deg)
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
331
mods/modlib/schema.lua
Normal file
331
mods/modlib/schema.lua
Normal file
|
@ -0,0 +1,331 @@
|
|||
-- Localize globals
|
||||
local assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type = assert, error, ipairs, math, minetest, modlib, pairs, setmetatable, table, tonumber, tostring, type
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
|
||||
function new(def)
|
||||
-- TODO type inference, sanity checking etc.
|
||||
return setmetatable(def, metatable)
|
||||
end
|
||||
|
||||
local function field_name_to_title(name)
|
||||
local title = modlib.text.split(name, "_")
|
||||
title[1] = modlib.text.upper_first(title[1])
|
||||
return table.concat(title, " ")
|
||||
end
|
||||
|
||||
function generate_settingtypes(self)
|
||||
local typ = self.type
|
||||
local settingtype, type_args
|
||||
self.title = self.title or field_name_to_title(self.name)
|
||||
self._level = self._level or 0
|
||||
local default = self.default
|
||||
if typ == "boolean" then
|
||||
settingtype = "bool"
|
||||
default = default and "true" or "false"
|
||||
elseif typ == "string" then
|
||||
settingtype = "string"
|
||||
if self.values then
|
||||
local values = {}
|
||||
for value in pairs(self.values) do
|
||||
if value:find"," then
|
||||
values = nil
|
||||
break
|
||||
end
|
||||
table.insert(values, value)
|
||||
end
|
||||
if values then
|
||||
settingtype = "enum"
|
||||
type_args = table.concat(values, ",")
|
||||
end
|
||||
end
|
||||
elseif typ == "number" then
|
||||
settingtype = self.int and "int" or "float"
|
||||
if self.range and (self.range.min or self.range.max) then
|
||||
-- TODO handle exclusive min/max
|
||||
type_args = (self.int and "%d %d" or "%f %f"):format(self.range.min or (2 ^ -30), self.range.max or (2 ^ 30))
|
||||
end
|
||||
elseif typ == "table" then
|
||||
local settings = {}
|
||||
if self._level > 0 then
|
||||
-- HACK: Minetest automatically adds the modname
|
||||
-- TODO simple names (not modname.field.other_field)
|
||||
settings = {"[" .. ("*"):rep(self._level - 1) .. self.name .. "]"}
|
||||
end
|
||||
local function setting(key, value_scheme)
|
||||
key = tostring(key)
|
||||
assert(not key:find("[=%.%s]"))
|
||||
value_scheme.name = self.name .. "." .. key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._level = self._level + 1
|
||||
table.insert(settings, generate_settingtypes(value_scheme))
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys, function(key, other_key)
|
||||
-- Force leaves before subtrees to prevent them from being accidentally graphically treated as part of the subtree
|
||||
local is_subtree = self.entries[key].type == "table"
|
||||
local other_is_subtree = self.entries[other_key].type == "table"
|
||||
if is_subtree ~= other_is_subtree then
|
||||
return not is_subtree
|
||||
end
|
||||
return key < other_key
|
||||
end)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local description = self.description
|
||||
-- TODO extend description by range etc.?
|
||||
-- TODO enum etc. support
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
description = {description}
|
||||
end
|
||||
description = "# " .. table.concat(description, "\n# ") .. "\n"
|
||||
else
|
||||
description = ""
|
||||
end
|
||||
return description .. self.name .. " (" .. self.title .. ") " .. settingtype .. " " .. (default or "") .. (type_args and (" " .. type_args) or "")
|
||||
end
|
||||
|
||||
function generate_markdown(self)
|
||||
-- TODO address redundancies
|
||||
local function description(lines)
|
||||
local description = self.description
|
||||
if description then
|
||||
if type(description) ~= "table" then
|
||||
table.insert(lines, description)
|
||||
else
|
||||
modlib.table.append(lines, description)
|
||||
end
|
||||
end
|
||||
end
|
||||
local typ = self.type
|
||||
self.title = self.title or field_name_to_title(self._md_name)
|
||||
self._md_level = self._md_level or 1
|
||||
if typ == "table" then
|
||||
local settings = {}
|
||||
description(settings)
|
||||
-- TODO generate Markdown for key/value-checks
|
||||
local function setting(key, value_scheme)
|
||||
value_scheme._md_name = key
|
||||
value_scheme.title = value_scheme.title or self.title .. " " .. field_name_to_title(key)
|
||||
value_scheme._md_level = self._md_level + 1
|
||||
table.insert(settings, table.concat(modlib.table.repetition("#", self._md_level)) .. " `" .. key .. "`")
|
||||
table.insert(settings, "")
|
||||
table.insert(settings, generate_markdown(value_scheme))
|
||||
table.insert(settings, "")
|
||||
end
|
||||
local keys = {}
|
||||
for key in pairs(self.entries or {}) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
setting(key, self.entries[key])
|
||||
end
|
||||
return table.concat(settings, "\n")
|
||||
end
|
||||
if not typ then
|
||||
return ""
|
||||
end
|
||||
local lines = {}
|
||||
description(lines)
|
||||
local function line(text)
|
||||
table.insert(lines, "* " .. text)
|
||||
end
|
||||
table.insert(lines, "")
|
||||
line("Type: " .. self.type)
|
||||
if self.default ~= nil then
|
||||
line("Default: `" .. tostring(self.default) .. "`")
|
||||
end
|
||||
if self.int then
|
||||
line"Integer"
|
||||
elseif self.list then
|
||||
line"List"
|
||||
end
|
||||
if self.infinity then
|
||||
line"Infinities allowed"
|
||||
end
|
||||
if self.nan then
|
||||
line"Not-a-Number (NaN) allowed"
|
||||
end
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
line(">= `" .. self.range.min .. "`")
|
||||
elseif self.range.min_exclusive then
|
||||
line("> `" .. self.range.min_exclusive .. "`")
|
||||
end
|
||||
if self.range.max then
|
||||
line("<= `" .. self.range.max .. "`")
|
||||
elseif self.range.max_exclusive then
|
||||
line("< `" .. self.range.max_exclusive .. "`")
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
line("Possible values:")
|
||||
for value in pairs(self.values) do
|
||||
table.insert(lines, " * " .. value)
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function settingtypes(self)
|
||||
self.settingtypes = self.settingtypes or generate_settingtypes(self)
|
||||
return self.settingtypes
|
||||
end
|
||||
|
||||
function load(self, override, params)
|
||||
local converted
|
||||
if params.convert_strings and type(override) == "string" then
|
||||
converted = true
|
||||
if self.type == "boolean" then
|
||||
if override == "true" then
|
||||
override = true
|
||||
elseif override == "false" then
|
||||
override = false
|
||||
end
|
||||
elseif self.type == "number" then
|
||||
override = tonumber(override)
|
||||
else
|
||||
converted = false
|
||||
end
|
||||
end
|
||||
if override == nil and not converted then
|
||||
if self.type == "table" and self.default == nil then
|
||||
override = {}
|
||||
else
|
||||
return self.default
|
||||
end
|
||||
end
|
||||
local _error = error
|
||||
local function format_error(typ, ...)
|
||||
if typ == "type" then
|
||||
return "mismatched type: expected " .. self.type ..", got " .. type(override) .. (converted and " (converted)" or "")
|
||||
end
|
||||
if typ == "range" then
|
||||
local conditions = {}
|
||||
local function push(condition, bound)
|
||||
if self.range[bound] then
|
||||
table.insert(conditions, " " .. condition .. " " .. minetest.write_json(self.range[bound]))
|
||||
end
|
||||
end
|
||||
push(">", "min_exclusive")
|
||||
push(">=", "min")
|
||||
push("<", "max_exclusive")
|
||||
push("<=", "max")
|
||||
return "out of range: expected value" .. table.concat(conditions, " and")
|
||||
end
|
||||
if typ == "int" then
|
||||
return "expected integer"
|
||||
end
|
||||
if typ == "infinity" then
|
||||
return "expected no infinity"
|
||||
end
|
||||
if typ == "nan" then
|
||||
return "expected no nan"
|
||||
end
|
||||
if typ == "required" then
|
||||
local key = ...
|
||||
return "required field " .. minetest.write_json(key) .. " missing"
|
||||
end
|
||||
if typ == "additional" then
|
||||
local key = ...
|
||||
return "superfluous field " .. minetest.write_json(key)
|
||||
end
|
||||
if typ == "list" then
|
||||
return "not a list"
|
||||
end
|
||||
if typ == "values" then
|
||||
return "expected one of " .. minetest.write_json(modlib.table.keys(self.values)) .. ", got " .. minetest.write_json(override)
|
||||
end
|
||||
_error("unknown error type")
|
||||
end
|
||||
local function error(type, ...)
|
||||
if params.error_message then
|
||||
local formatted = format_error(type, ...)
|
||||
_error("Invalid value: " .. (self.name and (self.name .. ": ") or "") .. formatted)
|
||||
end
|
||||
_error{
|
||||
type = type,
|
||||
self = self,
|
||||
override = override,
|
||||
converted = converted
|
||||
}
|
||||
end
|
||||
local function assert(value, ...)
|
||||
if not value then
|
||||
error(...)
|
||||
end
|
||||
return value
|
||||
end
|
||||
assert(self.type == type(override), "type")
|
||||
if self.type == "number" or self.type == "string" then
|
||||
if self.range then
|
||||
if self.range.min then
|
||||
assert(self.range.min <= override, "range")
|
||||
elseif self.range.min_exclusive then
|
||||
assert(self.range.min_exclusive < override, "range")
|
||||
end
|
||||
if self.range.max then
|
||||
assert(self.range.max >= override, "range")
|
||||
elseif self.range.max_exclusive then
|
||||
assert(self.range.max_exclusive > override, "range")
|
||||
end
|
||||
end
|
||||
if self.type == "number" then
|
||||
assert((not self.int) or (override % 1 == 0), "int")
|
||||
assert(self.infinity or math.abs(override) ~= math.huge, "infinity")
|
||||
assert(self.nan or override == override, "nan")
|
||||
end
|
||||
elseif self.type == "table" then
|
||||
if self.keys then
|
||||
for key, value in pairs(override) do
|
||||
override[load(self.keys, key, params)], override[key] = value, nil
|
||||
end
|
||||
end
|
||||
if self.values then
|
||||
for key, value in pairs(override) do
|
||||
override[key] = load(self.values, value, params)
|
||||
end
|
||||
end
|
||||
if self.entries then
|
||||
for key, schema in pairs(self.entries) do
|
||||
if schema.required and override[key] == nil then
|
||||
error("required", key)
|
||||
end
|
||||
override[key] = load(schema, override[key], params)
|
||||
end
|
||||
if self.additional == false then
|
||||
for key in pairs(override) do
|
||||
if self.entries[key] == nil then
|
||||
error("additional", key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
assert((not self.list) or modlib.table.count(override) == #override, "list")
|
||||
end
|
||||
-- Apply the values check only for primitive types where table indexing is by value;
|
||||
-- the `values` field has a different meaning for tables (constraint all values must fulfill)
|
||||
if self.type ~= "table" then
|
||||
assert((not self.values) or self.values[override], "values")
|
||||
end
|
||||
if self.func then self.func(override) end
|
||||
return override
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
889
mods/modlib/table.lua
Normal file
889
mods/modlib/table.lua
Normal file
|
@ -0,0 +1,889 @@
|
|||
-- Localize globals
|
||||
local assert, ipairs, math, next, pairs, rawget, rawset, getmetatable, setmetatable, select, string, table, type
|
||||
= assert, ipairs, math, next, pairs, rawget, rawset, getmetatable, setmetatable, select, string, table, type
|
||||
|
||||
local lt = modlib.func.lt
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
-- Empty table
|
||||
empty = {}
|
||||
|
||||
-- Table helpers
|
||||
|
||||
function from_iterator(...)
|
||||
local table = {}
|
||||
for key, value in ... do
|
||||
table[key] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function default(table, value)
|
||||
return setmetatable(table, {
|
||||
__index = function()
|
||||
return value
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function map_index(table, func)
|
||||
local mapping_metatable = {
|
||||
__index = function(table, key)
|
||||
return rawget(table, func(key))
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
rawset(table, func(key), value)
|
||||
end
|
||||
}
|
||||
return setmetatable(table, mapping_metatable)
|
||||
end
|
||||
|
||||
function set_case_insensitive_index(table)
|
||||
return map_index(table, string.lower)
|
||||
end
|
||||
|
||||
--+ nilget(a, "b", "c") == a?.b?.c
|
||||
function nilget(value, ...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n do
|
||||
if value == nil then return nil end
|
||||
value = value[select(i, ...)]
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
deepget = nilget
|
||||
|
||||
--+ `deepset(a, "b", "c", d)` is the same as `a.b = a.b ?? {}; a.b.c = d`
|
||||
function deepset(table, ...)
|
||||
local n = select("#", ...)
|
||||
for i = 1, n - 2 do
|
||||
local key = select(i, ...)
|
||||
local parent = table
|
||||
table = parent[key]
|
||||
if table == nil then
|
||||
table = {}
|
||||
parent[key] = table
|
||||
end
|
||||
end
|
||||
table[select(n - 1, ...)] = select(n, ...)
|
||||
end
|
||||
|
||||
-- Fisher-Yates
|
||||
function shuffle(table)
|
||||
for index = 1, #table - 1 do
|
||||
local index_2 = math.random(index, #table)
|
||||
table[index], table[index_2] = table[index_2], table[index]
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
local rope_metatable = {__index = {
|
||||
write = function(self, text)
|
||||
table.insert(self, text)
|
||||
end,
|
||||
to_text = function(self)
|
||||
return table.concat(self)
|
||||
end
|
||||
}}
|
||||
--> rope with simple metatable (:write(text) and :to_text())
|
||||
function rope(table)
|
||||
return setmetatable(table or {}, rope_metatable)
|
||||
end
|
||||
|
||||
local rope_len_metatable = {__index = {
|
||||
write = function(self, text)
|
||||
self.len = self.len + text:len()
|
||||
end
|
||||
}}
|
||||
--> rope for determining length of text supporting `:write(text)` and `.len` to get the length of written text
|
||||
function rope_len(len)
|
||||
return setmetatable({len = len or 0}, rope_len_metatable)
|
||||
end
|
||||
|
||||
function is_circular(table)
|
||||
assert(type(table) == "table")
|
||||
local known = {}
|
||||
local function _is_circular(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if known[value] then
|
||||
return true
|
||||
end
|
||||
known[value] = true
|
||||
for key, value in pairs(value) do
|
||||
if _is_circular(key) or _is_circular(value) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return _is_circular(table)
|
||||
end
|
||||
|
||||
--+ Simple table equality check. Stack overflow if tables are too deep or circular.
|
||||
--+ Use `is_circular(table)` to check whether a table is circular.
|
||||
--> Equality of noncircular tables if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_noncircular(table, other_table)
|
||||
local is_equal = table == other_table
|
||||
if is_equal or type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_table then
|
||||
return false
|
||||
end
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local value_2 = other_table[key]
|
||||
if not equals_noncircular(value, value_2) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found
|
||||
for table, value in pairs(table_keys) do
|
||||
if equals_noncircular(other_key, table) and equals_noncircular(other_value, value) then
|
||||
table_keys[table] = nil
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
equals = equals_noncircular
|
||||
|
||||
--+ Table equality check properly handling circular tables - tables are equal as long as they provide equal key/value-pairs
|
||||
--> Table content equality if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_content(table, other_table)
|
||||
local equal_tables = {}
|
||||
local function _equals(table, other_equal_table)
|
||||
local function set_equal_tables(value)
|
||||
equal_tables[table] = equal_tables[table] or {}
|
||||
equal_tables[table][other_equal_table] = value
|
||||
return value
|
||||
end
|
||||
local is_equal = table == other_equal_table
|
||||
if is_equal or type(table) ~= "table" or type(other_equal_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if #table ~= #other_equal_table then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
local lookup_equal = (equal_tables[table] or {})[other_equal_table]
|
||||
if lookup_equal ~= nil then
|
||||
return lookup_equal
|
||||
end
|
||||
-- Premise
|
||||
set_equal_tables(true)
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
local other_value = other_equal_table[key]
|
||||
if not _equals(value, other_value) then
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
for other_key, other_value in pairs(other_equal_table) do
|
||||
if type(other_key) == "table" then
|
||||
local found = false
|
||||
for table_key, value in pairs(table_keys) do
|
||||
if _equals(table_key, other_key) and _equals(value, other_value) then
|
||||
table_keys[table_key] = nil
|
||||
found = true
|
||||
-- Breaking is fine as per transitivity
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
else
|
||||
if table[other_key] == nil then
|
||||
return set_equal_tables(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return _equals(table, other_table)
|
||||
end
|
||||
|
||||
--+ Table equality check: content has to be equal, relations between tables as well
|
||||
--+ The only difference may be in the memory addresses ("identities") of the (sub)tables
|
||||
--+ Performance may suffer if the tables contain table keys
|
||||
--+ equals(table, copy(table)) is true
|
||||
--> equality (same tables after table reference substitution) of circular tables if `table` and `other_table` are tables
|
||||
--> `table == other_table` else
|
||||
function equals_references(table, other_table)
|
||||
local function _equals(table, other_table, equal_refs)
|
||||
if equal_refs[table] then
|
||||
return equal_refs[table] == other_table
|
||||
end
|
||||
local is_equal = table == other_table
|
||||
-- this check could be omitted if table key equality is being checked
|
||||
if type(table) ~= "table" or type(other_table) ~= "table" then
|
||||
return is_equal
|
||||
end
|
||||
if is_equal then
|
||||
equal_refs[table] = other_table
|
||||
return true
|
||||
end
|
||||
-- Premise: table = other table
|
||||
equal_refs[table] = other_table
|
||||
local table_keys = {}
|
||||
for key, value in pairs(table) do
|
||||
if type(key) == "table" then
|
||||
table_keys[key] = value
|
||||
else
|
||||
local other_value = other_table[key]
|
||||
if not _equals(value, other_value, equal_refs) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
local other_table_keys = {}
|
||||
for other_key, other_value in pairs(other_table) do
|
||||
if type(other_key) == "table" then
|
||||
other_table_keys[other_key] = other_value
|
||||
elseif table[other_key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function _next(current_key, equal_refs, available_keys)
|
||||
local key, value = next(table_keys, current_key)
|
||||
if key == nil then
|
||||
return true
|
||||
end
|
||||
for other_key, other_value in pairs(other_table_keys) do
|
||||
local copy_equal_refs = shallowcopy(equal_refs)
|
||||
if _equals(key, other_key, copy_equal_refs) and _equals(value, other_value, copy_equal_refs) then
|
||||
local copy_available_keys = shallowcopy(available_keys)
|
||||
copy_available_keys[other_key] = nil
|
||||
if _next(key, copy_equal_refs, copy_available_keys) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
return _next(nil, equal_refs, other_table_keys)
|
||||
end
|
||||
return _equals(table, other_table, {})
|
||||
end
|
||||
|
||||
-- Supports circular tables; does not support table keys
|
||||
--> `true` if a mapping of references exists, `false` otherwise
|
||||
function same(a, b)
|
||||
local same = {}
|
||||
local function is_same(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then
|
||||
return a == b
|
||||
end
|
||||
if same[a] or same[b] then
|
||||
return same[a] == b and same[b] == a
|
||||
end
|
||||
if a == b then
|
||||
return true
|
||||
end
|
||||
|
||||
same[a], same[b] = b, a
|
||||
local count = 0
|
||||
for k, v in pairs(a) do
|
||||
count = count + 1
|
||||
assert(type(k) ~= "table", "table keys not supported")
|
||||
if not is_same(v, b[k], same) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for _ in pairs(b) do
|
||||
count = count - 1
|
||||
if count < 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return is_same(a, b)
|
||||
end
|
||||
|
||||
function shallowcopy(
|
||||
table -- table to copy
|
||||
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
|
||||
)
|
||||
if type(table) ~= "table" then
|
||||
return table
|
||||
end
|
||||
|
||||
local copy = {}
|
||||
if not strip_metatables then
|
||||
setmetatable(copy, getmetatable(table))
|
||||
end
|
||||
for key, value in pairs(table) do
|
||||
copy[key] = value
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
function deepcopy_tree(
|
||||
table -- table; may not contain circular references; cross references will be copied multiple times
|
||||
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
|
||||
)
|
||||
if type(table) ~= "table" then
|
||||
return table
|
||||
end
|
||||
|
||||
local copy = {}
|
||||
if not strip_metatables then
|
||||
setmetatable(copy, getmetatable(table))
|
||||
end
|
||||
for key, value in pairs(table) do
|
||||
copy[deepcopy_tree(key)] = deepcopy_tree(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
deepcopy_noncircular = deepcopy_tree
|
||||
|
||||
function deepcopy(
|
||||
table -- table to copy; reference equality will be preserved
|
||||
, strip_metatables -- whether to strip metatables; falsy by default; metatables are not copied
|
||||
)
|
||||
local copies = {}
|
||||
local function _deepcopy(table)
|
||||
local copy = copies[table]
|
||||
if copy then
|
||||
return copy
|
||||
end
|
||||
copy = {}
|
||||
if not strip_metatables then
|
||||
setmetatable(copy, getmetatable(table))
|
||||
end
|
||||
copies[table] = copy
|
||||
local function _copy(value)
|
||||
if type(value) ~= "table" then
|
||||
return value
|
||||
end
|
||||
|
||||
if copies[value] then
|
||||
return copies[value]
|
||||
end
|
||||
|
||||
return _deepcopy(value)
|
||||
end
|
||||
for key, value in pairs(table) do
|
||||
copy[_copy(key)] = _copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
return _deepcopy(table)
|
||||
end
|
||||
|
||||
copy = deepcopy
|
||||
|
||||
function count(table)
|
||||
local count = 0
|
||||
for _ in pairs(table) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function count_equals(table, count)
|
||||
local k
|
||||
for _ = 1, count do
|
||||
k = next(table, k)
|
||||
if k == nil then return false end -- less than n keys
|
||||
end
|
||||
return next(table, k) == nil -- no (n + 1)th entry
|
||||
end
|
||||
|
||||
function is_empty(table)
|
||||
return next(table) == nil
|
||||
end
|
||||
|
||||
function clear(table)
|
||||
for k in pairs(table) do
|
||||
table[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function foreach(table, func)
|
||||
for k, v in pairs(table) do
|
||||
func(k, v)
|
||||
end
|
||||
end
|
||||
|
||||
function deep_foreach_any(table, func)
|
||||
local seen = {}
|
||||
local function visit(value)
|
||||
func(value)
|
||||
if type(value) == "table" then
|
||||
if seen[value] then return end
|
||||
seen[value] = true
|
||||
for k, v in pairs(value) do
|
||||
visit(k)
|
||||
visit(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
visit(table)
|
||||
end
|
||||
|
||||
-- Recursively counts occurences of objects (non-primitives including strings) in a table.
|
||||
function count_objects(value)
|
||||
local counts = {}
|
||||
if value == nil then
|
||||
-- Early return for nil
|
||||
return counts
|
||||
end
|
||||
local function count_values(value)
|
||||
local type_ = type(value)
|
||||
if type_ == "boolean" or type_ == "number" then return end
|
||||
local count = counts[value]
|
||||
counts[value] = (count or 0) + 1
|
||||
if not count and type_ == "table" then
|
||||
for k, v in pairs(value) do
|
||||
count_values(k)
|
||||
count_values(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
count_values(value)
|
||||
return counts
|
||||
end
|
||||
|
||||
function foreach_value(table, func)
|
||||
for _, v in pairs(table) do
|
||||
func(v)
|
||||
end
|
||||
end
|
||||
|
||||
function call(table, ...)
|
||||
for _, func in pairs(table) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function icall(table, ...)
|
||||
for _, func in ipairs(table) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function foreach_key(table, func)
|
||||
for key, _ in pairs(table) do
|
||||
func(key)
|
||||
end
|
||||
end
|
||||
|
||||
function map(table, func)
|
||||
for key, value in pairs(table) do
|
||||
table[key] = func(value)
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
map_values = map
|
||||
|
||||
function map_keys(table, func)
|
||||
local new_tab = {}
|
||||
for key, value in pairs(table) do
|
||||
new_tab[func(key)] = value
|
||||
end
|
||||
return new_tab
|
||||
end
|
||||
|
||||
function process(tab, func)
|
||||
local results = {}
|
||||
for key, value in pairs(tab) do
|
||||
table.insert(results, func(key, value))
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
function call(funcs, ...)
|
||||
for _, func in ipairs(funcs) do
|
||||
func(...)
|
||||
end
|
||||
end
|
||||
|
||||
function find(list, value)
|
||||
for index, other_value in pairs(list) do
|
||||
if value == other_value then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
contains = find
|
||||
|
||||
function to_add(table, after_additions)
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
end
|
||||
|
||||
difference = to_add
|
||||
|
||||
function deep_to_add(table, after_additions)
|
||||
local additions = {}
|
||||
for key, value in pairs(after_additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
local sub_additions = deep_to_add(table[key], value)
|
||||
if next(sub_additions) ~= nil then
|
||||
additions[key] = sub_additions
|
||||
end
|
||||
elseif table[key] ~= value then
|
||||
additions[key] = value
|
||||
end
|
||||
end
|
||||
return additions
|
||||
end
|
||||
|
||||
function add_all(table, additions)
|
||||
for key, value in pairs(additions) do
|
||||
table[key] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function deep_add_all(table, additions)
|
||||
for key, value in pairs(additions) do
|
||||
if type(table[key]) == "table" and type(value) == "table" then
|
||||
deep_add_all(table[key], value)
|
||||
else
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function complete(table, completions)
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function deepcomplete(table, completions)
|
||||
for key, value in pairs(completions) do
|
||||
if table[key] == nil then
|
||||
table[key] = value
|
||||
elseif type(table[key]) == "table" and type(value) == "table" then
|
||||
deepcomplete(table[key], value)
|
||||
end
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function merge(table, other_table, merge_func)
|
||||
merge_func = merge_func or merge
|
||||
local res = {}
|
||||
for key, value in pairs(table) do
|
||||
local other_value = other_table[key]
|
||||
if other_value == nil then
|
||||
res[key] = value
|
||||
else
|
||||
res[key] = merge_func(value, other_value)
|
||||
end
|
||||
end
|
||||
for key, value in pairs(other_table) do
|
||||
if table[key] == nil then
|
||||
res[key] = value
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function merge_tables(table, other_table)
|
||||
return add_all(shallowcopy(table), other_table)
|
||||
end
|
||||
|
||||
union = merge_tables
|
||||
|
||||
function intersection(table, other_table)
|
||||
local result = {}
|
||||
for key, value in pairs(table) do
|
||||
if other_table[key] then
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function append(table, other_table)
|
||||
local length = #table
|
||||
for index, value in ipairs(other_table) do
|
||||
table[length + index] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function keys(table)
|
||||
local keys = {}
|
||||
for key, _ in pairs(table) do
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
function values(table)
|
||||
local values = {}
|
||||
for _, value in pairs(table) do
|
||||
values[#values + 1] = value
|
||||
end
|
||||
return values
|
||||
end
|
||||
|
||||
function flip(table)
|
||||
local flipped = {}
|
||||
for key, value in pairs(table) do
|
||||
flipped[value] = key
|
||||
end
|
||||
return flipped
|
||||
end
|
||||
|
||||
function set(table)
|
||||
local flipped = {}
|
||||
for _, value in pairs(table) do
|
||||
flipped[value] = true
|
||||
end
|
||||
return flipped
|
||||
end
|
||||
|
||||
function unique(table)
|
||||
return keys(set(table))
|
||||
end
|
||||
|
||||
function ivalues(table)
|
||||
local index = 0
|
||||
return function()
|
||||
index = index + 1
|
||||
return table[index]
|
||||
end
|
||||
end
|
||||
|
||||
function rpairs(table)
|
||||
local index = #table
|
||||
return function()
|
||||
if index >= 1 then
|
||||
local value = table[index]
|
||||
index = index - 1
|
||||
if value ~= nil then
|
||||
return index + 1, value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Iterates the hash (= non-list) part of the table. The list part may not be modified while iterating.
|
||||
function hpairs(table)
|
||||
local len = #table -- length only has to be determined once as hnext is a closure
|
||||
local function hnext(key)
|
||||
local value
|
||||
key, value = next(table, key)
|
||||
if type(key) == "number" and key % 1 == 0 and key >= 1 and key <= len then -- list entry, skip
|
||||
return hnext(key)
|
||||
end
|
||||
return key, value
|
||||
end
|
||||
return hnext
|
||||
end
|
||||
|
||||
function min_key(table, less_than)
|
||||
less_than = less_than or lt
|
||||
local min_key = next(table)
|
||||
if min_key == nil then
|
||||
return -- empty table
|
||||
end
|
||||
for candidate_key in next, table, min_key do
|
||||
if less_than(candidate_key, min_key) then
|
||||
min_key = candidate_key
|
||||
end
|
||||
end
|
||||
return min_key
|
||||
end
|
||||
|
||||
function min_value(table, less_than)
|
||||
less_than = less_than or lt
|
||||
local min_key, min_value = next(table)
|
||||
if min_key == nil then
|
||||
return -- empty table
|
||||
end
|
||||
for candidate_key, candidate_value in next, table, min_key do
|
||||
if less_than(candidate_value, min_value) then
|
||||
min_key, min_value = candidate_key, candidate_value
|
||||
end
|
||||
end
|
||||
return min_value, min_key
|
||||
end
|
||||
|
||||
-- TODO move all of the below functions to modlib.list eventually
|
||||
|
||||
--! deprecated
|
||||
function default_comparator(value, other_value)
|
||||
if value == other_value then
|
||||
return 0
|
||||
end
|
||||
if value > other_value then
|
||||
return 1
|
||||
end
|
||||
return -1
|
||||
end
|
||||
|
||||
--! deprecated, use `binary_search(list, value, less_than)` instead
|
||||
--> index if element found
|
||||
--> -index for insertion if not found
|
||||
function binary_search_comparator(comparator)
|
||||
return function(list, value)
|
||||
local min, max = 1, #list
|
||||
while min <= max do
|
||||
local pivot = min + math.floor((max - min) / 2)
|
||||
local element = list[pivot]
|
||||
local compared = comparator(value, element)
|
||||
if compared == 0 then
|
||||
return pivot
|
||||
elseif compared > 0 then
|
||||
min = pivot + 1
|
||||
else
|
||||
max = pivot - 1
|
||||
end
|
||||
end
|
||||
return -min
|
||||
end
|
||||
end
|
||||
|
||||
function binary_search(
|
||||
list -- sorted list
|
||||
, value -- value to be be searched for
|
||||
, less_than -- function(a, b) return a < b end
|
||||
)
|
||||
less_than = less_than or lt
|
||||
local min, max = 1, #list
|
||||
while min <= max do
|
||||
local mid = math.floor((min + max) / 2)
|
||||
local element = list[mid]
|
||||
if less_than(value, element) then
|
||||
max = mid - 1
|
||||
elseif less_than(element, value) then
|
||||
min = mid + 1
|
||||
else -- neither smaller nor larger => must be equal
|
||||
return mid -- index if found
|
||||
end
|
||||
end
|
||||
return nil, min -- nil, insertion index if not found
|
||||
end
|
||||
|
||||
--> whether the list is sorted in ascending order
|
||||
function is_sorted(list, less_than --[[function(a, b) return a < b end]])
|
||||
less_than = less_than or function(a, b) return a < b end
|
||||
for index = 2, #list do
|
||||
if less_than(list[index], list[index - 1]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function reverse(table)
|
||||
local len = #table
|
||||
for index = 1, len / 2 do
|
||||
local index_from_end = len + 1 - index
|
||||
table[index_from_end], table[index] = table[index], table[index_from_end]
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function repetition(value, count)
|
||||
local table = {}
|
||||
for index = 1, count do
|
||||
table[index] = value
|
||||
end
|
||||
return table
|
||||
end
|
||||
|
||||
function slice(list, from, to)
|
||||
from, to = from or 1, to or #list
|
||||
local res = {}
|
||||
for i = from, to do
|
||||
res[#res + 1] = list[i]
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- JS-ish array splice
|
||||
function splice(
|
||||
list, -- to modify
|
||||
start, -- index (inclusive) for where to start modifying the array (defaults to after the last element)
|
||||
delete_count, -- how many elements to remove (defaults to `0`)
|
||||
... -- elements to insert after `start`
|
||||
)
|
||||
start, delete_count = start or (#list + 1), delete_count or 0
|
||||
if start < 0 then
|
||||
start = start + #list + 1
|
||||
end
|
||||
|
||||
local add_count = select("#", ...)
|
||||
local shift = add_count - delete_count
|
||||
if shift > 0 then -- shift up
|
||||
for i = #list, start + delete_count, -1 do
|
||||
list[i + shift] = list[i]
|
||||
end
|
||||
elseif shift < 0 then -- shift down
|
||||
for i = start, #list do
|
||||
list[i] = list[i - shift]
|
||||
end
|
||||
end
|
||||
|
||||
-- Add elements
|
||||
for i = 1, add_count do
|
||||
list[start + i - 1] = select(i, ...)
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
-- Equivalent to to_list[to], ..., to_list[to + count] = from_list[from], ..., from_list[from + count]
|
||||
function move(from_list, from, to, count, to_list)
|
||||
from, to, count, to_list = from or 1, to or 1, count or #from_list, to_list or from_list
|
||||
if to_list ~= from_list or to < from then
|
||||
for i = 0, count do
|
||||
to_list[to + i] = from_list[from + i]
|
||||
end
|
||||
else -- iterate in reverse order
|
||||
for i = count, 0, -1 do
|
||||
to_list[to + i] = from_list[from + i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
386
mods/modlib/tex.lua
Normal file
386
mods/modlib/tex.lua
Normal file
|
@ -0,0 +1,386 @@
|
|||
--[[
|
||||
This file does not follow the usual conventions;
|
||||
it duplicates some code for performance reasons.
|
||||
|
||||
In particular, use of `modlib.minetest.colorspec` is avoided.
|
||||
|
||||
Most methods operate *in-place* (imperative method names)
|
||||
rather than returning a modified copy (past participle method names).
|
||||
|
||||
Outside-facing methods consistently use 1-based indexing; indices are inclusive.
|
||||
]]
|
||||
|
||||
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
|
||||
local function round(x) return floor(x + 0.5) end
|
||||
local function clamp(x, mn, mx) return max(min(x, mx), mn) end
|
||||
|
||||
-- ARGB handling utilities
|
||||
|
||||
local function unpack_argb(argb)
|
||||
return floor(argb / 0x1000000),
|
||||
floor(argb / 0x10000) % 0x100,
|
||||
floor(argb / 0x100) % 0x100,
|
||||
argb % 0x100
|
||||
end
|
||||
|
||||
local function pack_argb(a, r, g, b)
|
||||
local argb = (((a * 0x100 + r) * 0x100) + g) * 0x100 + b
|
||||
return argb
|
||||
end
|
||||
|
||||
local function round_argb(a, r, g, b)
|
||||
return round(a), round(r), round(g), round(b)
|
||||
end
|
||||
|
||||
local function scale_0_1_argb(a, r, g, b)
|
||||
return a / 255, r / 255, g / 255, b / 255
|
||||
end
|
||||
|
||||
local function scale_0_255_argb(a, r, g, b)
|
||||
return a * 255, r * 255, g * 255, b * 255
|
||||
end
|
||||
|
||||
local tex = {}
|
||||
local metatable = {__index = tex}
|
||||
|
||||
function metatable:__eq(other)
|
||||
if self.w ~= other.w or self.h ~= other.h then return false end
|
||||
for i = 1, #self do if self[i] ~= other[i] then return false end end
|
||||
return true
|
||||
end
|
||||
|
||||
function tex:new()
|
||||
return setmetatable(self, metatable)
|
||||
end
|
||||
|
||||
function tex.filled(w, h, argb)
|
||||
local self = {w = w, h = h}
|
||||
for i = 1, w*h do
|
||||
self[i] = argb
|
||||
end
|
||||
return tex.new(self)
|
||||
end
|
||||
|
||||
function tex:copy()
|
||||
local copy = {w = self.w, h = self.h}
|
||||
for i = 1, #self do
|
||||
copy[i] = self[i]
|
||||
end
|
||||
return tex.new(copy)
|
||||
end
|
||||
|
||||
-- Reading & writing
|
||||
|
||||
function tex.read_png_string(str)
|
||||
local stream = modlib.text.inputstream(str)
|
||||
local png = modlib.minetest.decode_png(stream)
|
||||
assert(stream:read(1) == nil, "eof expected")
|
||||
modlib.minetest.convert_png_to_argb8(png)
|
||||
png.data.w, png.data.h = png.width, png.height
|
||||
return tex.new(png.data)
|
||||
end
|
||||
|
||||
function tex.read_png(path)
|
||||
local png
|
||||
modlib.file.with_open(path, "rb", function(f)
|
||||
png = modlib.minetest.decode_png(f)
|
||||
assert(f:read(1) == nil, "eof expected")
|
||||
end)
|
||||
modlib.minetest.convert_png_to_argb8(png)
|
||||
png.data.w, png.data.h = png.width, png.height
|
||||
return tex.new(png.data)
|
||||
end
|
||||
|
||||
function tex:write_png_string()
|
||||
return modlib.minetest.encode_png(self.w, self.h, self)
|
||||
end
|
||||
|
||||
function tex:write_png(path)
|
||||
modlib.file.write_binary(path, self:write_png_string())
|
||||
end
|
||||
|
||||
function tex:fill(sx, sy, argb)
|
||||
local w, h = self.w, self.h
|
||||
for y = sy, h do
|
||||
local i = (y - 1) * w + sx
|
||||
for _ = sx, w do
|
||||
self[i] = argb
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function tex:in_bounds(x, y)
|
||||
return x >= 1 and y >= 1 and x <= self.w and y <= self.h
|
||||
end
|
||||
|
||||
function tex:get_argb_packed(x, y)
|
||||
return self[(y - 1) * self.w + x]
|
||||
end
|
||||
|
||||
function tex:get_argb(x, y)
|
||||
return unpack_argb(self[(y - 1) * self.w + x])
|
||||
end
|
||||
|
||||
function tex:set_argb_packed(x, y, argb)
|
||||
self[(y - 1) * self.w + x] = argb
|
||||
end
|
||||
|
||||
function tex:set_argb(x, y, a, r, g, b)
|
||||
self[(y - 1) * self.w + x] = pack_argb(a, r, g, b)
|
||||
end
|
||||
|
||||
function tex:map_argb(func)
|
||||
for i = 1, #self do
|
||||
self[i] = pack_argb(func(unpack_argb(self[i])))
|
||||
end
|
||||
end
|
||||
|
||||
local function blit(s, x, y, t, o)
|
||||
local sw, sh = s.w, s.h
|
||||
local tw, th = t.w, t.h
|
||||
-- Restrict to overlapping region
|
||||
x, y = clamp(x, 1, sw), clamp(y, 1, sh)
|
||||
local min_tx, min_ty = max(1, 2 - x), max(1, 2 - y)
|
||||
local max_tx, max_ty = min(tw, sw - x + 1), min(th, sh - y + 1)
|
||||
for ty = min_ty, max_ty do
|
||||
local ti, si = (ty - 1) * tw, (y + ty - 2) * sw + x - 1
|
||||
for _ = min_tx, max_tx do
|
||||
ti, si = ti + 1, si + 1
|
||||
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(s[si]))
|
||||
if sa == 1 or not o then -- HACK because of dirty `[cracko`
|
||||
local ta, tr, tg, tb = scale_0_1_argb(unpack_argb(t[ti]))
|
||||
-- "`t` over `s`" (Porter-Duff-Algorithm)
|
||||
local sata = sa * (1 - ta)
|
||||
local ra = ta + sata
|
||||
assert(ra > 0 or (sa == 0 and ta == 0))
|
||||
if ra > 0 then
|
||||
s[si] = pack_argb(round_argb(scale_0_255_argb(
|
||||
ra,
|
||||
(ta * tr + sata * sr) / ra,
|
||||
(ta * tg + sata * sg) / ra,
|
||||
(ta * tb + sata * sb) / ra
|
||||
)))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Blitting with proper alpha blending.
|
||||
function tex.blit(s, x, y, t)
|
||||
return blit(s, x, y, t, false)
|
||||
end
|
||||
|
||||
-- Blit, but only on fully opaque base pixels. Only `[cracko` uses this.
|
||||
function tex.blito(s, x, y, t)
|
||||
return blit(s, x, y, t, true)
|
||||
end
|
||||
|
||||
function tex.combine_argb(s, t, cf)
|
||||
assert(#s == #t)
|
||||
for i = 1, #s do
|
||||
s[i] = cf(s[i], t[i])
|
||||
end
|
||||
end
|
||||
|
||||
-- See https://github.com/TheAlgorithms/Lua/blob/162c4c59f5514c6115e0add8a2b4d56afd6d3204/src/bit/uint53/and.lua
|
||||
-- TODO (?) optimize fallback band using caching, move somewhere else
|
||||
local band = bit and bit.band or function(n, m)
|
||||
local res = 0
|
||||
local bit = 1
|
||||
while n * m ~= 0 do -- while both are nonzero
|
||||
local n_bit, m_bit = n % 2, m % 2 -- extract LSB
|
||||
res = res + (n_bit * m_bit) * bit -- add AND of LSBs
|
||||
n, m = (n - n_bit) / 2, (m - m_bit) / 2 -- remove LSB from n & m
|
||||
bit = bit * 2 -- next bit
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function tex.band(s, t)
|
||||
return s:combine_argb(t, band)
|
||||
end
|
||||
|
||||
function tex.hardlight_blend(s, t)
|
||||
return s:combine_argb(t, function(sargb, targb)
|
||||
local sa, sr, sg, sb = scale_0_1_argb(unpack_argb(sargb))
|
||||
local _, tr, tg, tb = scale_0_1_argb(unpack_argb(targb))
|
||||
return pack_argb(round_argb(scale_0_255_argb(
|
||||
sa,
|
||||
sr < 0.5 and 2*sr*tr or 1 - 2*(1-sr)*(1-tr),
|
||||
sr < 0.5 and 2*sg*tg or 1 - 2*(1-sg)*(1-tg),
|
||||
sr < 0.5 and 2*sb*tb or 1 - 2*(1-sb)*(1-tb)
|
||||
)))
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:brighten()
|
||||
return self:map_argb(function(a, r, g, b)
|
||||
return round_argb((255 + a) / 2, (255 + r) / 2, (255 + g) / 2, (255 + b) / 2)
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:noalpha()
|
||||
for i = 1, #self do
|
||||
self[i] = 0xFF000000 + self[i] % 0x1000000
|
||||
end
|
||||
end
|
||||
|
||||
function tex:makealpha(r, g, b)
|
||||
local mrgb = r * 0x10000 + g * 0x100 + b
|
||||
for i = 1, #self do
|
||||
local rgb = self[i] % 0x1000000
|
||||
if rgb == mrgb then
|
||||
self[i] = rgb
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function tex:opacity(factor)
|
||||
for i = 1, #self do
|
||||
self[i] = round(floor(self[i] / 0x1000000) * factor) * 0x1000000 + self[i] % 0x1000000
|
||||
end
|
||||
end
|
||||
|
||||
function tex:invert(ir, ig, ib, ia)
|
||||
return self:map_argb(function(a, r, g, b)
|
||||
if ia then a = 255 - a end
|
||||
if ir then r = 255 - r end
|
||||
if ig then g = 255 - g end
|
||||
if ib then b = 255 - b end
|
||||
return a, r, g, b
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:multiply_rgb(r, g, b)
|
||||
return self:map_argb(function(sa, sr, sg, sb)
|
||||
return round_argb(sa, r * sr, g * sg, b * sb)
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:screen_blend_rgb(r, g, b)
|
||||
return self:map_argb(function(sa, sr, sg, sb)
|
||||
return round_argb(sa,
|
||||
255 - ((255 - sr) * (255 - r)) / 255,
|
||||
255 - ((255 - sg) * (255 - g)) / 255,
|
||||
255 - ((255 - sb) * (255 - b)) / 255)
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:colorize(cr, cg, cb, ratio)
|
||||
return self:map_argb(function(a, r, g, b)
|
||||
local rat = ratio == "alpha" and a or ratio
|
||||
return round_argb(
|
||||
a,
|
||||
rat * r + (1 - rat) * cr,
|
||||
rat * g + (1 - rat) * cg,
|
||||
rat * b + (1 - rat) * cb
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
function tex:crop(from_x, from_y, to_x, to_y)
|
||||
local w = self.w
|
||||
local i = 1
|
||||
for y = from_y, to_y do
|
||||
local j = (y - 1) * w + from_x
|
||||
for _ = from_x, to_x do
|
||||
self[i] = self[j]
|
||||
i, j = i + 1, j + 1
|
||||
end
|
||||
end
|
||||
-- Remove remaining pixels
|
||||
for j = i, #self do self[j] = nil end
|
||||
self.w, self.h = to_x - from_x + 1, to_y - from_y + 1
|
||||
end
|
||||
|
||||
function tex:flip_x()
|
||||
for y = 1, self.h do
|
||||
local i = (y - 1) * self.w
|
||||
local j = i + self.w + 1
|
||||
while i < j do
|
||||
i, j = i + 1, j - 1
|
||||
self[i], self[j] = self[j], self[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function tex:flip_y()
|
||||
for x = 1, self.w do
|
||||
local i, j = x, (self.h - 1) * self.w + x
|
||||
while i < j do
|
||||
i, j = i + self.w, j - self.w
|
||||
self[i], self[j] = self[j], self[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--> copy of the texture, rotated 90 degrees clockwise
|
||||
function tex:rotated_90()
|
||||
local w, h = self.w, self.h
|
||||
local t = {w = h, h = w}
|
||||
local i = 0
|
||||
for y = 1, w do
|
||||
for x = 1, h do
|
||||
i = i + 1
|
||||
t[i] = self[(h-x)*w + y]
|
||||
end
|
||||
end
|
||||
t = tex.new(t)
|
||||
return t
|
||||
end
|
||||
|
||||
-- Uses box sampling. Hard to optimize.
|
||||
-- TODO (...) interpolate box samples; match what Minetest does
|
||||
--> copy of `self` resized to `w` x `h`
|
||||
function tex:resized(w, h)
|
||||
--! This function works with 0-based indices.
|
||||
local sw, sh = self.w, self.h
|
||||
local fx, fy = sw / w, sh / h
|
||||
local t = {w = w, h = h}
|
||||
local i = 0
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
-- Sample the area
|
||||
local vy_from = y * fy
|
||||
local vy_to = vy_from + fy
|
||||
local vx_from = x * fx
|
||||
local vx_to = vx_from + fx
|
||||
|
||||
local a, r, g, b = 0, 0, 0, 0
|
||||
local pf_sum = 0
|
||||
|
||||
local function blend(sx, sy, pf)
|
||||
if pf <= 0 then return end
|
||||
local sa, sr, sg, sb = unpack_argb(self[sy * sw + sx + 1])
|
||||
pf_sum = pf_sum + pf -- TODO (?) eliminate `pf_sum`
|
||||
sa = sa * pf
|
||||
a = a + sa
|
||||
r, g, b = r + sa * sr, g + sa * sg, b + sa * sb
|
||||
end
|
||||
|
||||
local function srow(sy, pf)
|
||||
if pf <= 0 then return end
|
||||
local sx_from, sx_to = ceil(vx_from), floor(vx_to)
|
||||
for sx = sx_from, sx_to - 1 do blend(sx, sy, pf) end -- whole pixels
|
||||
-- Pixels at edges
|
||||
blend(floor(vx_from), sy, pf * (sx_from - vx_from))
|
||||
blend(floor(vx_to), sy, pf * (vx_to - sx_to))
|
||||
end
|
||||
|
||||
local sy_from, sy_to = ceil(vy_from), floor(vy_to)
|
||||
for sy = sy_from, sy_to - 1 do srow(sy, 1) end -- whole pixels
|
||||
-- Pixels at edges
|
||||
srow(floor(vy_from), sy_from - vy_from)
|
||||
srow(floor(vy_to), vy_to - sy_to)
|
||||
if a > 0 then r, g, b = r / a, g / a, b / a end
|
||||
assert(pf_sum > 0)
|
||||
i = i + 1
|
||||
t[i] = pack_argb(round_argb(a / pf_sum, r, g, b))
|
||||
end
|
||||
end
|
||||
return tex.new(t)
|
||||
end
|
||||
|
||||
return tex
|
189
mods/modlib/text.lua
Normal file
189
mods/modlib/text.lua
Normal file
|
@ -0,0 +1,189 @@
|
|||
-- Localize globals
|
||||
local assert, math, modlib, setmetatable, string, table
|
||||
= assert, math, modlib, setmetatable, string, table
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
function upper_first(text) return text:sub(1, 1):upper() .. text:sub(2) end
|
||||
|
||||
function lower_first(text) return text:sub(1, 1):lower() .. text:sub(2) end
|
||||
|
||||
function starts_with(text, prefix) return text:sub(1, #prefix) == prefix end
|
||||
|
||||
function ends_with(text, suffix) return text:sub(-#suffix) == suffix end
|
||||
|
||||
function contains(str, substr, plain)
|
||||
return not not str:find(substr, 1, plain == nil and true or plain)
|
||||
end
|
||||
|
||||
function trim_spacing(text)
|
||||
return text:match"^%s*(.-)%s*$"
|
||||
end
|
||||
|
||||
local inputstream_metatable = {
|
||||
__index = {
|
||||
read = function(self, count)
|
||||
local cursor = self.cursor + 1
|
||||
self.cursor = self.cursor + count
|
||||
local text = self.text:sub(cursor, self.cursor)
|
||||
return text ~= "" and text or nil
|
||||
end,
|
||||
seek = function(self) return self.cursor end
|
||||
}
|
||||
}
|
||||
--> inputstream "handle"; only allows reading characters (given a count), seeking does not accept any arguments
|
||||
function inputstream(text)
|
||||
return setmetatable({text = text, cursor = 0}, inputstream_metatable)
|
||||
end
|
||||
|
||||
function hexdump(text)
|
||||
local dump = {}
|
||||
for index = 1, text:len() do
|
||||
dump[index] = ("%02X"):format(text:byte(index))
|
||||
end
|
||||
return table.concat(dump)
|
||||
end
|
||||
|
||||
function spliterator(str, delim, plain)
|
||||
assert(delim ~= "")
|
||||
local last_delim_end = 0
|
||||
|
||||
-- Iterator of possibly empty substrings between two matches of the delimiter
|
||||
-- To exclude empty strings, filter the iterator or use `:gmatch"[...]+"` instead
|
||||
return function()
|
||||
if not last_delim_end then
|
||||
return
|
||||
end
|
||||
|
||||
local delim_start, delim_end = str:find(delim, last_delim_end + 1, plain)
|
||||
local substr
|
||||
if delim_start then
|
||||
substr = str:sub(last_delim_end + 1, delim_start - 1)
|
||||
else
|
||||
substr = str:sub(last_delim_end + 1)
|
||||
end
|
||||
last_delim_end = delim_end
|
||||
return substr
|
||||
end
|
||||
end
|
||||
|
||||
function split(text, delimiter, limit, plain)
|
||||
limit = limit or math.huge
|
||||
local parts = {}
|
||||
local occurences = 1
|
||||
local last_index = 1
|
||||
local index = string.find(text, delimiter, 1, plain)
|
||||
while index and occurences < limit do
|
||||
table.insert(parts, string.sub(text, last_index, index - 1))
|
||||
last_index = index + string.len(delimiter)
|
||||
index = string.find(text, delimiter, index + string.len(delimiter), plain)
|
||||
occurences = occurences + 1
|
||||
end
|
||||
table.insert(parts, string.sub(text, last_index))
|
||||
return parts
|
||||
end
|
||||
|
||||
function split_without_limit(text, delimiter, plain)
|
||||
return split(text, delimiter, nil, plain)
|
||||
end
|
||||
|
||||
split_unlimited = split_without_limit
|
||||
|
||||
--! Does not support Macintosh pre-OSX CR-only line endings
|
||||
--! Deprecated in favor of the `lines` iterator below
|
||||
function split_lines(text, limit)
|
||||
return modlib.text.split(text, "\r?\n", limit, true)
|
||||
end
|
||||
|
||||
-- When reading from a file, directly use `io.lines` instead
|
||||
-- Lines are possibly empty substrings separated by CR, LF or CRLF
|
||||
-- A trailing linefeed is ignored
|
||||
function lines(str)
|
||||
local line_start = 1
|
||||
-- Line iterator
|
||||
return function()
|
||||
if line_start > #str then
|
||||
return
|
||||
end
|
||||
local linefeed_start, _, linefeed = str:find("([\r\n][\r\n]?)", line_start)
|
||||
local line
|
||||
if linefeed_start then
|
||||
line = str:sub(line_start, linefeed_start - 1)
|
||||
line_start = linefeed_start + (linefeed == "\r\n" and 2 or 1)
|
||||
else
|
||||
line = str:sub(line_start)
|
||||
line_start = #str + 1
|
||||
end
|
||||
return line
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local zero = string.byte"0"
|
||||
local nine = string.byte"9"
|
||||
local letter_a = string.byte"A"
|
||||
local letter_f = string.byte"F"
|
||||
|
||||
function is_hexadecimal(byte)
|
||||
return byte >= zero and byte <= nine or byte >= letter_a and byte <= letter_f
|
||||
end
|
||||
|
||||
magic_charset = "[" .. ("%^$+-*?.[]()"):gsub(".", "%%%1") .. "]"
|
||||
|
||||
function escape_pattern(text)
|
||||
return text:gsub(magic_charset, "%%%1")
|
||||
end
|
||||
|
||||
escape_magic_chars = escape_pattern
|
||||
|
||||
local keywords = modlib.table.set{"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}
|
||||
keywords["goto"] = true -- Lua 5.2 (LuaJIT) support
|
||||
|
||||
function is_keyword(text)
|
||||
return keywords[text]
|
||||
end
|
||||
|
||||
function is_identifier(text)
|
||||
return (not keywords[text]) and text:match"^[A-Za-z_][A-Za-z%d_]*$"
|
||||
end
|
||||
|
||||
local function inextchar(text, i)
|
||||
if i >= #text then return end
|
||||
i = i + 1
|
||||
return i, text:sub(i, i)
|
||||
end
|
||||
|
||||
function ichars(text, start)
|
||||
-- Iterator over `index, character`
|
||||
return inextchar, text, (start or 1) - 1
|
||||
end
|
||||
|
||||
local function inextbyte(text, i)
|
||||
if i >= #text then return end
|
||||
i = i + 1
|
||||
return i, text:byte(i, i)
|
||||
end
|
||||
|
||||
function ibytes(text, start)
|
||||
-- Iterator over `index, byte`
|
||||
return inextbyte, text, (start or 1) - 1
|
||||
end
|
||||
|
||||
local function _random_bytes(count)
|
||||
if count == 0 then return end
|
||||
return math.random(0, 0xFF), _random_bytes(count - 1)
|
||||
end
|
||||
|
||||
function random_bytes(
|
||||
-- number, how many random bytes the string should have, defaults to 1
|
||||
-- limited by stack size
|
||||
count
|
||||
)
|
||||
count = count or 1
|
||||
return string.char(_random_bytes(count))
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
133
mods/modlib/trie.lua
Normal file
133
mods/modlib/trie.lua
Normal file
|
@ -0,0 +1,133 @@
|
|||
-- Localize globals
|
||||
local math, next, pairs, setmetatable, string, table, unpack = math, next, pairs, setmetatable, string, table, unpack
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local metatable = {__index = _ENV}
|
||||
|
||||
-- Setting the metatable is fine as it does not contain single-character keys.
|
||||
-- TODO (?) encapsulate in "root" field for better code quality?
|
||||
function new(table) return setmetatable(table or {}, metatable) end
|
||||
|
||||
function insert(self, word, value, overwrite)
|
||||
for i = 1, word:len() do
|
||||
local char = word:sub(i, i)
|
||||
self[char] = self[char] or {}
|
||||
self = self[char]
|
||||
end
|
||||
local previous_value = self.value
|
||||
if not previous_value or overwrite then self.value = value or true end
|
||||
return previous_value
|
||||
end
|
||||
|
||||
function remove(self, word)
|
||||
local branch, character = self, word:sub(1, 1)
|
||||
for i = 1, word:len() - 1 do
|
||||
local char = word:sub(i, i)
|
||||
if not self[char] then return end
|
||||
if self[char].value or next(self, next(self)) then
|
||||
branch = self
|
||||
character = char
|
||||
end
|
||||
self = self[char]
|
||||
end
|
||||
local char = word:sub(word:len())
|
||||
if not self[char] then return end
|
||||
self = self[char]
|
||||
local previous_value = self.value
|
||||
self.value = nil
|
||||
if branch and not next(self) then branch[character] = nil end
|
||||
return previous_value
|
||||
end
|
||||
|
||||
--> value if found
|
||||
--> nil else
|
||||
function get(self, word)
|
||||
for i = 1, word:len() do
|
||||
local char = word:sub(i, i)
|
||||
self = self[char]
|
||||
if not self then return end
|
||||
end
|
||||
return self.value
|
||||
end
|
||||
|
||||
function suggestion(self, remainder)
|
||||
local until_now = {}
|
||||
local subtries = { [self] = until_now }
|
||||
local suggestion, value
|
||||
while next(subtries) do
|
||||
local new_subtries = {}
|
||||
local leaves = {}
|
||||
for trie, word in pairs(subtries) do
|
||||
if trie.value then table.insert(leaves, { word = word, value = trie.value }) end
|
||||
end
|
||||
if #leaves > 0 then
|
||||
if remainder then
|
||||
local best_leaves = {}
|
||||
local best_score = 0
|
||||
for _, leaf in pairs(leaves) do
|
||||
local score = 0
|
||||
for i = 1, math.min(#leaf.word, string.len(remainder)) do
|
||||
-- calculate intersection
|
||||
if remainder:sub(i, i) == leaf.word[i] then score = score + 1 end
|
||||
end
|
||||
if score == best_score then table.insert(best_leaves, leaf)
|
||||
elseif score > best_score then best_leaves = { leaf } end
|
||||
end
|
||||
leaves = best_leaves
|
||||
end
|
||||
-- TODO select best instead of random
|
||||
local leaf = leaves[math.random(1, #leaves)]
|
||||
suggestion, value = table.concat(leaf.word), leaf.value
|
||||
break
|
||||
end
|
||||
for trie, word in pairs(subtries) do
|
||||
for char, subtrie in pairs(trie) do
|
||||
local word = { unpack(word) }
|
||||
table.insert(word, char)
|
||||
new_subtries[subtrie] = word
|
||||
end
|
||||
end
|
||||
subtries = new_subtries
|
||||
end
|
||||
return suggestion, value
|
||||
end
|
||||
|
||||
--> value if found
|
||||
--> nil, suggestion, value of suggestion else
|
||||
function search(self, word)
|
||||
for i = 1, word:len() do
|
||||
local char = word:sub(i, i)
|
||||
if not self[char] then
|
||||
local until_now = word:sub(1, i - 1)
|
||||
local suggestion, value = suggestion(self, word:sub(i))
|
||||
return nil, until_now .. suggestion, value
|
||||
end
|
||||
self = self[char]
|
||||
end
|
||||
local value = self.value
|
||||
if value then return value end
|
||||
local until_now = word
|
||||
local suggestion, value = suggestion(self)
|
||||
return nil, until_now .. suggestion, value
|
||||
end
|
||||
|
||||
function find_longest(self, query, query_offset)
|
||||
local leaf_pos = query_offset
|
||||
local last_leaf
|
||||
for i = query_offset, query:len() do
|
||||
local char = query:sub(i, i)
|
||||
self = self[char]
|
||||
if not self then break
|
||||
elseif self.value then
|
||||
last_leaf = self.value
|
||||
leaf_pos = i
|
||||
end
|
||||
end
|
||||
return last_leaf, leaf_pos
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
102
mods/modlib/utf8.lua
Normal file
102
mods/modlib/utf8.lua
Normal file
|
@ -0,0 +1,102 @@
|
|||
local assert, error, select, string_char, table_concat
|
||||
= assert, error, select, string.char, table.concat
|
||||
|
||||
local utf8 = {}
|
||||
|
||||
-- Overly permissive pattern that greedily matches a single UTF-8 codepoint
|
||||
utf8.charpattern = "[%z-\127\194-\253][\128-\191]*"
|
||||
|
||||
function utf8.is_valid_codepoint(codepoint)
|
||||
-- Must be in bounds & must not be a surrogate
|
||||
return codepoint <= 0x10FFFF and (codepoint < 0xD800 or codepoint > 0xDFFF)
|
||||
end
|
||||
|
||||
local function utf8_bytes(codepoint)
|
||||
if codepoint <= 0x007F then
|
||||
return codepoint
|
||||
end if codepoint <= 0x7FF then
|
||||
local payload_2 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_2) / 0x40
|
||||
return 0xC0 + codepoint, 0x80 + payload_2
|
||||
end if codepoint <= 0xFFFF then
|
||||
local payload_3 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_3) / 0x40
|
||||
local payload_2 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_2) / 0x40
|
||||
return 0xE0 + codepoint, 0x80 + payload_2, 0x80 + payload_3
|
||||
end if codepoint <= 0x10FFFF then
|
||||
local payload_4 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_4) / 0x40
|
||||
local payload_3 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_3) / 0x40
|
||||
local payload_2 = codepoint % 0x40
|
||||
codepoint = (codepoint - payload_2) / 0x40
|
||||
return 0xF0 + codepoint, 0x80 + payload_2, 0x80 + payload_3, 0x80 + payload_4
|
||||
end error"codepoint out of range"
|
||||
end
|
||||
|
||||
function utf8.char(...)
|
||||
local n_args = select("#", ...)
|
||||
if n_args == 0 then
|
||||
return
|
||||
end if n_args == 1 then
|
||||
return string_char(utf8_bytes(...))
|
||||
end
|
||||
local chars = {}
|
||||
for i = 1, n_args do
|
||||
chars[i] = string_char(utf8_bytes(select(i, ...)))
|
||||
end
|
||||
return table_concat(chars)
|
||||
end
|
||||
|
||||
local function utf8_next_codepoint(str, i)
|
||||
local first_byte = str:byte(i)
|
||||
if first_byte < 0x80 then
|
||||
return i + 1, first_byte
|
||||
end
|
||||
|
||||
local len, head_bits
|
||||
if first_byte >= 0xC0 and first_byte <= 0xDF then -- 110_00000 to 110_11111
|
||||
len, head_bits = 2, first_byte % 0x20 -- last 5 bits
|
||||
elseif first_byte >= 0xE0 and first_byte <= 0xEF then -- 1110_0000 to 1110_1111
|
||||
len, head_bits = 3, first_byte % 0x10 -- last 4 bits
|
||||
elseif first_byte >= 0xF0 and first_byte <= 0xF7 then -- 11110_000 to 11110_111
|
||||
len, head_bits = 4, first_byte % 0x8 -- last 3 bits
|
||||
else error"invalid UTF-8" end
|
||||
|
||||
local codepoint = 0
|
||||
local pow = 1
|
||||
for j = i + len - 1, i + 1, -1 do
|
||||
local byte = assert(str:byte(j), "invalid UTF-8")
|
||||
local val_bits = byte % 0x40 -- extract last 6 bits xxxxxx from 10xxxxxx
|
||||
assert(byte - val_bits == 0x80) -- assert that first two bits are 10
|
||||
codepoint = codepoint + val_bits * pow
|
||||
pow = pow * 0x40
|
||||
end
|
||||
return i + len, codepoint + head_bits * pow
|
||||
end
|
||||
|
||||
function utf8.codepoint(str, i, j)
|
||||
i, j = i or 1, j or #str
|
||||
if i > j then return end
|
||||
local codepoint
|
||||
i, codepoint = utf8_next_codepoint(str, i)
|
||||
assert(i - j <= 1, "invalid UTF-8")
|
||||
return codepoint, utf8.codepoint(str, i)
|
||||
end
|
||||
|
||||
-- Iterator to loop over the UTF-8 characters as `index, codepoint`
|
||||
function utf8.codes(text, i)
|
||||
i = i or 1
|
||||
return function()
|
||||
if i > #text then
|
||||
return
|
||||
end
|
||||
local prev_index = i
|
||||
local codepoint
|
||||
i, codepoint = utf8_next_codepoint(text, i)
|
||||
return prev_index, codepoint
|
||||
end
|
||||
end
|
||||
|
||||
return utf8
|
64
mods/modlib/vararg.lua
Normal file
64
mods/modlib/vararg.lua
Normal file
|
@ -0,0 +1,64 @@
|
|||
local select, setmetatable, unpack = select, setmetatable, unpack
|
||||
|
||||
local vararg = {}
|
||||
|
||||
function vararg.aggregate(binary_func, initial, ...)
|
||||
local total = initial
|
||||
for i = 1, select("#", ...) do
|
||||
total = binary_func(total, select(i, ...))
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
local metatable = {__index = {}}
|
||||
|
||||
function vararg.pack(...)
|
||||
return setmetatable({["#"] = select("#", ...); ...}, metatable)
|
||||
end
|
||||
|
||||
local va = metatable.__index
|
||||
|
||||
function va:unpack()
|
||||
return unpack(self, 1, self["#"])
|
||||
end
|
||||
|
||||
function va:select(n)
|
||||
if n > self["#"] then return end
|
||||
return self[n]
|
||||
end
|
||||
|
||||
local function inext(self, i)
|
||||
i = i + 1
|
||||
if i > self["#"] then return end
|
||||
return i, self[i]
|
||||
end
|
||||
|
||||
function va:ipairs()
|
||||
return inext, self, 0
|
||||
end
|
||||
|
||||
function va:concat(other)
|
||||
local self_len, other_len = self["#"], other["#"]
|
||||
local res = {["#"] = self_len + other_len}
|
||||
for i = 1, self_len do
|
||||
res[i] = self[i]
|
||||
end
|
||||
for i = 1, other_len do
|
||||
res[self_len + i] = other[i]
|
||||
end
|
||||
return setmetatable(res, metatable)
|
||||
end
|
||||
metatable.__concat = va.concat
|
||||
|
||||
function va:equals(other)
|
||||
if self["#"] ~= other["#"] then return false end
|
||||
for i = 1, self["#"] do if self[i] ~= other[i] then return false end end
|
||||
return true
|
||||
end
|
||||
metatable.__eq = va.equals
|
||||
|
||||
function va:aggregate(binary_func, initial)
|
||||
return vararg.aggregate(binary_func, initial, self:unpack())
|
||||
end
|
||||
|
||||
return vararg
|
274
mods/modlib/vector.lua
Normal file
274
mods/modlib/vector.lua
Normal file
|
@ -0,0 +1,274 @@
|
|||
-- Localize globals
|
||||
local assert, math, pairs, rawget, rawset, setmetatable, unpack, vector = assert, math, pairs, rawget, rawset, setmetatable, unpack, vector
|
||||
|
||||
-- Set environment
|
||||
local _ENV = {}
|
||||
setfenv(1, _ENV)
|
||||
|
||||
local mt_vector = vector
|
||||
|
||||
index_aliases = {
|
||||
x = 1,
|
||||
y = 2,
|
||||
z = 3,
|
||||
w = 4;
|
||||
"x", "y", "z", "w";
|
||||
}
|
||||
|
||||
metatable = {
|
||||
__index = function(table, key)
|
||||
local index = index_aliases[key]
|
||||
if index ~= nil then
|
||||
return rawget(table, index)
|
||||
end
|
||||
return _ENV[key]
|
||||
end,
|
||||
__newindex = function(table, key, value)
|
||||
-- TODO
|
||||
local index = index_aliases[key]
|
||||
if index ~= nil then
|
||||
return rawset(table, index, value)
|
||||
end
|
||||
return rawset(table, key, value)
|
||||
end
|
||||
}
|
||||
|
||||
function new(v)
|
||||
return setmetatable(v, metatable)
|
||||
end
|
||||
|
||||
function zeros(n)
|
||||
local v = {}
|
||||
for i = 1, n do
|
||||
v[i] = 0
|
||||
end
|
||||
return new(v)
|
||||
end
|
||||
function from_xyzw(v)
|
||||
return new{v.x, v.y, v.z, v.w}
|
||||
end
|
||||
|
||||
function from_minetest(v)
|
||||
return new{v.x, v.y, v.z}
|
||||
end
|
||||
|
||||
function to_xyzw(v)
|
||||
return {x = v[1], y = v[2], z = v[3], w = v[4]}
|
||||
end
|
||||
|
||||
--+ not necessarily required, as Minetest respects the metatable
|
||||
function to_minetest(v)
|
||||
return mt_vector.new(unpack(v))
|
||||
end
|
||||
|
||||
function equals(v, w)
|
||||
for k, v in pairs(v) do
|
||||
if v ~= w[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
metatable.__eq = equals
|
||||
|
||||
function combine(v, w, f)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, w[key])
|
||||
end
|
||||
return new(new_vector)
|
||||
end
|
||||
|
||||
function apply(v, f, ...)
|
||||
local new_vector = {}
|
||||
for key, value in pairs(v) do
|
||||
new_vector[key] = f(value, ...)
|
||||
end
|
||||
return new(new_vector)
|
||||
end
|
||||
|
||||
function combinator(f)
|
||||
return function(v, w)
|
||||
return combine(v, w, f)
|
||||
end, function(v, ...)
|
||||
return apply(v, f, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function invert(v)
|
||||
local res = {}
|
||||
for key, value in pairs(v) do
|
||||
res[key] = -value
|
||||
end
|
||||
return new(res)
|
||||
end
|
||||
|
||||
add, add_scalar = combinator(function(v, w) return v + w end)
|
||||
subtract, subtract_scalar = combinator(function(v, w) return v - w end)
|
||||
multiply, multiply_scalar = combinator(function(v, w) return v * w end)
|
||||
divide, divide_scalar = combinator(function(v, w) return v / w end)
|
||||
pow, pow_scalar = combinator(function(v, w) return v ^ w end)
|
||||
|
||||
metatable.__add = add
|
||||
metatable.__unm = invert
|
||||
metatable.__sub = subtract
|
||||
metatable.__mul = multiply
|
||||
metatable.__div = divide
|
||||
|
||||
--+ linear interpolation
|
||||
--: ratio number from 0 (all the first vector) to 1 (all the second vector)
|
||||
function interpolate(v, w, ratio)
|
||||
return add(multiply_scalar(v, 1 - ratio), multiply_scalar(w, ratio))
|
||||
end
|
||||
|
||||
function norm(v)
|
||||
local sum = 0
|
||||
for _, value in pairs(v) do
|
||||
sum = sum + value ^ 2
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
function length(v)
|
||||
return math.sqrt(norm(v))
|
||||
end
|
||||
|
||||
-- Minor code duplication for the sake of performance
|
||||
function distance(v, w)
|
||||
local sum = 0
|
||||
for key, value in pairs(v) do
|
||||
sum = sum + (value - w[key]) ^ 2
|
||||
end
|
||||
return math.sqrt(sum)
|
||||
end
|
||||
|
||||
function normalize(v)
|
||||
return divide_scalar(v, length(v))
|
||||
end
|
||||
|
||||
function normalize_zero(v)
|
||||
local len = length(v)
|
||||
if len == 0 then
|
||||
-- Return a zeroed vector with the same keys
|
||||
local zeroed = {}
|
||||
for k in pairs(v) do
|
||||
zeroed[k] = 0
|
||||
end
|
||||
return new(zeroed)
|
||||
end
|
||||
return divide_scalar(v, len)
|
||||
end
|
||||
|
||||
function floor(v)
|
||||
return apply(v, math.floor)
|
||||
end
|
||||
|
||||
function ceil(v)
|
||||
return apply(v, math.ceil)
|
||||
end
|
||||
|
||||
function clamp(v, min, max)
|
||||
return apply(apply(v, math.max, min), math.min, max)
|
||||
end
|
||||
|
||||
function cross3(v, w)
|
||||
assert(#v == 3 and #w == 3)
|
||||
return new{
|
||||
v[2] * w[3] - v[3] * w[2],
|
||||
v[3] * w[1] - v[1] * w[3],
|
||||
v[1] * w[2] - v[2] * w[1]
|
||||
}
|
||||
end
|
||||
|
||||
function dot(v, w)
|
||||
local sum = 0
|
||||
for i, c in pairs(v) do
|
||||
sum = sum + c * w[i]
|
||||
end
|
||||
return sum
|
||||
end
|
||||
|
||||
function reflect(v, normal --[[**normalized** plane normal vector]])
|
||||
return subtract(v, multiply_scalar(normal, 2 * dot(v, normal))) -- reflection of v at the plane
|
||||
end
|
||||
|
||||
--+ Angle between two vectors
|
||||
--> Signed angle in radians
|
||||
function angle(v, w)
|
||||
-- Based on dot(v, w) = |v| * |w| * cos(x)
|
||||
return math.acos(dot(v, w) / length(v) / length(w))
|
||||
end
|
||||
|
||||
-- See https://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToAngle/
|
||||
function axis_angle3(euler_rotation)
|
||||
assert(#euler_rotation == 3)
|
||||
euler_rotation = divide_scalar(euler_rotation, 2)
|
||||
local cos = apply(euler_rotation, math.cos)
|
||||
local sin = apply(euler_rotation, math.sin)
|
||||
return normalize_zero{
|
||||
sin[1] * sin[2] * cos[3] + cos[1] * cos[2] * sin[3],
|
||||
sin[1] * cos[2] * cos[3] + cos[1] * sin[2] * sin[3],
|
||||
cos[1] * sin[2] * cos[3] - sin[1] * cos[2] * sin[3],
|
||||
}, 2 * math.acos(cos[1] * cos[2] * cos[3] - sin[1] * sin[2] * sin[3])
|
||||
end
|
||||
|
||||
-- Uses Rodrigues' rotation formula
|
||||
-- axis must be normalized
|
||||
function rotate3(v, axis, angle)
|
||||
assert(#v == 3 and #axis == 3)
|
||||
local cos = math.cos(angle)
|
||||
return multiply_scalar(v, cos)
|
||||
-- Minetest's coordinate system is *left-handed*, so `v` and `axis` must be swapped here
|
||||
+ multiply_scalar(cross3(v, axis), math.sin(angle))
|
||||
+ multiply_scalar(axis, dot(axis, v) * (1 - cos))
|
||||
end
|
||||
|
||||
function box_box_collision(diff, box, other_box)
|
||||
for index, diff in pairs(diff) do
|
||||
if box[index] + diff > other_box[index + 3] or other_box[index] > box[index + 3] + diff then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function moeller_trumbore(origin, direction, triangle, is_tri)
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
local h = cross3(direction, edge_2)
|
||||
local a = dot(edge_1, h)
|
||||
if math.abs(a) < 1e-9 then
|
||||
return
|
||||
end
|
||||
local f = 1 / a
|
||||
local diff = subtract(origin, point_1)
|
||||
local u = f * dot(diff, h)
|
||||
if u < 0 or u > 1 then
|
||||
return
|
||||
end
|
||||
local q = cross3(diff, edge_1)
|
||||
local v = f * dot(direction, q)
|
||||
if v < 0 or (is_tri and u or 0) + v > 1 then
|
||||
return
|
||||
end
|
||||
local pos_on_line = f * dot(edge_2, q)
|
||||
if pos_on_line >= 0 then
|
||||
return pos_on_line, u, v
|
||||
end
|
||||
end
|
||||
|
||||
function ray_triangle_intersection(origin, direction, triangle)
|
||||
return moeller_trumbore(origin, direction, triangle, true)
|
||||
end
|
||||
|
||||
function ray_parallelogram_intersection(origin, direction, parallelogram)
|
||||
return moeller_trumbore(origin, direction, parallelogram)
|
||||
end
|
||||
|
||||
function triangle_normal(triangle)
|
||||
local point_1, point_2, point_3 = unpack(triangle)
|
||||
local edge_1, edge_2 = subtract(point_2, point_1), subtract(point_3, point_1)
|
||||
return normalize(cross3(edge_1, edge_2))
|
||||
end
|
||||
|
||||
-- Export environment
|
||||
return _ENV
|
7
mods/modlib/web.lua
Normal file
7
mods/modlib/web.lua
Normal file
|
@ -0,0 +1,7 @@
|
|||
return setmetatable({}, {__index = function(self, module_name)
|
||||
if module_name == "uri" or module_name == "html" then
|
||||
local module = assert(loadfile(modlib.mod.get_resource(modlib.modname, "web", module_name .. ".lua")))()
|
||||
self[module_name] = module
|
||||
return module
|
||||
end
|
||||
end})
|
27
mods/modlib/web/html.lua
Normal file
27
mods/modlib/web/html.lua
Normal file
|
@ -0,0 +1,27 @@
|
|||
local html = setmetatable({}, {__index = function(self, key)
|
||||
if key == "unescape" then
|
||||
local func = assert(loadfile(modlib.mod.get_resource("modlib", "web", "html", "entities.lua")))
|
||||
setfenv(func, {})
|
||||
local named_entities = assert(func())
|
||||
local function unescape(text)
|
||||
return text
|
||||
:gsub("&([A-Za-z]+);", named_entities) -- named
|
||||
:gsub("&#(%d+);", function(digits) return modlib.utf8.char(tonumber(digits)) end) -- decimal
|
||||
:gsub("&#x(%x+);", function(digits) return modlib.utf8.char(tonumber(digits, 16)) end) -- hex
|
||||
end
|
||||
self.unescape = unescape
|
||||
return unescape
|
||||
end
|
||||
end})
|
||||
|
||||
function html.escape(text)
|
||||
return text:gsub(".", {
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
["&"] = "&",
|
||||
["'"] = "'",
|
||||
['"'] = """,
|
||||
})
|
||||
end
|
||||
|
||||
return html
|
3
mods/modlib/web/html/entities.lua
Normal file
3
mods/modlib/web/html/entities.lua
Normal file
File diff suppressed because one or more lines are too long
42
mods/modlib/web/uri.lua
Normal file
42
mods/modlib/web/uri.lua
Normal file
|
@ -0,0 +1,42 @@
|
|||
-- URI escaping utilities
|
||||
-- See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
|
||||
|
||||
local uri_unescaped_chars = {}
|
||||
for char in ("-_.!~*'()"):gmatch(".") do
|
||||
uri_unescaped_chars[char] = true
|
||||
end
|
||||
local function add_unescaped_range(from, to)
|
||||
for byte = from:byte(), to:byte() do
|
||||
uri_unescaped_chars[string.char(byte)] = true
|
||||
end
|
||||
end
|
||||
add_unescaped_range("0", "9")
|
||||
add_unescaped_range("a", "z")
|
||||
add_unescaped_range("A", "Z")
|
||||
|
||||
local uri_allowed_chars = table.copy(uri_unescaped_chars)
|
||||
for char in (";,/?:@&=+$#"):gmatch(".") do
|
||||
-- Reserved characters are allowed
|
||||
uri_allowed_chars[char] = true
|
||||
end
|
||||
|
||||
local function encode(text, allowed_chars)
|
||||
return text:gsub(".", function(char)
|
||||
if allowed_chars[char] then
|
||||
return char
|
||||
end
|
||||
return ("%%%02X"):format(char:byte())
|
||||
end)
|
||||
end
|
||||
|
||||
local uri = {}
|
||||
|
||||
function uri.encode_component(text)
|
||||
return encode(text, uri_unescaped_chars)
|
||||
end
|
||||
|
||||
function uri.encode(text)
|
||||
return encode(text, uri_allowed_chars)
|
||||
end
|
||||
|
||||
return uri
|
Loading…
Add table
Add a link
Reference in a new issue