sfence
2022-11-04 66e20a9231b9fea823f9b683e495c57ef9a4ad41
commit | author | age
987cc5 1 -- See also technic/doc/api.md
ee5c6c 2
468d79 3 technic.networks = {}
d3f40e 4 technic.cables = {}
a84b5f 5 technic.redundant_warn = {}
be2f30 6
6abd85 7 local mesecons_path = minetest.get_modpath("mesecons")
D 8 local digilines_path = minetest.get_modpath("digilines")
9
be2f30 10 local S = technic.getter
468d79 11
54004f 12 local cable_entry = "^technic_cable_connection_overlay.png"
VE 13
ee0765 14 minetest.register_craft({
S 15     output = "technic:switching_station",
16     recipe = {
83c649 17         {"",                     "technic:lv_transformer", ""},
S 18         {"default:copper_ingot", "technic:machine_casing", "default:copper_ingot"},
19         {"technic:lv_cable",     "technic:lv_cable",       "technic:lv_cable"}
ee0765 20     }
S 21 })
ee5c6c 22
338f3b 23 local mesecon_def
D 24 if mesecons_path then
25     mesecon_def = {effector = {
26         rules = mesecon.rules.default,
27     }}
28 end
29
ee0765 30 minetest.register_node("technic:switching_station",{
be2f30 31     description = S("Switching Station"),
54004f 32     tiles  = {
VE 33         "technic_water_mill_top_active.png",
34         "technic_water_mill_top_active.png"..cable_entry,
35         "technic_water_mill_top_active.png",
36         "technic_water_mill_top_active.png",
37         "technic_water_mill_top_active.png",
38         "technic_water_mill_top_active.png"},
83c649 39     groups = {snappy=2, choppy=2, oddly_breakable_by_hand=2, technic_all_tiers=1},
S 40     connect_sides = {"bottom"},
ee0765 41     sounds = default.node_sound_wood_defaults(),
S 42     on_construct = function(pos)
43         local meta = minetest.get_meta(pos)
be2f30 44         meta:set_string("infotext", S("Switching Station"))
563a4c 45         meta:set_string("active", 1)
6abd85 46         meta:set_string("channel", "switching_station"..minetest.pos_to_string(pos))
D 47         meta:set_string("formspec", "field[channel;Channel;${channel}]")
a84b5f 48         technic.redundant_warn.poshash = nil
ee0765 49     end,
088eea 50     after_dig_node = function(pos)
CK 51         minetest.forceload_free_block(pos)
52         pos.y = pos.y - 1
53         minetest.forceload_free_block(pos)
a84b5f 54         technic.redundant_warn.poshash = nil
088eea 55     end,
6abd85 56     on_receive_fields = function(pos, formname, fields, sender)
D 57         if not fields.channel then
58             return
59         end
60         local plname = sender:get_player_name()
61         if minetest.is_protected(pos, plname) then
62             minetest.record_protection_violation(pos, plname)
63             return
64         end
65         local meta = minetest.get_meta(pos)
66         meta:set_string("channel", fields.channel)
67     end,
338f3b 68     mesecons = mesecon_def,
6abd85 69     digiline = {
D 70         receptor = {action = function() end},
71         effector = {
72             action = function(pos, node, channel, msg)
73                 if msg ~= "GET" and msg ~= "get" then
74                     return
75                 end
76                 local meta = minetest.get_meta(pos)
77                 if channel ~= meta:get_string("channel") then
78                     return
79                 end
80                 digilines.receptor_send(pos, digilines.rules.default, channel, {
81                     supply = meta:get_int("supply"),
82                     demand = meta:get_int("demand")
83                 })
84             end
85         },
86     },
ee0765 87 })
ee5c6c 88
K 89 --------------------------------------------------
90 -- Functions to traverse the electrical network
91 --------------------------------------------------
86fb98 92 local function flatten(map)
NZ 93     local list = {}
94     for key, value in pairs(map) do
95         list[#list + 1] = value
96     end
97     return list
98 end
ee5c6c 99
K 100 -- Add a wire node to the LV/MV/HV network
f9b090 101 -- Returns: indicator whether the cable is new in the network
S 102 local hash_node_position = minetest.hash_node_position
86fb98 103 local function add_network_node(nodes, pos, network_id)
f9b090 104     local node_id = hash_node_position(pos)
86fb98 105     technic.cables[node_id] = network_id
NZ 106     if nodes[node_id] then
107         return false
ee0765 108     end
86fb98 109     nodes[node_id] = pos
ee0765 110     return true
S 111 end
ee5c6c 112
86fb98 113 local function add_cable_node(nodes, pos, network_id, queue)
NZ 114     if add_network_node(nodes, pos, network_id) then
115         queue[#queue + 1] = pos
116     end
117 end
118
ee5c6c 119 -- Generic function to add found connected nodes to the right classification array
f9b090 120 local check_node_subp = function(network, pos, machines, sw_pos, from_below, network_id, queue)
c38da0 121     technic.get_or_load_node(pos)
ee0765 122     local name = minetest.get_node(pos).name
S 123
f9b090 124     if technic.is_tier_cable(name, network.tier) then
S 125         add_cable_node(network.all_nodes, pos, network_id, queue)
126         return
ee0765 127     end
f9b090 128
S 129     local eu_type = machines[name]
130     if not eu_type then
131         return
132     end
133
134     --dprint(name.." is a "..machines[name])
135     local meta = minetest.get_meta(pos)
6a9dfe 136     -- Normal tostring() does not have enough precision, neither does meta:set_int()
S 137     meta:set_string(network.tier.."_network", string.format("%X", network_id))
f9b090 138
S 139     if     eu_type == technic.producer then
140         add_network_node(network.PR_nodes, pos, network_id)
141     elseif eu_type == technic.receiver then
142         add_network_node(network.RE_nodes, pos, network_id)
143     elseif eu_type == technic.producer_receiver then
144         add_network_node(network.PR_nodes, pos, network_id)
145         add_network_node(network.RE_nodes, pos, network_id)
146     elseif eu_type == technic.battery then
147         add_network_node(network.BA_nodes, pos, network_id)
148     elseif eu_type == "SPECIAL" and from_below and
149             not vector.equals(pos, sw_pos) then
150         -- Another switching station -> disable it
151         add_network_node(network.SP_nodes, pos, network_id)
152         meta:set_int("active", 0)
153     end
154
155     meta:set_int(network.tier.."_EU_timeout", 2) -- Touch node
ee0765 156 end
ee5c6c 157
K 158 -- Traverse a network given a list of machines and a cable type name
f9b090 159 local traverse_network = function(network, pos, machines, sw_pos, network_id, queue)
ee0765 160     local positions = {
S 161         {x=pos.x+1, y=pos.y,   z=pos.z},
162         {x=pos.x-1, y=pos.y,   z=pos.z},
163         {x=pos.x,   y=pos.y+1, z=pos.z},
164         {x=pos.x,   y=pos.y-1, z=pos.z},
165         {x=pos.x,   y=pos.y,   z=pos.z+1},
166         {x=pos.x,   y=pos.y,   z=pos.z-1}}
167     for i, cur_pos in pairs(positions) do
f9b090 168         check_node_subp(network, cur_pos, machines, sw_pos, i == 3, network_id, queue)
ee0765 169     end
S 170 end
ee5c6c 171
f4ac2b 172 local touch_nodes = function(list, tier)
N 173     for _, pos in ipairs(list) do
174         local meta = minetest.get_meta(pos)
175         meta:set_int(tier.."_EU_timeout", 2) -- Touch node
176     end
177 end
178
f9b090 179 local get_network = function(sw_pos, cable_pos, tier)
S 180     local network_id = minetest.hash_node_position(cable_pos)
86fb98 181     local cached = technic.networks[network_id]
f4ac2b 182     if cached and cached.tier == tier then
f9b090 183         -- Re-use cached system data
f4ac2b 184         touch_nodes(cached.PR_nodes, tier)
N 185         touch_nodes(cached.BA_nodes, tier)
186         touch_nodes(cached.RE_nodes, tier)
563a4c 187         for _, pos in ipairs(cached.SP_nodes) do
f9b090 188             -- Disable all other switching stations (again)
563a4c 189             local meta = minetest.get_meta(pos)
N 190             meta:set_int("active", 0)
191         end
f4ac2b 192         return cached.PR_nodes, cached.BA_nodes, cached.RE_nodes
N 193     end
f9b090 194
S 195     local machines = technic.machines[tier]
196     local network = {
197         tier = tier,
198         PR_nodes = {},
199         BA_nodes = {},
200         RE_nodes = {},
201         SP_nodes = {},
202         all_nodes = {}
203     }
204     -- Traverse the network step by step starting from the node underneath the switching station
86fb98 205     local queue = {}
f9b090 206     add_cable_node(network.all_nodes, cable_pos, network_id, queue)
86fb98 207     while next(queue) do
NZ 208         local to_visit = {}
209         for _, pos in ipairs(queue) do
f9b090 210             traverse_network(network, pos, machines, sw_pos, network_id, to_visit)
86fb98 211         end
NZ 212         queue = to_visit
213     end
f9b090 214
S 215     -- Convert { [hash] = pos, ... } to { pos, ... }
216     network.PR_nodes = flatten(network.PR_nodes)
217     network.BA_nodes = flatten(network.BA_nodes)
218     network.RE_nodes = flatten(network.RE_nodes)
219     network.SP_nodes = flatten(network.SP_nodes)
220     network.all_nodes = flatten(network.all_nodes)
221     technic.networks[network_id] = network
222
223     return network.PR_nodes, network.BA_nodes, network.RE_nodes
f4ac2b 224 end
N 225
ee0765 226 -----------------------------------------------
S 227 -- The action code for the switching station --
228 -----------------------------------------------
9d5bd9 229
VE 230 technic.powerctrl_state = true
231
232 minetest.register_chatcommand("powerctrl", {
f9b090 233     params = "[on/off]",
9d5bd9 234     description = "Enables or disables technic's switching station ABM",
VE 235     privs = { basic_privs = true },
236     func = function(name, state)
f9b090 237         technic.powerctrl_state = (state:trim():lower() == "on")
S 238         minetest.chat_send_player(name, "Technic switching station: " ..
239             (technic.powerctrl_state and "on" or "off"))
9d5bd9 240     end
VE 241 })
242
f9b090 243 -- Run `technic_run` on all nodes in the power grid
d119a6 244 local function run_nodes(list, run_stage)
C 245     for _, pos in ipairs(list) do
246         technic.get_or_load_node(pos)
247         local node = minetest.get_node_or_nil(pos)
248         if node and node.name then
249             local nodedef = minetest.registered_nodes[node.name]
250             if nodedef and nodedef.technic_run then
251                 nodedef.technic_run(pos, node, run_stage)
252             end
253         end
254     end
255 end
256
ee0765 257 minetest.register_abm({
S 258     nodenames = {"technic:switching_station"},
f9b090 259     label = "Switching Station", -- name for the Minetest mod profiler
ee5c6c 260     interval   = 1,
K 261     chance     = 1,
262     action = function(pos, node, active_object_count, active_object_count_wider)
9d5bd9 263         if not technic.powerctrl_state then return end
0f6bdb 264         local meta = minetest.get_meta(pos)
a8daa4 265         local meta1
f9b090 266         local PR_nodes, BA_nodes, RE_nodes
be2f30 267         local machine_name = S("Switching Station")
6abd85 268
ee0765 269         -- Which kind of network are we on:
f9b090 270         local cable_pos = {x=pos.x, y=pos.y-1, z=pos.z}
ee5c6c 271
088eea 272         --Disable if necessary
CK 273         if meta:get_int("active") ~= 1 then
274             minetest.forceload_free_block(pos)
f9b090 275             minetest.forceload_free_block(cable_pos)
088eea 276             meta:set_string("infotext",S("%s Already Present"):format(machine_name))
a84b5f 277
VE 278             local poshash = minetest.hash_node_position(pos)
279
215de5 280             if not technic.redundant_warn[poshash] then
E 281                 technic.redundant_warn[poshash] = true
a84b5f 282                 print("[TECHNIC] Warning: redundant switching station found near "..minetest.pos_to_string(pos))
VE 283             end
088eea 284             return
CK 285         end
286
f9b090 287         local name = minetest.get_node(cable_pos).name
ee0765 288         local tier = technic.get_cable_tier(name)
S 289         if tier then
088eea 290             -- Forceload switching station
CK 291             minetest.forceload_block(pos)
f9b090 292             minetest.forceload_block(cable_pos)
S 293             PR_nodes, BA_nodes, RE_nodes = get_network(pos, cable_pos, tier)
ee0765 294         else
S 295             --dprint("Not connected to a network")
be2f30 296             meta:set_string("infotext", S("%s Has No Network"):format(machine_name))
088eea 297             minetest.forceload_free_block(pos)
f9b090 298             minetest.forceload_free_block(cable_pos)
ee0765 299             return
563a4c 300         end
6abd85 301
10307f 302         run_nodes(PR_nodes, technic.producer)
M'P 303         run_nodes(RE_nodes, technic.receiver)
304         run_nodes(BA_nodes, technic.battery)
ee5c6c 305
ee0765 306         -- Strings for the meta data
S 307         local eu_demand_str    = tier.."_EU_demand"
308         local eu_input_str     = tier.."_EU_input"
309         local eu_supply_str    = tier.."_EU_supply"
ee5c6c 310
7cfb38 311         -- Distribute charge equally across multiple batteries.
KZ 312         local charge_total = 0
313         local battery_count = 0
314
315         for n, pos1 in pairs(BA_nodes) do
316             meta1 = minetest.get_meta(pos1)
317             local charge = meta1:get_int("internal_EU_charge")
318
319             if (meta1:get_int(eu_demand_str) ~= 0) then
320                 charge_total = charge_total + charge
321                 battery_count = battery_count + 1
322             end
323         end
324
325         local charge_distributed = math.floor(charge_total / battery_count)
326
327         for n, pos1 in pairs(BA_nodes) do
328             meta1 = minetest.get_meta(pos1)
329
330             if (meta1:get_int(eu_demand_str) ~= 0) then
331                 meta1:set_int("internal_EU_charge", charge_distributed)
332             end
333         end
334
ee0765 335         -- Get all the power from the PR nodes
S 336         local PR_eu_supply = 0 -- Total power
337         for _, pos1 in pairs(PR_nodes) do
338             meta1 = minetest.get_meta(pos1)
339             PR_eu_supply = PR_eu_supply + meta1:get_int(eu_supply_str)
340         end
341         --dprint("Total PR supply:"..PR_eu_supply)
ee5c6c 342
ee0765 343         -- Get all the demand from the RE nodes
S 344         local RE_eu_demand = 0
345         for _, pos1 in pairs(RE_nodes) do
346             meta1 = minetest.get_meta(pos1)
347             RE_eu_demand = RE_eu_demand + meta1:get_int(eu_demand_str)
348         end
349         --dprint("Total RE demand:"..RE_eu_demand)
ee5c6c 350
f9b090 351         -- Batteries
S 352         local BA_eu_supply, BA_eu_demand = 0, 0
ee0765 353         for _, pos1 in pairs(BA_nodes) do
S 354             meta1 = minetest.get_meta(pos1)
355             BA_eu_supply = BA_eu_supply + meta1:get_int(eu_supply_str)
356             BA_eu_demand = BA_eu_demand + meta1:get_int(eu_demand_str)
357         end
705961 358         -- Expose value for the supply converter
S 359         meta:set_int("ba_demand", BA_eu_demand)
f9b090 360         --dprint("Total BA supply:"..BA_eu_supply)
ee0765 361         --dprint("Total BA demand:"..BA_eu_demand)
ee5c6c 362
41f175 363         meta:set_string("infotext", S("@1. Supply: @2 Demand: @3",
H 364                 machine_name, technic.EU_string(PR_eu_supply),
365                 technic.EU_string(RE_eu_demand)))
ee5c6c 366
6abd85 367         -- If mesecon signal and power supply or demand changed then
D 368         -- send them via digilines.
369         if mesecons_path and digilines_path and mesecon.is_powered(pos) then
370             if PR_eu_supply ~= meta:get_int("supply") or
371                     RE_eu_demand ~= meta:get_int("demand") then
372                 local channel = meta:get_string("channel")
373                 digilines.receptor_send(pos, digilines.rules.default, channel, {
374                     supply = PR_eu_supply,
375                     demand = RE_eu_demand
376                 })
377             end
378         end
379
088eea 380         -- Data that will be used by the power monitor
f9b090 381         meta:set_int("supply", PR_eu_supply)
S 382         meta:set_int("demand", RE_eu_demand)
088eea 383
ee0765 384         -- If the PR supply is enough for the RE demand supply them all
S 385         if PR_eu_supply >= RE_eu_demand then
386         --dprint("PR_eu_supply"..PR_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
387             for _, pos1 in pairs(RE_nodes) do
388                 meta1 = minetest.get_meta(pos1)
389                 local eu_demand = meta1:get_int(eu_demand_str)
390                 meta1:set_int(eu_input_str, eu_demand)
391             end
392             -- We have a surplus, so distribute the rest equally to the BA nodes
393             -- Let's calculate the factor of the demand
394             PR_eu_supply = PR_eu_supply - RE_eu_demand
395             local charge_factor = 0 -- Assume all batteries fully charged
396             if BA_eu_demand > 0 then
397                 charge_factor = PR_eu_supply / BA_eu_demand
398             end
399             for n, pos1 in pairs(BA_nodes) do
400                 meta1 = minetest.get_meta(pos1)
401                 local eu_demand = meta1:get_int(eu_demand_str)
402                 meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
403                 --dprint("Charging battery:"..math.floor(eu_demand*charge_factor))
404             end
405             return
406         end
ee5c6c 407
ee0765 408         -- If the PR supply is not enough for the RE demand we will discharge the batteries too
S 409         if PR_eu_supply + BA_eu_supply >= RE_eu_demand then
410             --dprint("PR_eu_supply "..PR_eu_supply.."+BA_eu_supply "..BA_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
411             for _, pos1 in pairs(RE_nodes) do
412                 meta1  = minetest.get_meta(pos1)
413                 local eu_demand = meta1:get_int(eu_demand_str)
414                 meta1:set_int(eu_input_str, eu_demand)
415             end
416             -- We have a deficit, so distribute to the BA nodes
417             -- Let's calculate the factor of the supply
418             local charge_factor = 0 -- Assume all batteries depleted
419             if BA_eu_supply > 0 then
420                 charge_factor = (PR_eu_supply - RE_eu_demand) / BA_eu_supply
421             end
422             for n,pos1 in pairs(BA_nodes) do
423                 meta1 = minetest.get_meta(pos1)
ee5c6c 424                 local eu_supply = meta1:get_int(eu_supply_str)
ee0765 425                 meta1:set_int(eu_input_str, math.floor(eu_supply * charge_factor))
S 426                 --dprint("Discharging battery:"..math.floor(eu_supply*charge_factor))
427             end
428             return
429         end
ee5c6c 430
ee0765 431         -- If the PR+BA supply is not enough for the RE demand: Power only the batteries
S 432         local charge_factor = 0 -- Assume all batteries fully charged
433         if BA_eu_demand > 0 then
434             charge_factor = PR_eu_supply / BA_eu_demand
435         end
436         for n, pos1 in pairs(BA_nodes) do
437             meta1 = minetest.get_meta(pos1)
438             local eu_demand = meta1:get_int(eu_demand_str)
439             meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
440         end
441         for n, pos1 in pairs(RE_nodes) do
442             meta1 = minetest.get_meta(pos1)
443             meta1:set_int(eu_input_str, 0)
444         end
6abd85 445
ee5c6c 446     end,
K 447 })
ee0765 448
563a4c 449 -- Timeout ABM
N 450 -- Timeout for a node in case it was disconnected from the network
451 -- A node must be touched by the station continuously in order to function
452 local function switching_station_timeout_count(pos, tier)
453     local meta = minetest.get_meta(pos)
454     local timeout = meta:get_int(tier.."_EU_timeout")
455     if timeout <= 0 then
4ac36e 456         meta:set_int(tier.."_EU_input", 0) -- Not needed anymore <-- actually, it is for supply converter
563a4c 457         return true
N 458     else
459         meta:set_int(tier.."_EU_timeout", timeout - 1)
460         return false
461     end
462 end
463 minetest.register_abm({
78f16c 464     label = "Machines: timeout check",
563a4c 465     nodenames = {"group:technic_machine"},
N 466     interval   = 1,
467     chance     = 1,
468     action = function(pos, node, active_object_count, active_object_count_wider)
469         for tier, machines in pairs(technic.machines) do
470             if machines[node.name] and switching_station_timeout_count(pos, tier) then
471                 local nodedef = minetest.registered_nodes[node.name]
472                 if nodedef then
473                     local meta = minetest.get_meta(pos)
474                     meta:set_string("infotext", S("%s Has No Network"):format(nodedef.description))
475                 end
140701 476                 if nodedef and nodedef.technic_disabled_machine_name then
S 477                     node.name = nodedef.technic_disabled_machine_name
478                     minetest.swap_node(pos, node)
479                 end
480                 if nodedef and nodedef.technic_on_disable then
481                     nodedef.technic_on_disable(pos, node)
482                 end
563a4c 483             end
N 484         end
485     end,
486 })
487
088eea 488 --Re-enable disabled switching station if necessary, similar to the timeout above
CK 489 minetest.register_abm({
78f16c 490     label = "Machines: re-enable check",
088eea 491     nodenames = {"technic:switching_station"},
CK 492     interval   = 1,
493     chance     = 1,
494     action = function(pos, node, active_object_count, active_object_count_wider)
495         local pos1 = {x=pos.x,y=pos.y-1,z=pos.z}
496         local tier = technic.get_cable_tier(minetest.get_node(pos1).name)
497         if not tier then return end
498         if switching_station_timeout_count(pos, tier) then
499             local meta = minetest.get_meta(pos)
500             meta:set_int("active",1)
501         end
502     end,
503 })
504
ee0765 505 for tier, machines in pairs(technic.machines) do
S 506     -- SPECIAL will not be traversed
507     technic.register_machine(tier, "technic:switching_station", "SPECIAL")
508 end
509