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,51 @@
futil.bisect = {}
function futil.bisect.right(t, x, low, high, key)
low = low or 1
high = high or #t + 1
if key then
while low < high do
local mid = math.floor((low + high) / 2)
if x < key(t[mid]) then
high = mid
else
low = mid + 1
end
end
else
while low < high do
local mid = math.floor((low + high) / 2)
if x < t[mid] then
high = mid
else
low = mid + 1
end
end
end
return low
end
function futil.bisect.left(t, x, low, high, key)
low = low or 1
high = high or #t + 1
if key then
while low < high do
local mid = math.floor((low + high) / 2)
if key(t[mid]) < x then
low = mid + 1
else
high = mid
end
end
else
while low < high do
local mid = math.floor((low + high) / 2)
if t[mid] < x then
low = mid + 1
else
high = mid
end
end
end
return low
end

89
mods/futil/util/class.lua Normal file
View file

@ -0,0 +1,89 @@
function futil.class1(super)
local class = {}
class.__index = class -- this becomes the index "metamethod" of objects
setmetatable(class, {
__index = super and super.__index or super,
__call = function(this_class, ...)
local obj = setmetatable({}, this_class)
local init = obj._init
if init then
init(obj, ...)
end
return obj
end,
})
function class:is_a(class2)
if class == class2 then
return true
end
if super and super:is_a(class2) then
return true
end
return false
end
return class
end
function futil.class(...)
local class = {}
class.__index = class
local meta = {
__call = function(this_class, ...)
local obj = setmetatable({}, this_class)
local init = obj._init
if init then
init(obj, ...)
end
return obj
end,
}
local parents = { ... }
class._parents = parents
if #parents > 0 then
function meta:__index(key)
for i = #parents, 1, -1 do
local parent = parents[i]
local index = parent.__index
local v
if index then
if type(index) == "function" then
v = index(self, key)
else
v = index[key]
end
else
v = parent[key]
end
if v then
return v
end
end
end
end
setmetatable(class, meta)
function class:is_a(class2)
if class == class2 then
return true
end
for _, parent in ipairs(parents) do
if parent:is_a(class2) then
return true
end
end
return false
end
return class
end

View file

@ -0,0 +1,9 @@
function futil.coalesce(...)
local arg = futil.table.pack(...)
for i = 1, arg.n do
local v = arg[i]
if v ~= nil then
return v
end
end
end

View file

@ -0,0 +1,28 @@
local table_size = futil.table.size
local function equals(a, b)
local t = type(a)
if t ~= type(b) then
return false
end
if t ~= "table" then
return a == b
elseif a == b then
return true
end
local size_a = 0
for key, value in pairs(a) do
if not equals(value, b[key]) then
return false
end
size_a = size_a + 1
end
return size_a == table_size(b)
end
futil.equals = equals

View file

@ -0,0 +1,29 @@
function futil.safe_wrap(func, rv_on_fail, error_callback)
-- wrap a function w/ logic to avoid crashing
return function(...)
local rvs = { xpcall(func, debug.traceback, ...) }
if rvs[1] then
return unpack(rvs, 2)
else
if error_callback then
error_callback(debug.getinfo(func), { ... }, rvs[2])
else
futil.log(
"error",
"(check_call): %s args: %s out: %s",
dump(debug.getinfo(func)),
dump({ ... }),
rvs[2]
)
end
return rv_on_fail
end
end
end
function futil.safe_call(func, rv_on_fail, error_callback, ...)
return futil.safe_wrap(func, rv_on_fail, error_callback)(...)
end
futil.check_call = futil.safe_wrap -- backwards compatibility

37
mods/futil/util/file.lua Normal file
View file

@ -0,0 +1,37 @@
function futil.file_exists(path)
local f = io.open(path, "r")
if f then
io.close(f)
return true
else
return false
end
end
function futil.load_file(filename)
local file = io.open(filename, "r")
if not file then
return
end
local contents = file:read("*a")
file:close()
return contents
end
-- minetest.safe_file_write is apparently unreliable on windows
function futil.write_file(filename, contents)
local file = io.open(filename, "w")
if not file then
return false
end
file:write(contents)
file:close()
return true
end

