498 lines
15 KiB
Lua
498 lines
15 KiB
Lua
--[[
|
|
|
|
Networks
|
|
========
|
|
|
|
Copyright (C) 2021 Joachim Stolberg
|
|
|
|
AGPL v3
|
|
See LICENSE.txt for more information
|
|
|
|
]]--
|
|
|
|
-- for lazy programmers
|
|
local S2P = minetest.string_to_pos
|
|
local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end
|
|
local M = minetest.get_meta
|
|
local N = tubelib2.get_node_lvm
|
|
|
|
local Networks = {} -- cache for networks: {netw_type = {netID = <network>, ...}, ...}
|
|
local NetIDs = {} -- cache for netw IDs: {pos_hash = {outdir = netID, ...}, ...}
|
|
|
|
local MAX_NUM_NODES = 1000 -- per network including junctions
|
|
local TTL = 5 * 60 -- 5 minutes
|
|
local Route = {} -- Used to determine the already passed nodes while walking
|
|
local NumNodes = 0 -- Used to determine the number of network nodes
|
|
|
|
local Flip = tubelib2.Turn180Deg
|
|
local get_nodename = networks.get_nodename
|
|
local get_node = networks.get_node
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Debugging
|
|
-------------------------------------------------------------------------------
|
|
-- Table for all registered tubelib2 instances
|
|
networks.registered_networks = {} -- {api_type = {instance,...}}
|
|
|
|
-- Maintain simple numbers for the bulky netID hashes
|
|
local DbgNetIDs = {}
|
|
local DbgCounter = 1
|
|
|
|
local function netw_num(netID)
|
|
if not netID or netID < 1 then
|
|
return netID or 0
|
|
end
|
|
if not DbgNetIDs[netID] then
|
|
DbgNetIDs[netID] = DbgCounter
|
|
DbgCounter = DbgCounter + 1
|
|
end
|
|
return DbgNetIDs[netID]
|
|
end
|
|
|
|
local function network_nodes(netID, network)
|
|
local tbl = {}
|
|
for node_type,table in pairs(network or {}) do
|
|
if type(table) == "table" then
|
|
tbl[#tbl+1] = "#" .. node_type .. " = " .. #table
|
|
end
|
|
end
|
|
tbl[#tbl+1] = "num_nodes = " .. (network.num_nodes or 0)
|
|
return "Network " .. netw_num(netID) .. ": " .. table.concat(tbl, ", ")
|
|
end
|
|
|
|
-- Marker entities for debugging purposes
|
|
function networks.set_marker(pos, text, size, ttl)
|
|
local marker = minetest.add_entity(pos, "networks:marker_cube")
|
|
if marker ~= nil then
|
|
marker:set_nametag_attributes({color = "#FFFFFF", text = text})
|
|
size = size or 1
|
|
marker:set_properties({visual_size = {x = size, y = size}})
|
|
if ttl then
|
|
minetest.after(ttl, marker.remove, marker)
|
|
end
|
|
end
|
|
end
|
|
|
|
minetest.register_entity("networks:marker_cube", {
|
|
initial_properties = {
|
|
visual = "cube",
|
|
textures = {
|
|
"networks_marker.png",
|
|
"networks_marker.png",
|
|
"networks_marker.png",
|
|
"networks_marker.png",
|
|
"networks_marker.png",
|
|
"networks_marker.png",
|
|
},
|
|
physical = false,
|
|
visual_size = {x = 1.1, y = 1.1},
|
|
collisionbox = {-0.55,-0.55,-0.55, 0.55,0.55,0.55},
|
|
glow = 8,
|
|
static_save = false,
|
|
},
|
|
on_punch = function(self)
|
|
self.object:remove()
|
|
end,
|
|
})
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Helper
|
|
-------------------------------------------------------------------------------
|
|
-- return the networks table from the node definition
|
|
local function net_def(pos, netw_type)
|
|
local ndef = minetest.registered_nodes[get_nodename(pos)]
|
|
if ndef and ndef.networks then
|
|
return ndef.networks[netw_type]
|
|
end
|
|
error("Node " .. get_nodename(pos) .. " at ".. P2S(pos) .. " has no 'ndef.networks'")
|
|
end
|
|
|
|
local function net_def2(pos, node_name, netw_type)
|
|
local ndef = minetest.registered_nodes[node_name]
|
|
if ndef and ndef.networks then
|
|
return ndef.networks[netw_type]
|
|
end
|
|
return net_def(pos, netw_type)
|
|
end
|
|
|
|
-- Don't allow direct connections between to nodes of the same type
|
|
local function valid_secondary_node_connection(tlib2, pos, dir)
|
|
local node1 = tlib2:get_secondary_node(pos, dir)
|
|
if node1 then
|
|
local node2 = N(pos)
|
|
local ndef1 = minetest.registered_nodes[node1.name]
|
|
local ndef2 = minetest.registered_nodes[node2.name]
|
|
local ntype1 = ((ndef1.networks or {})[tlib2.tube_type] or {}).ntype
|
|
local ntype2 = ((ndef2.networks or {})[tlib2.tube_type] or {}).ntype
|
|
return ntype1 == "junc" or ntype1 ~= ntype2
|
|
end
|
|
end
|
|
|
|
-- Returns true if node is connected with another network node
|
|
local function connected(tlib2, pos, dir)
|
|
local param2, npos = tlib2:get_primary_node_param2(pos, dir)
|
|
if param2 then
|
|
local d1, d2, num = tlib2:decode_param2(npos, param2)
|
|
if not num then return end
|
|
return Flip[dir] == d1 or Flip[dir] == d2
|
|
end
|
|
-- secondary nodes allowed?
|
|
if tlib2.force_to_use_tubes then
|
|
return tlib2:is_special_node(pos, dir)
|
|
else
|
|
return valid_secondary_node_connection(tlib2, pos, dir)
|
|
end
|
|
end
|
|
|
|
local function side_to_outdir(pos, side)
|
|
return tubelib2.side_to_dir(side, N(pos).param2)
|
|
end
|
|
|
|
-- determine outdir based on node type
|
|
local function get_outdir(node_type, indir)
|
|
if node_type == "junc" then
|
|
return 0 -- same network on all sides
|
|
else
|
|
return Flip[indir]
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Node Connections
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Get tlib2 connection dirs as table
|
|
-- used e.g. for the connection walk
|
|
local function get_node_connection_dirs(pos, netw_type)
|
|
local val = M(pos):get_int(netw_type.."_conn")
|
|
local tbl = {}
|
|
if val % 0x40 >= 0x20 then tbl[#tbl+1] = 1 end
|
|
if val % 0x20 >= 0x10 then tbl[#tbl+1] = 2 end
|
|
if val % 0x10 >= 0x08 then tbl[#tbl+1] = 3 end
|
|
if val % 0x08 >= 0x04 then tbl[#tbl+1] = 4 end
|
|
if val % 0x04 >= 0x02 then tbl[#tbl+1] = 5 end
|
|
if val % 0x02 >= 0x01 then tbl[#tbl+1] = 6 end
|
|
return tbl
|
|
end
|
|
|
|
local function get_node_connection_dirs_table(pos, netw_type)
|
|
local val = M(pos):get_int(netw_type.."_conn")
|
|
local tbl = {}
|
|
if val % 0x40 >= 0x20 then tbl[1] = true end
|
|
if val % 0x20 >= 0x10 then tbl[2] = true end
|
|
if val % 0x10 >= 0x08 then tbl[3] = true end
|
|
if val % 0x08 >= 0x04 then tbl[4] = true end
|
|
if val % 0x04 >= 0x02 then tbl[5] = true end
|
|
if val % 0x02 >= 0x01 then tbl[6] = true end
|
|
return tbl
|
|
end
|
|
|
|
-- store all node sides with tube connections as nodemeta
|
|
local function store_node_connection_sides(pos, tlib2)
|
|
local node = get_node(pos)
|
|
local val = 0
|
|
for dir = 1,6 do
|
|
val = val * 2
|
|
if tlib2:is_valid_dir(node, dir) and connected(tlib2, pos, dir) then
|
|
val = val + 1
|
|
end
|
|
end
|
|
M(pos):set_int(tlib2.tube_type.."_conn", val)
|
|
end
|
|
|
|
-- If outdir is given, return outdir, otherwise return all valid node dirs with tube connections
|
|
local function get_outdirs(pos, tlib2, outdir)
|
|
if outdir then
|
|
return {outdir}
|
|
end
|
|
return get_node_connection_dirs(pos, tlib2.tube_type)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Connection Walk
|
|
-------------------------------------------------------------------------------
|
|
local function pos_already_reached(pos)
|
|
local key = minetest.hash_node_position(pos)
|
|
if not Route[key] and NumNodes < MAX_NUM_NODES then
|
|
Route[key] = true
|
|
NumNodes = NumNodes + 1
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- check if the given tube dir into the node is valid
|
|
local function valid_indir(pos, indir, node, tlib2)
|
|
local outdir = Flip[indir]
|
|
return tlib2:is_valid_dir(node, outdir)
|
|
end
|
|
|
|
local function is_junction(pos, name, tlib2)
|
|
local ndef = net_def2(pos, name, tlib2.tube_type)
|
|
if ndef then
|
|
return ndef.ntype == "junc"
|
|
end
|
|
end
|
|
|
|
-- Do the walk through the tubelib2 network.
|
|
-- `indir` is the direction which should not be covered by the walk
|
|
-- (coming from there).
|
|
-- if outdir is given, only this dir is used
|
|
local function connection_walk(pos, outdir, indir, node, tlib2, clbk)
|
|
if clbk then clbk(pos, indir, node) end
|
|
if outdir or is_junction(pos, node.name, tlib2) then
|
|
for _,outdir in ipairs(get_outdirs(pos, tlib2, outdir)) do
|
|
local pos2, indir2 = tlib2:get_connected_node_pos(pos, outdir)
|
|
local node = get_node(pos2)
|
|
if valid_indir(pos2, indir2, node, tlib2) and not pos_already_reached(pos2) then
|
|
connection_walk(pos2, nil, indir2, node, tlib2, clbk)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function collect_network_nodes(pos, tlib2, outdir)
|
|
local t = minetest.get_us_time()
|
|
Route = {}
|
|
NumNodes = 0
|
|
pos_already_reached(pos)
|
|
local netw = {}
|
|
local node = N(pos)
|
|
local netw_type = tlib2.tube_type
|
|
local tbl = get_node_connection_dirs_table(pos, netw_type)
|
|
if tbl[outdir] then -- valid conncetion
|
|
-- outdir corresponds to the indir coming from
|
|
connection_walk(pos, outdir, Flip[outdir], node, tlib2, function(pos, indir, node)
|
|
local ndef = net_def2(pos, node.name, netw_type)
|
|
if ndef then
|
|
local ntype = ndef.ntype
|
|
if not netw[ntype] then netw[ntype] = {} end
|
|
netw[ntype][#netw[ntype] + 1] = {pos = pos, indir = indir}
|
|
end
|
|
end)
|
|
end
|
|
netw.ttl = minetest.get_gametime() + TTL
|
|
netw.num_nodes = NumNodes
|
|
t = minetest.get_us_time() - t
|
|
--print("collect_network_nodes in " .. t .. " us", NumNodes, P2S(pos), N(pos).name)
|
|
return netw
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Maintain Network
|
|
-------------------------------------------------------------------------------
|
|
local function set_network(netw_type, netID, network)
|
|
assert(netID)
|
|
if netID > 0 then
|
|
Networks[netw_type] = Networks[netw_type] or {}
|
|
Networks[netw_type][netID] = network
|
|
Networks[netw_type][netID].ttl = minetest.get_gametime() + TTL
|
|
end
|
|
end
|
|
|
|
-- Return network if available, or dummy network.
|
|
-- The function updates the network TTL, thus keeping the network alive.
|
|
local function get_network(netw_type, netID)
|
|
assert(netID)
|
|
if netID > 0 then
|
|
local netw = Networks[netw_type] and Networks[netw_type][netID]
|
|
if netw then
|
|
netw.ttl = minetest.get_gametime() + TTL
|
|
return netw
|
|
end
|
|
end
|
|
return {num_nodes = 0}
|
|
end
|
|
|
|
local function delete_network(netw_type, netID)
|
|
assert(netID)
|
|
if netID > 0 then
|
|
if Networks[netw_type] and Networks[netw_type][netID] then
|
|
Networks[netw_type][netID] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Keep data in memory small
|
|
local function remove_outdated_networks()
|
|
local to_be_deleted = {}
|
|
local t = minetest.get_gametime()
|
|
for net_name,tbl in pairs(Networks) do
|
|
for netID,network in pairs(tbl) do
|
|
local valid = (network.ttl or 0) - t
|
|
if valid < 0 then
|
|
to_be_deleted[#to_be_deleted+1] = {net_name, netID}
|
|
end
|
|
end
|
|
end
|
|
for _,item in ipairs(to_be_deleted) do
|
|
local net_name, netID = unpack(item)
|
|
Networks[net_name][netID] = nil
|
|
print("Network " .. netw_num(netID) .. " timed out")
|
|
end
|
|
minetest.after(60, remove_outdated_networks)
|
|
end
|
|
minetest.after(60, remove_outdated_networks)
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Maintain netID
|
|
-------------------------------------------------------------------------------
|
|
local function set_netID(pos, outdir, netID)
|
|
local hash = minetest.hash_node_position(pos)
|
|
NetIDs[hash] = NetIDs[hash] or {}
|
|
NetIDs[hash][outdir] = netID
|
|
end
|
|
|
|
local function get_netID(pos, outdir)
|
|
local hash = minetest.hash_node_position(pos)
|
|
NetIDs[hash] = NetIDs[hash] or {}
|
|
return NetIDs[hash][outdir]
|
|
end
|
|
|
|
-- determine network ID (largest hash number of all nodes with given type)
|
|
local function determine_netID(netw)
|
|
local netID = 0
|
|
for node_type, table in pairs(netw) do
|
|
if type(table) == "table" then
|
|
for _, item in ipairs(netw[node_type] or {}) do
|
|
local outdir = Flip[item.indir]
|
|
local new = minetest.hash_node_position(item.pos) * 8 + outdir
|
|
if netID <= new then
|
|
netID = new
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return netID
|
|
end
|
|
|
|
-- store network ID for each network node
|
|
local function store_netID(tlib2, netw, netID)
|
|
for node_type, table in pairs(netw) do
|
|
if type(table) == "table" then
|
|
for _, item in ipairs(table) do
|
|
local outdir = get_outdir(node_type, item.indir)
|
|
set_netID(item.pos, outdir, netID)
|
|
end
|
|
end
|
|
end
|
|
set_network(tlib2.tube_type, netID, netw)
|
|
end
|
|
|
|
-- delete network and netID for all nodes in the network
|
|
-- `outdir` shall be 0 for junctions
|
|
local function delete_netID(pos, tlib2, outdir)
|
|
local netID = get_netID(pos, outdir)
|
|
if netID then
|
|
local netw = get_network(tlib2.tube_type, netID)
|
|
for node_type, table in pairs(netw) do
|
|
if type(table) == "table" then
|
|
for _, item in ipairs(table) do
|
|
local outdir = get_outdir(node_type, item.indir)
|
|
set_netID(item.pos, outdir, nil)
|
|
end
|
|
end
|
|
end
|
|
set_netID(pos, outdir, nil)
|
|
delete_network(tlib2.tube_type, netID)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- API Functions
|
|
-------------------------------------------------------------------------------
|
|
|
|
networks.MAX_NUM_NODES = MAX_NUM_NODES
|
|
|
|
-- Table for a 180 degree turn: indir => outdir and vice versa
|
|
networks.Flip = tubelib2.Turn180Deg
|
|
|
|
-- networks.net_def(pos, netw_type)
|
|
networks.net_def = net_def
|
|
|
|
-- sides: outdir:
|
|
-- U
|
|
-- | B
|
|
-- | / 6 (N)
|
|
-- +--|-----+ | 1
|
|
-- / o /| | /
|
|
-- +--------+ | |/
|
|
-- L <----| |o----> R (W) 4 <-------+-------> 2 (O)
|
|
-- | o | | /|
|
|
-- | / | + / |
|
|
-- | / |/ 3 |
|
|
-- +-/------+ (S) 5
|
|
-- / |
|
|
-- F |
|
|
-- D
|
|
--
|
|
-- networks.side_to_outdir(pos, side)
|
|
networks.side_to_outdir = side_to_outdir
|
|
|
|
-- networks.is_junction(pos, name, tlib2)
|
|
networks.is_junction = is_junction
|
|
|
|
-- Return a simple number instead of the netID
|
|
-- Useful for debuging purposes
|
|
-- networks.netw_num(netID)
|
|
networks.netw_num = netw_num
|
|
|
|
-- For debugging purposes
|
|
-- networks.network_nodes(netID, network)
|
|
networks.network_nodes = network_nodes
|
|
|
|
-- networks.get_network(netw_type, netID)
|
|
networks.get_network = get_network
|
|
|
|
-- return the networks table from the node definition
|
|
-- networks.net_def(pos, netw_type)
|
|
networks.net_def = net_def
|
|
|
|
-- Function returns {outdir} or all node dirs with connections
|
|
-- networks.get_outdirs(pos, tlib2, outdir)
|
|
networks.get_outdirs = get_outdirs
|
|
|
|
-- Provide own netID
|
|
-- networks.get_netID(pos, outdir)
|
|
networks.get_netID = get_netID
|
|
|
|
-- networks.get_node_connection_dirs(pos, netw_type)
|
|
networks.get_node_connection_dirs = get_node_connection_dirs
|
|
|
|
-- To be called from each node via 'tubelib2_on_update2'
|
|
-- 'output' is optional and only needed for nodes with dedicated
|
|
-- pipe sides. Junctions have to provide 0 (= same network on all sides).
|
|
function networks.update_network(pos, outdir, tlib2, node)
|
|
store_node_connection_sides(pos, tlib2) -- update node internal data
|
|
delete_netID(pos, tlib2, outdir) -- delete node netIDs and network
|
|
end
|
|
|
|
-- Provide or determine netID
|
|
function networks.determine_netID(pos, tlib2, outdir)
|
|
assert(outdir)
|
|
local netID = get_netID(pos, outdir)
|
|
if netID and Networks[tlib2.tube_type] and Networks[tlib2.tube_type][netID] then
|
|
return netID
|
|
elseif netID == 0 then
|
|
return -- no network available
|
|
end
|
|
|
|
local netw = collect_network_nodes(pos, tlib2, outdir)
|
|
if netw.num_nodes > 1 then
|
|
netID = determine_netID(netw)
|
|
store_netID(tlib2, netw, netID)
|
|
return netID
|
|
end
|
|
-- mark as "no network"
|
|
set_netID(pos, outdir, 0)
|
|
end
|
|
|
|
-- Provide network with all node tables
|
|
function networks.get_network_table(pos, tlib2, outdir)
|
|
assert(outdir)
|
|
local netID = networks.determine_netID(pos, tlib2, outdir)
|
|
if netID and netID > 0 then
|
|
return get_network(tlib2.tube_type, netID)
|
|
end
|
|
end
|