--[[ Networks ======== Copyright (C) 2021-2023 Joachim Stolberg AGPL v3 See LICENSE.txt for more information Power API for power consuming and generating nodes ]]-- -- for lazy programmers local S2P = minetest.string_to_pos local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end local M = minetest.get_meta local N = tubelib2.get_node_lvm local OBS = networks.node_observer local Flip = tubelib2.Turn180Deg networks.power = {} networks.registered_networks.power = {} local DEFAULT_DATA = { curr_load = 0, -- network storage value max_capa = 0, -- network storage capacity consumed = 0, -- consumed power by consumers provided = 0, -- provided power by generators available = 0, -- max. available generator power netw_num = 0, -- network number } -- Storage parameters: -- capa = maximum value in power units -- load = current value in power units -- level = ratio value (load/capa) (0..1) local Power = {} -- {netID = {curr_load, max_capa, consumed, provided, available}} -- Determine load, capa and other power network data local function get_power_data(pos, tlib2, outdir, netID) assert(outdir) local netw = networks.get_network_table(pos, tlib2, outdir) or {} local max_capa = 1 -- to prevent nan local max_perf = 0 local curr_load = 0 -- Generators for _,item in ipairs(netw.gen or {}) do local ndef = minetest.registered_nodes[N(item.pos).name] local data = ndef.get_generator_data and ndef.get_generator_data(item.pos, Flip[item.indir], tlib2) if data then OBS("get_power_data", item.pos, data) max_capa = max_capa + (data.capa or 0) max_perf = max_perf + (data.perf or 0) curr_load = curr_load + ((data.level or 0) * (data.capa or 0)) end end -- Storage systems for _,item in ipairs(netw.sto or {}) do local ndef = minetest.registered_nodes[N(item.pos).name] local data = ndef.get_storage_data and ndef.get_storage_data(item.pos, Flip[item.indir], tlib2) if data then OBS("get_power_data", item.pos, data) max_capa = max_capa + (data.capa or 0) curr_load = curr_load + ((data.level or 0) * (data.capa or 0)) end end Power[netID] = { curr_load = curr_load, -- network storage value max_capa = max_capa, -- network storage capacity max_perf = max_perf, -- max. available power consumed = 0, -- consumed power provided = 0, -- provided power available = 0, -- available power num_nodes = netw.num_nodes, } return Power[netID] end ------------------------------------------------------------------------------- -- For all types of nodes ------------------------------------------------------------------------------- -- names: list of node names -- tlib2: tubelib2 instance -- node_type: one of "gen", "con", "sto", "junc" -- valid_sides: something like {"L", "R"} or nil function networks.power.register_nodes(names, tlib2, node_type, valid_sides) if node_type == "gen" then assert(#valid_sides <= 2) elseif node_type == "sto" then assert(#valid_sides == 1) elseif node_type == "con" or node_type == "junc" then assert(not valid_sides or type(valid_sides) == "table") valid_sides = valid_sides or {"B", "R", "F", "L", "D", "U"} elseif node_type and type(node_type) == "string" then valid_sides = valid_sides or {"B", "R", "F", "L", "D", "U"} else error("parameter error") end tlib2:add_secondary_node_names(names) networks.registered_networks.power[tlib2.tube_type] = tlib2 for _, name in ipairs(names) do local ndef = minetest.registered_nodes[name] local tbl = ndef.networks or {} assert(tbl[tlib2.tube_type] == nil, "more than one call of 'networks.power.register_nodes' for " .. names[1]) tbl[tlib2.tube_type] = {ntype = node_type} minetest.override_item(name, {networks = tbl}) tlib2:set_valid_sides(name, valid_sides) end end -- To be called for each power network change via -- tubelib2_on_update2 or register_on_tube_update2 function networks.power.update_network(pos, outdir, tlib2, node) local ndef = networks.net_def(pos, tlib2.tube_type) assert(ndef, "node " .. N(pos).name .. " has no 'networks." .. tlib2.tube_type .. "' table") if ndef.ntype == "junc" then outdir = 0 end local netID = networks.get_netID(pos, outdir) if netID then Power[netID] = nil end networks.update_network(pos, outdir, tlib2, node) end ------------------------------------------------------------------------------- -- Consumer ------------------------------------------------------------------------------- -- Function checks for a power grid, not for enough power -- Param outdir is optional function networks.power.power_available(pos, tlib2, outdir) for _,outdir in ipairs(networks.get_outdirs(pos, tlib2, outdir)) do local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) OBS("power_available", pos, pwr) return pwr.curr_load > 0 end end end -- Param outdir is optional function networks.power.consume_power(pos, tlib2, outdir, amount) assert(amount) for _,outdir in ipairs(networks.get_outdirs(pos, tlib2, outdir)) do local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) OBS("consume_power", pos, {outdir = outdir, amount = amount}, pwr) if pwr.curr_load >= amount then pwr.curr_load = pwr.curr_load - amount pwr.consumed = pwr.consumed + amount return amount else local consumed = pwr.curr_load pwr.curr_load = 0 pwr.consumed = pwr.consumed + consumed return consumed end end end return 0 end ------------------------------------------------------------------------------- -- Generator ------------------------------------------------------------------------------- -- amount is the maximum power, the generator can provide. -- cp1 and cp2 are control points for the charge regulator. -- From cp1 the charging power is reduced more and more and reaches zero at cp2. -- -- A -- | -- 100 % |-------------------__ -- | --__ -- | --__ -- | --__ -- --+------------------+---------------+----> -- | cp1 cp2 -- function networks.power.provide_power(pos, tlib2, outdir, amount, cp1, cp2) assert(outdir) assert(amount and amount > 0) local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) local x = pwr.curr_load / pwr.max_capa OBS("provide_power", pos, {outdir = outdir, amount = amount}, pwr) pwr.available = pwr.available + amount amount = math.min(amount, pwr.max_capa - pwr.curr_load) cp1 = cp1 or 0.8 cp2 = cp2 or 1.0 if x < cp1 then -- charge with full power pwr.curr_load = pwr.curr_load + amount pwr.provided = pwr.provided + amount return amount elseif x < cp2 then -- charge with reduced power local factor = 1 - ((x - cp1) / (cp2 - cp1)) local provided = amount * factor pwr.curr_load = pwr.curr_load + provided pwr.provided = pwr.provided + provided return provided else -- turn off return 0 end end return 0 end -- Function for generators with storage capacity function networks.power.get_storage_load(pos, tlib2, outdir, amount) local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) OBS("get_storage_load", pos, pwr) if pwr.max_capa and pwr.max_capa > 0 then return pwr.curr_load / pwr.max_capa * amount else error("invalid pwr.max_capa", pwr.max_capa) end end return 0 end ------------------------------------------------------------------------------- -- Storage ------------------------------------------------------------------------------- -- Function returns a table with storage level as ratio (0..1) and the -- charging state (1 = charging, -1 = uncharging, or 0) -- Function provides nil if no network is available function networks.power.get_storage_data(pos, tlib2, outdir) assert(outdir) local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) OBS("get_storage_data", pos, pwr) local charging = (pwr.provided > pwr.consumed and 1) or (pwr.provided < pwr.consumed and -1) or 0 return {level = pwr.curr_load / pwr.max_capa, charging = charging} end end -- To be called for each network storage change (turn on/off of storage/generator nodes) function networks.power.start_storage_calc(pos, tlib2, outdir) assert(outdir) local netID = networks.determine_netID(pos, tlib2, outdir) OBS("start_storage_calc", pos) if netID then Power[netID] = nil end end ------------------------------------------------------------------------------- -- Transformer ------------------------------------------------------------------------------- -- Charge transfer in both directions between network 1 and network 2 -- 'netw1' and 'netw2' are tubelib2 network instances. -- Function returns a table with result values for: -- {curr_load1, curr_load2, max_capa1, max_capa2, moved} function networks.power.transfer_duplex(pos, netw1, outdir1, netw2, outdir2, amount) local netID1 = networks.determine_netID(pos, netw1, outdir1) local netID2 = networks.determine_netID(pos, netw2, outdir2) if netID1 and netID2 then local pwr1 = Power[netID1] or get_power_data(pos, netw1, outdir1, netID1) local pwr2 = Power[netID2] or get_power_data(pos, netw2, outdir2, netID2) local lvl = pwr1.curr_load / pwr1.max_capa - pwr2.curr_load / pwr2.max_capa local moved pwr2.available = pwr2.available + amount pwr1.available = pwr1.available + amount if lvl > 0 then -- transfer from netw1 to netw2 moved = math.min(amount, lvl * math.min(pwr1.max_capa, pwr2.max_capa)) moved = math.max(moved, 0) pwr1.curr_load = pwr1.curr_load - moved pwr2.curr_load = pwr2.curr_load + moved pwr1.consumed = (pwr1.consumed or 0) + moved pwr2.provided = (pwr2.provided or 0) + moved elseif lvl < 0 then -- transfer from netw2 to netw1 moved = math.min(amount, lvl * math.min(pwr1.max_capa, pwr2.max_capa)) moved = math.max(moved, 0) pwr2.curr_load = pwr2.curr_load - moved pwr1.curr_load = pwr1.curr_load + moved pwr2.consumed = (pwr2.consumed or 0) + moved pwr1.provided = (pwr1.provided or 0) + moved else moved = 0 end OBS("transfer_duplex", pos, pwr1, pwr2) return { curr_load1 = pwr1.curr_load, curr_load2 = pwr2.curr_load, max_capa1 = pwr1.max_capa, max_capa2 = pwr2.max_capa, moved = moved} end end -- Charge transfer in one direction from network 1 to network 2 -- 'netw1' and 'netw2' are tubelib2 network instances. -- Function returns a table with result values for: -- {curr_load1, curr_load2, max_capa1, max_capa2, moved} function networks.power.transfer_simplex(pos, netw1, outdir1, netw2, outdir2, amount) local netID1 = networks.determine_netID(pos, netw1, outdir1) local netID2 = networks.determine_netID(pos, netw2, outdir2) if netID1 and netID2 then local pwr1 = Power[netID1] or get_power_data(pos, netw1, outdir1, netID1) local pwr2 = Power[netID2] or get_power_data(pos, netw2, outdir2, netID2) local lvl = pwr1.curr_load / pwr1.max_capa - pwr2.curr_load / pwr2.max_capa local moved pwr2.available = pwr2.available + amount if lvl > 0 then -- transfer from netw1 to netw2 moved = math.min(amount, lvl * math.min(pwr1.max_capa, pwr2.max_capa)) moved = math.max(moved, 0) pwr1.curr_load = pwr1.curr_load - moved pwr2.curr_load = pwr2.curr_load + moved pwr1.consumed = (pwr1.consumed or 0) + moved pwr2.provided = (pwr2.provided or 0) + moved else moved = 0 end OBS("transfer_simplex", pos, pwr1, pwr2) return { curr_load1 = pwr1.curr_load, curr_load2 = pwr2.curr_load, max_capa1 = pwr1.max_capa, max_capa2 = pwr2.max_capa, moved = moved} end end ------------------------------------------------------------------------------- -- Switch ------------------------------------------------------------------------------- function networks.power.turn_switch_on(pos, tlib2, name_off, name_on) local node = N(pos) local meta = M(pos) local changed = false if node.name == name_off then node.name = name_on changed = true elseif meta:get_string("netw_name") == name_off then meta:set_string("netw_name", name_on) else return false end if meta:contains("netw_param2") then meta:set_int("netw_param2", meta:get_int("netw_param2_copy")) else node.param2 = meta:get_int("netw_param2_copy") end meta:set_int("netw_param2_copy", 0) if changed then minetest.swap_node(pos, node) end tlib2:after_place_tube(pos) return true end function networks.power.turn_switch_off(pos, tlib2, name_off, name_on) local node = N(pos) local meta = M(pos) local changed = false if node.name == name_on then node.name = name_off changed = true elseif meta:get_string("netw_name") == name_on then meta:set_string("netw_name", name_off) else return false end if meta:contains("netw_param2") then meta:set_int("netw_param2_copy", meta:get_int("netw_param2")) --meta:set_int("netw_param2", 0) else meta:set_int("netw_param2_copy", node.param2) end if changed then minetest.swap_node(pos, node) end if meta:contains("netw_param2") then node.param2 = meta:get_int("netw_param2") end tlib2:after_dig_tube(pos, node) return true end ------------------------------------------------------------------------------- -- Statistics ------------------------------------------------------------------------------- function networks.power.get_network_data(pos, tlib2, outdir) for _,outdir in ipairs(networks.get_outdirs(pos, tlib2, outdir)) do local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) local consumed, provided, available if pwr.available > 0 and pwr.max_perf > 0 then local fac = pwr.max_perf / pwr.available available = pwr.max_perf provided = pwr.provided * fac consumed = pwr.consumed * fac else available = pwr.max_perf provided = 0 consumed = pwr.consumed end local res = { curr_load = pwr.curr_load, -- network storage value max_capa = pwr.max_capa, -- network storage capacity consumed = consumed, -- consumed power by consumers provided = provided, -- provided power by generators available = available, -- max. available generator power netw_num = networks.netw_num(netID), -- network number } pwr.consumed = 0 pwr.provided = 0 pwr.available = 0 return res end end return DEFAULT_DATA end function networks.power.get_storage_percent(pos, tlib2, outdir) assert(outdir) local netID = networks.determine_netID(pos, tlib2, outdir) if netID then local pwr = Power[netID] or get_power_data(pos, tlib2, outdir, netID) return (pwr.curr_load or 0) * 100 / (pwr.max_capa or 1) end return 0 end