View file

@ -0,0 +1,159 @@
local functional = {}
local t_iterate = futil.table.iterate
local t_insert = table.insert
function functional.noop()
-- the NOTHING function does nothing.
end
function functional.identity(x)
return x
end
function functional.izip(...)
local is = { ... }
if #is == 0 then
return functional.noop
end
return function()
local t = {}
for i in t_iterate(is) do
local v = i()
if v ~= nil then
t_insert(t, v)
else
return
end
end
return t
end
end
function functional.zip(...)
local is = {}
for t in t_iterate({ ... }) do
t_insert(is, t_iterate(t))
end
return functional.izip(unpack(is))
end
function functional.imap(func, ...)
local zipper = functional.izip(...)
return function()
local args = zipper()
if args then
return func(unpack(args))
end
end
end
function functional.map(func, ...)
local zipper = functional.zip(...)
return function()
local args = zipper()
if args then
return func(unpack(args))
end
end
end
function functional.apply(func, t)
local t2 = {}
for k, v in pairs(t) do
t2[k] = func(v)
end
return t2
end
function functional.reduce(func, t, initial)
local i = t_iterate(t)
if not initial then
initial = i()
end
local next = i()
while next do
initial = func(initial, next)
next = i()
end
return initial
end
function functional.partial(func, ...)
local args = { ... }
return function(...)
return func(unpack(args), ...)
end
end
function functional.compose(a, b)
return function(...)
return a(b(...))
end
end
function functional.ifilter(pred, i)
local v
return function()
v = i()
while v ~= nil and not pred(v) do
v = i()
end
return v
end
end
function functional.filter(pred, t)
return functional.ifilter(pred, t_iterate(t))
end
function functional.iall(i)
while true do
local v = i()
if v == false then
return false
elseif v == nil then
return true
end
end
end
function functional.all(t)
for i = 1, #t do
if not t[i] then
return false
end
end
return true
end
function functional.iany(i)
while true do
local v = i()
if v == nil then
return false
elseif v then
return true
end
end
end
function functional.any(t)
for i = 1, #t do
if t[i] then
return true
end
end
return false
end
function functional.wrap(f)
return function(...)
return f(...)
end
end
futil.functional = functional

10
mods/futil/util/http.lua Normal file
View file

@ -0,0 +1,10 @@
local function char_to_hex(c)
return string.format("%%%02X", string.byte(c))
end
function futil.urlencode(text)
text = text:gsub("\n", "\r\n")
text = text:gsub("([^0-9a-zA-Z !'()*._~-])", char_to_hex)
text = text:gsub(" ", "+")
return text
end

24
mods/futil/util/init.lua Normal file
View file

@ -0,0 +1,24 @@
futil.dofile("util", "bisect")
futil.dofile("util", "class")
futil.dofile("util", "coalesce")
futil.dofile("util", "exception")
futil.dofile("util", "file")
futil.dofile("util", "http")
futil.dofile("util", "list")
futil.dofile("util", "math")
futil.dofile("util", "matrix")
futil.dofile("util", "memoization")
futil.dofile("util", "memory")
futil.dofile("util", "path")
futil.dofile("util", "predicates")
futil.dofile("util", "string")
futil.dofile("util", "table")
futil.dofile("util", "equals") -- depends on table
futil.dofile("util", "functional") -- depends on table
futil.dofile("util", "iterators") -- depends on functional
futil.dofile("util", "random") -- depends on math
futil.dofile("util", "regex") -- depends on exception
futil.dofile("util", "selection") -- depends on table, math
futil.dofile("util", "time") -- depends on math
futil.dofile("util", "limiters") -- depends on functional

View file

