386 lines
13 KiB
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 |