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