Charakterbewegungen hinzugefügt, Deko hinzugefügt, Kochrezepte angepasst

This commit is contained in:
N-Nachtigal 2025-05-14 16:36:42 +02:00
parent 95945c0306
commit a0c893ca0b
1124 changed files with 64294 additions and 763 deletions

186
mods/futil/minetest/box.lua Normal file
View file

@ -0,0 +1,186 @@
-- box definition below node boxes: https://github.com/minetest/minetest/blob/master/doc/lua_api.md#node-boxes
local x1 = 1
local y1 = 2
local z1 = 3
local x2 = 4
local y2 = 5
local z2 = 6
function futil.boxes_intersect(box1, box2)
return not (
(box1[x2] < box2[x1] or box2[x2] < box1[x1])
or (box1[y2] < box2[y1] or box2[y2] < box1[y1])
or (box1[z2] < box2[z1] or box2[z2] < box1[z1])
)
end
function futil.box_offset(box, number_or_vector)
if type(number_or_vector) == "number" then
return {
box[1] + number_or_vector,
box[2] + number_or_vector,
box[3] + number_or_vector,
box[4] + number_or_vector,
box[5] + number_or_vector,
box[6] + number_or_vector,
}
else
return {
box[1] + number_or_vector.x,
box[2] + number_or_vector.y,
box[3] + number_or_vector.z,
box[4] + number_or_vector.x,
box[5] + number_or_vector.y,
box[6] + number_or_vector.z,
}
end
end
function futil.is_box(box)
if type(box) == "table" and #box == 6 then
for _, x in ipairs(box) do
if type(x) ~= "number" then
return false
end
end
return box[1] <= box[4] and box[2] <= box[5] and box[3] <= box[6]
end
return false
end
function futil.is_boxes(boxes)
if type(boxes) ~= "table" or #boxes == 0 then
return false
end
for _, box in ipairs(boxes) do
if not futil.is_box(box) then
return false
end
end
return true
end
-- given a set of boxes, return a single box that covers all of them
function futil.cover_boxes(boxes)
if not futil.is_boxes(boxes) then
return { 0, 0, 0, 0, 0, 0 }
end
local cover = boxes[1]
for i = 2, #boxes do
for j = 1, 3 do
cover[j] = math.min(cover[j], boxes[i][j])
end
for j = 4, 6 do
cover[j] = math.max(cover[j], boxes[i][j])
end
end
return cover
end
--[[
for nodes:
A nodebox is defined as any of:
{
-- A normal cube; the default in most things
type = "regular"
}
{
-- A fixed box (or boxes) (facedir param2 is used, if applicable)
type = "fixed",
fixed = box OR {box1, box2, ...}
}
{
-- A variable height box (or boxes) with the top face position defined
-- by the node parameter 'leveled = ', or if 'paramtype2 == "leveled"'
-- by param2.
-- Other faces are defined by 'fixed = {}' as with 'type = "fixed"'.
type = "leveled",
fixed = box OR {box1, box2, ...}
}
{
-- A box like the selection box for torches
-- (wallmounted param2 is used, if applicable)
type = "wallmounted",
wall_top = box,
wall_bottom = box,
wall_side = box
}
{
-- A node that has optional boxes depending on neighboring nodes'
-- presence and type. See also `connects_to`.
type = "connected",
fixed = box OR {box1, box2, ...}
connect_top = box OR {box1, box2, ...}
connect_bottom = box OR {box1, box2, ...}
connect_front = box OR {box1, box2, ...}
connect_left = box OR {box1, box2, ...}
connect_back = box OR {box1, box2, ...}
connect_right = box OR {box1, box2, ...}
-- The following `disconnected_*` boxes are the opposites of the
-- `connect_*` ones above, i.e. when a node has no suitable neighbor
-- on the respective side, the corresponding disconnected box is drawn.
disconnected_top = box OR {box1, box2, ...}
disconnected_bottom = box OR {box1, box2, ...}
disconnected_front = box OR {box1, box2, ...}
disconnected_left = box OR {box1, box2, ...}
disconnected_back = box OR {box1, box2, ...}
disconnected_right = box OR {box1, box2, ...}
disconnected = box OR {box1, box2, ...} -- when there is *no* neighbor
disconnected_sides = box OR {box1, box2, ...} -- when there are *no*
-- neighbors to the sides
}
for objects:
collisionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }, -- default
selectionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = false },
-- { xmin, ymin, zmin, xmax, ymax, zmax } in nodes from object position.
-- Collision boxes cannot rotate, setting `rotate = true` on it has no effect.
-- If not set, the selection box copies the collision box, and will also not rotate.
-- If `rotate = false`, the selection box will not rotate with the object itself, remaining fixed to the axes.
-- If `rotate = true`, it will match the object's rotation and any attachment rotations.
-- Raycasts use the selection box and object's rotation, but do *not* obey attachment rotations
]]
futil.default_collision_box = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }
function futil.node_collision_box_to_object_collisionbox(collision_box)
if type(collision_box) ~= "table" then
return table.copy(futil.default_collision_box)
elseif collision_box.type == "regular" then
return table.copy(futil.default_collision_box)
elseif collision_box.type == "fixed" or collision_box.type == "leveled" or collision_box.type == "connected" then
if futil.is_box(collision_box.fixed) then
return collision_box.fixed
elseif futil.is_boxes(collision_box.fixed) then
return futil.cover_boxes(collision_box.fixed)
else
return table.copy(futil.default_collision_box)
end
elseif collision_box.type == "wallmounted" then
local boxes = {}
if collision_box.wall_top then
table.insert(boxes, collision_box.wall_top)
end
if collision_box.wall_bottom then
table.insert(boxes, collision_box.wall_bottom)
end
if collision_box.wall_side then
table.insert(boxes, collision_box.wall_side)
end
return futil.cover_boxes(boxes)
else
return table.copy(futil.default_collision_box)
end
end
function futil.node_selection_box_to_object_selectionbox(selection_box, rotate)
local selectionbox = futil.node_collision_box_to_object_collisionbox(selection_box)
selectionbox.rotate = rotate or false
return selectionbox
end

View file

@ -0,0 +1,31 @@
-- utilities to dedupe messages
local last_by_func = {}
function futil.dedupe(func, ...)
local cur = { ... }
if futil.equals(last_by_func[func], cur) then
return
end
last_by_func[func] = cur
return func(...)
end
local last_by_player_name_by_func = futil.DefaultTable(function()
return {}
end)
function futil.dedupe_by_player(func, player, ...)
local cur = { ... }
local last_by_player_name = last_by_player_name_by_func[func]
local player_name
if type(player) == "string" then
player_name = player
else
player_name = player:get_player_name()
end
if futil.equals(last_by_player_name[player_name], cur) then
return
end
last_by_player_name[player_name] = cur
return func(player, ...)
end

View file

