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