SmallJoker
2022-11-24 521c0b74bba5dd73cf7c0a59fde4a018813e835d
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()
521c0b 137     -- Bug: Cannot use hexadecimal notation for compression (LuaJIT Windows bug, #911)
S 138     meta:set_string(network.tier.."_network", string.format("%.20g", network_id))
f9b090 139
S 140     if     eu_type == technic.producer then
141         add_network_node(network.PR_nodes, pos, network_id)
142     elseif eu_type == technic.receiver then
143         add_network_node(network.RE_nodes, pos, network_id)
144     elseif eu_type == technic.producer_receiver then
145         add_network_node(network.PR_nodes, pos, network_id)
146         add_network_node(network.RE_nodes, pos, network_id)
147     elseif eu_type == technic.battery then
148         add_network_node(network.BA_nodes, pos, network_id)
149     elseif eu_type == "SPECIAL" and from_below and
150             not vector.equals(pos, sw_pos) then
151         -- Another switching station -> disable it
152         add_network_node(network.SP_nodes, pos, network_id)
153         meta:set_int("active", 0)
154     end
155
156     meta:set_int(network.tier.."_EU_timeout", 2) -- Touch node
ee0765 157 end
ee5c6c 158
K 159 -- Traverse a network given a list of machines and a cable type name
f9b090 160 local traverse_network = function(network, pos, machines, sw_pos, network_id, queue)
ee0765 161     local positions = {
S 162         {x=pos.x+1, y=pos.y,   z=pos.z},
163         {x=pos.x-1, y=pos.y,   z=pos.z},
164         {x=pos.x,   y=pos.y+1, z=pos.z},
165         {x=pos.x,   y=pos.y-1, z=pos.z},
166         {x=pos.x,   y=pos.y,   z=pos.z+1},
167         {x=pos.x,   y=pos.y,   z=pos.z-1}}
168     for i, cur_pos in pairs(positions) do
f9b090 169         check_node_subp(network, cur_pos, machines, sw_pos, i == 3, network_id, queue)
ee0765 170     end
S 171 end
ee5c6c 172
f4ac2b 173 local touch_nodes = function(list, tier)
N 174     for _, pos in ipairs(list) do
175         local meta = minetest.get_meta(pos)
176         meta:set_int(tier.."_EU_timeout", 2) -- Touch node
177     end
178 end
179
f9b090 180 local get_network = function(sw_pos, cable_pos, tier)
S 181     local network_id = minetest.hash_node_position(cable_pos)
86fb98 182     local cached = technic.networks[network_id]
f4ac2b 183     if cached and cached.tier == tier then
f9b090 184         -- Re-use cached system data
f4ac2b 185         touch_nodes(cached.PR_nodes, tier)
N 186         touch_nodes(cached.BA_nodes, tier)
187         touch_nodes(cached.RE_nodes, tier)
563a4c 188         for _, pos in ipairs(cached.SP_nodes) do
f9b090 189             -- Disable all other switching stations (again)
563a4c 190             local meta = minetest.get_meta(pos)
N 191             meta:set_int("active", 0)
192         end
f4ac2b 193         return cached.PR_nodes, cached.BA_nodes, cached.RE_nodes
N 194     end
f9b090 195
S 196     local machines = technic.machines[tier]
197     local network = {
198         tier = tier,
199         PR_nodes = {},
200         BA_nodes = {},
201         RE_nodes = {},
202         SP_nodes = {},
203         all_nodes = {}
204     }
205     -- Traverse the network step by step starting from the node underneath the switching station
86fb98 206     local queue = {}
f9b090 207     add_cable_node(network.all_nodes, cable_pos, network_id, queue)
86fb98 208     while next(queue) do
NZ 209         local to_visit = {}
210         for _, pos in ipairs(queue) do
f9b090 211             traverse_network(network, pos, machines, sw_pos, network_id, to_visit)
86fb98 212         end
NZ 213         queue = to_visit
214     end
f9b090 215
S 216     -- Convert { [hash] = pos, ... } to { pos, ... }
217     network.PR_nodes = flatten(network.PR_nodes)
218     network.BA_nodes = flatten(network.BA_nodes)
219     network.RE_nodes = flatten(network.RE_nodes)
220     network.SP_nodes = flatten(network.SP_nodes)
221     network.all_nodes = flatten(network.all_nodes)
222     technic.networks[network_id] = network
223
224     return network.PR_nodes, network.BA_nodes, network.RE_nodes
f4ac2b 225 end
N 226
ee0765 227 -----------------------------------------------
S 228 -- The action code for the switching station --
229 -----------------------------------------------
9d5bd9 230
VE 231 technic.powerctrl_state = true
232
233 minetest.register_chatcommand("powerctrl", {
f9b090 234     params = "[on/off]",
9d5bd9 235     description = "Enables or disables technic's switching station ABM",
VE 236     privs = { basic_privs = true },
237     func = function(name, state)
f9b090 238         technic.powerctrl_state = (state:trim():lower() == "on")
S 239         minetest.chat_send_player(name, "Technic switching station: " ..
240             (technic.powerctrl_state and "on" or "off"))
9d5bd9 241     end
VE 242 })
243
f9b090 244 -- Run `technic_run` on all nodes in the power grid
d119a6 245 local function run_nodes(list, run_stage)
C 246     for _, pos in ipairs(list) do
247         technic.get_or_load_node(pos)
248         local node = minetest.get_node_or_nil(pos)
249         if node and node.name then
250             local nodedef = minetest.registered_nodes[node.name]
251             if nodedef and nodedef.technic_run then
252                 nodedef.technic_run(pos, node, run_stage)
253             end
254         end
255     end
256 end
257
ee0765 258 minetest.register_abm({
S 259     nodenames = {"technic:switching_station"},
f9b090 260     label = "Switching Station", -- name for the Minetest mod profiler
ee5c6c 261     interval   = 1,
K 262     chance     = 1,
263     action = function(pos, node, active_object_count, active_object_count_wider)
9d5bd9 264         if not technic.powerctrl_state then return end
0f6bdb 265         local meta = minetest.get_meta(pos)
a8daa4 266         local meta1
f9b090 267         local PR_nodes, BA_nodes, RE_nodes
be2f30 268         local machine_name = S("Switching Station")
6abd85 269
ee0765 270         -- Which kind of network are we on:
f9b090 271         local cable_pos = {x=pos.x, y=pos.y-1, z=pos.z}
ee5c6c 272
088eea 273         --Disable if necessary
CK 274         if meta:get_int("active") ~= 1 then
275             minetest.forceload_free_block(pos)
f9b090 276             minetest.forceload_free_block(cable_pos)
088eea 277             meta:set_string("infotext",S("%s Already Present"):format(machine_name))
a84b5f 278
VE 279             local poshash = minetest.hash_node_position(pos)
280
215de5 281             if not technic.redundant_warn[poshash] then
E 282                 technic.redundant_warn[poshash] = true
a84b5f 283                 print("[TECHNIC] Warning: redundant switching station found near "..minetest.pos_to_string(pos))
VE 284             end
088eea 285             return
CK 286         end
287
f9b090 288         local name = minetest.get_node(cable_pos).name
ee0765 289         local tier = technic.get_cable_tier(name)
S 290         if tier then
088eea 291             -- Forceload switching station
CK 292             minetest.forceload_block(pos)
f9b090 293             minetest.forceload_block(cable_pos)
S 294             PR_nodes, BA_nodes, RE_nodes = get_network(pos, cable_pos, tier)
ee0765 295         else
S 296             --dprint("Not connected to a network")
be2f30 297             meta:set_string("infotext", S("%s Has No Network"):format(machine_name))
088eea 298             minetest.forceload_free_block(pos)
f9b090 299             minetest.forceload_free_block(cable_pos)
ee0765 300             return
563a4c 301         end
6abd85 302
10307f 303         run_nodes(PR_nodes, technic.producer)
M'P 304         run_nodes(RE_nodes, technic.receiver)
305         run_nodes(BA_nodes, technic.battery)
ee5c6c 306
ee0765 307         -- Strings for the meta data
S 308         local eu_demand_str    = tier.."_EU_demand"
309         local eu_input_str     = tier.."_EU_input"
310         local eu_supply_str    = tier.."_EU_supply"
ee5c6c 311
7cfb38 312         -- Distribute charge equally across multiple batteries.
KZ 313         local charge_total = 0
314         local battery_count = 0
315
316         for n, pos1 in pairs(BA_nodes) do
317             meta1 = minetest.get_meta(pos1)
318             local charge = meta1:get_int("internal_EU_charge")
319
320             if (meta1:get_int(eu_demand_str) ~= 0) then
321                 charge_total = charge_total + charge
322                 battery_count = battery_count + 1
323             end
324         end
325
326         local charge_distributed = math.floor(charge_total / battery_count)
327
328         for n, pos1 in pairs(BA_nodes) do
329             meta1 = minetest.get_meta(pos1)
330
331             if (meta1:get_int(eu_demand_str) ~= 0) then
332                 meta1:set_int("internal_EU_charge", charge_distributed)
333             end
334         end
335
ee0765 336         -- Get all the power from the PR nodes
S 337         local PR_eu_supply = 0 -- Total power
338         for _, pos1 in pairs(PR_nodes) do
339             meta1 = minetest.get_meta(pos1)
340             PR_eu_supply = PR_eu_supply + meta1:get_int(eu_supply_str)
341         end
342         --dprint("Total PR supply:"..PR_eu_supply)
ee5c6c 343
ee0765 344         -- Get all the demand from the RE nodes
S 345         local RE_eu_demand = 0
346         for _, pos1 in pairs(RE_nodes) do
347             meta1 = minetest.get_meta(pos1)
348             RE_eu_demand = RE_eu_demand + meta1:get_int(eu_demand_str)
349         end
350         --dprint("Total RE demand:"..RE_eu_demand)
ee5c6c 351
f9b090 352         -- Batteries
S 353         local BA_eu_supply, BA_eu_demand = 0, 0
ee0765 354         for _, pos1 in pairs(BA_nodes) do
S 355             meta1 = minetest.get_meta(pos1)
356             BA_eu_supply = BA_eu_supply + meta1:get_int(eu_supply_str)
357             BA_eu_demand = BA_eu_demand + meta1:get_int(eu_demand_str)
358         end
705961 359         -- Expose value for the supply converter
S 360         meta:set_int("ba_demand", BA_eu_demand)
f9b090 361         --dprint("Total BA supply:"..BA_eu_supply)
ee0765 362         --dprint("Total BA demand:"..BA_eu_demand)
ee5c6c 363
41f175 364         meta:set_string("infotext", S("@1. Supply: @2 Demand: @3",
H 365                 machine_name, technic.EU_string(PR_eu_supply),
366                 technic.EU_string(RE_eu_demand)))
ee5c6c 367
6abd85 368         -- If mesecon signal and power supply or demand changed then
D 369         -- send them via digilines.
370         if mesecons_path and digilines_path and mesecon.is_powered(pos) then
371             if PR_eu_supply ~= meta:get_int("supply") or
372                     RE_eu_demand ~= meta:get_int("demand") then
373                 local channel = meta:get_string("channel")
374                 digilines.receptor_send(pos, digilines.rules.default, channel, {
375                     supply = PR_eu_supply,
376                     demand = RE_eu_demand
377                 })
378             end
379         end
380
088eea 381         -- Data that will be used by the power monitor
f9b090 382         meta:set_int("supply", PR_eu_supply)
S 383         meta:set_int("demand", RE_eu_demand)
088eea 384
ee0765 385         -- If the PR supply is enough for the RE demand supply them all
S 386         if PR_eu_supply >= RE_eu_demand then
387         --dprint("PR_eu_supply"..PR_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
388             for _, pos1 in pairs(RE_nodes) do
389                 meta1 = minetest.get_meta(pos1)
390                 local eu_demand = meta1:get_int(eu_demand_str)
391                 meta1:set_int(eu_input_str, eu_demand)
392             end
393             -- We have a surplus, so distribute the rest equally to the BA nodes
394             -- Let's calculate the factor of the demand
395             PR_eu_supply = PR_eu_supply - RE_eu_demand
396             local charge_factor = 0 -- Assume all batteries fully charged
397             if BA_eu_demand > 0 then
398                 charge_factor = PR_eu_supply / BA_eu_demand
399             end
400             for n, pos1 in pairs(BA_nodes) do
401                 meta1 = minetest.get_meta(pos1)
402                 local eu_demand = meta1:get_int(eu_demand_str)
403                 meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
404                 --dprint("Charging battery:"..math.floor(eu_demand*charge_factor))
405             end
406             return
407         end
ee5c6c 408
ee0765 409         -- If the PR supply is not enough for the RE demand we will discharge the batteries too
S 410         if PR_eu_supply + BA_eu_supply >= RE_eu_demand then
411             --dprint("PR_eu_supply "..PR_eu_supply.."+BA_eu_supply "..BA_eu_supply.." >= RE_eu_demand"..RE_eu_demand)
412             for _, pos1 in pairs(RE_nodes) do
413                 meta1  = minetest.get_meta(pos1)
414                 local eu_demand = meta1:get_int(eu_demand_str)
415                 meta1:set_int(eu_input_str, eu_demand)
416             end
417             -- We have a deficit, so distribute to the BA nodes
418             -- Let's calculate the factor of the supply
419             local charge_factor = 0 -- Assume all batteries depleted
420             if BA_eu_supply > 0 then
421                 charge_factor = (PR_eu_supply - RE_eu_demand) / BA_eu_supply
422             end
423             for n,pos1 in pairs(BA_nodes) do
424                 meta1 = minetest.get_meta(pos1)
ee5c6c 425                 local eu_supply = meta1:get_int(eu_supply_str)
ee0765 426                 meta1:set_int(eu_input_str, math.floor(eu_supply * charge_factor))
S 427                 --dprint("Discharging battery:"..math.floor(eu_supply*charge_factor))
428             end
429             return
430         end
ee5c6c 431
ee0765 432         -- If the PR+BA supply is not enough for the RE demand: Power only the batteries
S 433         local charge_factor = 0 -- Assume all batteries fully charged
434         if BA_eu_demand > 0 then
435             charge_factor = PR_eu_supply / BA_eu_demand
436         end
437         for n, pos1 in pairs(BA_nodes) do
438             meta1 = minetest.get_meta(pos1)
439             local eu_demand = meta1:get_int(eu_demand_str)
440             meta1:set_int(eu_input_str, math.floor(eu_demand * charge_factor))
441         end
442         for n, pos1 in pairs(RE_nodes) do
443             meta1 = minetest.get_meta(pos1)
444             meta1:set_int(eu_input_str, 0)
445         end
6abd85 446
ee5c6c 447     end,
K 448 })
ee0765 449
563a4c 450 -- Timeout ABM
N 451 -- Timeout for a node in case it was disconnected from the network
452 -- A node must be touched by the station continuously in order to function
453 local function switching_station_timeout_count(pos, tier)
454     local meta = minetest.get_meta(pos)
455     local timeout = meta:get_int(tier.."_EU_timeout")
456     if timeout <= 0 then
4ac36e 457         meta:set_int(tier.."_EU_input", 0) -- Not needed anymore <-- actually, it is for supply converter
563a4c 458         return true
N 459     else
460         meta:set_int(tier.."_EU_timeout", timeout - 1)
461         return false
462     end
463 end
464 minetest.register_abm({
78f16c 465     label = "Machines: timeout check",
563a4c 466     nodenames = {"group:technic_machine"},
N 467     interval   = 1,
468     chance     = 1,
469     action = function(pos, node, active_object_count, active_object_count_wider)
470         for tier, machines in pairs(technic.machines) do
471             if machines[node.name] and switching_station_timeout_count(pos, tier) then
472                 local nodedef = minetest.registered_nodes[node.name]
473                 if nodedef then
474                     local meta = minetest.get_meta(pos)
475                     meta:set_string("infotext", S("%s Has No Network"):format(nodedef.description))
476                 end
140701 477                 if nodedef and nodedef.technic_disabled_machine_name then
S 478                     node.name = nodedef.technic_disabled_machine_name
479                     minetest.swap_node(pos, node)
480                 end
481                 if nodedef and nodedef.technic_on_disable then
482                     nodedef.technic_on_disable(pos, node)
483                 end
563a4c 484             end
N 485         end
486     end,
487 })
488
088eea 489 --Re-enable disabled switching station if necessary, similar to the timeout above
CK 490 minetest.register_abm({
78f16c 491     label = "Machines: re-enable check",
088eea 492     nodenames = {"technic:switching_station"},
CK 493     interval   = 1,
494     chance     = 1,
495     action = function(pos, node, active_object_count, active_object_count_wider)
496         local pos1 = {x=pos.x,y=pos.y-1,z=pos.z}
497         local tier = technic.get_cable_tier(minetest.get_node(pos1).name)
498         if not tier then return end
499         if switching_station_timeout_count(pos, tier) then
500             local meta = minetest.get_meta(pos)
501             meta:set_int("active",1)
502         end
503     end,
504 })
505
ee0765 506 for tier, machines in pairs(technic.machines) do
S 507     -- SPECIAL will not be traversed
508     technic.register_machine(tier, "technic:switching_station", "SPECIAL")
509 end
510