@ -0,0 +1,111 @@
-- adapted from https://github.com/minetest/minetest/blob/master/builtin/common/misc_helpers.lua
-- but tables are sorted
local function sorter(a, b)
local ta, tb = type(a), type(b)
if ta ~= tb then
return ta < tb
end
if ta == "function" or ta == "userdata" or ta == "thread" or ta == "table" then
return tostring(a) < tostring(b)
else
return a < b
end
end
local keywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true, -- Lua 5.2
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function is_valid_identifier(str)
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
return false
end
return true
end
local function basic_dump(o)
local tp = type(o)
if tp == "number" then
return tostring(o)
elseif tp == "string" then
return string.format("%q", o)
elseif tp == "boolean" then
return tostring(o)
elseif tp == "nil" then
return "nil"
-- Uncomment for full function dumping support.
-- Not currently enabled because bytecode isn't very human-readable and
-- dump's output is intended for humans.
--elseif tp == "function" then
-- return string.format("loadstring(%q)", string.dump(o))
elseif tp == "userdata" then
return tostring(o)
else
return string.format("<%s>", tp)
end
end
function futil.dump(o, indent, nested, level)
local t = type(o)
if not level and t == "userdata" then
-- when userdata (e.g. player) is passed directly, print its metatable:
return "userdata metatable: " .. futil.dump(getmetatable(o))
end
if t ~= "table" then
return basic_dump(o)
end
-- Contains table -> true/nil of currently nested tables
nested = nested or {}
if nested[o] then
return "<circular reference>"
end
nested[o] = true
indent = indent or "\t"
level = level or 1
local ret = {}
local dumped_indexes = {}
for i, v in ipairs(o) do
ret[#ret + 1] = futil.dump(v, indent, nested, level + 1)
dumped_indexes[i] = true
end
for k, v in futil.table.pairs_by_key(o, sorter) do
if not dumped_indexes[k] then
if type(k) ~= "string" or not is_valid_identifier(k) then
k = "[" .. futil.dump(k, indent, nested, level + 1) .. "]"
end
v = futil.dump(v, indent, nested, level + 1)
ret[#ret + 1] = k .. " = " .. v
end
end
nested[o] = nil
if indent ~= "" then
local indent_str = "\n" .. string.rep(indent, level)
local end_indent_str = "\n" .. string.rep(indent, level - 1)
return string.format("{%s%s%s}", indent_str, table.concat(ret, "," .. indent_str), end_indent_str)
end
return "{" .. table.concat(ret, ", ") .. "}"
end

View file

@ -0,0 +1,271 @@
local FakeInventory = futil.class1()
local function copy_list(list)
local copy = {}
for i = 1, #list do
copy[i] = ItemStack(list[i])
end
return copy
end
function FakeInventory:_init()
self._lists = {}
end
function FakeInventory.create_copy(inv)
local fake_inv = FakeInventory()
for listname, contents in pairs(inv:get_lists()) do
fake_inv:set_size(listname, inv:get_size(listname))
fake_inv:set_width(listname, inv:get_width(listname))
fake_inv:set_list(listname, contents)
end
return fake_inv
end
function FakeInventory.room_for_all(inv, listname, items)
local fake_inv = FakeInventory.create_copy(inv)
for i = 1, #items do
local item = items[i]
local remainder = fake_inv:add_item(listname, item)
if not remainder:is_empty() then
return false
end
end
return true
end
function FakeInventory:is_empty(listname)
local list = self._lists[listname]
if not list then
return true
end
for _, stack in ipairs(list) do
if not stack:is_empty() then
return false
end
end
return true
end
function FakeInventory:get_size(listname)
local list = self._lists[listname]
if not list then
return 0
end
return #list
end
function FakeInventory:set_size(listname, size)
if size == 0 then
self._lists[listname] = nil
return
end
local list = self._lists[listname] or {}
while #list < size do
list[#list + 1] = ItemStack()
end
for i = size + 1, #list do
list[i] = nil
end
self._lists[listname] = list
end
function FakeInventory:get_width(listname)
local list = self._lists[listname] or {}
return list.width or 0
end
function FakeInventory:set_width(listname, width)
local list = self._lists[listname] or {}
list.width = width
self._lists[listname] = list
end
function FakeInventory:get_stack(listname, i)
local list = self._lists[listname]
if not list or i > #list then
return ItemStack()
end
return ItemStack(list[i])
end
function FakeInventory:set_stack(listname, i, stack)
local list = self._lists[listname]
if not list or i > #list then
return
end
list[i] = ItemStack(stack)
end
function FakeInventory:get_list(listname)
local list = self._lists[listname]
if not list then
return
end
return copy_list(list)
end
function FakeInventory:set_list(listname, list)
local ourlist = self._lists[listname]
if not ourlist then
return
end
for i = 1, #ourlist do
ourlist[i] = ItemStack(list[i])
end
end
function FakeInventory:get_lists()
local lists = {}
for listname, list in pairs(self._lists) do
lists[listname] = copy_list(list)
end
return lists
end
function FakeInventory:set_lists(lists)
for listname, list in pairs(lists) do
self:set_list(listname, list)
end
end
-- add item somewhere in list, returns leftover `ItemStack`.
function FakeInventory:add_item(listname, new_item)
local list = self._lists[listname]
new_item = ItemStack(new_item)
if new_item:is_empty() or not list or #list == 0 then
return new_item
end
-- first try to find if it could be added to some existing items
for _, our_stack in ipairs(list) do
if not our_stack:is_empty() then
new_item = our_stack:add_item(new_item)
if new_item:is_empty() then
return new_item
end
end
end
-- then try to add it to empty slots
for _, our_stack in ipairs(list) do
new_item = our_stack:add_item(new_item)
if new_item:is_empty() then
break
end
end
return new_item
end
-- returns `true` if the stack of items can be fully added to the list
function FakeInventory:room_for_item(listname, stack)
local list = self._lists[listname]
if not list then
return false
end
stack = ItemStack(stack)
local copy = copy_list(list)
for _, our_stack in ipairs(copy) do
stack = our_stack:add_item(stack)
if stack:is_empty() then
break
end
end
return stack:is_empty()
end
-- take as many items as specified from the list, returns the items that were actually removed (as an `ItemStack`)
-- note that any item metadata is ignored, so attempting to remove a specific unique item this way will likely remove
-- the wrong one -- to do that use `set_stack` with an empty `ItemStack`.
function FakeInventory:remove_item(listname, stack)
local removed = ItemStack()
stack = ItemStack(stack)
local list = self._lists[listname]
if not list or stack:is_empty() then
return removed
end
local name = stack:get_name()
local count_remaining = stack:get_count()
local taken = 0
for i = #list, 1, -1 do
local our_stack = list[i]
if our_stack:get_name() == name then
local n = our_stack:take_item(count_remaining):get_count()
count_remaining = count_remaining - n
taken = taken + n
end
if count_remaining == 0 then
break
end
end
stack:set_count(taken)
return stack
end
-- returns `true` if the stack of items can be fully taken from the list.
-- If `match_meta` is false, only the items' names are compared (default: `false`).
function FakeInventory:contains_item(listname, stack, match_meta)
local list = self._lists[listname]
if not list then
return false
end
stack = ItemStack(stack)
if match_meta then
local name = stack:get_name()
local wear = stack:get_wear()
local meta = stack:get_meta()
local needed_count = stack:get_count()
for _, our_stack in ipairs(list) do
if our_stack:get_name() == name and our_stack:get_wear() == wear and our_stack:get_meta():equals(meta) then
local n = our_stack:peek_item(needed_count):get_count()
needed_count = needed_count - n
end
if needed_count == 0 then
break
end
end
return needed_count == 0
else
local name = stack:get_name()
local needed_count = stack:get_count()
for _, our_stack in ipairs(list) do
if our_stack:get_name() == name then
local n = our_stack:peek_item(needed_count):get_count()
needed_count = needed_count - n
end
if needed_count == 0 then
break
end
end
return needed_count == 0
end
end
function FakeInventory:get_location()
return {
type = "undefined",
subtype = "FakeInventory",
}
end
futil.FakeInventory = FakeInventory

View file

@ -0,0 +1,88 @@
--[[
execute the globalstep after the specified period. the actual amount of time elapsed is passed to the function,
and will always be greater than or equal to the length of the period.
futil.register_globalstep({
period = 1,
func = function(elapsed) end,
})
execute the globalstep after the specified period. if more time has elapsed than the period specified, the remainder
will be counted against the next cycle, allowing the execution to "catch up". the expected time between executions
will tend towards the specified period. IMPORTANT: do not specify a period which is less than the length of the
dedicated server step.
futil.register_globalstep({
period = 1,
catchup = "single"
func = function(period) end,
})
execute the globalstep after the specified period. if more time has elapsed than the period specified, the callback
will be executed repeatedly until the elapsed time is less than the period, and the remainder will still be counted
against the next cycle.
futil.register_globalstep({
period = 1,
catchup = "full"
func = function(period) end,
})
this is just a light wrapper over a normal minetest globalstep callback, and is only provided for completeness.
futil.register_globalstep({
func = function(dtime) end,
})
]]
local f = string.format
local dedicated_server_step = tonumber(minetest.settings:get("dedicated_server_step")) or 0.09
function futil.register_globalstep(def)
if def.period then
local elapsed = 0
if def.catchup == "full" then
assert(def.period > 0, "full catchup will cause an infinite loop if period is 0")
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
elapsed = elapsed - def.period
def.func(def.period)
while elapsed > def.period do
elapsed = elapsed - def.period
def.func(def.period)
end
end)
elseif def.catchup == "single" or def.catchup == true then
assert(
def.period >= dedicated_server_step,
f(
"if period (%s) is less than dedicated_server_step (%s), single catchup will never fully catch up.",
def.period,
dedicated_server_step
)
)
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
elapsed = elapsed - def.period
def.func(def.period)
end)
else
-- no catchup, just reset
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
def.func(elapsed)
elapsed = 0
end)
end
else
-- we do nothing useful
minetest.register_globalstep(function(dtime)
def.func(dtime)
end)
end
end