@ -0,0 +1,106 @@
local iterators = {}
function iterators.range(...)
local a, b, c = ...
if type(a) ~= "number" then
error("invalid range")
end
if not b then
a, b = 1, a
end
if type(b) ~= "number" then
error("invalid range")
end
c = c or 1
if type(c) ~= "number" or c == 0 then
error("invalid range")
end
if c > 0 then
return function()
if a > b then
return
end
local to_return = a
a = a + c
return to_return
end
else
return function()
if a < b then
return
end
local to_return = a
a = a + c
return to_return
end
end
end
function iterators.repeat_(value, times)
if times then
local i = 0
return function()
i = i + 1
if i <= times then
return value
end
end
else
return function()
return value
end
end
end
function iterators.chain(...)
local arg = { ... }
local i = 1
return function()
while i <= #arg do
local v = arg[i]()
if v then
return v
end
end
end
end
function iterators.count(start, step)
step = step or 1
return function()
local rv = start
start = start + step
return rv
end
end
function iterators.values(t)
local k
return function()
local value
k, value = next(t, k)
return value
end
end
function iterators.accumulate(t, composer, initial)
local value = initial
local i = futil.table.iterate(t)
return function()
local next_value = i()
if next_value then
if value == nil then
value = next_value
elseif composer then
value = composer(value, next_value)
else
value = value + next_value
end
return value
end
end
end
futil.iterators = iterators

View file

@ -0,0 +1,42 @@
--[[
functions to limit the growth of a variable.
the intention here is to provide a family of functions for which
* f(x) is defined for all x >= 0
* f(0) = 0
* f(1) = 1
* f is continuous
* f is nondecreasing
* f\'(x) is nonincreasing when x > 1 (when the parameters are appropriate)
]]
local log = math.log
local pow = math.pow
local tanh = math.tanh
futil.limiters = {
-- no limiting
none = futil.functional.identity,
-- f(x) = x ^ param_1. param_1 should be < 1 for f\'(x) to be nonincreasing
-- f(x) will grow arbitrarily, but at a decreasing rate.
gamma = function(x, param_1)
return pow(x, param_1)
end,
-- the hyperbolic tangent scaled so that f(0) = 0 and f(1) = 1.
-- f(x) will grow approximately linearly for small x, but it will never grow beyond a maximum value, which is
-- approximately equal to param_1 + 1
tanh = function(x, param_1)
return (tanh((x - 1) / param_1) - tanh(-1 / param_1)) / -tanh(-1 / param_1)
end,
-- f(x) = log^param_2(param_1 * x + 1), scaled so that f(0) = 0 and f(1) = 1.
-- f(x) will grow arbitrarily, but at a much slower rate than a gamma limiter
log__n = function(x, param_1, param_2)
return (log(x + 1) * pow(log(param_1 * x + 1), param_2) / (log(2) * pow(log(param_1 + 1), param_2)))
end,
}
function futil.create_limiter(name, param_1, param_2)
local f = futil.limiters[name]
return function(x)
return f(x, param_1, param_2)
end
end

19
mods/futil/util/list.lua Normal file
View file

