Aaron Suen b7261ab792 Allow overriding optic opacity via groups
Looking into the possibility of making everything
overridable for game-logic purposes by group, it
turned out to be a bit of a nightmare.  Also, instead
of overriding them at the "heuristic" layer, it's
probably better to simply allow overriding at the
specific game logic level, e.g. so that multiple
systems that use the same source data for heuristic
purposes can have differing behavior.
2022-10-09 11:57:36 -04:00

321 lines
9.3 KiB
Lua

-- LUALOCALS < ---------------------------------------------------------
local error, math, minetest, nodecore, pairs, string, type, vector
= error, math, minetest, nodecore, pairs, string, type, vector
local math_floor, math_random, string_format
= math.floor, math.random, string.format
-- LUALOCALS > ---------------------------------------------------------
local modname = minetest.get_current_modname()
local optic_distance = nodecore.setting_float(modname .. "_optic_distance", 16,
"Optic beam distance", [[WARNING: FUNDAMENTAL CONSTANT. Maximum distance
from which an optic beam can be sensed by an optic node's input face.
Changing this may fundamentally alter the game, including making your
builds incompatible across hosts.]])
local optic_speed = nodecore.setting_float(modname .. "_optic_speed", 12,
"Optic tick rate", [[WARNING: FUNDAMENTAL CONSTANT. Rate in Hz of
optic ticks. Changing this may fundamentally alter the game, including
making your builds incompatible across hosts.]])
local optic_tick_limit = nodecore.setting_float(modname .. "_tick_limit", 0.2,
"Optic tick limit", [[Maximum amount of time in seconds that may be
spent during a single server step to calculate optic state. Optics
will be allowed to slow don to stay within this limit.]])
local optic_interval = nodecore.setting_float(modname .. "_interval", 5,
"Optic check interval", [[ABM interval for periodically pushing
optics into the "passive" queue.
Passive checks are used to catch optics in an inconsistent state, e.g.
that missed their change event.]])
local optic_passive_max = nodecore.setting_float(modname .. "_passive_max", 25,
"Optic check passive max", [[The maximum number of optics that can be
queued in a single pass for "passive" checks to run; pending passive
checks will be included to fill up remaining spaces up to this total.
Passive checks are used to catch optics in an inconsistent state, e.g.
that missed their change event.]])
local optic_passive_min = nodecore.setting_float(modname .. "_passive_min", 5,
"Optic check passive min", [[The minimum number of "passive" optic
checks that are run each cycle, overriding the max if needed.
Passive checks are used to catch optics in an inconsistent state, e.g.
that missed their change event.]])
local microtime = minetest.get_us_time
local hashpos = minetest.hash_node_position
local unhash = minetest.get_position_from_hash
local get_node = minetest.get_node
local node_optic_checks = {}
local node_optic_sources = {}
local node_opaque = {}
local node_visinv = {}
minetest.after(0, function()
for k, v in pairs(minetest.registered_nodes) do
node_optic_checks[k] = v.optic_check or nil
node_optic_sources[k] = v.optic_source or nil
node_visinv[k] = v.groups and v.groups.visinv or nil
local grp_t = minetest.get_item_group(k, "optic_transparent") ~= 0
local grp_o = minetest.get_item_group(k, "optic_opaque") ~= 0
if (grp_t and grp_o) then
error("node cannot be BOTH optic_opaque and optic_transparent")
end
node_opaque[k] = grp_o or (not (grp_t or v.sunlight_propagates)) or nil
end
end)
local optic_queue = {}
local passive_queue = {}
local dependency_index = {}
local dependency_reverse = {}
local function mapblock(pos)
return {
x = math_floor((pos.x + 0.5) / 16),
y = math_floor((pos.y + 0.5) / 16),
z = math_floor((pos.z + 0.5) / 16),
}
end
local function scan(pos, dir, max, getnode, cbbs)
local p = pos
if (not max) or (max > optic_distance) then max = optic_distance end
for _ = 1, max do
local o = p
p = vector.add(p, dir)
if cbbs and not vector.equals(mapblock(o), mapblock(p)) then
cbbs[#cbbs + 1] = {
pos = vector.add(o, vector.multiply(dir, 0.5)),
dir = dir,
plane = {
x = dir.x == 0 and 1 or 0,
y = dir.y == 0 and 1 or 0,
z = dir.z == 0 and 1 or 0,
}
}
end
local node = getnode(p)
if (not node) or node.name == "ignore" then return end
if node_opaque[node.name] and not node_visinv[node.name] then return p, node end
if node_visinv[node.name] then
if node_opaque[node.name] then
local def = minetest.registered_nodes[node.name] or {}
if not def.storebox_access then return p, node end
-- check that “light” can come in from the old position by checking for access
if not def.storebox_access(
{above = o, under = p}, p, {}) then return p, node end
-- check that “light” can go out: this should be checked after checking
-- if the content is opaque, but we're going to return the same p, node anyway
-- so it doesn't really matter
if not def.storebox_access(
{above = vector.add(p, dir), under = p}, p, {}) then return p, node end
end
local stack = nodecore.stack_get(p)
if node_opaque[stack:get_name()] then
return p, node
end
end
end
end
nodecore.optic_scan = scan
local function scan_recv(pos, dir, max, getnode)
local hit, node = scan(pos, dir, max, getnode)
if not node then return end
local src = node_optic_sources[node.name]
src = src and src(hit, node)
if not src then return end
local rev = vector.multiply(dir, -1)
for _, v in pairs(src) do
if vector.equals(v, rev) then
return hit, node
end
end
end
nodecore.optic_scan_recv = scan_recv
local function optic_check(pos)
optic_queue[hashpos(pos)] = pos
end
nodecore.optic_check = optic_check
local function optic_trigger(start, dir, max)
local pos, node = scan(start, dir, max, get_node)
if node and node_optic_checks[node.name] then
return optic_check(pos)
end
end
local function optic_process(trans, pos)
local node = get_node(pos)
if node.name == "ignore" then return end
local check = node_optic_checks[node.name]
if check then
local ignored
local deps = {}
local getnode = function(p)
local gn = get_node(p)
deps[hashpos(p)] = true
ignored = ignored or gn.name == "ignore"
return gn
end
local recv = function(dir, max)
return scan_recv(pos, dir, max, getnode)
end
local nn = check(pos, node, recv, getnode)
if (not ignored) and nn then
trans[hashpos(pos)] = {
pos = pos,
nn = nn,
deps = deps
}
end
end
end
local function optic_commit(v)
local node = get_node(v.pos)
local oldidx = {}
local oldsrc = node_optic_sources[node.name]
oldsrc = oldsrc and oldsrc(v.pos, node)
if oldsrc then
for _, dir in pairs(oldsrc) do
oldidx[hashpos(dir)] = dir
end
end
local nn = v.nn
if type(nn) == "string" then nn = {name = nn} end
nn.param = nn.param or node.param
nn.param2 = nn.param2 or node.param2
local vhash = hashpos(v.pos)
if node.name ~= nn.name or node.param ~= nn.param or node.param2 ~= nn.param2 then
minetest.set_node(v.pos, nn)
local src = node_optic_sources[nn.name]
src = src and src(v.pos, nn)
local newidx = {}
if src then
for _, dir in pairs(src) do
local hash = hashpos(dir)
if not oldidx[hash] then
optic_trigger(v.pos, dir)
end
newidx[hash] = dir
end
end
for hash, dir in pairs(oldidx) do
if not newidx[hash] then optic_trigger(v.pos, dir) end
end
end
local olddep = dependency_reverse[vhash]
if olddep then
for k in pairs(olddep) do
local t = dependency_index[k]
if t then t[vhash] = nil end
end
end
for k in pairs(v.deps) do
local t = dependency_index[k]
if not t then
t = {}
dependency_index[k] = t
end
t[vhash] = true
end
end
minetest.register_abm({
label = "optic check",
interval = optic_interval,
chance = 1,
nodenames = {"group:optic_check"},
action = function(pos)
passive_queue[#passive_queue + 1] = pos
end
})
nodecore.register_lbm({
name = modname .. ":optic_check",
run_at_every_load = true,
nodenames = {"group:optic_check"},
action = optic_check
})
local optic_check_pump
do
local passive_batch = {}
optic_check_pump = function()
local batch = optic_queue
optic_queue = {}
if nodecore.stasis then
passive_queue = {}
return
end
if #passive_queue > 0 then
passive_batch = passive_queue
passive_queue = {}
for i = 1, #passive_batch do
local j = math_random(1, #passive_batch)
local t = passive_batch[i]
passive_batch[i] = passive_batch[j]
passive_batch[j] = t
end
end
local max = optic_passive_max - #batch
if max < optic_passive_min then max = optic_passive_min end
if max > #passive_batch then max = #passive_batch end
for _ = 1, max do
local pos = passive_batch[#passive_batch]
passive_batch[#passive_batch] = nil
batch[hashpos(pos)] = pos
end
local trans = {}
for _, pos in pairs(batch) do
optic_process(trans, pos)
end
for _, v in pairs(trans) do
optic_commit(v)
end
end
end
do
local tick = 1 / optic_speed
local total = 0
nodecore.register_globalstep("optic tick", function(dtime)
total = total + dtime / tick
local starttime = microtime()
local exp = starttime + optic_tick_limit * 1000000
local starttotal = total
while total > 1 do
optic_check_pump()
if microtime() >= exp then
nodecore.log("warning", string_format("optics stopped"
.. " after running %d cycle(s) in %0.3fs"
.. ", behind %0.2f",
starttotal - total,
(microtime() - starttime) / 1000000,
total))
total = 0
else
total = total - 1
end
end
end)
end
local function optic_check_dependents(pos)
local t = dependency_index[hashpos(pos)]
if t then
for k in pairs(t) do
optic_check(unhash(k))
end
end
end
nodecore.optic_check_dependents = optic_check_dependents
nodecore.register_on_nodeupdate(optic_check_dependents)