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

View file

@ -0,0 +1,125 @@
-- pack bits into doubles
-- this is quite slow compared to how you'd do this in c or something.
-- there's https://bitop.luajit.org/api.html but that seems limited to 32 bits. we can use the full 53 bit mantissa.
local BITS_PER_NUMBER = 53
local f = string.format
local m_floor = math.floor
local s_byte = string.byte
local s_char = string.char
local BitArray = futil.class1()
function BitArray:_init(size_or_bitmap)
if type(size_or_bitmap) == "number" then
local data = {}
self._size = size_or_bitmap
for i = 1, math.ceil(size_or_bitmap / BITS_PER_NUMBER) do
data[i] = 0
end
self._data = data
elseif type(size_or_bitmap) == "table" then
if size_or_bitmap.is_a and size_or_bitmap:is_a(BitArray) then
self._size = size_or_bitmap._size
self._data = table.copy(size_or_bitmap._data)
end
end
if not self._data then
error("bitmap must be initialized w/ a size or another bitmap")
end
end
function BitArray:__eq(other)
if self._size ~= other._size then
return false
end
for i = 1, self._size do
if self._data[i] ~= other._data[i] then
return false
end
end
return true
end
local function get_bit(n, j)
n = n % (2 ^ j)
return m_floor(n / (2 ^ (j - 1))) == 1
end
function BitArray:get(k)
if type(k) ~= "number" then
return nil
elseif k <= 0 or k > self._size then
return nil
end
local i = math.ceil(k / BITS_PER_NUMBER)
local n = self._data[i]
local j = ((k - 1) % BITS_PER_NUMBER) + 1
return get_bit(n, j)
end
local function set_bit(n, j, v)
local current = get_bit(n, j)
if current == v then
return n
elseif v then
return n + (2 ^ (j - 1))
else
return n - (2 ^ (j - 1))
end
end
function BitArray:set(k, v)
if type(v) == "number" then
if v < 0 or v > 1 then
error(f("invalid argument %s", v))
end
v = v == 1
elseif type(v) ~= "boolean" then
error(f("invalid argument of type %s", type(v)))
end
local i = math.ceil(k / BITS_PER_NUMBER)
local n = self._data[i]
local j = ((k - 1) % BITS_PER_NUMBER) + 1
self._data[i] = set_bit(n, j, v)
end
function BitArray:serialize()
local data = self._data
local parts = {}
for i = 1, #data do
local datum = data[i]
parts[i] = s_char(datum % 256)
.. s_char(m_floor(datum / 256) % 256)
.. s_char(m_floor(datum / (256 ^ 2)) % 256)
.. s_char(m_floor(datum / (256 ^ 3)) % 256)
.. s_char(m_floor(datum / (256 ^ 4)) % 256)
.. s_char(m_floor(datum / (256 ^ 5)) % 256)
.. s_char(m_floor(datum / (256 ^ 6)) % 256)
end
return table.concat(parts, "")
end
function BitArray.deserialize(s)
if type(s) ~= "string" then
error(f("invalid argument of type %s", type(s)))
elseif #s % 7 ~= 0 then
error(f("invalid serialized string (wrong length)"))
end
local ba = BitArray(#s / 7)
local i = 1
for a = 1, #s, 7 do
local bs = {}
for j = 0, 6 do
bs[j + 1] = s_byte(s:sub(a + j, a + j))
end
ba._data[i] = bs[1]
+ 256 * (bs[2] + 256 * (bs[3] + 256 * (bs[4] + 256 * (bs[5] + 256 * (bs[6] + 256 * bs[7])))))
i = i + 1
end
return ba
end
futil.BitArray = BitArray

View file

@ -0,0 +1,11 @@
-- https://www.lua.org/pil/13.4.3.html
function futil.DefaultTable(initializer)
return setmetatable({}, {
__index = function(t, k)
local v = initializer(k)
t[k] = v
return v
end,
})
end

View file

@ -0,0 +1,88 @@
-- inspired by https://www.lua.org/pil/11.4.html
local Deque = futil.class1()
function Deque:_init(def)
self._a = 0
self._z = -1
self._m = def and def.max_size
end
function Deque:size()
return self._z - self._a + 1
end
function Deque:push_front(value)
local max_size = self._m
if max_size and (self._z - self._a + 1) >= max_size then
return false
end
local a = self._a - 1
self._a = a
self[a] = value
return true, a
end
function Deque:peek_front()
return self[self._a]
end
function Deque:pop_front()
local a = self._a
if a > self._z then
return nil
end
local value = self[a]
self[a] = nil
self._a = a + 1
return value
end
function Deque:push_back(value)
local max_size = self._m
if max_size and (self._z - self._a + 1) >= max_size then
return false
end
local z = self._z + 1
self._z = z
self[z] = value
return true, z
end
function Deque:peek_back()
return self[self._z]
end
function Deque:pop_back()
local z = self._z
if self._a > z then
return nil
end
local value = self[z]
self[z] = nil
self._z = z + 1
return value
end
-- this iterator is kinda wonky, and the behavior may be changed in the future.
-- unexpected behavior may result from modifying a deque *while* iterating it.
-- note that you *cannot* iterate the deque directly using `pairs()` because of e.g. "_a" and "_z"
function Deque:iterate()
local i = self._a - 1
return function()
i = i + 1
return self[i]
end
end
function Deque:clear()
for k in pairs(self) do
if type(k) == "number" then
self[k] = nil
end
end
self._a = 0
self._z = -1
end
futil.Deque = Deque

View file

@ -0,0 +1,7 @@
futil.dofile("data_structures", "bitarray")
futil.dofile("data_structures", "default_table")
futil.dofile("data_structures", "deque")
futil.dofile("data_structures", "pairing_heap")
futil.dofile("data_structures", "point_search_tree")
futil.dofile("data_structures", "set")
futil.dofile("data_structures", "sparse_graph") -- requires default_table and set

View file

@ -0,0 +1,156 @@
-- https://en.wikipedia.org/wiki/Pairing_heap
-- https://www.cs.cmu.edu/~sleator/papers/pairing-heaps.pdf
-- https://www.cise.ufl.edu/~sahni/dsaaj/enrich/c13/pairing.htm
local inf = math.huge
local function add_child(node1, node2)
node2.parent = node1
node2.sibling = node1.child
node1.child = node2
end
local function meld(node1, node2)
if node1 == nil or node1.value == nil then
return node2
elseif node2 == nil or node2.value == nil then
return node1
elseif node1.priority > node2.priority then
add_child(node1, node2)
return node1
else
add_child(node2, node1)
return node2
end
end
local function merge_pairs(node)
if node.value == nil or not node.sibling then
return
end
local sibling = node.sibling
local siblingsibling = sibling.sibling
node.sibling = nil
sibling.sibling = nil
node = meld(node, sibling)
if siblingsibling then
return meld(node, merge_pairs(siblingsibling))
else
return node
end
end
local function cut(node)
local parent = node.parent
if parent.child == node then
parent.child = node.sibling
else
parent = parent.child
local sibling = parent.sibling
while sibling ~= node do
parent = sibling
sibling = parent.sibling
end
parent.sibling = node.sibling
end
node.parent = nil
node.sibling = nil
end
local function need_to_move(node, new_priority)
local cur_priority = node.priority
if cur_priority < new_priority then
-- priority increase, make sure we don't dominate our parent
local parent = node.parent
return (parent and new_priority > parent.priority)
elseif cur_priority > new_priority then
-- priority decrease, make sure our children don't dominate us
local child = node.child
while child and child ~= node do
if child.priority > new_priority then
return true
end
child = child.sibling
end
return false
else
return false
end
end
local PairingHeap = futil.class1()
function PairingHeap:_new()
self._nodes_by_value = {}
self._size = 0
end
function PairingHeap:size()
return self._size
end
function PairingHeap:peek()
local hn = self._max_node
if not hn then
error("empty")
end
return hn.value, hn.priority
end
function PairingHeap:remove(value)
self:set_priority(value, inf)
return self:pop()
end
function PairingHeap:pop()
local max = self._max_node
if not max then
error("empty")
end
local child = max.child
if child then
self._max_node = merge_pairs(child)
end
local value = max._value
self._nodes_by_value[value] = nil
self._size = self._size - 1
return value
end
function PairingHeap:get_priority(value)
return self._nodes_by_value[value].priority
end
function PairingHeap:set_priority(value, priority)
local cur_node = self._nodes_by_value[value]
if cur_node then
local need_to = need_to_move(cur_node, priority)
if need_to then
cut(cur_node)
self._max_node = meld(cur_node, self._max_node)
else
cur_node.priority = priority
end
else
local node = { value = value, priority = priority }
self._nodes_by_value[value] = node
self._max_node = meld(self._max_node, node)
self._size = self._size + 1
end
end
futil.PairingHeap = PairingHeap

View file

@ -0,0 +1,268 @@
--[[
a data structure which can efficiently retrieve values within specific rectangular regions of 3d space.
the closest relevant descriptions of this problem and solution are in the following:
https://en.wikipedia.org/wiki/Min/max_kd-tree
https://medium.com/omarelgabrys-blog/geometric-applications-of-bsts-e58f0a5019f3
creation is O(n log n)
finding objects in a region is O(m + log n) if there's m objects in the region.
the hope here is that this will provide a faster alternative to `minetest.get_objects_in_area()`. that currently
iterates over *all* active objects in the world, which can be slow when there's ten thousand or more of objects in the
world, and you are only interested in a few of them. in particular, the your-land server usually has 5-8 thousand
objects loaded at once, and can have hundreds of mobs calling `get_objects_in_area` every couple server steps. perftop
definitively implicated this routine as being a major source of lag. the question, now, is what to do about it.
the current implementation doesn't allow for insertion, deletion, or changes in location. if your points move around,
you're gonna have to rebuild the whole tree from scratch, which currently limits the usefulness.
TODO: read this and incorporate if applicable: https://arxiv.org/abs/1410.5420
== footnotes about the algorithms ==
currently, we're hard-coding the usage of the [median of medians](https://en.wikipedia.org/wiki/Median_of_medians)
algorithm as the pivot strategy, as this resulted in unexpectedly dramatic improvements over random selection in
some informal performance tests i did.
== footnotes about results ==
currently, performance can range from taking 1/20th of the time of the engine call, to 100 times as long. this
makes me realize that this is worth pursuing, but probably this will need to be ported to c++ to consistently provide
a benefit. but, i'll absolutely need to figure out a self-balancing strategy before that'll be appropriate.
]]
local sort = table.sort
local in_area = vector.in_area
or function(pos, pmin, pmax)
local x, y, z = pos.x, pos.y, pos.z
return pmin.x <= x and x <= pmax.x and pmin.y <= y and y <= pmax.y and pmin.z <= z and z <= pmax.z
end
local axes = { "x", "y", "z" }
local POS = 1
local VALUE = 2
local Leaf = futil.class1()
function Leaf:_init(pos_and_value)
self[POS] = pos_and_value[POS]
self[VALUE] = pos_and_value[VALUE]
end
local Node = futil.class1()
function Node:_init(min, max, left, right)
self.min = min
self.max = max
self.left = left
self.right = right
end
local PointSearchTree = futil.class1()
futil.min_median_max = {}
function futil.min_median_max.sort(t, indexer)
if indexer then
sort(t, function(a, b)
return indexer(a) < indexer(b)
end)
else
sort(t)
end
return t[1], math.floor(#t / 2), t[#t]
end
function futil.min_median_max.gen_select(pivot_alg)
return function(t, indexer)
local median = futil.selection.select(t, pivot_alg, function(a, b)
return indexer(a) < indexer(b)
end)
local min = indexer(t[1])
local max = min
for i = 2, #t do
local v = indexer(t[i])
if v < min then
min = v
elseif v > max then
max = v
end
end
return min, median, max
end
end
local function bisect(pos_and_values, axis_i, min_median_max)
if #pos_and_values == 1 then
return Leaf(pos_and_values[1])
end
local axis = axes[axis_i]
local min, median, max = min_median_max(pos_and_values, function(i)
return i[POS][axis]
end)
local next_axis_i = (axis_i % #axes) + 1
return Node(
min,
max,
bisect({ unpack(pos_and_values, 1, median) }, next_axis_i, min_median_max),
bisect({ unpack(pos_and_values, median + 1) }, next_axis_i, min_median_max)
)
end
function PointSearchTree:_init(pos_and_values, min_median_max)
pos_and_values = pos_and_values or {}
min_median_max = min_median_max or futil.min_median_max.gen_select(futil.selection.pivot.median_of_medians)
self._len = #pos_and_values
if #pos_and_values > 0 then
self._root = bisect(pos_and_values, 1, min_median_max)
end
end
-- -DLUAJIT_ENABLE_LUA52COMPAT
function PointSearchTree:__len()
return self._len
end
function PointSearchTree:dump()
local function getlines(node, axis_i)
local axis = axes[axis_i]
if not node then
return {}
elseif node:is_a(Leaf) then
return { minetest.pos_to_string(node[POS], 1) }
else
local lines = {}
for _, line in ipairs(getlines(node.left, (axis_i % #axes) + 1)) do
lines[#lines + 1] = string.format("%s=[%.1f,%.1f] %s", axis, node.min, node.max, line)
end
for _, line in ipairs(getlines(node.right, (axis_i % #axes) + 1)) do
lines[#lines + 1] = string.format("%s=[%.1f,%.1f] %s", axis, node.min, node.max, line)
end
return lines
end
end
return table.concat(getlines(self._root, 1), "\n")
end
local function make_iterator(pmin, pmax, predicate, accumulate)
local function iterate(node, axis_i)
local next_axis_i = (axis_i % 3) + 1
local next_axis = axes[next_axis_i]
local left = node.left
if left then
if left:is_a(Leaf) then
if predicate(left) then
accumulate(left)
end
elseif pmin[next_axis] <= left.max then
iterate(left, next_axis_i)
end
end
local right = node.right
if right then
if right:is_a(Leaf) then
if predicate(right) then
accumulate(right)
end
elseif right.min <= pmax[next_axis] then
iterate(right, next_axis_i)
end
end
end
return iterate
end
function PointSearchTree:iterate_values_in_area(pmin, pmax)
if not self._root then
return function() end
end
pmin, pmax = vector.sort(pmin, pmax)
if self._root.max < pmin.x or pmax.x < self._root.min then
return function() end
end
return coroutine.wrap(function()
make_iterator(pmin, pmax, function(leaf)
return in_area(leaf[POS], pmin, pmax)
end, function(leaf)
coroutine.yield(leaf[POS], leaf[VALUE])
end)(self._root, 1)
end)
end
function PointSearchTree:get_values_in_area(pmin, pmax)
local via = {}
if not self._root then
return via
end
pmin, pmax = vector.sort(pmin, pmax)
if self._root.max < pmin.x or pmax.x < self._root.min then
return via
end
make_iterator(pmin, pmax, function(leaf)
return in_area(leaf[POS], pmin, pmax)
end, function(leaf)
via[#via + 1] = leaf[VALUE]
end)(self._root, 1)
return via
end
function PointSearchTree:iterate_values_inside_radius(center, radius)
if not self._root then
return function() end
end
local pmin = vector.subtract(center, radius)
local pmax = vector.add(center, radius)
if self._root.max < pmin.x or pmax.x < self._root.min then
return function() end
end
local v_distance = vector.distance
return coroutine.wrap(function()
make_iterator(pmin, pmax, function(leaf)
return v_distance(center, leaf[POS]) <= radius
end, function(leaf)
coroutine.yield(leaf[POS], leaf[VALUE])
end)(self._root, 1)
end)
end
function PointSearchTree:get_values_inside_radius(center, radius)
local vir = {}
if not self._root then
return vir
end
local pmin = vector.subtract(center, radius)
local pmax = vector.add(center, radius)
if self._root.max < pmin.x or pmax.x < self._root.min then
return vir
end
local v_distance = vector.distance
make_iterator(pmin, pmax, function(leaf)
return v_distance(center, leaf[POS]) <= radius
end, function(leaf)
vir[#vir + 1] = leaf[VALUE]
end)(self._root, 1)
return vir
end
futil.PointSearchTree = PointSearchTree

View file

@ -0,0 +1,267 @@
-- based more-or-less on python's set
local f = string.format
local Set = futil.class1()
local function is_a_set(thing)
return type(thing) == "table" and type(thing.is_a) == "function" and thing:is_a(Set)
end
function Set:_init(t_or_i)
self._size = 0
self._set = {}
if t_or_i then
if type(t_or_i.is_a) == "function" then
if t_or_i:is_a(Set) then
self._set = table.copy(t_or_i._set)
self._size = t_or_i._size
else
for v in t_or_i:iterate() do
self:add(v)
end
end
elseif type(t_or_i) == "table" then
for i = 1, #t_or_i do
self:add(t_or_i[i])
end
elseif type(t_or_i) == "function" or getmetatable(t_or_i).__call then
for v in t_or_i do
self:add(v)
end
else
error(f("unknown argument of type %s", type(t_or_i)))
end
end
end
-- turn a table like {foo=true, bar=true} into a Set
function Set.convert(t)
local set = Set()
set._set = t
set._size = futil.table.size(t)
return set
end
-- -DLUAJIT_ENABLE_LUA52COMPAT
function Set:__len()
return self._size
end
function Set:len()
return self._size
end
function Set:size()
return self._size
end
function Set:is_empty()
return self._size == 0
end
function Set:__tostring()
local elements = {}
for element in pairs(self._set) do
elements[#elements + 1] = f("%q", element)
end
return f("Set({%s})", table.concat(elements, ", "))
end
function Set:__eq(other)
if not is_a_set(other) then
return false
end
for k in pairs(self._set) do
if not other._set[k] then
return false
end
end
return self._size == other._size
end
function Set:contains(element)
return self._set[element] == true
end
function Set:add(element)
if not self:contains(element) then
self._set[element] = true
self._size = self._size + 1
end
end
function Set:remove(element)
if not self:contains(element) then
error(f("set does not contain %s", element))
end
self._set[element] = nil
self._size = self._size - 1
end
function Set:discard(element)
if self:contains(element) then
self._set[element] = nil
self._size = self._size - 1
end
end
function Set:clear()
self._set = {}
self._size = 0
end
function Set:iterate()
return futil.table.ikeys(self._set)
end
function Set:intersects(other)
if not is_a_set(other) then
other = Set(other)
end
local smaller, bigger
if other:size() < self:size() then
smaller = other
bigger = self
else
smaller = self
bigger = other
end
for element in smaller:iterate() do
if bigger:contains(element) then
return true
end
end
return false
end
function Set:isdisjoint(other)
if not is_a_set(other) then
other = Set(other)
end
local smaller, bigger
if other:size() < self:size() then
smaller = other
bigger = self
else
smaller = self
bigger = other
end
for element in smaller:iterate() do
if bigger:contains(element) then
return false
end
end
return true
end
function Set:issubset(other)
if not is_a_set(other) then
other = Set(other)
end
if self:size() > other:size() then
return false
end
for element in self:iterate() do
if not other:contains(element) then
return false
end
end
return true
end
function Set:__le(other)
return self:issubset(other)
end
function Set:__lt(other)
if not is_a_set(other) then
other = Set(other)
end
if self:size() >= other:size() then
return false
end
for element in self:iterate() do
if not other:contains(element) then
return false
end
end
return true
end
function Set:issuperset(other)
if not is_a_set(other) then
other = Set(other)
end
return other:issubset(self)
end
function Set:update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in other:iterate() do
self:add(element)
end
end
function Set:union(other)
if not is_a_set(other) then
other = Set(other)
end
local union = Set(self)
union:update(other)
return union
end
function Set:__add(other)
return self:union(other)
end
function Set:intersection_update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in self:iterate() do
if not other:contains(element) then
self:remove(element)
end
end
end
function Set:intersection(other)
if not is_a_set(other) then
other = Set(other)
end
local intersection = Set()
for element in self:iterate() do
if other:contains(element) then
intersection:add(element)
end
end
return intersection
end
function Set:difference_update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in other:iterate() do
self:discard(element)
end
end
function Set:difference(other)
if not is_a_set(other) then
other = Set(other)
end
local difference = Set(self)
difference:difference_update(other)
return difference
end
function Set:__sub(other)
return self:difference(other)
end
futil.Set = Set

View file

@ -0,0 +1,32 @@
local f = string.format
local SparseGraph = futil.class1()
function SparseGraph:_init(size)
self._size = size or 0
self._adj_by_vertex = futil.DefaultTable(function()
return futil.Set()
end)
end
function SparseGraph:size()
return self._size
end
function SparseGraph:add_vertex()
self._size = self._size + 1
end
function SparseGraph:add_edge(a, b)
assert(1 <= a and a <= self._size, f("invalid vertex a %s", a))
assert(1 <= b and b <= self._size, f("invalid vertex b %s", b))
self._adj_by_vertex[a]:add(b)
end
function SparseGraph:has_edge(a, b)
assert(1 <= a and a <= self._size, f("invalid vertex a %s", a))
assert(1 <= b and b <= self._size, f("invalid vertex b %s", b))
return self._adj_by_vertex[a]:contains(b)
end
futil.SparseGraph = SparseGraph