@ -0,0 +1,19 @@
function futil.list(iterator)
local t = {}
local v = iterator()
while v do
t[#t + 1] = v
v = iterator()
end
return t
end
function futil.list_multiple(iterator)
local t = {}
local v = { iterator() }
while #v > 0 do
t[#t + 1] = v
v = { iterator() }
end
return t
end

162
mods/futil/util/math.lua Normal file
View file

@ -0,0 +1,162 @@
futil.math = {}
local floor = math.floor
local huge = math.huge
local max = math.max
local min = math.min
function futil.math.idiv(a, b)
local rem = a % b
return (a - rem) / b, rem
end
function futil.math.bound(m, v, M)
return max(m, min(v, M))
end
function futil.math.in_bounds(m, v, M)
return m <= v and v <= M
end
local in_bounds = futil.math.in_bounds
function futil.math.is_integer(v)
return floor(v) == v
end
local is_integer = futil.math.is_integer
function futil.math.is_u8(i)
return (type(i) == "number" and is_integer(i) and in_bounds(0, i, 0xFF))
end
function futil.math.is_u16(i)
return (type(i) == "number" and is_integer(i) and in_bounds(0, i, 0xFFFF))
end
function futil.math.sum(t, initial)
local sum
local start
if initial then
sum = initial
start = 1
else
sum = t[1]
start = 2
end
for i = start, #t do
sum = sum + t[i]
end
return sum
end
function futil.math.isum(i, initial)
local sum
if initial == nil then
sum = i()
else
sum = initial
end
local v = i()
while v do
sum = sum + v
v = i()
end
return sum
end
function futil.math.product(t, initial)
local product
local start
if initial then
product = initial
start = 1
else
product = t[1]
start = 2
end
for i = start, #t do
product = product * t[i]
end
return product
end
function futil.math.iproduct(i, initial)
local product
if initial == nil then
product = i()
else
product = initial
end
local v = i()
while v do
product = product * v
v = i()
end
return product
end
function futil.math.probabilistic_round(v)
return floor(v + math.random())
end
function futil.math.cmp(a, b)
return a < b
end
futil.math.deg2rad = math.deg
futil.math.rad2deg = math.rad
function futil.math.do_intervals_overlap(min1, max1, min2, max2)
return min1 <= max2 and min2 <= max1
end
-- i took one class from kahan and can't stop doing this
local function round(n)
local d = n % 1
local i = n - d
if i % 2 == 0 then
if d <= 0.5 then
return i
else
return i + 1
end
else
if d < 0.5 then
return i
else
return i + 1
end
end
end
function futil.math.round(number, mult)
if mult then
return round(number / mult) * mult
else
return round(number)
end
end
-- TODO this doesn't handle out-of-bounds exponents
function futil.math.to_float32(number)
if number == huge or number == -huge or number ~= number then
return number
end
local sign, significand, exponent = ("%a"):format(number):match("^(-?)0x([0-9a-f\\.]+)p([0-9+-]+)$")
return tonumber(("%s0x%sp%s"):format(sign, significand:sub(1, 8), exponent))
end

View file

@ -0,0 +1,30 @@
futil.matrix = {}
function futil.matrix.multiply(m1, m2)
assert(#m1[1] == #m2, "width of first argument must be height of second")
local product = {}
for i = 1, #m1 do
local row = {}
for j = 1, #m2[1] do
local value = 0
for k = 1, #m2 do
value = value + m1[i][k] * m2[k][j]
end
row[j] = value
end
product[i] = row
end
return product
end
function futil.matrix.transpose(m)
local t = {}
for i = 1, #m[1] do
local row = {}
for j = 1, #m do
row[j] = m[j][i]
end
t[i] = row
end
return t
end

View file

@ -0,0 +1,51 @@
local private_state = ...
local mod_storage = private_state.mod_storage
function futil.memoize1(func)
local memo = {}
return function(arg)
if arg == nil then
return func(arg)
end
local rv = memo[arg]
if not rv then
rv = func(arg)
memo[arg] = rv
end
return rv
end
end
function futil.memoize_dumpable(func)
local memo = {}
return function(...)
local key = dump({ ... })
local rv = memo[key]
if not rv then
rv = func(...)
memo[key] = rv
end
return rv
end
end
function futil.memoize1_modstorage(id, func)
local key_format = ("%%s:%s:memoize"):format(id)
return function(arg)
local key_key = key_format:format(tostring(arg))
local rv = mod_storage:get(key_key)
if not rv then
rv = func(arg)
mod_storage:set_string(key_key, tostring(rv))
end
return rv
end
end
futil.memoize1ms = futil.memoize1_modstorage -- backwards compatibility

View file

@ -0,0 +1,45 @@
-- i have no idea how accurate this is, i use documentation from the below link for a few things
-- https://wowwiki-archive.fandom.com/wiki/Lua_object_memory_sizes
local function estimate_memory_usage(thing, seen)
local typ = type(thing)
if typ == "nil" then
return 0
end
seen = seen or {}
if seen[thing] then
return 0
end
seen[thing] = true
if typ == "boolean" then
return 4
elseif typ == "number" then
return 8 -- this is probably larger?
elseif typ == "string" then
return 25 + typ:len()
elseif typ == "function" then
-- TODO: we can calculate the usage of upvalues, but that's complicated
return 40
elseif typ == "userdata" then
return 0 -- this is probably larger
elseif typ == "thread" then
return 1224 -- this is probably larger
elseif typ == "table" then
local size = 64
for k, v in pairs(thing) do
if type(k) == "number" then
size = size + 16 + estimate_memory_usage(v, seen)
else
size = size + 40 + estimate_memory_usage(k, seen) + estimate_memory_usage(v, seen)
end
end
return size
else
futil.log("warning", "estimate_memory_usage: unknown type %s", typ)
return 0 -- ????
end
end
futil.estimate_memory_usage = estimate_memory_usage

7
mods/futil/util/path.lua Normal file
View file

@ -0,0 +1,7 @@
function futil.path_concat(...)
return table.concat({ ... }, DIR_DELIM)
end
function futil.path_split(path)
return string.split(path, DIR_DELIM, true)
end

View file

@ -0,0 +1,43 @@
function futil.is_nil(v)
return v == nil
end
function futil.is_boolean(v)
return v == true or v == false
end
function futil.is_number(v)
return type(v) == "number"
end
function futil.is_positive(v)
return v > 0
end
function futil.is_integer(v)
return v % 1 == 0
end
function futil.is_positive_integer(v)
return type(v) == "number" and v > 0 and v % 1 == 0
end
function futil.is_string(v)
return type(v) == "string"
end
function futil.is_userdata(v)
return type(v) == "userdata"
end
function futil.is_function(v)
return type(v) == "function"
end
function futil.is_thread(v)
return type(v) == "thread"
end
function futil.is_table(v)
return type(v) == "table"
end

View file

@ -0,0 +1,84 @@
local f = string.format
futil.random = {}
function futil.random.choice(t, random)
random = random or math.random
return t[random(#t)]
end
function futil.random.weighted_choice(t, random)
random = random or math.random
local elements, weights = {}, {}
local i = 1
for element, weight in pairs(t) do
elements[i] = element
weights[i] = weight
i = i + 1
end
local breaks = futil.list(futil.iterators.accumulate(weights))
local value = random() * breaks[#breaks]
return elements[futil.bisect.right(breaks, value)]
end
local WeightedChooser = futil.class1()
function WeightedChooser:_init(t)
local elements, weights = {}, {}
local i = 1
for element, weight in pairs(t) do
elements[i] = element
weights[i] = weight
i = i + 1
end
self._elements = elements
self._breaks = futil.list(futil.iterators.accumulate(weights))
end
function WeightedChooser:next(random)
random = random or math.random
local breaks = self._breaks
local value = random() * breaks[#breaks]
return self._elements[futil.bisect.right(breaks, value)]
end
futil.random.WeightedChooser = WeightedChooser
function futil.random.choice(t, random)
assert(#t > 0, "cannot get choice from an empty table")
random = random or math.random
return t[random(#t)]
end
-- https://stats.stackexchange.com/questions/569647/
function futil.random.sample(t, k, random)
assert(k <= #t, f("cannot sample %i items from a set of size %i", k, #t))
random = random or math.random
local sample = {}
for i = 1, k do
sample[i] = t[i]
end
for j = k + 1, #t do
if random() < k / j then
sample[random(1, k)] = t[j]
end
end
return sample
end
function futil.random.sample_with_indices(t, k, random)
assert(k <= #t, f("cannot sample %i items from a set of size %i", k, #t))
random = random or math.random
local sample = {}
for i = 1, k do
sample[i] = { i, t[i] }
end
for j = k + 1, #t do
if random() < k / j then
sample[random(1, k)] = { j, t[j] }
end
end
return sample
end

View file

@ -0,0 +1,6 @@
function futil.is_valid_regex(pattern)
return futil.safe_call(function()
(""):match(pattern)
return true
end, false, futil.functional.noop)
end

View file

@ -0,0 +1,109 @@
local floor = math.floor
local min = math.min
local random = math.random
local swap = futil.table.swap
local default_cmp = futil.math.cmp
futil.selection = {}
local function partition5(t, left, right, cmp)
cmp = cmp or default_cmp
local i = left + 1
while i <= right do
local j = i
while j > left and cmp(t[j], t[j - 1]) do
swap(t, j - 1, j)
j = j - 1
end
i = i + 1
end
return floor((left + right) / 2)
end
local function partition(t, left, right, pivot_i, i, cmp)
cmp = cmp or default_cmp
local pivot_v = t[pivot_i]
swap(t, pivot_i, right)
local store_i = left
for j = left, right - 1 do
if cmp(t[j], pivot_v) then
swap(t, store_i, j)
store_i = store_i + 1
end
end
local store_i_eq = store_i
for j = store_i, right - 1 do
if t[j] == pivot_v then
swap(t, store_i_eq, j)
store_i_eq = store_i_eq + 1
end
end
swap(t, right, store_i_eq)
if i < store_i then
return store_i
elseif i <= store_i_eq then
return i
else
return store_i_eq
end
end
local function quickselect(t, left, right, i, pivot_alg, cmp)
cmp = cmp or default_cmp
while true do
if left == right then
return left
end
local pivot_i = partition(t, left, right, pivot_alg(t, left, right, cmp), i, cmp)
if i == pivot_i then
return i
elseif i < pivot_i then
right = pivot_i - 1
else
left = pivot_i + 1
end
end
end
futil.selection.quickselect = quickselect
futil.selection.pivot = {}
function futil.selection.pivot.random(t, left, right, cmp)
return random(left, right)
end
local function pivot_medians_of_medians(t, left, right, cmp)
cmp = cmp or default_cmp
if right - left < 5 then
return partition5(t, left, right, cmp)
end
for i = left, right, 5 do
local sub_right = min(i + 4, right)
local median5 = partition5(t, i, sub_right, cmp)
swap(t, median5, left + floor((i - left) / 5))
end
local mid = floor((right - left) / 10) + left + 1
return quickselect(t, left, left + floor((right - left) / 5), mid, pivot_medians_of_medians, cmp)
end
futil.selection.pivot.median_of_medians = pivot_medians_of_medians
--[[
make use of quickselect to munge a table:
median_index = math.floor(#t / 2)
after calling this,
t[1] through t[median_index - 1] will be the elements less than t[median_index]
t[median_index] will be the median (or element less-than-the-median for even length tables)
t[median_index + 1] through t[#t] will be the elements greater than t[median_index]
pivot is a pivot algorithm, defaults to random selection
cmp is a comparison function.
returns median_index.
]]
function futil.selection.select(t, pivot_alg, cmp)
cmp = cmp or default_cmp
pivot_alg = pivot_alg or futil.selection.pivot.random
local median_index = math.floor(#t / 2)
return quickselect(t, 1, #t, median_index, pivot_alg, cmp)
end

View file

@ -0,0 +1,62 @@
futil.string = {}
function futil.string.truncate(s, max_length, suffix)
suffix = suffix or "..."
if s:len() > max_length then
return s:sub(1, max_length - suffix:len()) .. suffix
else
return s
end
end
function futil.string.lc_cmp(a, b)
return a:lower() < b:lower()
end
function futil.string.startswith(s, start, start_i, end_i)
return s:sub(start_i or 0, end_i or #s):sub(1, #start) == start
end
local escape_pattern = "([%(%)%.%%%+%-%*%?%[%^%$])"
local function escape_regex(str)
return str:gsub(escape_pattern, "%%%1")
end
local glob_patterns = {
["?"] = ".",
["*"] = ".*",
}
local function transform_pattern(pattern)
local parts = {}
local start = 1
for i = 1, #pattern do
local glob_pattern = glob_patterns[pattern:sub(i)]
if glob_pattern then
if start < i then
parts[#parts + 1] = escape_regex(pattern:sub(start, i - 1))
end
parts[#parts + 1] = glob_pattern
start = i + 1
end
end
if start < #pattern then
parts[#parts + 1] = escape_regex(pattern:sub(start, #pattern))
end
return table.concat(parts, "")
end
function futil.string.globmatch(str, pattern)
return str:match(transform_pattern(pattern))
end
futil.GlobMatcher = futil.class1()
function futil.GlobMatcher:_init(pattern)
self._pattern = transform_pattern(pattern)
end
function futil.GlobMatcher:match(str)
return str:match(self._pattern)
end

188
mods/futil/util/table.lua Normal file
View file

@ -0,0 +1,188 @@
local default_cmp = futil.math.cmp
futil.table = {}
function futil.table.set_all(t1, t2)
for k, v in pairs(t2) do
t1[k] = v
end
return t1
end
function futil.table.compose(t1, t2)
local t = table.copy(t1)
futil.table.set_all(t, t2)
return t
end
function futil.table.pairs_by_value(t, cmp)
cmp = cmp or default_cmp
local s = {}
for k, v in pairs(t) do
table.insert(s, { k, v })
end
table.sort(s, function(a, b)
return cmp(a[2], b[2])
end)
local i = 0
return function()
i = i + 1
local v = s[i]
if v then
return unpack(v)
else
return nil
end
end
end
function futil.table.pairs_by_key(t, cmp)
cmp = cmp or default_cmp
local s = {}
for k, v in pairs(t) do
table.insert(s, { k, v })
end
table.sort(s, function(a, b)
return cmp(a[1], b[1])
end)
local i = 0
return function()
i = i + 1
local v = s[i]
if v then
return unpack(v)
else
return nil
end
end
end
function futil.table.size(t)
local size = 0
for _ in pairs(t) do
size = size + 1
end
return size
end
function futil.table.is_empty(t)
return next(t) == nil
end
function futil.table.count_elements(t)
local counts = {}
for _, item in ipairs(t) do
counts[item] = (counts[item] or 0) + 1
end
return counts
end
function futil.table.sets_intersect(set1, set2)
for k in pairs(set1) do
if set2[k] then
return true
end
end
return false
end
function futil.table.iterate(t)
local i = 0
return function()
i = i + 1
return t[i]
end
end
function futil.table.reversed(t)
local len = #t
local reversed = {}
for i = len, 1, -1 do
reversed[len - i + 1] = t[i]
end
return reversed
end
function futil.table.contains(t, value)
for _, v in ipairs(t) do
if v == value then
return true
end
end
return false
end
function futil.table.keys(t)
local keys = {}
for key in pairs(t) do
keys[#keys + 1] = key
end
return keys
end
function futil.table.ikeys(t)
local key
return function()
key = next(t, key)
return key
end
end
function futil.table.values(t)
local values = {}
for _, value in pairs(t) do
values[#values + 1] = value
end
return values
end
function futil.table.sort_keys(t, cmp)
local keys = futil.table.keys(t)
table.sort(keys, cmp)
return keys
end
-- https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
function futil.table.shuffle(t, rnd)
rnd = rnd or math.random
for i = #t, 2, -1 do
local j = rnd(i)
t[i], t[j] = t[j], t[i]
end
return t
end
local function swap(t, i, j)
t[i], t[j] = t[j], t[i]
end
futil.table.swap = swap
function futil.table.get(t, key, default)
local value = t[key]
if value == nil then
return default
end
return value
end
function futil.table.setdefault(t, key, default)
local value = t[key]
if value == nil then
t[key] = default
return default
end
return value
end
function futil.table.pack(...)
return { n = select("#", ...), ... }
end

29
mods/futil/util/time.lua Normal file
View file

@ -0,0 +1,29 @@
local idiv = futil.math.idiv
-- convert a number of seconds into a more human-readable value
-- ignores the actual passage of time and assumes all years are 365 days
function futil.seconds_to_interval(time)
local s, m, h, d
time, s = idiv(time, 60)
time, m = idiv(time, 60)
time, h = idiv(time, 24)
time, d = idiv(time, 365)
if time ~= 0 then
return ("%d years %d days %02d:%02d:%02d"):format(time, d, h, m, s)
elseif d ~= 0 then
return ("%d days %02d:%02d:%02d"):format(d, h, m, s)
elseif h ~= 0 then
return ("%02d:%02d:%02d"):format(h, m, s)
elseif m ~= 0 then
return ("%02d:%02d"):format(m, s)
else
return ("%ds"):format(s)
end
end
-- ISO 8601 date format
function futil.format_utc(timestamp)
return os.date("!%Y-%m-%dT%TZ", timestamp)
end