waterworks/network.lua

386 lines
13 KiB
Lua

local pipe_networks = waterworks.pipe_networks
local invalidate_cache = function(pipe_network)
pipe_network.cache_valid = false
waterworks.dirty_data = true
end
local cardinal_dirs = {
{x= 0, y=0, z= 1},
{x= 1, y=0, z= 0},
{x= 0, y=0, z=-1},
{x=-1, y=0, z= 0},
{x= 0, y=-1, z= 0},
{x= 0, y=1, z= 0},
}
-- Mapping from facedir value to index in cardinal_dirs.
local facedir_to_dir_map = {
[0]=1, 2, 3, 4,
5, 2, 6, 4,
6, 2, 5, 4,
1, 5, 3, 6,
1, 6, 3, 5,
1, 4, 3, 2,
}
-- Turn the cardinal directions into a set of integers you can add to a hash to step in that direction.
local cardinal_dirs_hash = {}
for i, dir in ipairs(cardinal_dirs) do
cardinal_dirs_hash[i] = minetest.hash_node_position(dir) - minetest.hash_node_position({x=0, y=0, z=0})
end
local facedir_to_dir_index = function(param2)
return facedir_to_dir_map[param2 % 32]
end
local facedir_to_cardinal_hash = function(dir_index)
return cardinal_dirs_hash[dir_index]
end
waterworks.facedir_to_hash = function(param2)
return facedir_to_cardinal_hash(facedir_to_dir_index(param2))
end
local init_new_network = function(hash_pos)
waterworks.dirty_data = true
return {pipes = {[hash_pos] = true}, connected = {}, cache_valid = false}
end
local get_neighbor_pipes = function(pos)
local neighbor_pipes = {}
local neighbor_connected = {}
for _, dir in ipairs(cardinal_dirs) do
local potential_pipe_pos = vector.add(pos, dir)
local neighbor = minetest.get_node(potential_pipe_pos)
if minetest.get_item_group(neighbor.name, "waterworks_pipe") > 0 then
table.insert(neighbor_pipes, potential_pipe_pos)
elseif minetest.get_item_group(neighbor.name, "waterworks_connected") > 0 then
table.insert(neighbor_connected, potential_pipe_pos)
end
end
return neighbor_pipes, neighbor_connected
end
local merge_networks = function(index_list)
table.sort(index_list)
local first_index = table.remove(index_list, 1)
local merged_network = pipe_networks[first_index]
-- remove in reverse order so that indices of earlier tables to remove don't get disrupted
for i = #index_list, 1, -1 do
local index = index_list[i]
local net_to_merge = pipe_networks[index]
for pipe_hash, _ in pairs(net_to_merge.pipes) do
merged_network.pipes[pipe_hash] = true
end
for item_type, item_list in pairs(net_to_merge.connected) do
merged_network.connected[item_type] = merged_network.connected[item_type] or {}
for connection_hash, connection_data in pairs(item_list) do
merged_network.connected[item_type][connection_hash] = connection_data
end
end
table.remove(pipe_networks, index)
end
invalidate_cache(merged_network)
return first_index
end
local handle_connected = function(connected_positions)
for _, pos in ipairs(connected_positions) do
local node = minetest.get_node(pos)
local node_def = minetest.registered_nodes[node.name]
if node_def._waterworks_update_connected then
node_def._waterworks_update_connected(pos)
else
minetest.log("error", "[waterworks] Node def for " .. node.name .. " had no _waterworks_update_connected defined")
end
end
end
-- When placing a pipe at pos, identifies what pipe network to add it to and updates the network map.
-- Note that this can result in fusing multiple networks together into one network.
waterworks.place_pipe = function(pos)
local hash_pos = minetest.hash_node_position(pos)
local neighbor_pipes, neighbor_connected = get_neighbor_pipes(pos)
local neighbor_count = #neighbor_pipes
if neighbor_count == 0 then
-- this newly-placed pipe has no other pipes next to it, so make a new network for it.
local new_net = init_new_network(hash_pos)
table.insert(pipe_networks, new_net)
handle_connected(neighbor_connected)
return #pipe_networks
elseif neighbor_count == 1 then
-- there's only one pipe neighbor. Look up what network it belongs to and add this pipe to it too.
local neighbor_pos_hash = minetest.hash_node_position(neighbor_pipes[1])
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[neighbor_pos_hash] then
pipes[hash_pos] = true
invalidate_cache(net)
handle_connected(neighbor_connected)
return i
end
end
else
local neighbor_index_set = {} -- set of indices for networks that neighbors belong to
local neighbor_index_list = {} -- list version of above
for _, neighbor_pos in ipairs(neighbor_pipes) do
local neighbor_hash = minetest.hash_node_position(neighbor_pos)
for i, net in ipairs(pipe_networks) do
if net.pipes[neighbor_hash] then
if not neighbor_index_set[i] then
table.insert(neighbor_index_list, i)
neighbor_index_set[i] = true
end
end
end
end
if #neighbor_index_list == 1 then -- all neighbors belong to one network. Add this node to that network.
local target_network_index = neighbor_index_list[1]
pipe_networks[target_network_index]["pipes"][hash_pos] = true
invalidate_cache(pipe_networks[target_network_index])
handle_connected(neighbor_connected)
return target_network_index
end
-- The most complicated case, this new pipe segment bridges multiple networks.
if #neighbor_index_list > 1 then
local new_index = merge_networks(neighbor_index_list)
pipe_networks[new_index]["pipes"][hash_pos] = true
handle_connected(neighbor_connected)
return new_index
end
end
-- if we get here we're in a strange state - there are neighbor pipe nodes but none are registered in a network.
-- We could be trying to recover from corruption, so pretend the neighbors don't exist and start a new network.
-- The unregistered neighbors may join it soon.
local new_net = init_new_network(hash_pos)
table.insert(pipe_networks, new_net)
handle_connected(neighbor_connected)
return #pipe_networks
end
waterworks.remove_pipe = function(pos)
local hash_pos = minetest.hash_node_position(pos)
local neighbor_pipes = get_neighbor_pipes(pos)
local neighbor_count = #neighbor_pipes
if neighbor_count == 0 then
-- no neighbors, so this is the last of its network.
for i, net in ipairs(pipe_networks) do
if net.pipes[hash_pos] then
table.remove(pipe_networks, i)
waterworks.dirty_data = true
return i
end
end
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks. Something went wrong to get to this state.")
return -1
elseif neighbor_count == 1 then
-- there's only one pipe neighbor. This pipe is at the end of a line, so just remove it.
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[hash_pos] then
pipes[hash_pos] = nil
invalidate_cache(net)
-- If there's anything connected to the pipe here, remove it from the network too
for _, connected_items in pairs(net.connected) do
connected_items[hash_pos] = nil
end
return i
end
end
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks, despite being neighbor to one at " ..
minetest.pos_to_string(neighbor_pipes[1]) ..
". Something went wrong to get to this state.")
return -1
else
-- we may be splitting networks. This is complicated.
-- find the network we currently belong to. Remove ourselves from it.
local old_net
local old_pipes
local old_connected
local old_index
for i, net in ipairs(pipe_networks) do
local pipes = net.pipes
if pipes[hash_pos] then
old_connected = net.connected
old_net = net
old_pipes = pipes
old_index = i
old_pipes[hash_pos] = nil
-- if there's anything connected to the pipe here, remove it
for _, connected_items in pairs(old_connected) do
connected_items[hash_pos] = nil
end
end
end
if old_index == nil then
minetest.log("error", "[waterworks] pipe removed from pos " .. minetest.pos_to_string(pos) ..
" didn't belong to any networks, despite being neighbor to several. Something went wrong to get to this state.")
return -1
end
-- get the hashes of the neighbor positions.
-- We're maintaining a set as well as a list because they're
-- efficient for different purposes. The list is easy to count,
-- the set is easy to test membership of.
local neighbor_hashes_list = {}
local neighbor_hashes_set = {}
for i, neighbor_pos in ipairs(neighbor_pipes) do
local neighbor_hash = minetest.hash_node_position(neighbor_pos)
neighbor_hashes_list[i] = neighbor_hash
neighbor_hashes_set[neighbor_hash] = true
end
-- We're going to need to traverse through the old network, starting from each of our neighbors,
-- to establish what's still connected.
local to_visit = {}
local visited = {[hash_pos] = true} -- set of hashes we've visited already. We know the starting point is not valid.
local new_nets = {} -- this will be where we put new sets of connected nodes.
while #neighbor_hashes_list > 0 do
local current_neighbor = table.remove(neighbor_hashes_list) -- pop neighbor hash and push it into the to_visit list.
neighbor_hashes_set[current_neighbor] = nil
table.insert(to_visit, current_neighbor) -- file that neighbor hash as our starting point.
local new_net = init_new_network(current_neighbor) -- we know that hash is in old_net, so initialize the new_net with it.
local new_pipes = new_net.pipes
while #to_visit > 0 do
local current_hash = table.remove(to_visit)
for _, cardinal_hash in ipairs(cardinal_dirs_hash) do
local test_hash = cardinal_hash + current_hash
if not visited[test_hash] then
if old_pipes[test_hash] then
-- we've traversed to a node that was in the old network
old_pipes[test_hash] = nil -- remove from old network
new_pipes[test_hash] = true -- add to one we're building
table.insert(to_visit, test_hash) -- flag it as next one to traverse from
if neighbor_hashes_set[test_hash] then
--we've encountered another neighbor while traversing
--eliminate it from future consideration as a starting point.
neighbor_hashes_set[test_hash] = nil
for i, neighbor_hash_in_list in ipairs(neighbor_hashes_list) do
if neighbor_hash_in_list == test_hash then
table.remove(neighbor_hashes_list, i)
break
end
end
if #neighbor_hashes_list == 0 then
--Huzzah! We encountered all neighbors. The rest of the nodes in old_net should belong to new_net.
--We can skip all remaining pathfinding flood-fill and connected testing
for remaining_hash, _ in pairs(old_pipes) do
new_pipes[remaining_hash] = true
to_visit = {}
end
break
end
end
end
end
end
visited[current_hash] = true
end
table.insert(new_nets, new_net)
end
-- distribute connected items to the new nets
if #new_nets == 1 then
-- net didn't split, just keep the old stuff
new_nets[1].connected = old_connected
else
for _, new_net in ipairs(new_nets) do
local new_pipes = new_net.pipes
for item_type, item_list in pairs(old_connected) do
new_net.connected[item_type] = new_net.connected[item_type] or {}
for connection_hash, connection_data in pairs(item_list) do
if new_pipes[connection_hash] then
new_net.connected[item_type][connection_hash] = connection_data
end
end
end
end
end
-- replace the old net with one of the new nets
pipe_networks[old_index] = table.remove(new_nets)
-- if there are any additional nets left, add those as brand new ones.
for _, new_net in ipairs(new_nets) do
table.insert(pipe_networks, new_net)
end
return old_index
end
end
waterworks.place_connected = function(pos, item_type, data)
local node = minetest.get_node(pos)
local dir_index = facedir_to_dir_index(node.param2)
local dir_hash = facedir_to_cardinal_hash(dir_index)
local pos_hash = minetest.hash_node_position(pos)
local connection_hash = pos_hash + dir_hash
for i, net in ipairs(pipe_networks) do
if net.pipes[connection_hash] then
net.connected[item_type] = net.connected[item_type] or {}
net.connected[item_type][connection_hash] = net.connected[item_type][connection_hash] or {}
net.connected[item_type][connection_hash][dir_index] = data
invalidate_cache(net)
return i
end
end
return -1
end
waterworks.remove_connected = function(pos, item_type)
local node = minetest.get_node(pos)
local dir_index = facedir_to_dir_index(node.param2)
local dir_hash = facedir_to_cardinal_hash(dir_index)
local pos_hash = minetest.hash_node_position(pos)
local connection_hash = pos_hash + dir_hash
for i, net in ipairs(pipe_networks) do
if net.pipes[connection_hash] then
local item_list = net.connected[item_type]
if item_list then
if item_list[connection_hash] ~= nil then
local connected_items = item_list[connection_hash]
connected_items[dir_index] = nil
local count = 0
for _, data in pairs(connected_items) do
count = count + 1
end
if count == 0 then
item_list[connection_hash] = nil
end
count = 0
for _, item in pairs(item_list) do
count = count + 1
end
if count == 0 then
net.connected[item_type] = nil
end
invalidate_cache(net)
return i
end
end
break -- If we get here, we didn't find the connected node even though we should have.
end
end
return -1
end
waterworks.find_network_for_pipe_hash = function(hash)
for i, net in ipairs(pipe_networks) do
if net.pipes[hash] then
return i
end
end
return -1
end