nodecore-cd2025/mods/nc_api/util_misc.lua
Aaron Suen fd181833f4 Limit smoke effects, e.g. from concrete cure
Standardize a "fair limiting" mechanism that
limits the items accepted into a queue, and
returns a uniform random sample when flushed.

Pass all cooking/curing smoke effects into fair
limit queue.

Apply fairl limit queue to fire sparks too.

Pliant concrete curing checks still seem to lag
the server somewhat, but at least now they
shouldn't hammer the client with particles and
kill framerate too.
2021-08-19 22:19:27 -04:00

611 lines
16 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local ItemStack, PcgRandom, error, ipairs, math, minetest, next,
nodecore, pairs, string, tostring, type, unpack, vector
= ItemStack, PcgRandom, error, ipairs, math, minetest, next,
nodecore, pairs, string, tostring, type, unpack, vector
local math_abs, math_cos, math_floor, math_log, math_pi, math_pow,
math_random, math_sin, math_sqrt, string_format, string_gsub,
string_lower, string_sub
= math.abs, math.cos, math.floor, math.log, math.pi, math.pow,
math.random, math.sin, math.sqrt, string.format, string.gsub,
string.lower, string.sub
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
for k, v in pairs(minetest) do
if type(v) == "function" then
-- Late-bind in case minetest methods overridden.
nodecore[k] = function(...) return minetest[k](...) end
else
nodecore[k] = v
end
end
local function underride(t, u, u2, ...)
if u2 then underride(u, u2, ...) end
for k, v in pairs(u) do
if t[k] == nil then
t[k] = v
elseif type(t[k]) == "table" and type(v) == "table" then
underride(t[k], v)
end
end
return t
end
nodecore.underride = underride
function nodecore.mkreg()
local t = {}
local f = function(x) t[#t + 1] = x end
return f, t
end
function nodecore.fairlimit(max)
local queue = {}
local qty = 0
local function add(item)
qty = qty + 1
if qty > max then
local id = math_random(1, qty)
if id <= max then
queue[id] = item
end
else
queue[qty] = item
end
end
local function flush()
local batch = queue
queue = {}
qty = 0
return batch
end
return add, flush
end
function nodecore.memoize(func)
local cachedval
local cached
return function()
if cached then return cachedval end
cachedval = func()
cached = true
return cachedval
end
end
function nodecore.dirs()
return {
{n = "e", x = 1, y = 0, z = 0},
{n = "w", x = -1, y = 0, z = 0},
{n = "u", x = 0, y = 1, z = 0},
{n = "d", x = 0, y = -1, z = 0},
{n = "n", x = 0, y = 0, z = 1},
{n = "s", x = 0, y = 0, z = -1}
}
end
function nodecore.pickrand(tbl, weight, rng)
weight = weight or function() end
local t = {}
local max = 0
for k, v in pairs(tbl) do
local w = weight(v) or 1
if w > 0 then
max = max + w
t[#t + 1] = {w = w, k = k, v = v}
end
end
if max <= 0 then return end
max = (rng or math_random)() * max
for _, v in ipairs(t) do
max = max - v.w
if max <= 0 then return v.v, v.k end
end
end
do
local saved
function nodecore.boxmuller()
local old = saved
if old then
saved = nil
return old
end
local r = math_sqrt(-2 * math_log(math_random()))
local t = 2 * math_pi * math_random()
saved = r * math_sin(t)
return r * math_cos(t)
end
end
function nodecore.exporand(mean, rng)
local r = 0
while r == 0 do r = (rng or math_random)() end
return math_floor(-math_log(r) * (mean + 0.5))
end
function nodecore.seeded_rng(seed)
if PcgRandom then
seed = math_floor((seed - math_floor(seed)) * 2 ^ 32 - 2 ^ 31)
local pcg = PcgRandom(seed)
return function(a, b)
if b then
return pcg:next(a, b)
elseif a then
return pcg:next(1, a)
end
return (pcg:next() + 2 ^ 31) / 2 ^ 32
end
end
return math_random
end
function nodecore.extend_item(name, func)
local orig = minetest.registered_items[name] or {}
local copy = {}
for k, v in pairs(orig) do copy[k] = v end
copy = func(copy, orig) or copy
minetest.register_item(":" .. name, copy)
end
function nodecore.fixedbox(x, ...)
return {type = "fixed", fixed = {
x or {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
...
}}
end
function nodecore.interact(player)
if not player then return end
if type(player) ~= "string" then
if not (player.is_player and player:is_player()) then
return true
end
player = player:get_player_name()
end
return minetest.get_player_privs(player).interact
end
function nodecore.player_visible(player)
if type(player) == "string" then player = minetest.get_player_by_name(player) end
if not player then return end
local props = player:get_properties()
local vs = props and props.visual_size
return vs and vs.x > 0 and vs.y > 0
end
function nodecore.wieldgroup(who, group)
local wielded = who and who:get_wielded_item()
local nodedef = minetest.registered_nodes[wielded:get_name()]
if nodedef then return nodedef.groups and nodedef.groups[group] end
local caps = wielded and wielded:get_tool_capabilities()
return caps and caps.groupcaps and caps.groupcaps[group]
end
function nodecore.interval(after, func)
local go
local setnext = (type(after) == "function")
and function() return minetest.after(after(), go) end
or function() return minetest.after(after, go) end
go = function() setnext() return func() end
minetest.after(0, go)
end
function nodecore.wear_wield(player, groups, qty)
local wielded = player:get_wielded_item()
if wielded then
local wdef = wielded:get_definition()
local tp = wielded:get_tool_capabilities()
local dp = minetest.get_dig_params(groups, tp)
if wdef and wdef.after_use then
wielded = wdef.after_use(wielded, player, nil, dp) or wielded
else
wielded:add_wear(dp.wear * (qty or 1))
if wielded:get_count() <= 0 and wdef.sound
and wdef.sound.breaks then
nodecore.sound_play(wdef.sound.breaks,
{object = player, gain = 0.5})
end
end
return player:set_wielded_item(wielded)
end
end
function nodecore.consume_wield(player, qty)
local wielded = player:get_wielded_item()
if wielded then
local wdef = wielded:get_definition()
if wdef.stack_max > 1 and qty then
local have = wielded:get_count() - qty
if have <= 0 then
wielded = ItemStack("")
else
wielded:set_count(have)
end
end
return player:set_wielded_item(wielded)
end
end
function nodecore.loaded_mods()
local t = {}
for _, v in pairs(minetest.get_modnames()) do
t[v] = true
end
return t
end
function nodecore.node_group(name, pos, node)
node = node or minetest.get_node(pos)
local def = minetest.registered_nodes[node.name] or {}
return def.groups and def.groups[name]
end
function nodecore.find_nodes_around(pos, spec, r, s)
r = r or 1
if type(r) == "number" then
return minetest.find_nodes_in_area(
{x = pos.x - r, y = pos.y - r, z = pos.z - r},
{x = pos.x + r, y = pos.y + r, z = pos.z + r},
spec)
end
s = s or r
return minetest.find_nodes_in_area(
{x = pos.x - (r.x or r[1]), y = pos.y - (r.y or r[2]), z = pos.z - (r.z or r[3])},
{x = pos.x + (s.x or s[1]), y = pos.y + (s.y or s[2]), z = pos.z + (s.z or s[3])},
spec)
end
function nodecore.quenched(pos, r)
local qty = #nodecore.find_nodes_around(pos, "group:coolant", r)
return (qty > 0) and qty or nil
end
function nodecore.node_spin_custom(...)
local arr = {...}
arr[0] = false
local lut = {}
for i = 1, #arr do
lut[arr[i - 1]] = arr[i]
end
lut[arr[#arr]] = arr[1]
local qty = #arr
return function(pos, node, clicker, itemstack)
if nodecore.protection_test(pos, clicker) then return end
node = node or minetest.get_node(pos)
node.param2 = lut[node.param2] or lut[false]
if clicker:is_player() then
nodecore.log("action", clicker:get_player_name() .. " spins "
.. node.name .. " at " .. minetest.pos_to_string(pos)
.. " to param2 " .. node.param2 .. " ("
.. qty .. " total)")
end
minetest.swap_node(pos, node)
nodecore.node_sound(pos, "place")
local def = minetest.registered_items[node.name] or {}
if def.on_spin then def.on_spin(pos, node) end
return itemstack
end
end
function nodecore.node_spin_filtered(func)
local rots = {}
for i = 0, 23 do
local f = nodecore.facedirs[i]
local hit
for j = 1, #rots do
if not hit then
local o = nodecore.facedirs[rots[j]]
hit = hit or func(f, o)
end
end
if not hit then rots[#rots + 1] = f.id end
end
return nodecore.node_spin_custom(unpack(rots))
end
local function scrubkey(s)
return string_lower(string_gsub(tostring(s), "%W+", "_"))
end
function nodecore.rate_adjustment(...)
local rate = 1
local key = modname .. "_rate"
local name = ""
for _, k in ipairs({...}) do
if not k then break end
key = key .. "_" .. scrubkey(k)
name = name .. " > " .. k
local adj = nodecore.setting_float(key, 1, "Speed adjust" .. name,
[[Speed adjustment ratio, multiplied by all parent ratios.
Intended for custom servers and special sub-game types only.]])
if adj then rate = rate * adj end
end
return rate
end
function nodecore.obstructed(minpos, maxpos)
if not maxpos then
maxpos = {x = minpos.x + 0.5, y = minpos.y + 0.5, z = minpos.z + 0.5}
minpos = {x = minpos.x - 0.5, y = minpos.y - 0.5, z = minpos.z - 0.5}
end
local avgpos = vector.multiply(vector.add(minpos, maxpos), 0.5)
local radius = 4 + vector.distance(minpos, maxpos) / 2
for _, obj in pairs(minetest.get_objects_inside_radius(avgpos, radius)) do
local op = obj:get_pos()
local props = obj:get_properties()
local cb = props.collisionbox
if props.static_save
and maxpos.x > op.x + cb[1] and minpos.x < op.x + cb[4]
and maxpos.y > op.y + cb[2] and minpos.y < op.y + cb[5]
and maxpos.z > op.z + cb[3] and minpos.z < op.z + cb[6]
and obj.get_luaentity and obj:get_luaentity() then
return obj
end
end
end
local gravity = nodecore.setting_float("movement_gravity", 9.81)
local friction = nodecore.setting_float(modname .. "_air_friction", 0.0004,
"Air friction", [[Air friction coefficient for velocity-squared
term. Used to adjust air resistance for player and moving entities,
especially tuning terminal velocity. Lower terminal velocity may
reduce players stopping on onloaded chunks while falling on a
busy/slow server.]])
local function air_accel_factor(v)
local q = (friction * v * v) * 2 - 1
return q > 0 and q or 0
end
function nodecore.grav_air_physics_player(v)
if v.y > 0 then return 1 end
return 1 - air_accel_factor(v.y)
end
local function air_accel_net(v)
return v == 0 and 0 or v / -math_abs(v) * gravity * air_accel_factor(v)
end
function nodecore.grav_air_accel(v)
return {
x = air_accel_net(v.x),
y = air_accel_net(v.y) - gravity,
z = air_accel_net(v.z)
}
end
function nodecore.grav_air_accel_ent(obj)
local cur = obj:get_acceleration()
local new = nodecore.grav_air_accel(obj:get_velocity())
if vector.equals(cur, new) then return end
return obj:set_acceleration(new)
end
function nodecore.near_unloaded(pos, radius)
return minetest.find_node_near(pos, radius or 1, {"ignore"}, true)
end
function nodecore.get_objects_at_pos(pos)
pos = vector.round(pos)
local t = {}
-- get_objects_inside_radius just loops over these and does a euclidian
-- distance check anyway, which we can skip
for _, obj in pairs(minetest.object_refs) do
local p = obj:get_pos()
if p and vector.equals(vector.round(p), pos) then
t[#t + 1] = obj
end
end
return t
end
function nodecore.get_depth_light(y, qty)
qty = qty or 4/5
if y < 0 then qty = qty * math_pow(2, y / 64) end
return qty
end
nodecore.light_sun = 15
nodecore.light_sky = math_floor(0.5 + nodecore.light_sun * nodecore.get_depth_light(0))
function nodecore.is_full_sun(pos)
return pos.y >= 0 and minetest.get_node_light(pos, 0.5) == nodecore.light_sun
end
function nodecore.get_node_light(pos)
local artificial = minetest.get_node_light(pos, 0)
if not artificial then return end
local natural = math_floor(0.5 + minetest.get_node_light(pos, 0.5)
* nodecore.get_depth_light(pos.y))
return artificial > natural and artificial or natural
end
local liquids = {}
minetest.after(0, function()
for k, v in pairs(minetest.registered_items) do
if v.liquidtype and v.liquidtype ~= "none" then
liquids[k] = v
end
end
end)
nodecore.registered_liquids = liquids
local player_was_swimming = {}
function nodecore.player_swimming(player)
local pname = player:get_player_name()
local pos = player:get_pos()
local r = 0.6
local swimming = true
for dz = -r, r, r do
for dx = -r, r, r do
local p = {
x = pos.x + dx,
y = pos.y,
z = pos.z + dz
}
local node = minetest.get_node(p)
if (node.name == "air" or liquids[node.name]) then
p.y = p.y - 0.35
node = minetest.get_node(p)
end
if node.name == "air" then swimming = nil
elseif not liquids[node.name] then
player_was_swimming[pname] = nil
return
end
end
end
if swimming then
player_was_swimming[pname] = true
return true
end
return player_was_swimming[pname]
end
local function deepcopy(x)
if type(x) == "table" then
local t = {}
for k, v in pairs(x) do t[k] = deepcopy(v) end
return t
end
return x
end
nodecore.deepcopy = deepcopy
local function mismatch(a, b, exact)
if type(a) == "table" then
if type(b) ~= "table" then return true end
for k, v in pairs(a) do
if mismatch(v, b[k], exact) then return true end
end
return
end
if (not exact) and type(a) == "number" and type(b) == "number" then
local ratio = a / b
-- Floating point rounding...
if ratio > 0.99999 and ratio < 1.00001 then return end
end
return a ~= b
end
nodecore.prop_mismatch = mismatch
local grp = "group:"
local function would_match(name, def, itemnames)
if not (name and def) then return end
if itemnames == true or not itemnames then return itemnames end
if name == itemnames then return true end
local namestype = type(itemnames)
if namestype == "string" then
if string_sub(itemnames, 1, #grp) == grp then
local gn = string_sub(itemnames, #grp + 1)
return def and def.groups and def.groups[gn] and def.groups[gn] > 0
end
return name == itemnames
elseif namestype == "table" then
for i = 1, #itemnames do
if would_match(name, def, itemnames[i]) then
return true
end
end
else
error("invalid would_match type " .. namestype)
end
end
nodecore.would_match = would_match
local function group_expand(itemnames, deferred)
local deffunc = type(deferred) == "function" and deferred or nil
local idx = {}
local function populate()
while true do
local k = next(idx)
if not k then break end
idx[k] = nil
end
for k, v in pairs(minetest.registered_items) do
if would_match(k, v, itemnames) then
idx[k] = true
if deffunc then deffunc(k, idx) end
end
end
end
populate()
if deferred then minetest.after(0, populate) end
return idx
end
nodecore.group_expand = group_expand
function nodecore.item_matching_index(items, getnames, idxname, asarray, keymod)
local index = {}
local function itemadd(key, item)
local t = index[key]
if not t then
t = {}
index[key] = t
end
if asarray then
t[#t + 1] = item
else
t[item] = true
end
end
keymod = keymod or function(x) return x end
local report_pending
local function rebuild()
for k in pairs(index) do index[k] = nil end
for _, item in pairs(items) do
for k in pairs(nodecore.group_expand(getnames(item))) do
itemadd(keymod(k, item), item)
end
end
if idxname and not report_pending then
report_pending = true
minetest.after(0, function()
report_pending = nil
local keys = 0
local defs = 0
local peak = 0
for _, v in pairs(index) do
keys = keys + 1
local n = 0
for _ in pairs(v) do n = n + 1 end
defs = defs + n
if n > peak then peak = n end
end
nodecore.log("action", string_format(
"%s %s: %d keys, %d defs, %d peak",
"item_matching_index",
idxname, keys, defs, peak))
end)
end
end
minetest.after(0, rebuild)
return index, rebuild
end
function nodecore.protection_test(pos, player)
if not player then return end
if type(player) ~= "string" then
if not player:is_player() then return end
player = player:get_player_name()
end
if minetest.is_protected(pos, player) then
minetest.record_protection_violation(pos, player)
return true
end
end
function nodecore.meta_serializable(meta)
local mt = type(meta)
if mt == "table" or mt == "userdata" then
if type(meta.to_table) == "function" then
meta = meta:to_table()
end
for _, list in pairs(meta.inventory or {}) do
for i, stack in pairs(list) do
if type(stack) == "userdata" then
list[i] = stack:to_string()
end
end
end
end
return meta
end