View file

@ -0,0 +1,61 @@
function futil.add_groups(itemstring, new_groups)
local def = minetest.registered_items[itemstring]
if not def then
error(("attempting to override unknown item %s"):format(itemstring))
end
local groups = table.copy(def.groups or {})
futil.table.set_all(groups, new_groups)
minetest.override_item(itemstring, { groups = groups })
end
function futil.remove_groups(itemstring, ...)
local def = minetest.registered_items[itemstring]
if not def then
error(("attempting to override unknown item %s"):format(itemstring))
end
local groups = table.copy(def.groups or {})
for _, group in ipairs({ ... }) do
groups[group] = nil
end
minetest.override_item(itemstring, { groups = groups })
end
function futil.get_items_with_group(group)
if futil.items_by_group then
return futil.items_by_group[group] or {}
end
local items = {}
for item in pairs(minetest.registered_items) do
if minetest.get_item_group(item, group) > 0 then
table.insert(items, item)
end
end
return items
end
function futil.get_item_with_group(group)
return futil.get_items_with_group(group)[1]
end
function futil.generate_items_by_group()
local items_by_group = {}
for item, def in pairs(minetest.registered_items) do
for group in pairs(def.groups or {}) do
local items = items_by_group[group] or {}
table.insert(items, item)
items_by_group[group] = items
end
end
futil.items_by_group = items_by_group
end
if INIT == "game" then
-- it's not 100% safe to assume items and groups can't change after this point.
-- but please, don't do that :\
minetest.register_on_mods_loaded(futil.generate_items_by_group)
end

View file

@ -0,0 +1,100 @@
local f = string.format
local current_id = 0
local function get_next_id()
current_id = current_id + 1
return current_id
end
local EphemeralHud = futil.class1()
function EphemeralHud:_init(player, hud_def)
self._player_name = player:get_player_name()
if (hud_def.type or hud_def.hud_elem_type) == "waypoint" then
self._id_field = "text2"
else
self._id_field = "name"
end
self._id = f("ephemeral_hud:%s:%i", hud_def[self._id_field] or "", get_next_id())
hud_def[self._id_field] = self._id
self._hud_id = player:hud_add(hud_def)
end
function EphemeralHud:is_active()
if not self._hud_id then
return false
end
local player = minetest.get_player_by_name(self._player_name)
if not player then
self._hud_id = nil
return false
end
local hud_def = player:hud_get(self._hud_id)
if not hud_def then
self._hud_id = nil
return false
end
if hud_def[self._id_field] ~= self._id then
self._hud_id = nil
return false
end
return true
end
function EphemeralHud:change(new_hud_def)
if not self:is_active() then
futil.log("warning", "[ephemeral hud] cannot update an inactive hud")
return false
end
local player = minetest.get_player_by_name(self._player_name)
local old_hud_def = player:hud_get(self._hud_id)
for key, value in pairs(new_hud_def) do
if key == "hud_elem_type" then
if value ~= (old_hud_def.type or old_hud_def.hud_elem_type) then
error("cannot change hud_elem_type")
end
elseif key == "type" then
if value ~= (old_hud_def.type or old_hud_def.hud_elem_type) then
error("cannot change type")
end
elseif key == self._id_field then
if value ~= self._id then
error(f("cannot change the value of %q, as this is an ID", self._id_field))
end
else
if key == "position" or key == "scale" or key == "align" or key == "offset" then
value = futil.vector.v2f_to_float_32(value)
end
if not futil.equals(old_hud_def[key], value) then
player:hud_change(self._hud_id, key, value)
end
end
end
return true
end
function EphemeralHud:remove()
if not self:is_active() then
futil.log("warning", "[ephemeral hud] cannot remove an inactive hud")
return false
end
local player = minetest.get_player_by_name(self._player_name)
player:hud_remove(self._hud_id)
self._hud_id = nil
end
futil.EphemeralHud = EphemeralHud
-- note: sometimes HUDs can fail to get created. if so, the HUD object returned here will be "inactive".
function futil.create_ephemeral_hud(player, timeout, hud_def)
local hud = EphemeralHud(player, hud_def)
minetest.after(timeout, function()
if hud:is_active() then
hud:remove()
end
end)
return hud
end

View file

@ -0,0 +1,172 @@
--[[
local my_hud = futil.define_hud("my_mod:my_hud", {
period = 1,
catchup = nil, -- not currently supported
name_field = nil, -- in case you want to override the id field
enabled_by_default = nil, -- set to true to enable by default
get_hud_data = function()
-- get data that's identical for all players
-- passed to get_hud_def
end,
get_hud_def = function(player, data)
return {}
end,
})
my_hud:toggle_enabled(player)
]]
local f = string.format
local ManagedHud = futil.class1()
function ManagedHud:_init(hud_name, def)
self.name = hud_name
self._name_field = def.name_field or ((def.type or def.hud_elem_type) == "waypoint" and "text2" or "name")
self._period = def.period
self._get_hud_data = def.get_hud_data
self._get_hud_def = def.get_hud_def
self._enabled_by_default = def.enabled_by_default
self._hud_id_by_player_name = {}
self._hud_enabled_key = f("hud_manager:%s_enabled", hud_name)
self._hud_name = f("hud_manager:%s", hud_name)
end
function ManagedHud:is_enabled(player)
local meta = player:get_meta()
local value = meta:get(self._hud_enabled_key)
if value == nil then
return self._enabled_by_default
else
return minetest.is_yes(value)
end
end
function ManagedHud:set_enabled(player, value)
local meta = player:get_meta()
if minetest.is_yes(value) then
meta:set_string(self._hud_enabled_key, "y")
else
meta:set_string(self._hud_enabled_key, "n")
end
end
function ManagedHud:toggle_enabled(player)
local meta = player:get_meta()
local enabled = not self:is_enabled(player)
if enabled then
meta:set_string(self._hud_enabled_key, "y")
else
meta:set_string(self._hud_enabled_key, "n")
end
return enabled
end
function ManagedHud:update(player, data)
local is_enabled = self:is_enabled(player)
local player_name = player:get_player_name()
local hud_id = self._hud_id_by_player_name[player_name]
local old_hud_def
if hud_id then
old_hud_def = player:hud_get(hud_id)
if old_hud_def and old_hud_def[self._name_field] == self._hud_name then
if not is_enabled then
player:hud_remove(hud_id)
self._hud_id_by_player_name[player_name] = nil
return
end
else
-- hud_id is bad
hud_id = nil
old_hud_def = nil
end
end
if is_enabled then
local new_hud_def = self._get_hud_def(player, data)
if not new_hud_def then
if hud_id then
player:hud_remove(hud_id)
self._hud_id_by_player_name[player_name] = nil
end
return
elseif new_hud_def[self._name_field] and new_hud_def[self._name_field] ~= self._hud_name then
error(f("you cannot specify the value of the %q field, this is generated", self._name_field))
end
if old_hud_def then
for k, v in pairs(new_hud_def) do
if k == "position" or k == "scale" or k == "align" or k == "offset" then
v = futil.vector.v2f_to_float_32(v)
end
if not futil.equals(old_hud_def[k], v) and k ~= "type" and k ~= "hud_elem_type" then
player:hud_change(hud_id, k, v)
end
end
else
new_hud_def[self._name_field] = self._hud_name
hud_id = player:hud_add(new_hud_def)
end
end
self._hud_id_by_player_name[player_name] = hud_id
end
futil.defined_huds = {}
function futil.define_hud(hud_name, def)
if futil.defined_huds[hud_name] then
error(f("hud %s already exists", hud_name))
end
local hud = ManagedHud(hud_name, def)
futil.defined_huds[hud_name] = hud
return hud
end
-- TODO: register_hud instead of define_hud, plus alias the old
local function update_hud(hud, players)
local data
if hud._get_hud_data then
local is_any_enabled = false
for i = 1, #players do
if hud:is_enabled(players[i]) then
is_any_enabled = true
break
end
end
if is_any_enabled then
data = hud._get_hud_data()
end
end
for i = 1, #players do
hud:update(players[i], data)
end
end
-- TODO refactor to use futil.register_globalstep for each hud, to allow use of catchup mechanics
-- ... why would HUD updates need catchup mechanics?
local elapsed_by_hud_name = {}
minetest.register_globalstep(function(dtime)
local players = minetest.get_connected_players()
if #players == 0 then
return
end
for hud_name, hud in pairs(futil.defined_huds) do
if hud._period then
local elapsed = (elapsed_by_hud_name[hud_name] or 0) + dtime
if elapsed < hud._period then
elapsed_by_hud_name[hud_name] = elapsed
else
elapsed_by_hud_name[hud_name] = 0
update_hud(hud, players)
end
else
update_hud(hud, players)
end
end
end)

