waterworks/execute.lua

205 lines
5.8 KiB
Lua

local pressure_margin = 20
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
-- We don't need to recalculate, nothing about the pipe network has changed since last time
inlets = pipe_cache[net_index].inlets
outlets = pipe_cache[net_index].outlets
else
-- Find all the inlets and outlets and sort them by pressure
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)
-- Cache the results
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))
-- pressure_margin allows us to check sources that are a little bit below sinks,
-- in case the extra pressure from their water depth is sufficient to force water through
if source.pressure + pressure_margin >= 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