Initial version

master
FaceDeer 2019-03-24 21:07:23 -06:00
parent 27dab3fdd0
commit ffe9e9cf15
29 changed files with 1119 additions and 0 deletions

54
README.md Normal file
View File

@ -0,0 +1,54 @@
This mod provides a set of nodes that allow large quantities of water to be pumped from one place to another, potentially over very long distances.
**Important Note:** The default behaviour of water in Minetest does not actually lend itself well to this kind of activity. In particular, ``default:water_source`` has ``liquid_renewable = true`` set in its node definition, which often causes new water nodes to be created where old ones are removed.
This mod includes an optional setting that disables ``liquid_renewable`` for default water, but for best effect it is highly recommended that this mod be used in conjunction with the [dynamic_liquid](https://github.com/minetest-mods/dynamic_liquid) mod.
The [airtanks](https://github.com/minetest-mods/airtanks) mod can also be helpful for players who are interested in doing large-scale underwater construction projects.
## Pipes
The core node type introduced by this mod is the pipe. When pipes are laid adjacent to each other they connect up to form a pipe network, to which inlets, outlets, and pumps can be connected. All contiguously pipes are part of the same network, and all terminal nodes connected to that network will be able to interact with each other.
Pipes automatically connect to other pipes through any of their six faces.
## Terminals
Terminals can only be connected to a pipe network via one face, the side that by default is facing away from the player when they place the node in world. They interact with water only on the opposite face - the one facing toward the player when they place the node in world.
A screwdriver can be used to reorient terminals if you want one facing upward or downward.
The types of terminals in this mod are:
* Inlets let water enter the pipe but not leave
* Outlets let water out but not in
* Grates let water flow either way depending on pressure
* Pumps are inlets that force water into the network at a higher pressure than their elevation would normally give it.
## Valves
A valve can be used to connect or disconnect sections of a pipe network with a simple right-click. When a valve is "open" it acts like a pipe section, and when it's "closed" it does not act like a pipe.
## Elevation and pressure
The flow of water through the network is determined by two main factors; the directionality of each type of terminal, and the pressure of the water at that terminal.
Water flows from high pressure terminals to low pressure terminals. The rise and fall of the pipe in between those two terminals doesn't really matter, just the pressure at the terminals themselves.
The following figure illustrates the basics of how this works with a very simple three-terminal pipe network:
![Figure 1](/screenshots/waterworks_figure_1.png)
The two terminals on the left side are "inlets", only permitting water to enter the network, and the terminal on the right is a grate that allows water in or out.
If terminal 1 were to be immersed in water, water nodes would be transferred from terminal 1 to terminal 2 because terminal 1's higher elevation gives it higher pressure than terminal 2. Water would *not* be transferred to terminal 3 as terminal 3 is an inlet only.
If terminal 2 were to be immersed, likewise no water would be transferred because although terminal 2 can allow water to enter the pipe (it's a grate) there are no valid outlet terminals it could go to.
If terminal 3 were immersed in water, no water would be transferred because terminal 2 is higher elevation and therefore there isn't enough pressure at terminal 3 to reach it.
![Figure 2](/screenshots/waterworks_figure_2.png)
In this example termimal 3 is a pump, which acts as if it were an inlet located at an elevation 100 meters higher than it actually is. There are two potential outlets for water entering the system. Water is preferentially emitted from the *lowest* pressure outlet, so if terminal 3 was immersed in water it would be sent to terminal 2. However, if terminal 2 was contained in an enclosed space that had run out of room for additional water, the water would then be sent to the next-lowest outlet and come out of terminal 1.
If terminal 1 was immersed, then water would transfer from it to terminal 2. Terminal 3 is an inlet, so water wouldn't come out of it.

1
depends.txt Normal file
View File

@ -0,0 +1 @@
default

197
execute.lua Normal file
View File

@ -0,0 +1,197 @@
local pipe_cache = {}
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},
}
local sort_by_pressure = function(first, second)
local first_pressure = first.pressure
local second_pressure = second.pressure
if first_pressure == nil or second_pressure == nil then
minetest.log("error", "[waterworks] attempted to sort something by pressure that had no pressure value: " .. dump(first) .. "\n" .. dump(second))
return
end
return first_pressure > second_pressure
end
local valid_sink = function(node_name)
return node_name == "air" or node_name == "default:water_flowing"
end
local valid_source = function(node_name)
return node_name == "default:water_source"
end
-- breadth-first search passing through water searching for air or flowing water, limited to y <= pressure.
-- I could try to be fancy about water flowing downward preferentially, let's leave that as a TODO for now.
local flood_search_outlet = function(start_pos, pressure)
local start_node = minetest.get_node(start_pos)
local start_node_name = start_node.name
if valid_sink(start_node_name) then
return start_pos
end
local visited = {}
visited[minetest.hash_node_position(start_pos)] = true
local queue = {start_pos}
local queue_pointer = 1
while #queue >= queue_pointer do
local current_pos = queue[queue_pointer]
queue_pointer = queue_pointer + 1
for _, cardinal_dir in ipairs(cardinal_dirs) do
local new_pos = vector.add(current_pos, cardinal_dir)
local new_hash = minetest.hash_node_position(new_pos)
if visited[new_hash] == nil and new_pos.y <= pressure then
local new_node = minetest.get_node(new_pos)
local new_node_name = new_node.name
if valid_sink(new_node_name) then
return new_pos
end
visited[new_hash] = true
if valid_source(new_node_name) then
table.insert(queue, new_pos)
end
end
end
end
return nil
end
local upward_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},
}
local shuffle = function(tbl)
for i = #tbl, 2, -1 do
local rand = math.random(i)
tbl[i], tbl[rand] = tbl[rand], tbl[i]
end
return tbl
end
-- depth-first random-walk search trending in an upward direction, returns when it gets cornered
local find_source = function(start_pos)
local current_node = minetest.get_node(start_pos)
local current_node_name = current_node.name
if not valid_source(current_node_name) then
return nil
end
local visited = {[minetest.hash_node_position(start_pos)] = true}
local current_pos = start_pos
local continue = true
while continue do
continue = false
shuffle(upward_dirs)
for _, dir in ipairs(upward_dirs) do
local next_pos = vector.add(current_pos, dir)
local next_hash = minetest.hash_node_position(next_pos)
if visited[next_hash] == nil then
visited[next_hash] = true
local next_node = minetest.get_node(next_pos)
local next_node_name = next_node.name
if valid_source(next_node_name) then
current_pos = next_pos
continue = true
break
end
end
end
end
return current_pos
end
waterworks.execute_pipes = function(net_index, net_capacity)
local net = waterworks.pipe_networks[net_index]
if net == nil then
minetest.log("error", "[waterworks] Invalid net index given to execute: " .. tostring(net_index))
return
end
local inlets
local outlets
if net.cache_valid then
inlets = pipe_cache[net_index].inlets
outlets = pipe_cache[net_index].outlets
else
inlets = {}
if net.connected.inlet ~= nil then
for _, inlet_set in pairs(net.connected.inlet) do
for _, inlet in pairs(inlet_set) do
table.insert(inlets, inlet)
end
end
end
table.sort(inlets, sort_by_pressure)
outlets = {}
if net.connected.outlet ~= nil then
for _, outlet_set in pairs(net.connected.outlet) do
for _, outlet in pairs(outlet_set) do
table.insert(outlets, outlet)
end
end
end
table.sort(outlets, sort_by_pressure)
pipe_cache[net_index] = {}
pipe_cache[net_index].inlets = inlets
pipe_cache[net_index].outlets = outlets
net.cache_valid = true
end
local inlet_index = 1
local outlet_index = #outlets
local inlet_count = #inlets
local count = 0
-- Starting with the highest-pressure inlet and the lowest-pressure outlet, attempt to move water.
-- We then proceed to steadily lower-pressure inlets and higher-pressure outlets until we meet in the middle, at which point
-- the system is in equilibrium.
while inlet_index <= inlet_count and outlet_index > 0 and count < net_capacity do
local source = inlets[inlet_index]
local sink = outlets[outlet_index]
--minetest.debug("source: " .. dump(source))
--minetest.debug("sink: " .. dump(sink))
if source.pressure >= sink.pressure then
local source_pos = find_source(source.target)
local sink_pos
if source_pos ~= nil then
sink_pos = flood_search_outlet(sink.target, math.max(source.pressure, source_pos.y))
if sink_pos ~= nil then
minetest.swap_node(sink_pos, {name="default:water_source"})
minetest.swap_node(source_pos, {name="air"})
count = count + 1
end
end
if source_pos == nil then
-- the outlet had available space but the inlet didn't provide
inlet_index = inlet_index + 1
elseif sink_pos == nil then
-- the inlet provided but the outlet didn't have space
outlet_index = outlet_index - 1
end
else
break
end
end
end

112
globalstep.lua Normal file
View File

@ -0,0 +1,112 @@
local worldpath = minetest.get_worldpath()
local network_filename = worldpath.."/waterworks_network.json"
-- Json storage
local save_data = function()
if waterworks.dirty_data ~= true then
return
end
local file = io.open(network_filename, "w")
if file then
file:write(minetest.serialize(waterworks.pipe_networks))
file:close()
waterworks.dirty_data = false
end
end
local read_data = function()
local file = io.open(network_filename, "r")
if file then
waterworks.pipe_networks = minetest.deserialize(file:read("*all")) -- note: any cached references to pipe_networks is invalidated here, so do this once at the beginning of the run and never again thereafter.
file:close()
else
waterworks.pipe_networks = {}
end
waterworks.dirty_data = false
for _, net in ipairs(waterworks.pipe_networks) do
net.cache_valid = false
end
end
read_data()
----------------------------------------------
local nets_near_players = {}
minetest.register_abm ({
label = "Active connected node tracking",
nodenames = {"group:waterworks_connected"},
interval = 1.0,
chance = 1,
action = function(pos, node, active_object_count, active_object_count_wider)
local player_close = false
for _, player in ipairs(minetest.get_connected_players()) do
local player_pos = player:get_pos()
if math.abs(player_pos.x - pos.x) < 81 and math.abs(player_pos.z - pos.z) < 81 and math.abs(player_pos.y - pos.y) < 81 then
player_close = true
break
end
end
if not player_close then return end
local hash = minetest.hash_node_position(pos) + waterworks.facedir_to_hash(node.param2)
local net_index = waterworks.find_network_for_pipe_hash(hash)
if net_index < 0 then return end
--minetest.chat_send_all("net near player " .. tostring(net_index))
nets_near_players[net_index] = 5.0
end,
})
local forceloads = {}
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer > 1.0 then
if waterworks.dirty_data then
-- it's possible that a pipe network was split or merged, invalidating the nets_near_players values here.
-- Best to clear them and do nothing for one globalstep, they'll be repopulated shortly.
nets_near_players = {}
end
-- find connected node positions for all networks with connected nodes near players
local ensure_forceload = {}
for index, live_time in pairs(nets_near_players) do
local new_time = live_time - timer
--minetest.chat_send_all("new time " .. tostring(new_time))
if new_time < 0 then
nets_near_players[index] = nil
else
nets_near_players[index] = new_time
for connection_type, connections in pairs(waterworks.pipe_networks[index].connected) do
for hash, _ in pairs(connections) do
ensure_forceload[hash] = true
end
end
end
end
-- clear forceloads that are no longer needed
for hash, _ in pairs(forceloads) do
if not ensure_forceload[hash] then
minetest.forceload_free_block(minetest.get_position_from_hash(hash), true)
end
end
forceloads = ensure_forceload
-- enable forceloads that are needed
for hash, _ in pairs(forceloads) do
minetest.forceload_block(minetest.get_position_from_hash(hash), true)
end
timer = timer - 1.0
save_data()
for index, _ in pairs(nets_near_players) do
--minetest.chat_send_all("executing index " .. tostring(index))
waterworks.execute_pipes(index, 8)
end
end
end)

69
init.lua Normal file
View File

@ -0,0 +1,69 @@
waterworks = {}
local modpath = minetest.get_modpath(minetest.get_current_modname())
dofile(modpath .. "/globalstep.lua")
dofile(modpath .. "/network.lua")
dofile(modpath .. "/execute.lua")
dofile(modpath .. "/nodes.lua")
if minetest.settings:get_bool("waterworks_make_default_water_non_renewable") then
local override_def = {liquid_renewable = false}
minetest.override_item("default:water_source", override_def)
minetest.override_item("default:water_flowing", override_def)
end
-- For test purposes, this rebuilds pipe networks without needing to persist them.
-- May be useful later for fixing broken stuff.
-- Note that this doesn't *remove* pipes that are inappropriately listed in the network,
-- that may be tricky.
--minetest.register_lbm({
-- label = "Validate waterworks pipe networks",
-- name = "waterworks:validate_pipe_networks",
-- nodenames = {"group:waterworks_pipe"},
-- run_at_every_load = true,
-- action = function(pos, node)
-- local hash_pos = minetest.hash_node_position(pos)
-- local found = false
-- for i, net in ipairs(waterworks.pipe_networks) do
-- if net.pipes[hash_pos] then
-- found = true
-- break
-- end
-- end
-- if not found then
-- local new_net_index = waterworks.place_pipe(pos)
-- minetest.log("warning", "[waterworks] detected an unregistered pipe at " ..
-- minetest.pos_to_string(pos) .. ", added it to pipe network " ..
-- tostring(new_net_index))
-- end
-- end,
--})
--minetest.register_chatcommand("dump_pipes", {
---- params = "pos", -- Short parameter description
-- description = "dump pipe network to debug log",
-- func = function(name, param)
-- --minetest.debug(dump(pipe_networks))
--
-- for i, net in ipairs(waterworks.pipe_networks) do
-- minetest.debug("net #" .. tostring(i) .. " cache valid: " .. dump(net.cache_valid))
-- minetest.debug("\tpipes:")
-- local count = 0
-- for hash_pos, _ in pairs(net.pipes) do
-- minetest.debug("\t\t"..minetest.pos_to_string(minetest.get_position_from_hash(hash_pos)))
-- count = count + 1
-- end
-- minetest.debug("\tpipe count: " .. tostring(count))
-- minetest.debug("\tconnections:")
-- for connection_type, connection_list in pairs(net.connected) do
-- minetest.debug("\t\t"..connection_type..":")
-- for connection_hash, data in pairs(connection_list) do
-- minetest.debug("\t\t\t"..minetest.pos_to_string(minetest.get_position_from_hash(connection_hash))..": "..string.gsub(string.gsub(dump(data), "\t", ""), "\n", " "))
-- end
-- end
-- end
--
-- end,
--})

3
mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = waterworks
description = Provides pipes, pumps, and valves for moving water over long distances
depends = default

386
network.lua Normal file
View File

@ -0,0 +1,386 @@
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

279
nodes.lua Normal file
View File

@ -0,0 +1,279 @@
minetest.register_node("waterworks:pipe", {
description = "Waterworks Pipe",
tiles = {
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets.png", scale=4, align_style="world"},
{name="waterworks_pipe.png^waterworks_pipe_rivets_offset_2.png", scale=4, align_style="world"},
},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.25,-0.25,-0.25,0.25,0.25,0.25},
connect_top = {-0.375, 0, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, 0, 0.375},
connect_back = {-0.375, -0.375, 0, 0.375, 0.375, 0.5},
connect_right = {0, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, 0},
connect_left = {-0.5, -0.375, -0.375, 0, 0.375, 0.375},
disconnected = {-0.375,-0.375,-0.375,0.375,0.375,0.375},
},
paramtype = "light",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_pipe = 1},
sounds = default.node_sound_metal_defaults(),
on_construct = function(pos)
waterworks.place_pipe(pos)
end,
on_destruct = function(pos)
waterworks.remove_pipe(pos)
end,
})
-----------------------------------------------------------------
minetest.register_node("waterworks:valve_on", {
description = "Waterworks Valve (open)",
tiles = {"waterworks_metal.png^waterworks_valve_seam.png^waterworks_valve_on.png",},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.4375,-0.4375,-0.4375,0.4375,0.4375,0.4375},
connect_top = {-0.375, 0.4375, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, -0.4375, 0.375},
connect_back = {-0.375, -0.375, 0.4375, 0.375, 0.375, 0.5},
connect_right = {0.4375, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, -0.4375},
connect_left = {-0.5, -0.375, -0.375, -0.4375, 0.375, 0.375},
},
paramtype = "light",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_pipe = 1},
sounds = default.node_sound_metal_defaults(),
on_construct = function(pos)
waterworks.place_pipe(pos)
end,
on_destruct = function(pos)
waterworks.remove_pipe(pos)
end,
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
node.name = "waterworks:valve_off"
minetest.set_node(pos, node)
end,
})
minetest.register_node("waterworks:valve_off", {
description = "Waterworks Valve (closed)",
tiles = {"waterworks_metal.png^waterworks_valve_seam.png^waterworks_valve_off.png",},
connects_to = {"group:waterworks_pipe", "group:waterworks_connected", "group:waterworks_inert"},
connect_sides = { "top", "bottom", "front", "left", "back", "right" },
drawtype = "nodebox",
node_box = {
type = "connected",
fixed = {-0.4375,-0.4375,-0.4375,0.4375,0.4375,0.4375},
connect_top = {-0.375, 0.4375, -0.375, 0.375, 0.5, 0.375},
connect_bottom = {-0.375, -0.5, -0.375, 0.375, -0.4375, 0.375},
connect_back = {-0.375, -0.375, 0.4375, 0.375, 0.375, 0.5},
connect_right = {0.4375, -0.375, -0.375, 0.5, 0.375, 0.375},
connect_front = {-0.375, -0.375, -0.5, 0.375, 0.375, -0.4375},
connect_left = {-0.5, -0.375, -0.375, -0.4375, 0.375, 0.375},
},
paramtype = "light",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_inert = 1},
sounds = default.node_sound_metal_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
node.name = "waterworks:valve_on"
minetest.set_node(pos, node)
end,
})
-----------------------------------------------------------------
local place_inlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "inlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Inlet elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:inlet", {
description = "Waterworks Inlet",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_inlet.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
sounds = default.node_sound_metal_defaults(),
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
_waterworks_update_connected = place_inlet,
on_construct = function(pos)
place_inlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_inlet(pos)
return true
end,
})
local place_pumped_inlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "inlet", {pos = pos, target = target, pressure = target.y + 100})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Pump effective elevation " .. tostring(target.y + 100))
end
minetest.register_node("waterworks:pumped_inlet", {
description = "Waterworks Pumped Inlet",
tiles = {
"waterworks_turbine_base.png",
"waterworks_turbine_base.png",
"waterworks_turbine_side.png^[transformFX",
"waterworks_turbine_side.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_turbine_base.png^waterworks_turbine.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
sounds = default.node_sound_metal_defaults(),
paramtype = "light",
drawtype = "normal",
_waterworks_update_connected = place_pumped_inlet,
on_construct = function(pos)
place_pumped_inlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_pumped_inlet(pos)
return true
end,
})
local place_outlet = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "outlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Outlet elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:outlet", {
description = "Waterworks Outlet",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_outlet.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
sounds = default.node_sound_metal_defaults(),
_waterworks_update_connected = place_outlet,
on_construct = function(pos)
place_outlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "outlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "outlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_outlet(pos)
return true
end,
})
local place_grate = function(pos)
local node = minetest.get_node(pos)
local dir = minetest.facedir_to_dir(node.param2)
local target = vector.subtract(pos, dir)
waterworks.place_connected(pos, "outlet", {pos = pos, target = target, pressure = target.y})
local meta = minetest.get_meta(pos)
meta:set_string("infotext", "Grate elevation " .. tostring(target.y))
end
minetest.register_node("waterworks:grate", {
description = "Waterworks Grate",
tiles = {
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png",
"waterworks_metal.png^waterworks_connected_back.png",
"waterworks_metal.png^waterworks_grate.png",
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {oddly_breakable_by_hand = 1, waterworks_connected = 1},
paramtype = "light",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {{-0.375, -0.375, -0.375, 0.375, 0.375, 0.5}, {-0.5, -0.5, -0.5, 0.5, 0.5, -0.375}},
},
sounds = default.node_sound_metal_defaults(),
_waterworks_update_connected = place_outlet,
on_construct = function(pos)
place_outlet(pos)
end,
on_destruct = function(pos)
waterworks.remove_connected(pos, "outlet")
waterworks.remove_connected(pos, "inlet")
end,
on_rotate = function(pos, node, user, mode, new_param2)
waterworks.remove_connected(pos, "outlet")
waterworks.remove_connected(pos, "inlet")
node.param2 = new_param2
minetest.swap_node(pos, node)
place_outlet(pos)
return true
end,
})

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

1
settingtypes.txt Normal file
View File

@ -0,0 +1 @@
waterworks_make_default_water_non_renewable (Disable self-replication of default water) bool false

17
textures/LICENSE.txt Normal file
View File

@ -0,0 +1,17 @@
The following textures are licensed under the MIT and CC0 licenses by FaceDeer:
waterworks_connected_back
waterworks_grate
waterworks_inlet
waterworks_metal
waterworks_outlet
waterworks_pipe
waterworks_pipe_rivets
waterworks_pipe_rivets_offset
waterworks_pipe_rivets_offset_2
waterworks_turbine
waterworks_turbine_base
waterworks_turbine_side
waterworks_valve_off
waterworks_valve_on
waterworks_valve_seam

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B