Charakterbewegungen hinzugefügt, Deko hinzugefügt, Kochrezepte angepasst
This commit is contained in:
parent
95945c0306
commit
a0c893ca0b
1124 changed files with 64294 additions and 763 deletions
186
mods/futil/minetest/box.lua
Normal file
186
mods/futil/minetest/box.lua
Normal 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
|
31
mods/futil/minetest/dedupe.lua
Normal file
31
mods/futil/minetest/dedupe.lua
Normal 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
|
111
mods/futil/minetest/dump.lua
Normal file
111
mods/futil/minetest/dump.lua
Normal 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
|
271
mods/futil/minetest/fake_inventory.lua
Normal file
271
mods/futil/minetest/fake_inventory.lua
Normal 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
|
88
mods/futil/minetest/globalstep.lua
Normal file
88
mods/futil/minetest/globalstep.lua
Normal 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
|
61
mods/futil/minetest/group.lua
Normal file
61
mods/futil/minetest/group.lua
Normal 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
|
100
mods/futil/minetest/hud_ephemeral.lua
Normal file
100
mods/futil/minetest/hud_ephemeral.lua
Normal 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
|
172
mods/futil/minetest/hud_manager.lua
Normal file
172
mods/futil/minetest/hud_manager.lua
Normal 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)
|
171
mods/futil/minetest/image.lua
Normal file
171
mods/futil/minetest/image.lua
Normal 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
|
24
mods/futil/minetest/init.lua
Normal file
24
mods/futil/minetest/init.lua
Normal 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
|
40
mods/futil/minetest/inventory.lua
Normal file
40
mods/futil/minetest/inventory.lua
Normal 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
|
133
mods/futil/minetest/item.lua
Normal file
133
mods/futil/minetest/item.lua
Normal 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
|
200
mods/futil/minetest/object.lua
Normal file
200
mods/futil/minetest/object.lua
Normal 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
|
158
mods/futil/minetest/object_properties.lua
Normal file
158
mods/futil/minetest/object_properties.lua
Normal 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
|
30
mods/futil/minetest/raycast.lua
Normal file
30
mods/futil/minetest/raycast.lua
Normal 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
|
15
mods/futil/minetest/registration.lua
Normal file
15
mods/futil/minetest/registration.lua
Normal 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
|
87
mods/futil/minetest/serialization.lua
Normal file
87
mods/futil/minetest/serialization.lua
Normal 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
|
7
mods/futil/minetest/set_look_dir.lua
Normal file
7
mods/futil/minetest/set_look_dir.lua
Normal 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
|
205
mods/futil/minetest/strip_translation.lua
Normal file
205
mods/futil/minetest/strip_translation.lua
Normal 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
|
9
mods/futil/minetest/texture.lua
Normal file
9
mods/futil/minetest/texture.lua
Normal 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
|
7
mods/futil/minetest/time.lua
Normal file
7
mods/futil/minetest/time.lua
Normal 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
|
402
mods/futil/minetest/vector.lua
Normal file
402
mods/futil/minetest/vector.lua
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue