-- -- Power network specific functions and data should live here -- local S = technic.getter local off_delay_seconds = tonumber(technic.config:get("switch_off_delay_seconds")) local network_node_arrays = {"PR_nodes","BA_nodes","RE_nodes"} technic.active_networks = {} local networks = {} technic.networks = networks local technic_cables = {} technic.cables = technic_cables local poshash = minetest.hash_node_position local hashpos = minetest.get_position_from_hash function technic.create_network(sw_pos) local network_id = poshash({x=sw_pos.x,y=sw_pos.y-1,z=sw_pos.z}) technic.build_network(network_id) return network_id end local function pos_in_array(pos, array) for _,pos2 in ipairs(array) do if pos.x == pos2.x and pos.y == pos2.y and pos.z == pos2.z then return true end end return false end function technic.merge_networks(net1, net2) -- TODO: Optimize for merging small network into larger by first checking network -- node counts for both networks and keep network id with most nodes. assert(type(net1) == "table", "Invalid net1 for technic.merge_networks") assert(type(net2) == "table", "Invalid net2 for technic.merge_networks") assert(net1 ~= net2, "Deadlock recipe: net1 & net2 equals for technic.merge_networks") -- Move data in cables table for node_id,cable_net_id in pairs(technic_cables) do if cable_net_id == net2.id then technic_cables[node_id] = net1.id end end -- Move data in machine tables for _,tablename in ipairs(network_node_arrays) do for _,pos in ipairs(net2[tablename]) do table.insert(net1[tablename], pos) end end -- Move data in all_nodes table for node_id,pos in pairs(net2.all_nodes) do net1.all_nodes[node_id] = pos end -- Merge queues for incomplete networks if net1.queue and net2.queue then for _,pos in ipairs(net2.queue) do if not pos_in_array(pos, net1.queue) then table.insert(net1.queue, pos) end end else net1.queue = net1.queue or net2.queue end -- Remove links to net2 networks[net2.id] = nil technic.active_networks[net2.id] = nil end function technic.activate_network(network_id, timeout) -- timeout is optional ttl for network in seconds, if not specified use default local network = networks[network_id] if network then -- timeout is absolute time in microseconds network.timeout = minetest.get_us_time() + ((timeout or off_delay_seconds) * 1000 * 1000) technic.active_networks[network_id] = network end end function technic.sw_pos2tier(pos, load_node) -- Get cable tier for switching station or nil if no cable -- load_node true to use minetest.load_area to load node local cable_pos = {x=pos.x,y=pos.y-1,z=pos.z} if load_node then minetest.load_area(cable_pos) end return technic.get_cable_tier(minetest.get_node(cable_pos).name) end -- Destroy network data function technic.remove_network(network_id) for pos_hash,cable_net_id in pairs(technic_cables) do if cable_net_id == network_id then technic_cables[pos_hash] = nil end end networks[network_id] = nil technic.active_networks[network_id] = nil end local function switch_index(pos, net) for index, spos in ipairs(net.swpos) do if pos.x == spos.x and pos.y == spos.y and pos.z == spos.z then return index end end end function technic.switch_insert(pos, net) if not switch_index(pos, net) then table.insert(net.swpos, table.copy(pos)) end return #net.swpos end function technic.switch_remove(pos, net) local swindex = switch_index(pos, net) if swindex then table.remove(net.swpos, swindex) end return #net.swpos end function technic.sw_pos2network(pos) return technic_cables[poshash({x=pos.x,y=pos.y-1,z=pos.z})] end function technic.pos2network(pos) return technic_cables[poshash(pos)] end function technic.network2pos(network_id) return hashpos(network_id) end function technic.network2sw_pos(network_id) -- Return switching station position for network. -- It is not guaranteed that position actually contains switching station. local sw_pos = hashpos(network_id) sw_pos.y = sw_pos.y + 1 return sw_pos end function technic.network_infotext(network_id, text) local network = networks[network_id] if network then if text then network.infotext = text elseif network.queue then local count = 0 for _ in pairs(network.all_nodes) do count = count + 1 end return S("Building Network: @1 Nodes", count) else return network.infotext end end end local node_timeout = {} local default_timeout = 2 function technic.set_default_timeout(timeout) default_timeout = timeout or 2 end function technic.get_timeout(tier, pos) if node_timeout[tier] == nil then -- it is normal that some multi tier nodes always drop here when checking all LV, MV and HV tiers return 0 end return node_timeout[tier][poshash(pos)] or 0 end local function touch_node(tier, pos, timeout) if node_timeout[tier] == nil then -- this should get built up during registration node_timeout[tier] = {} end node_timeout[tier][poshash(pos)] = timeout or default_timeout end technic.touch_node = touch_node function technic.disable_machine(pos, node) local nodedef = minetest.registered_nodes[node.name] if nodedef then local meta = minetest.get_meta(pos) meta:set_string("infotext", S("@1 Has No Network", nodedef.description)) end if nodedef and nodedef.technic_disabled_machine_name then node.name = nodedef.technic_disabled_machine_name minetest.swap_node(pos, node) end if nodedef and nodedef.technic_on_disable then nodedef.technic_on_disable(pos, node) end local node_id = poshash(pos) for _,nodes in pairs(node_timeout) do nodes[node_id] = nil end end local function match_cable_tier_filter(name, tiers) -- Helper to check for set of cable tiers if tiers then for _, tier in ipairs(tiers) do if technic.is_tier_cable(name, tier) then return true end end return false end return technic.get_cable_tier(name) ~= nil end local function get_neighbors(pos, tiers) local tier_machines = tiers and technic.machines[tiers[1]] local is_cable = match_cable_tier_filter(minetest.get_node(pos).name, tiers) local network = is_cable and technic.networks[technic.pos2network(pos)] local cables = {} local machines = {} local positions = { {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y, z=pos.z+1}, {x=pos.x, y=pos.y, z=pos.z-1}, } for _,connected_pos in ipairs(positions) do local name = minetest.get_node(connected_pos).name if tier_machines and tier_machines[name] then table.insert(machines, connected_pos) elseif match_cable_tier_filter(name, tiers) then local cable_network = technic.networks[technic.pos2network(connected_pos)] table.insert(cables,{ pos = connected_pos, network = cable_network, }) if not network then network = cable_network end end end return network, cables, machines end function technic.place_network_node(pos, tiers, name) -- Get connections and primary network if there's any local network, cables, machines = get_neighbors(pos, tiers) if not network then -- We're evidently not on a network, nothing to add ourselves to return end -- Attach to primary network, this must be done before building branches from this position technic.add_network_node(pos, network) if not match_cable_tier_filter(name, tiers) then if technic.machines[tiers[1]][name] == technic.producer_receiver then -- FIXME: Multi tier machine like supply converter should also attach to other networks around pos. -- Preferably also with connection rules defined for machine. -- nodedef.connect_sides could be used to generate these rules. -- For now, assume that all multi network machines belong to technic.producer_receiver group: -- Get cables and networks around PR_RE machine local _, machine_cables, _ = get_neighbors(pos) for _,connection in ipairs(machine_cables) do if connection.network and connection.network.id ~= network.id then -- Attach PR_RE machine to secondary networks (last added is primary until above note is resolved) technic.add_network_node(pos, connection.network) end end else -- Check connected cables for foreign networks, overload if machine was connected to multiple networks for _, connection in ipairs(cables) do if connection.network and connection.network.id ~= network.id then technic.overload_network(connection.network.id) technic.overload_network(network.id) end end end -- Machine added, skip all network building return end -- Attach neighbor machines if cable was added for _,machine_pos in ipairs(machines) do technic.add_network_node(machine_pos, network) end -- Attach neighbor cables for _,connection in ipairs(cables) do if connection.network then if connection.network.id ~= network.id then -- Remove network if position belongs to another network -- FIXME: Network requires partial rebuild but avoid doing it here if possible. -- This might cause problems when merging two active networks into one technic.remove_network(network.id) technic.remove_network(connection.network.id) connection.network = nil end else -- There's cable that does not belong to any network, attach whole branch technic.add_network_node(connection.pos, network) technic.add_network_branch({connection.pos}, network) end end end -- Remove machine or cable from network local function remove_network_node(network_id, pos) local network = networks[network_id] if not network then return end -- Clear hash tables, cannot use table.remove local node_id = poshash(pos) technic_cables[node_id] = nil network.all_nodes[node_id] = nil -- TODO: All following things can be skipped if node is not machine -- check here if it is or is not cable -- or add separate function to remove cables and move responsibility to caller -- Clear indexed arrays, do NOT leave holes local machine_removed = false for _,tblname in ipairs(network_node_arrays) do local tbl = network[tblname] for i=#tbl,1,-1 do local mpos = tbl[i] if mpos.x == pos.x and mpos.y == pos.y and mpos.z == pos.z then table.remove(tbl, i) machine_removed = true break end end end if machine_removed then -- Machine can still be in world, just not connected to any network. If so then disable it. local node = minetest.get_node(pos) technic.disable_machine(pos, node) end end function technic.remove_network_node(pos, tiers, name) -- Get the network and neighbors local network, cables, machines = get_neighbors(pos, tiers) if not network then return end if not match_cable_tier_filter(name, tiers) then -- Machine removed, skip cable checks to prevent unnecessary network cleanups for _,connection in ipairs(cables) do if connection.network then -- Remove machine from all networks around it remove_network_node(connection.network.id, pos) end end return end if #cables == 1 then -- Dead end cable removed, remove it from the network remove_network_node(network.id, pos) -- Remove neighbor machines from network if cable was removed if match_cable_tier_filter(name, tiers) then for _,machine_pos in ipairs(machines) do local net, _, _ = get_neighbors(machine_pos, tiers) if not net then -- Remove machine from network if it does not have other connected cables remove_network_node(network.id, machine_pos) end end end else -- TODO: Check branches around and switching stations for branches: -- remove branches that do not have switching station. Switching stations not tracked but could be easily tracked. -- remove branches not connected to another branch. Individual branches not tracked, requires simple AI heuristics. -- move branches that have switching station to new networks without checking or loading actual nodes in world. -- To do all this network must be aware of individual branches and switching stations, might not be worth it... -- For now remove whole network and let ABM rebuild it technic.remove_network(network.id) end end -- -- Functions to traverse the electrical network -- -- Add a machine node to the LV/MV/HV network local function add_network_machine(nodes, pos, network_id, all_nodes, multitier) local node_id = poshash(pos) local net_id_old = technic_cables[node_id] if net_id_old == nil or (multitier and net_id_old ~= network_id and all_nodes[node_id] == nil) then -- Add machine to network only if it is not already added table.insert(nodes, pos) -- FIXME: Machines connecting to multiple networks should have way to store multiple network ids technic_cables[node_id] = network_id all_nodes[node_id] = pos return true elseif not multitier and net_id_old ~= network_id then -- Do not allow running from multiple networks, trigger overload technic.overload_network(network_id) technic.overload_network(net_id_old) local meta = minetest.get_meta(pos) meta:set_string("infotext",S("Network Overloaded")) end end -- Add a wire node to the LV/MV/HV network local function add_cable_node(pos, network) local node_id = poshash(pos) if not technic_cables[node_id] then technic_cables[node_id] = network.id network.all_nodes[node_id] = pos if network.queue then table.insert(network.queue, pos) end elseif technic_cables[node_id] ~= network.id then -- Conflicting network connected, merge networks if both are still in building stage local net2 = networks[technic_cables[node_id]] if net2 and net2.queue then technic.merge_networks(network, net2) end end end -- Generic function to add found connected nodes to the right classification array local function add_network_node(network, pos, machines) local name = technic.get_or_load_node(pos).name if technic.get_cable_tier(name) == network.tier then add_cable_node(pos, network) elseif machines[name] then if machines[name] == technic.producer then add_network_machine(network.PR_nodes, pos, network.id, network.all_nodes) elseif machines[name] == technic.receiver then add_network_machine(network.RE_nodes, pos, network.id, network.all_nodes) elseif machines[name] == technic.producer_receiver then if add_network_machine(network.PR_nodes, pos, network.id, network.all_nodes, true) then table.insert(network.RE_nodes, pos) end elseif machines[name] == technic.battery then add_network_machine(network.BA_nodes, pos, network.id, network.all_nodes) end end end -- Generic function to add single nodes to the right classification array of existing network function technic.add_network_node(pos, network) add_network_node(network, pos, technic.machines[network.tier]) end -- Traverse a network given a list of machines and a cable type name local function traverse_network(network, pos, machines) local positions = { {x=pos.x+1, y=pos.y, z=pos.z}, {x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}, {x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y, z=pos.z+1}, {x=pos.x, y=pos.y, z=pos.z-1}} for i, cur_pos in pairs(positions) do if not network.all_nodes[poshash(cur_pos)] then add_network_node(network, cur_pos, machines) end end end local function touch_nodes(list, tier) for _, pos in ipairs(list) do touch_node(tier, pos) -- Touch node end end local function get_network(network_id, tier) local cached = networks[network_id] if cached and not cached.queue and cached.tier == tier then touch_nodes(cached.PR_nodes, tier) touch_nodes(cached.BA_nodes, tier) touch_nodes(cached.RE_nodes, tier) return cached.PR_nodes, cached.BA_nodes, cached.RE_nodes end return technic.build_network(network_id) end function technic.add_network_branch(queue, network) -- Adds whole branch to network, queue positions can be used to bypass sub branches local machines = technic.machines[network.tier] --print(string.format("technic.add_network_branch(%s, %s, %.17g)",queue,minetest.pos_to_string(sw_pos),network.id)) local t1 = minetest.get_us_time() while next(queue) do local to_visit = {} for _, pos in ipairs(queue) do network.queue = to_visit traverse_network(network, pos, machines) end queue = to_visit if minetest.get_us_time() - t1 > 10000 then -- time limit exceeded break end end -- Set build queue for network if network build was not finished within time limits network.queue = #queue > 0 and queue end -- Battery charge status updates for network local function update_battery(self, charge, max_charge, supply, demand) self.battery_charge = self.battery_charge + charge self.battery_charge_max = self.battery_charge_max + max_charge self.battery_supply = self.battery_supply + supply self.battery_demand = self.battery_demand + demand if demand ~= 0 then self.BA_count_active = self.BA_count_active + 1 self.BA_charge_active = self.BA_charge_active + charge end end -- Moving average function generator local function sma(period) local values = {} local index = 1 local sum = 0 return function(n) -- Add new value and return average sum = sum - (values[index] or 0) + n values[index] = n index = index ~= period and index + 1 or 1 return sum / #values end end function technic.build_network(network_id) local network = networks[network_id] if network and not network.queue then -- Network exists complete and cached return network.PR_nodes, network.BA_nodes, network.RE_nodes elseif not network then -- Build new network if network does not exist technic.remove_network(network_id) local sw_pos = technic.network2sw_pos(network_id) local tier = sw_pos and technic.sw_pos2tier(sw_pos) if not tier then -- Failed to get initial cable node for network return end network = { -- Build queue queue = {}, -- Basic network data and lookup table for attached nodes id = network_id, tier = tier, all_nodes = {}, swpos = {}, -- Indexed arrays for iteration by machine type PR_nodes = {}, RE_nodes = {}, BA_nodes = {}, -- Power generation, usage and capacity related variables supply = 0, demand = 0, battery_charge = 0, battery_charge_max = 0, BA_count_active = 0, BA_charge_active = 0, battery_supply = 0, battery_demand = 0, -- Battery status update function update_battery = update_battery, -- Network activation and excution control timeout = 0, skip = 0, lag = 0, average_lag = sma(5) } -- Add first cable (one that is holding network id) and build network add_cable_node(technic.network2pos(network_id), network) end -- Continue building incomplete network technic.add_network_branch(network.queue, network) network.battery_count = #network.BA_nodes -- Add newly built network to cache array networks[network_id] = network if not network.queue then -- And return producers, batteries and receivers (should this simply return network?) return network.PR_nodes, network.BA_nodes, network.RE_nodes end end -- -- Execute technic power network -- local node_technic_run = {} minetest.register_on_mods_loaded(function() for name, tiers in pairs(technic.machine_tiers) do local nodedef = minetest.registered_nodes[name] local on_construct = type(nodedef.on_construct) == "function" and nodedef.on_construct local on_destruct = type(nodedef.on_destruct) == "function" and nodedef.on_destruct local place_node = technic.place_network_node local remove_node = technic.remove_network_node minetest.override_item(name, { on_construct = on_construct and function(pos) on_construct(pos) place_node(pos, tiers, name) end or function(pos) place_node(pos, tiers, name) end, on_destruct = on_destruct and function(pos) on_destruct(pos) remove_node(pos, tiers, name) end or function(pos) remove_node(pos, tiers, name) end, }) end for name, _ in pairs(technic.machine_tiers) do if type(minetest.registered_nodes[name].technic_run) == "function" then node_technic_run[name] = minetest.registered_nodes[name].technic_run end end end) local function run_nodes(list, vm, run_stage, network) for _, pos in ipairs(list) do local node = minetest.get_node_or_nil(pos) if not node then vm:read_from_map(pos, pos) node = minetest.get_node_or_nil(pos) end if node and node.name and node_technic_run[node.name] then node_technic_run[node.name](pos, node, run_stage, network) end end end function technic.network_run(network_id) -- -- !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! -- TODO: This function requires a lot of cleanup -- It is moved here from switching_station.lua and still -- contain a lot of switching station specific stuff which -- should be removed and/or refactored. -- local t0 = minetest.get_us_time() local PR_nodes local BA_nodes local RE_nodes local network = networks[network_id] if network then PR_nodes, BA_nodes, RE_nodes = get_network(network_id, network.tier) if not PR_nodes or technic.is_overloaded(network_id) then return end else --dprint("Not connected to a network") technic.network_infotext(network_id, S("@1 Has No Network", S("Switching Station"))) return end -- Reset battery data for updates network.battery_charge = 0 network.battery_charge_max = 0 network.battery_supply = 0 network.battery_demand = 0 network.BA_count_active = 0 network.BA_charge_active = 0 local vm = VoxelManip() run_nodes(PR_nodes, vm, technic.producer, network) run_nodes(RE_nodes, vm, technic.receiver, network) run_nodes(BA_nodes, vm, technic.battery, network) -- Strings for the meta data local eu_demand_str = network.tier.."_EU_demand" local eu_input_str = network.tier.."_EU_input" local eu_supply_str = network.tier.."_EU_supply" -- Distribute charge equally across multiple batteries. local charge_distributed = math.floor(network.BA_charge_active / network.BA_count_active) for n, pos1 in pairs(BA_nodes) do local meta1 = minetest.get_meta(pos1) if (meta1:get_int(eu_demand_str) ~= 0) then meta1:set_int("internal_EU_charge", charge_distributed) end end -- Get all the power from the PR nodes local PR_eu_supply = 0 -- Total power for _, pos1 in pairs(PR_nodes) do local meta1 = minetest.get_meta(pos1) PR_eu_supply = PR_eu_supply + meta1:get_int(eu_supply_str) end --dprint("Total PR supply:"..PR_eu_supply) -- Get all the demand from the RE nodes local RE_eu_demand = 0 for _, pos1 in pairs(RE_nodes) do local meta1 = minetest.get_meta(pos1) RE_eu_demand = RE_eu_demand + meta1:get_int(eu_demand_str) end --dprint("Total RE demand:"..RE_eu_demand) technic.network_infotext(network_id, S("@1. Supply: @2 Demand: @3", S("Switching Station"), technic.EU_string(PR_eu_supply), technic.EU_string(RE_eu_demand))) -- Data that will be used by the power monitor network.supply = PR_eu_supply network.demand = RE_eu_demand network.battery_count = #BA_nodes -- If the PR supply is enough for the RE demand supply them all local BA_eu_demand = network.battery_demand if PR_eu_supply >= RE_eu_demand then --dprint("PR_eu_supply"..PR_eu_supply.." >= RE_eu_demand"..RE_eu_demand) for _, pos1 in pairs(RE_nodes) do local meta1 = minetest.get_meta(pos1) local eu_demand = meta1:get_int(eu_demand_str) meta1:set_int(eu_input_str, eu_demand) end -- We have a surplus, so distribute the rest equally to the BA nodes -- Let's calculate the factor of the demand PR_eu_supply = PR_eu_supply - RE_eu_demand local charge_factor = 0 -- Assume all batteries fully charged if BA_eu_demand > 0 then charge_factor = PR_eu_supply / BA_eu_demand end -- TODO: EU input for all batteries: math.floor(BA_eu_demand * charge_factor * #BA_nodes) for n, pos1 in pairs(BA_nodes) do local meta1 = minetest.get_meta(pos1) local eu_demand = meta1:get_int(eu_demand_str) meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor)) --dprint("Charging battery:"..math.floor(eu_demand*charge_factor)) end local t1 = minetest.get_us_time() local diff = t1 - t0 if diff > 50000 then minetest.log("warning", "[technic] [+supply] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(hashpos(network_id))) end return end -- If the PR supply is not enough for the RE demand we will discharge the batteries too local BA_eu_supply = network.battery_supply if PR_eu_supply + BA_eu_supply >= RE_eu_demand then --dprint("PR_eu_supply "..PR_eu_supply.."+BA_eu_supply "..BA_eu_supply.." >= RE_eu_demand"..RE_eu_demand) for _, pos1 in pairs(RE_nodes) do local meta1 = minetest.get_meta(pos1) local eu_demand = meta1:get_int(eu_demand_str) meta1:set_int(eu_input_str, eu_demand) end -- We have a deficit, so distribute to the BA nodes -- Let's calculate the factor of the supply local charge_factor = 0 -- Assume all batteries depleted if BA_eu_supply > 0 then charge_factor = (PR_eu_supply - RE_eu_demand) / BA_eu_supply end for n,pos1 in pairs(BA_nodes) do local meta1 = minetest.get_meta(pos1) local eu_supply = meta1:get_int(eu_supply_str) meta1:set_int(eu_input_str, math.floor(eu_supply * charge_factor)) --dprint("Discharging battery:"..math.floor(eu_supply*charge_factor)) end local t1 = minetest.get_us_time() local diff = t1 - t0 if diff > 50000 then minetest.log("warning", "[technic] [-supply] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(hashpos(network_id))) end return end -- If the PR+BA supply is not enough for the RE demand: Power only the batteries local charge_factor = 0 -- Assume all batteries fully charged if BA_eu_demand > 0 then charge_factor = PR_eu_supply / BA_eu_demand end for n, pos1 in pairs(BA_nodes) do local meta1 = minetest.get_meta(pos1) local eu_demand = meta1:get_int(eu_demand_str) meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor)) end for n, pos1 in pairs(RE_nodes) do local meta1 = minetest.get_meta(pos1) meta1:set_int(eu_input_str, 0) end local t1 = minetest.get_us_time() local diff = t1 - t0 if diff > 50000 then minetest.log("warning", "[technic] technic_run took " .. diff .. " us at " .. minetest.pos_to_string(hashpos(network_id))) end end