View file

@ -0,0 +1,171 @@
local f = string.format
local function is_vertical_frames(animation)
return (animation.type == "vertical_frames" and animation.aspect_w and animation.aspect_h)
end
local function get_single_frame(animation, image_name)
return ("[combine:%ix%i^[noalpha^[colorize:#FFF:255^[mask:%s"):format(
animation.aspect_w,
animation.aspect_h,
image_name
)
end
local function is_sheet_2d(animation)
return (animation.type == "sheet_2d" and animation.frames_w and animation.frames_h)
end
local function get_sheet_2d(animation, image_name)
return ("%s^[sheet:%ix%i:0,0"):format(image_name, animation.frames_w, animation.frames_h)
end
local get_image_from_tile = futil.memoize1(function(tile)
if type(tile) == "string" then
return tile
elseif type(tile) == "table" then
local image_name
if type(tile.image) == "string" then
image_name = tile.image
elseif type(tile.name) == "string" then
image_name = tile.name
end
if image_name then
local animation = tile.animation
if animation then
if is_vertical_frames(animation) then
return get_single_frame(animation, image_name)
elseif is_sheet_2d(animation) then
return get_sheet_2d(animation, image_name)
end
end
return image_name
end
end
return "unknown_node.png"
end)
local function get_image_cube(tiles)
if #tiles >= 6 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[6] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 5 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[5] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 4 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[4] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 3 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 2 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[2] or "no_texture.png"),
get_image_from_tile(tiles[2] or "no_texture.png")
)
elseif #tiles == 1 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[1] or "no_texture.png")
)
end
return "no_texture.png"
end
local function is_normal_node(drawtype)
return (
drawtype == "normal"
or drawtype == "allfaces"
or drawtype == "allfaces_optional"
or drawtype == "glasslike"
or drawtype == "glasslike_framed"
or drawtype == "glasslike_framed_optional"
or drawtype == "liquid"
)
end
local cache = {}
function futil.get_wield_image(item)
if type(item) == "string" then
item = ItemStack(item)
end
if item:is_empty() then
return "blank.png"
end
local def = item:get_definition()
if not def then
return "unknown_item.png"
end
local itemstring = item:to_string()
local cached = cache[itemstring]
if cached then
return cached
end
local meta = item:get_meta()
local color = meta:get("color") or def.color
local image = "no_texture.png"
if def.wield_image and def.wield_image ~= "" then
local parts = { def.wield_image }
if color then
parts[#parts + 1] = f("[colorize:%s:alpha", futil.escape_texture(color))
end
if def.wield_overlay then
parts[#parts + 1] = def.wield_overlay
end
image = table.concat(parts, "^")
elseif def.inventory_image and def.inventory_image ~= "" then
local parts = { def.inventory_image }
if color then
parts[#parts + 1] = f("[colorize:%s:alpha", futil.escape_texture(color))
end
if def.inventory_overlay then
parts[#parts + 1] = def.inventory_overlay
end
image = table.concat(parts, "^")
elseif def.type == "node" then
if def.drawtype == "nodebox" or def.drawtype == "mesh" then
image = "no_texture.png"
else
local tiles = def.tiles
if type(tiles) == "string" then
image = get_image_from_tile(tiles)
elseif type(tiles) == "table" then
if is_normal_node(def.drawtype) then
image = get_image_cube(tiles)
else
image = get_image_from_tile(tiles[1])
end
end
end
end
cache[itemstring] = image
return image
end

View file

@ -0,0 +1,24 @@
futil.dofile("minetest", "box")
futil.dofile("minetest", "dedupe")
futil.dofile("minetest", "dump")
futil.dofile("minetest", "fake_inventory")
futil.dofile("minetest", "group")
futil.dofile("minetest", "image")
futil.dofile("minetest", "item")
futil.dofile("minetest", "registration")
futil.dofile("minetest", "serialization")
futil.dofile("minetest", "set_look_dir")
futil.dofile("minetest", "strip_translation")
futil.dofile("minetest", "texture")
futil.dofile("minetest", "time")
futil.dofile("minetest", "vector")
if INIT == "game" then
futil.dofile("minetest", "globalstep")
futil.dofile("minetest", "hud_ephemeral")
futil.dofile("minetest", "hud_manager")
futil.dofile("minetest", "inventory")
futil.dofile("minetest", "object")
futil.dofile("minetest", "object_properties")
futil.dofile("minetest", "raycast")
end

View file

@ -0,0 +1,40 @@
function futil.get_location_string(inv)
local location = inv:get_location()
if location.type == "node" then
return ("nodemeta:%i,%i,%i"):format(location.pos.x, location.pos.y, location.pos.z)
elseif location.type == "player" then
return ("player:%s"):format(location.name)
elseif location.type == "detached" then
return ("detached:%s"):format(location.name)
else
error(("unexpected location? %s"):format(dump(location)))
end
end
-- InvRef:remove_item() ignores metadata, and sometimes that's wrong
-- for logic, see InventoryList::removeItem in inventory.cpp
function futil.remove_item_with_meta(inv, listname, itemstack)
itemstack = ItemStack(itemstack)
if itemstack:is_empty() then
return ItemStack()
end
local removed = ItemStack()
for i = 1, inv:get_size(listname) do
local invstack = inv:get_stack(listname, i)
if
invstack:get_name() == itemstack:get_name()
and invstack:get_wear() == itemstack:get_wear()
and invstack:get_meta() == itemstack:get_meta()
then
local still_to_remove = itemstack:get_count() - removed:get_count()
local leftover = removed:add_item(invstack:take_item(still_to_remove))
-- if we've requested to remove more than the stack size, ignore the limit
removed:set_count(removed:get_count() + leftover:get_count())
inv:set_stack(listname, i, invstack)
if removed:get_count() == itemstack:get_count() then
break
end
end
end
return removed
end

View file

@ -0,0 +1,133 @@
local f = string.format
-- if allow_unregistered is false or absent, if the original item or its alias is not a registered item, will return nil
function futil.resolve_item(item_or_string, allow_unregistered)
local item_stack = ItemStack(item_or_string)
local name = item_stack:get_name()
local seen = { [name] = true }
local alias = minetest.registered_aliases[name]
while alias do
name = alias
seen[name] = true
alias = minetest.registered_aliases[name]
if seen[alias] then
error(f("alias cycle on %s", name))
end
end
if minetest.registered_items[name] or allow_unregistered then
item_stack:set_name(name)
return item_stack:to_string()
end
end
function futil.resolve_itemstack(item_or_string)
return ItemStack(futil.resolve_item(item_or_string, true))
end
if ItemStack().equals then
-- https://github.com/minetest/minetest/pull/12771
function futil.items_equals(item1, item2)
item1 = type(item1) == "userdata" and item1 or ItemStack(item1)
item2 = type(item2) == "userdata" and item2 or ItemStack(item2)
return item1 == item2
end
else
local equals = futil.equals
function futil.items_equals(item1, item2)
item1 = type(item1) == "userdata" and item1 or ItemStack(item1)
item2 = type(item2) == "userdata" and item2 or ItemStack(item2)
return equals(item1:to_table(), item2:to_table())
end
end
-- TODO: probably this should have a 3nd argument to handle tool and tool_group stuff
function futil.get_primary_drop(stack, filter)
stack = ItemStack(stack)
local name = stack:get_name()
local meta = stack:get_meta()
local palette_index = tonumber(meta:get_int("palette_index"))
local def = stack:get_definition()
if palette_index then
-- https://github.com/mt-mods/unifieddyes/blob/36c8bb5f5b8a0485225d2547c8978291ff710291/api.lua#L70-L90
local del_color
if def.paramtype2 == "color" and palette_index == 240 and def.palette == "unifieddyes_palette_extended.png" then
del_color = true
elseif
def.paramtype2 == "colorwallmounted"
and palette_index == 0
and def.palette == "unifieddyes_palette_colorwallmounted.png"
then
del_color = true
elseif
def.paramtype2 == "colorfacedir"
and palette_index == 0
and string.find(def.palette, "unifieddyes_palette_")
then
del_color = true
end
if del_color then
meta:set_string("palette_index", "")
palette_index = nil
end
end
local drop = def.drop
if drop == nil then
stack:set_count(1)
return stack
elseif drop == "" then
return nil
elseif type(drop) == "string" then
drop = ItemStack(drop)
drop:set_count(1)
return drop
elseif type(drop) == "table" then
local most_common_item
local inherit_color = false
local rarity = math.huge
if not drop.items then
error(f("unexpected drop table for %s: %s", stack:to_string(), dump(drop)))
end
for _, items in ipairs(drop.items) do
if (items.rarity or 1) < rarity then
for item in ipairs(items.items) do
if (not filter) or filter(item) then
most_common_item = item
inherit_color = items.inherit_color or false
rarity = items.rarity
break
end
end
end
end
if not most_common_item then
return
end
most_common_item = ItemStack(most_common_item)
most_common_item:set_count(1)
if inherit_color and palette_index then
local meta2 = most_common_item:get_meta()
meta2:set_int("palette_index", palette_index)
end
return most_common_item
else
error(f("invalid drop of %s? %q", dump(name, drop)))
end
end

View file

@ -0,0 +1,200 @@
local v_new = vector.new
-- if object is attached, get the velocity of the object it is attached to
function futil.get_velocity(object)
local parent = object:get_attach()
while parent do
object = parent
parent = object:get_attach()
end
return object:get_velocity()
end
function futil.get_horizontal_speed(object)
local velocity = futil.get_velocity(object)
velocity.y = 0
return vector.length(velocity)
end
local function insert_connected(boxes, something)
if futil.is_box(something) then
table.insert(boxes, something)
elseif futil.is_boxes(something) then
table.insert_all(boxes, something)
end
end
local function get_boxes(cb)
if not cb then
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
if cb.type == "regular" then
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
elseif cb.type == "fixed" then
if futil.is_box(cb.fixed) then
return { cb.fixed }
elseif futil.is_boxes(cb.fixed) then
return cb.fixed
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "leveled" then
-- TODO: have to check param2
if futil.is_box(cb.fixed) then
return { cb.fixed }
elseif futil.is_boxes(cb.fixed) then
return cb.fixed
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "wallmounted" then
-- TODO: have to check param2? or?
local boxes = {}
if futil.is_box(cb.wall_top) then
table.insert(boxes, cb.wall_top)
end
if futil.is_box(cb.wall_bottom) then
table.insert(boxes, cb.wall_bottom)
end
if futil.is_box(cb.wall_side) then
table.insert(boxes, cb.wall_side)
end
if #boxes > 0 then
return boxes
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "connected" then
-- TODO: very very complicated to check, just fudge and add everything
local boxes = {}
insert_connected(boxes, cb.fixed)
insert_connected(boxes, cb.connect_top)
insert_connected(boxes, cb.connect_bottom)
insert_connected(boxes, cb.connect_front)
insert_connected(boxes, cb.connect_left)
insert_connected(boxes, cb.connect_back)
insert_connected(boxes, cb.connect_right)
insert_connected(boxes, cb.disconnected_top)
insert_connected(boxes, cb.disconnected_bottom)
insert_connected(boxes, cb.disconnected_front)
insert_connected(boxes, cb.disconnected_left)
insert_connected(boxes, cb.disconnected_back)
insert_connected(boxes, cb.disconnected_right)
insert_connected(boxes, cb.disconnected)
insert_connected(boxes, cb.disconnected_sides)
if #boxes > 0 then
return boxes
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
end
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
local function get_collision_boxes(node)
local node_def = minetest.registered_nodes[node.name]
if not node_def then
-- unknown nodes are regular solid nodes
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
if not node_def.walkable then
return {}
end
local boxes
if node_def.collision_box then
boxes = get_boxes(node_def.collision_box)
elseif node_def.drawtype == "nodebox" then
boxes = get_boxes(node_def.node_box)
else
boxes = { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
--[[
if node_def.paramtype2 == "facedir" then
-- TODO: re-orient boxes
end
]]
return boxes
end
local function is_pos_on_ground(feet_pos, player_box)
local node = minetest.get_node(feet_pos)
local node_boxes = get_collision_boxes(node)
for _, node_box in ipairs(node_boxes) do
local actual_node_box = futil.box_offset(node_box, feet_pos)
if futil.boxes_intersect(actual_node_box, player_box) then
return true
end
end
return false
end
function futil.is_on_ground(player)
local p_pos = player:get_pos()
local cb = player:get_properties().collisionbox
-- collect the positions of the nodes below the player's feet
local feet_poss = {
v_new(math.round(p_pos.x + cb[1]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[3])),
v_new(math.round(p_pos.x + cb[1]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[6])),
v_new(math.round(p_pos.x + cb[4]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[3])),
v_new(math.round(p_pos.x + cb[4]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[6])),
}
for _, feet_pos in ipairs(feet_poss) do
if is_pos_on_ground(feet_pos, futil.box_offset(cb, p_pos)) then
return true
end
end
return false
end
function futil.get_object_center(object)
local pos = object:get_pos()
if not pos then
return
end
local cb = object:get_properties().collisionbox
return v_new(pos.x + (cb[1] + cb[4]) / 2, pos.y + (cb[2] + cb[5]) / 2, pos.z + (cb[3] + cb[6]) / 2)
end
function futil.is_player(obj)
return minetest.is_player(obj) and not obj.is_fake_player
end
function futil.is_valid_object(obj)
return obj and type(obj.get_pos) == "function" and vector.check(obj:get_pos())
end
-- this is meant to be able to get the HP of any object, including "immortal" ones whose health is managed themselves
-- it is *NOT* complete - i've got no idea where every mob API stores its hp.
-- "health" is mobs_redo (which is actually redundant with `:get_hp()` because they're not actually immortal.
-- "hp" is mobkit (and petz, which comes with its own fork of mobkit), and also creatura.
function futil.get_hp(obj)
if not futil.is_valid_object(obj) then
-- not an object or dead
return 0
end
local ent = obj:get_luaentity()
if ent and (type(ent.hp) == "number" or type(ent.health) == "number") then
return ent.hp or ent.health
end
local armor_groups = obj:get_armor_groups()
if (armor_groups["immortal"] or 0) == 0 then
return obj:get_hp()
end
return math.huge -- presumably actually immortal
end

View file

@ -0,0 +1,158 @@
local f = string.format
local iall = futil.functional.iall
local map = futil.map
local in_bounds = futil.math.in_bounds
local is_integer = futil.math.is_integer
local is_number = futil.is_number
local is_string = futil.is_string
local is_table = futil.is_table
local function valid_box(value)
if value == nil then
return true
elseif not is_table(value) then
return false
elseif #value ~= 6 then
return false
else
return iall(map(is_number, value))
end
end
local function valid_visual_size(value)
if not is_table(value) then
return false
end
local z_type = type(value.z)
return is_number(value.x) and is_integer(value.y) and (z_type == "number" or z_type == nil)
end
local function valid_textures(value)
if not is_table(value) then
return false
end
return iall(map(is_string, value))
end
local function valid_color_spec(value)
local t = type(value)
if t == "string" then
-- TODO: we could check for valid values, but that's ... tedious
return true
elseif t == "table" then
local is_number_ = is_number
local is_integer_ = is_integer
local in_bounds_ = in_bounds
local x = value.x
local y = value.y
local z = value.z
local a = value.a
return (
is_number_(x)
and in_bounds_(0, x, 255)
and is_integer_(x)
and is_number_(y)
and in_bounds_(0, y, 255)
and is_integer_(y)
and is_number_(z)
and in_bounds_(0, z, 255)
and is_integer_(z)
and (a == nil or (is_number_(a) and in_bounds_(0, a, 255) and is_integer_(a)))
)
end
return false
end
local function valid_colors(value)
if not is_table(value) then
return false
end
return iall(map(valid_color_spec, value))
end
local function valid_spritediv(value)
if not is_table(value) then
return false
end
local x = value.x
local y = value.y
return is_number(x) and is_integer(x) and is_number(y) and is_number(y)
end
local function valid_automatic_face_movement_dir(value)
return value == false or is_number(value)
end
local function valid_hp_max(value)
return is_number(value) and is_integer(value) and in_bounds(1, value, 65535)
end
local object_property = {
visual = "string",
visual_size = valid_visual_size,
mesh = "string",
textures = valid_textures,
colors = valid_colors,
use_texture_alpha = "boolean",
spritediv = valid_spritediv,
initial_sprite_basepos = valid_spritediv,
is_visible = "boolean",
automatic_rotate = "number",
automatic_face_movement_dir = valid_automatic_face_movement_dir,
automatic_face_movement_max_rotation_per_sec = "number",
backface_culling = "number",
glow = "number",
damage_texture_modifier = "string",
shaded = "boolean",
hp_max = valid_hp_max,
physical = "boolean",
pointable = "boolean",
collide_with_objects = "boolean",
collisionbox = valid_box,
selectionbox = valid_box,
makes_footstep_sound = "boolean",
stepheight = "number",
nametag = "string",
nametag_color = valid_color_spec,
nametag_bgcolor = valid_color_spec,
infotext = "string",
static_save = "boolean",
show_on_minimap = "boolean",
}
function futil.is_property_key(key)
return object_property[key] ~= nil
end
function futil.is_valid_property_value(key, value)
local kind = object_property[key]
if not kind then
return false
end
if type(kind) == "string" then
return type(value) == kind
elseif type(kind) == "function" then
return kind(value)
else
error(f("coding error in futil for key %q", key))
end
end

View file

@ -0,0 +1,30 @@
-- before 5.9, raycasts can miss objects they should hit if the cast is too short
-- see https://github.com/minetest/minetest/issues/14337
function futil.safecast(start, stop, objects, liquids, margin)
margin = margin or 5
local ray = stop - start
local ray_length = ray:length()
if ray_length == 0 then
return function() end
elseif ray_length >= margin then
return Raycast(start, stop, objects, liquids)
end
local actual_stop = start + ray:normalize() * margin
local raycast = Raycast(start, actual_stop, objects, liquids)
local stopped = false
return function()
if stopped then
return
end
local pt = raycast()
if pt then
local ip = pt.intersection_point
if (ip - start):length() > ray_length then
stopped = true
return
end
return pt
end
end
end

View file

@ -0,0 +1,15 @@
function futil.make_registration()
local t = {}
local registerfunc = function(func)
t[#t + 1] = func
end
return t, registerfunc
end
function futil.make_registration_reverse()
local t = {}
local registerfunc = function(func)
table.insert(t, 1, func)
end
return t, registerfunc
end

View file

@ -0,0 +1,87 @@
local f = string.format
local deserialize = minetest.deserialize
local pairs_by_key = futil.table.pairs_by_key
function futil.serialize(x)
if type(x) == "number" or type(x) == "boolean" or type(x) == "nil" then
return tostring(x)
elseif type(x) == "string" then
return f("%q", x)
elseif type(x) == "table" then
local parts = {}
for k, v in pairs_by_key(x) do
table.insert(parts, f("[%s] = %s", futil.serialize(k), futil.serialize(v)))
end
return f("{%s}", table.concat(parts, ", "))
else
error(f("can't serialize type %s", type(x)))
end
end
function futil.deserialize(data)
return deserialize(f("return %s", data))
end
function futil.serialize_invlist(inv, listname)
local itemstrings = {}
local list = inv:get_list(listname)
if not list then
error(f("couldn't find list %s of %s", listname, minetest.write_json(inv:get_location())))
end
for _, stack in ipairs(list) do
table.insert(itemstrings, stack:to_string())
end
return futil.serialize(itemstrings)
end
function futil.deserialize_invlist(serialized_list, inv, listname)
if not inv:is_empty(listname) then
error(("trying to deserialize into a non-empty list %s (%s)"):format(listname, serialized_list))
end
local itemstrings = futil.deserialize(serialized_list) or minetest.parse_json(serialized_list)
inv:set_size(listname, #itemstrings)
for i, itemstring in ipairs(itemstrings) do
inv:set_stack(listname, i, ItemStack(itemstring))
end
end
function futil.serialize_inv(inv)
local serialized_lists = {}
for listname in pairs(inv:get_lists()) do
serialized_lists[listname] = futil.serialize_invlist(inv, listname)
end
return futil.serialize(serialized_lists)
end
function futil.deserialize_inv(serialized_lists, inv)
for listname, serialized_list in pairs(futil.deserialize(serialized_lists)) do
futil.deserialize_invlist(serialized_list, inv, listname)
end
end
function futil.serialize_node_meta(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
return futil.serialize({
fields = meta:to_table().fields,
inventory = futil.serialize_inv(inv),
})
end
function futil.deserialize_node_meta(serialized_node_meta, pos)
local meta = minetest.get_meta(pos)
local x = futil.deserialize(serialized_node_meta)
meta:from_table({ fields = x.fields })
local inv = meta:get_inventory()
futil.deserialize_inv(x.inventory, inv)
end

View file

@ -0,0 +1,7 @@
local pi = math.pi
function futil.set_look_dir(player, look_dir)
local pitch = math.asin(-look_dir.y)
local yaw = math.atan2(look_dir.z, look_dir.x)
player:set_look_vertical(pitch)
player:set_look_horizontal((yaw + 1.5 * pi) % (2.0 * pi))
end

View file

@ -0,0 +1,205 @@
local function tokenize(s)
local tokens = {}
local i = 1
local j = 1
while true do
if s:sub(j, j) == "" then
if i < j then
table.insert(tokens, s:sub(i, j - 1))
end
return tokens
elseif s:sub(j, j):byte() == 27 then
if i < j then
table.insert(tokens, s:sub(i, j - 1))
end
i = j
local n = s:sub(i + 1, i + 1)
if n == "(" then
local m = s:sub(i + 2, i + 2)
local k = s:find(")", i + 3, true)
if not k then
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
if m == "T" then
table.insert(tokens, {
type = "translation",
domain = s:sub(i + 4, k - 1),
})
elseif m == "c" then
table.insert(tokens, {
type = "color",
color = s:sub(i + 4, k - 1),
})
elseif m == "b" then
table.insert(tokens, {
type = "bgcolor",
color = s:sub(i + 4, k - 1),
})
else
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
i = k + 1
j = k + 1
elseif n == "F" then
table.insert(tokens, {
type = "start",
})
i = j + 2
j = j + 2
elseif n == "E" then
table.insert(tokens, {
type = "stop",
})
i = j + 2
j = j + 2
else
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
else
j = j + 1
end
end
end
local function parse(tokens, i, parsed)
parsed = parsed or {}
i = i or 1
while i <= #tokens do
local token = tokens[i]
if type(token) == "string" then
table.insert(parsed, token)
i = i + 1
elseif token.type == "color" or token.type == "bgcolor" then
table.insert(parsed, token)
i = i + 1
elseif token.type == "translation" then
local contents = {
type = "translation",
domain = token.domain,
}
i = i + 1
contents, i = parse(tokens, i, contents)
if i == -1 then
return "", -1
end
table.insert(parsed, contents)
elseif token.type == "start" then
local contents = {
type = "escape",
}
i = i + 1
contents, i = parse(tokens, i, contents)
if i == -1 then
return "", -1
end
table.insert(parsed, contents)
elseif token.type == "stop" then
i = i + 1
return parsed, i
else
futil.log("error", "strip_translation: couldn't parse %s", dump(token):gsub("%s+", ""))
return "", -1
end
end
return parsed, i
end
local function unparse_and_strip_translation(parsed, parts)
parts = parts or {}
for _, part in ipairs(parsed) do
if type(part) == "string" then
table.insert(parts, part)
else
if part.type == "bgcolor" then
table.insert(parts, ("\27(b@%s)"):format(part.color))
elseif part.type == "color" then
table.insert(parts, ("\27(c@%s)"):format(part.color))
elseif part.domain then
unparse_and_strip_translation(part, parts)
else
unparse_and_strip_translation(part, parts)
end
end
end
return parts
end
local function erase_after_newline(parsed, erasing)
local single_line_parsed = {}
for _, piece in ipairs(parsed) do
if type(piece) == "string" then
if not erasing then
if piece:find("\n") then
erasing = true
local single_line = piece:match("^([^\n]*)\n")
table.insert(single_line_parsed, single_line)
else
table.insert(single_line_parsed, piece)
end
end
elseif piece.type == "bgcolor" or piece.type == "color" then
table.insert(single_line_parsed, piece)
elseif piece.type == "escape" then
table.insert(single_line_parsed, erase_after_newline(piece, erasing))
elseif piece.type == "translation" then
local stuff = erase_after_newline(piece, erasing)
stuff.domain = piece.domain
table.insert(single_line_parsed, stuff)
else
futil.log("error", "strip_translation: couldn't erase_after_newline %s", dump(parsed):gsub("%s+", ""))
return {}
end
end
return single_line_parsed
end
local function unparse(parsed, parts)
parts = parts or {}
for _, part in ipairs(parsed) do
if type(part) == "string" then
table.insert(parts, part)
else
if part.type == "bgcolor" then
table.insert(parts, ("\27(b@%s)"):format(part.color))
elseif part.type == "color" then
table.insert(parts, ("\27(c@%s)"):format(part.color))
elseif part.domain then
table.insert(parts, ("\27(T@%s)"):format(part.domain))
unparse(part, parts)
table.insert(parts, "\27E")
else
table.insert(parts, "\27F")
unparse(part, parts)
table.insert(parts, "\27E")
end
end
end
return parts
end
function futil.strip_translation(msg)
local tokens = tokenize(msg)
local parsed = parse(tokens)
return table.concat(unparse_and_strip_translation(parsed), "")
end
function futil.get_safe_short_description(item)
item = type(item) == "userdata" and item or ItemStack(item)
local description = item:get_description()
local tokens = tokenize(description)
local parsed = parse(tokens)
local single_line_parsed = erase_after_newline(parsed)
local single_line = table.concat(unparse(single_line_parsed), "")
return single_line
end

View file

@ -0,0 +1,9 @@
-- https://github.com/minetest/minetest/blob/9fc018ded10225589d2559d24a5db739e891fb31/doc/lua_api.txt#L453-L462
function futil.escape_texture(texturestring)
-- store in a variable so we don't return both rvs of gsub
local v = texturestring:gsub("[%^:]", {
["^"] = "\\^",
[":"] = "\\:",
})
return v
end

View file

@ -0,0 +1,7 @@
function futil.wait(us)
local wait_until = minetest.get_us_time() + us
local get_us_time = minetest.get_us_time
while get_us_time() < wait_until do
-- the NOTHING function does nothing.
end
end

View file

@ -0,0 +1,402 @@
local m_abs = math.abs
local m_acos = math.acos
local m_asin = math.asin
local m_atan2 = math.atan2
local m_cos = math.cos
local m_floor = math.floor
local m_min = math.min
local m_max = math.max
local m_pi = math.pi
local m_pow = math.pow
local m_random = math.random
local m_sin = math.sin
local v_add = vector.add
local v_new = vector.new
local v_sort = vector.sort
local v_sub = vector.subtract
local in_bounds = futil.math.in_bounds
local bound = futil.math.bound
local mapblock_size = 16 -- can be redefined, but effectively hard-coded
local chunksize = m_floor(tonumber(minetest.settings:get("chunksize")) or 5) -- # of mapblocks in a chunk (1 dim)
local chunksize_nodes = mapblock_size * chunksize -- # of nodes in a chunk (1 dim)
local max_mapgen_limit = 31007 -- hard coded
local mapgen_limit =
bound(0, m_floor(tonumber(minetest.settings:get("mapgen_limit")) or max_mapgen_limit), max_mapgen_limit)
local mapgen_limit_b = m_floor(mapgen_limit / mapblock_size) -- # of mapblocks
-- *actual* minimum and maximum coordinates - one mapblock short of the theoretical min and max
local map_min_i = (-mapgen_limit_b * mapblock_size) + chunksize_nodes
local map_max_i = ((mapgen_limit_b + 1) * mapblock_size - 1) - chunksize_nodes
local map_min_p = v_new(map_min_i, map_min_i, map_min_i)
local map_max_p = v_new(map_max_i, map_max_i, map_max_i)
futil.vector = {}
function futil.vector.get_bounds(pos, radius)
return v_sub(pos, radius), v_add(pos, radius)
end
futil.get_bounds = futil.vector.get_bounds
function futil.vector.get_world_bounds()
return map_min_p, map_max_p
end
futil.get_world_bounds = futil.vector.get_world_bounds
function futil.vector.get_blockpos(pos)
return v_new(m_floor(pos.x / mapblock_size), m_floor(pos.y / mapblock_size), m_floor(pos.z / mapblock_size))
end
futil.get_blockpos = futil.vector.get_blockpos
function futil.vector.get_block_min(blockpos)
return v_new(blockpos.x * mapblock_size, blockpos.y * mapblock_size, blockpos.z * mapblock_size)
end
function futil.vector.get_block_max(blockpos)
return v_new(
blockpos.x * mapblock_size + (mapblock_size - 1),
blockpos.y * mapblock_size + (mapblock_size - 1),
blockpos.z * mapblock_size + (mapblock_size - 1)
)
end
function futil.vector.get_block_bounds(blockpos)
return futil.vector.get_block_min(blockpos), futil.vector.get_block_max(blockpos)
end
futil.get_block_bounds = futil.vector.get_block_bounds
function futil.vector.get_block_center(blockpos)
return v_add(futil.vector.get_block_min(blockpos), 8) -- 8 = 16 / 2
end
function futil.vector.get_chunkpos(pos)
return v_new(
m_floor((pos.x - map_min_i) / chunksize_nodes),
m_floor((pos.y - map_min_i) / chunksize_nodes),
m_floor((pos.z - map_min_i) / chunksize_nodes)
)
end
futil.get_chunkpos = futil.vector.get_chunkpos
function futil.vector.get_chunk_bounds(chunkpos)
return v_new(
chunkpos.x * chunksize_nodes + map_min_i,
chunkpos.y * chunksize_nodes + map_min_i,
chunkpos.z * chunksize_nodes + map_min_i
),
v_new(
chunkpos.x * chunksize_nodes + map_min_i + (chunksize_nodes - 1),
chunkpos.y * chunksize_nodes + map_min_i + (chunksize_nodes - 1),
chunkpos.z * chunksize_nodes + map_min_i + (chunksize_nodes - 1)
)
end
futil.get_chunk_bounds = futil.vector.get_chunk_bounds
function futil.vector.formspec_pos(pos)
return ("%i,%i,%i"):format(pos.x, pos.y, pos.z)
end
futil.formspec_pos = futil.vector.formspec_pos
function futil.vector.iterate_area(minp, maxp)
minp, maxp = v_sort(minp, maxp)
local min_x = minp.x
local min_z = minp.z
local x = min_x - 1
local y = minp.y
local z = min_z
local max_x = maxp.x
local max_y = maxp.y
local max_z = maxp.z
return function()
if y > max_y then
return
end
x = x + 1
if x > max_x then
x = min_x
z = z + 1
end
if z > max_z then
z = min_z
y = y + 1
end
if y <= max_y then
return v_new(x, y, z)
end
end
end
futil.iterate_area = futil.vector.iterate_area
function futil.vector.iterate_volume(pos, radius)
return futil.iterate_area(futil.get_bounds(pos, radius))
end
futil.iterate_volume = futil.vector.iterate_volume
function futil.is_pos_in_bounds(minp, pos, maxp)
minp, maxp = v_sort(minp, maxp)
return (in_bounds(minp.x, pos.x, maxp.x) and in_bounds(minp.y, pos.y, maxp.y) and in_bounds(minp.z, pos.z, maxp.z))
end
function futil.vector.is_inside_world_bounds(pos)
return futil.is_pos_in_bounds(map_min_p, pos, map_max_p)
end
function futil.vector.is_blockpos_inside_world_bounds(blockpos)
return futil.vector.is_inside_world_bounds(futil.vector.get_block_min(blockpos))
end
futil.is_inside_world_bounds = futil.vector.is_inside_world_bounds
function futil.vector.bound_position_to_world(pos)
return v_new(
bound(map_min_i, pos.x, map_max_i),
bound(map_min_i, pos.y, map_max_i),
bound(map_min_i, pos.z, map_max_i)
)
end
futil.bound_position_to_world = futil.vector.bound_position_to_world
function futil.vector.volume(pos1, pos2)
local minp, maxp = v_sort(pos1, pos2)
return (maxp.x - minp.x + 1) * (maxp.y - minp.y + 1) * (maxp.z - minp.z + 1)
end
function futil.split_region_by_mapblock(pos1, pos2, num_blocks)
local chunk_size = 16 * (num_blocks or 1)
local chunk_span = chunk_size - 1
pos1, pos2 = vector.sort(pos1, pos2)
local min_x = pos1.x
local min_y = pos1.y
local min_z = pos1.z
local max_x = pos2.x
local max_y = pos2.y
local max_z = pos2.z
local x1 = min_x - (min_x % chunk_size)
local x2 = max_x - (max_x % chunk_size) + chunk_span
local y1 = min_y - (min_y % chunk_size)
local y2 = max_y - (max_y % chunk_size) + chunk_span
local z1 = min_z - (min_z % chunk_size)
local z2 = max_z - (max_z % chunk_size) + chunk_span
local chunks = {}
for y = y1, y2, chunk_size do
local y_min = m_max(min_y, y)
local y_max = m_min(max_y, y + chunk_span)
for x = x1, x2, chunk_size do
local x_min = m_max(min_x, x)
local x_max = m_min(max_x, x + chunk_span)
for z = z1, z2, chunk_size do
local z_min = m_max(min_z, z)
local z_max = m_min(max_z, z + chunk_span)
chunks[#chunks + 1] = { v_new(x_min, y_min, z_min), v_new(x_max, y_max, z_max) }
end
end
end
return chunks
end
function futil.random_unit_vector()
local u = m_random()
local v = m_random()
local lambda = m_acos(2 * u - 1) - (m_pi / 2)
local phi = 2 * m_pi * v
return v_new(m_cos(lambda) * m_cos(phi), m_cos(lambda) * m_sin(phi), m_sin(lambda))
end
---- https://math.stackexchange.com/a/205589
--function futil.random_unit_vector_in_solid_angle(theta, direction)
-- local z = m_random() * (1 - m_cos(theta)) - 1
-- local phi = m_random() * 2 * m_pi
-- local z2 = (1 - z*z) ^ 0.5
-- local ruv = v_new(z2 * m_cos(phi), z2 * m_sin(phi), z)
-- direction = direction:normalize()
-- ...
--end
function futil.is_indoors(pos, distance, trials, hits_needed)
distance = distance or 20
trials = trials or 11
hits_needed = hits_needed or 9
local num_hits = 0
for _ = 1, trials do
local ruv = futil.random_unit_vector()
local target = pos + (distance * ruv)
local hit = Raycast(pos, target, false, false)()
if hit then
num_hits = num_hits + 1
if num_hits == hits_needed then
return true
end
end
end
return false
end
function futil.can_see_sky(pos, distance, trials, max_hits)
distance = distance or 200
trials = trials or 11
max_hits = max_hits or 9
local num_hits = 0
for _ = 1, trials do
local ruv = futil.random_unit_vector()
ruv.y = m_abs(ruv.y) -- look up, not at the ground
local target = pos + (distance * ruv)
local hit = Raycast(pos, target, false, false)()
if hit then
num_hits = num_hits + 1
if num_hits > max_hits then
return false
end
end
end
return true
end
function futil.vector.is_valid_position(pos)
if type(pos) ~= "table" then
return false
elseif not (type(pos.x) == "number" and type(pos.y) == "number" and type(pos.z) == "number") then
return false
else
return futil.is_inside_world_bounds(vector.round(pos))
end
end
-- minetest.hash_node_position only works with integer coordinates
function futil.vector.hash(pos)
return string.format("%a:%a:%a", pos.x, pos.y, pos.z)
end
function futil.vector.unhash(string)
local x, y, z = string:match("^([^:]+):([^:]+):([^:]+)$")
x, y, z = tonumber(x), tonumber(y), tonumber(z)
if not (x and y and z) then
return
end
return v_new(x, y, z)
end
function futil.vector.ldistance(pos1, pos2, p)
if p == math.huge then
return m_max(m_abs(pos1.x - pos2.x), m_abs(pos1.y - pos2.y), m_abs(pos1.z - pos2.z))
else
return m_pow(
m_pow(m_abs(pos1.x - pos2.x), p) + m_pow(m_abs(pos1.y - pos2.y), p) + m_pow(m_abs(pos1.z - pos2.z), p),
1 / p
)
end
end
function futil.vector.round(pos, mult)
local round = futil.math.round
return v_new(round(pos.x, mult), round(pos.y, mult), round(pos.z, mult))
end
-- https://msl.cs.uiuc.edu/planning/node102.html
function futil.vector.rotation_to_matrix(rotation)
local cosp = m_cos(rotation.x)
local sinp = m_sin(rotation.x)
local pitch = {
{ cosp, 0, sinp },
{ 0, 1, 0 },
{ -sinp, 0, cosp },
}
local cosy = m_cos(rotation.y)
local siny = m_sin(rotation.y)
local yaw = {
{ cosy, -siny, 0 },
{ siny, cosy, 0 },
{ 0, 0, 1 },
}
local cosr = m_cos(rotation.z)
local sinr = m_sin(rotation.z)
local roll = {
{ 1, 0, 0 },
{ 0, cosr, -sinr },
{ 0, sinr, cosr },
}
return futil.matrix.multiply(futil.matrix.multiply(yaw, pitch), roll)
end
-- https://msl.cs.uiuc.edu/planning/node103.html
function futil.vector.matrix_to_rotation(matrix)
local pitch = m_atan2(matrix[2][1], matrix[1][1])
local yaw = m_asin(-matrix[3][1])
local roll = m_atan2(matrix[3][2], matrix[3][3])
return v_new(pitch, yaw, roll)
end
function futil.vector.inverse_rotation(rot)
-- since the determinant of a rotation matrix is 1, the inverse is just the transpose and i don't have to write
-- a matrix inverter
return futil.vector.matrix_to_rotation(futil.matrix.transpose(futil.vector.rotation_to_matrix(rot)))
end
-- assumed in radians
function futil.vector.compose_rotations(rot1, rot2)
local m1 = futil.vector.rotation_to_matrix(rot1)
local m2 = futil.vector.rotation_to_matrix(rot2)
return futil.vector.matrix_to_rotation(futil.matrix.multiply(m1, m2))
end
-- https://palitri.com/vault/stuff/maths/Rays%20closest%20point.pdf
-- this was originally part of the ballistics mod but i don't need it there anymore
function futil.vector.closest_point_to_two_lines(last_pos, last_vel, cur_pos, cur_vel, threshold)
threshold = threshold or 0.0001 -- if certain values are too close to 0, the results will not be good
local a = cur_vel
local b = last_vel
local a2 = a:dot(a)
if a2 < threshold then
return
end
local b2 = b:dot(b)
if b2 < threshold then
return
end
local ab = a:dot(b)
local denom = (a2 * b2) - (ab * ab)
if denom < threshold then
return
end
local A = cur_pos
local B = last_pos
local c = last_pos - cur_pos
local bc = b:dot(c)
local ac = a:dot(c)
local D = A + a * ((ac * b2 - ab * bc) / denom)
local E = B + b * ((ab * ac - bc * a2) / denom)
return (D + E) / 2
end
function futil.vector.v2f_to_float_32(v)
return {
x = futil.math.to_float32(v.x),
y = futil.math.to_float32(v.y),
}
end