Cristiano Magro
2024-08-27 535e04d542520f58a2db8d4d7a222b73e5c96ef1
commit | author | age
85a984 1 --[[
S 2  The enriched uranium rod driven EU generator.
3 A very large and advanced machine providing vast amounts of power.
4 Very efficient but also expensive to run as it needs uranium.
5 Provides 10000 HV EUs for one week (only counted when loaded).
ee0765 6
85a984 7 The nuclear reactor core requires a casing of water and a protective
S 8 shield to work.  This is checked now and then and if the casing is not
9 intact the reactor will melt down!
10 --]]
11
12 local burn_ticks = 7 * 24 * 60 * 60  -- Seconds
13 local power_supply = 100000  -- EUs
14 local fuel_type = "technic:uranium_fuel"  -- The reactor burns this
bf58c7 15 local digiline_meltdown = technic.config:get_bool("enable_nuclear_reactor_digiline_selfdestruct")
D 16 local digiline_remote_path = minetest.get_modpath("digiline_remote")
ee0765 17
be2f30 18 local S = technic.getter
ee0765 19
1d20af 20 local reactor_desc = S("@1 Nuclear Reactor Core", S("HV"))
VE 21 local cable_entry = "^technic_cable_connection_overlay.png"
85a984 22
S 23 -- FIXME: Recipe should make more sense like a rod recepticle, steam chamber, HV generator?
ee0765 24 minetest.register_craft({
S 25     output = 'technic:hv_nuclear_reactor_core',
26     recipe = {
5e4a87 27         {'technic:carbon_plate',          'default:obsidian_glass', 'technic:carbon_plate'},
Z 28         {'technic:composite_plate',       'technic:machine_casing', 'technic:composite_plate'},
83c649 29         {'technic:stainless_steel_ingot', 'technic:hv_cable',       'technic:stainless_steel_ingot'},
ee0765 30     }
S 31 })
32
bf58c7 33 local function make_reactor_formspec(meta)
410e34 34     local f =
S 35         "formspec_version[4]"..
36         "size[10.75,10.75]"..
37         "label[0.2,0.4;"..S("Nuclear Reactor Rod Compartment").."]"..
38         "list[current_name;src;1.5,1;3,2;]"..
39         "list[current_player;main;0.5,5.5;8,4;]"..
40         "listring[]"..
41         "button[5.7,1;2,1;start;Start]"..
42         "checkbox[5.7,2.75;autostart;automatic Start;"..meta:get_string("autostart").."]"
bf58c7 43     if not digiline_remote_path then
D 44         return f
45     end
46     local digiline_enabled = meta:get_string("enable_digiline")
410e34 47     f = f.."checkbox[1.5,3.75;enable_digiline;Enable Digiline channel;"..digiline_enabled.."]"
bf58c7 48     if digiline_enabled ~= "true" then
D 49         return f
50     end
51     return f..
410e34 52         "field[2,4.2;4.25,1;remote_channel;;${remote_channel}]" ..
S 53         "button_exit[6.5,4.2;2,1;save;Save]"
bf58c7 54 end
ee0765 55
85a984 56 local SS_OFF = 0
S 57 local SS_DANGER = 1
58 local SS_CLEAR = 2
59
d59055 60 local reactor_siren = {}
85a984 61 local function siren_set_state(pos, state)
d59055 62     local hpos = minetest.hash_node_position(pos)
Z 63     local siren = reactor_siren[hpos]
64     if not siren then
85a984 65         if state == SS_OFF then return end
S 66         siren = {state=SS_OFF}
d59055 67         reactor_siren[hpos] = siren
Z 68     end
85a984 69     if state == SS_DANGER and siren.state ~= SS_DANGER then
d59055 70         if siren.handle then minetest.sound_stop(siren.handle) end
85a984 71         siren.handle = minetest.sound_play("technic_hv_nuclear_reactor_siren_danger_loop",
S 72                 {pos=pos, gain=1.5, loop=true, max_hear_distance=48})
73         siren.state = SS_DANGER
74     elseif state == SS_CLEAR then
d59055 75         if siren.handle then minetest.sound_stop(siren.handle) end
85a984 76         local clear_handle = minetest.sound_play("technic_hv_nuclear_reactor_siren_clear",
S 77                 {pos=pos, gain=1.5, loop=false, max_hear_distance=48})
d59055 78         siren.handle = clear_handle
85a984 79         siren.state = SS_CLEAR
S 80         minetest.after(10, function()
81             if siren.handle ~= clear_handle then return end
82             minetest.sound_stop(clear_handle)
83             if reactor_siren[hpos] == siren then
84                 reactor_siren[hpos] = nil
d59055 85             end
Z 86         end)
85a984 87     elseif state == SS_OFF and siren.state ~= SS_OFF then
d59055 88         if siren.handle then minetest.sound_stop(siren.handle) end
Z 89         reactor_siren[hpos] = nil
90     end
91 end
85a984 92
d59055 93 local function siren_danger(pos, meta)
Z 94     meta:set_int("siren", 1)
85a984 95     siren_set_state(pos, SS_DANGER)
d59055 96 end
85a984 97
d59055 98 local function siren_clear(pos, meta)
Z 99     if meta:get_int("siren") ~= 0 then
85a984 100         siren_set_state(pos, SS_CLEAR)
d59055 101         meta:set_int("siren", 0)
Z 102     end
103 end
104
85a984 105 --[[
S 106 The standard reactor structure consists of a 9x9x9 cube.  A cross
107 section through the middle:
108
109     CCCC CCCC
110     CBBB BBBC
8ccb6d 111     CBLL LLBC
S 112     CBLWWWLBC
113     CBLW#WLBC
114     CBLW|WLBC
115     CBLL|LLBC
85a984 116     CBBB|BBBC
S 117     CCCC|CCCC
8ccb6d 118     C = Concrete, B = Blast-resistant concrete, L = Lead,
85a984 119     W = water node, # = reactor core, | = HV cable
S 120
8ccb6d 121 The man-hole is optional (but necessary for refueling).
85a984 122
S 123 For the reactor to operate and not melt down, it insists on the inner
124 7x7x7 portion (from the core out to the blast-resistant concrete)
125 being intact.  Intactness only depends on the number of nodes of the
126 right type in each layer.  The water layer must have water in all but
127 at most one node; the steel and blast-resistant concrete layers must
128 have the right material in all but at most two nodes.  The permitted
129 gaps are meant for the cable and man-hole, but can actually be anywhere
130 and contain anything.  For the reactor to be useful, a cable must
131 connect to the core, but it can go in any direction.
132
133 The outer concrete layer of the standard structure is not required
134 for the reactor to operate.  It is noted here because it used to
135 be mandatory, and for historical reasons (that it predates the
136 implementation of radiation) it needs to continue being adequate
137 shielding of legacy reactors.  If it ever ceases to be adequate
138 shielding for new reactors, legacy ones should be grandfathered.
8ccb6d 139
S 140 For legacy reasons, if the reactor has a stainless steel layer instead
141 of a lead layer it will be converted to a lead layer.
85a984 142 --]]
S 143 local function reactor_structure_badness(pos)
ee0765 144     local vm = VoxelManip()
410e34 145
S 146     -- Blast-resistant Concrete Block layer outer positions
84cf65 147     local pos1 = vector.subtract(pos, 3)
Z 148     local pos2 = vector.add(pos, 3)
410e34 149
ee0765 150     local MinEdge, MaxEdge = vm:read_from_map(pos1, pos2)
S 151     local data = vm:get_data()
152     local area = VoxelArea:new({MinEdge=MinEdge, MaxEdge=MaxEdge})
153
154     local c_blast_concrete = minetest.get_content_id("technic:blast_resistant_concrete")
8ccb6d 155     local c_lead = minetest.get_content_id("technic:lead_block")
S 156     local c_steel = minetest.get_content_id("technic:stainless_steel_block")
ee0765 157     local c_water_source = minetest.get_content_id("default:water_source")
S 158     local c_water_flowing = minetest.get_content_id("default:water_flowing")
159
8ccb6d 160     local blast_layer, steel_layer, lead_layer, water_layer = 0, 0, 0, 0
ee0765 161
S 162     for z = pos1.z, pos2.z do
163     for y = pos1.y, pos2.y do
164     for x = pos1.x, pos2.x do
410e34 165         -- In the entire volume, make sure there is:
84cf65 166         local cid = data[area:index(x, y, z)]
ee0765 167         if x == pos1.x or x == pos2.x or
S 168            y == pos1.y or y == pos2.y or
169            z == pos1.z or z == pos2.z then
410e34 170             -- r=3 : Blast-resistant Concrete Block shell
84cf65 171             if cid == c_blast_concrete then
8ccb6d 172                 blast_layer = blast_layer + 1
ee0765 173             end
S 174         elseif x == pos1.x+1 or x == pos2.x-1 or
8ccb6d 175                y == pos1.y+1 or y == pos2.y-1 or
S 176                z == pos1.z+1 or z == pos2.z-1 then
410e34 177             -- r=2 : Lead Block shell
8ccb6d 178             if cid == c_lead then
S 179                 lead_layer = lead_layer + 1
180             elseif cid == c_steel then
181                 steel_layer = steel_layer + 1
ee0765 182             end
S 183         elseif x == pos1.x+2 or x == pos2.x-2 or
8ccb6d 184                y == pos1.y+2 or y == pos2.y-2 or
S 185                z == pos1.z+2 or z == pos2.z-2 then
410e34 186             -- r=1 : Water cooling
ee0765 187             if cid == c_water_source or cid == c_water_flowing then
8ccb6d 188                 water_layer = water_layer + 1
ee0765 189             end
S 190         end
191     end
192     end
193     end
8ccb6d 194
S 195     if steel_layer >= 96 then
410e34 196         -- Legacy: convert stainless steel to lead
S 197         -- Why don't we accept both without conversion?
8ccb6d 198         for z = pos1.z+1, pos2.z-1 do
S 199         for y = pos1.y+1, pos2.y-1 do
200         for x = pos1.x+1, pos2.x-1 do
201             local vi = area:index(x, y, z)
202             if x == pos1.x+1 or x == pos2.x-1 or
203                y == pos1.y+1 or y == pos2.y-1 or
204                z == pos1.z+1 or z == pos2.z-1 then
205                 if data[vi] == c_steel then
206                     data[vi] = c_lead
207                 end
208             end
209         end
210         end
211         end
212         vm:set_data(data)
213         vm:write_to_map()
214         lead_layer = steel_layer
215     end
216
217     if water_layer > 25 then water_layer = 25 end
218     if lead_layer > 96 then lead_layer = 96 end
219     if blast_layer > 216 then blast_layer = 216 end
410e34 220     -- Amount of missing blocks
8ccb6d 221     return (25 - water_layer) + (96 - lead_layer) + (216 - blast_layer)
ee0765 222 end
S 223
85a984 224
S 225 local function melt_down_reactor(pos)
226     minetest.log("action", "A reactor melted down at "..minetest.pos_to_string(pos))
bf58c7 227     minetest.set_node(pos, {name = "technic:corium_source"})
D 228 end
229
230
231 local function start_reactor(pos, meta)
11e43f 232     local correct_fuel_count = 6
CH 233     local msg_fuel_missing = "Error: You need to insert " .. correct_fuel_count .. " pieces of Uranium Fuel."
234
bf58c7 235     if minetest.get_node(pos).name ~= "technic:hv_nuclear_reactor_core" then
11e43f 236         return msg_fuel_missing
bf58c7 237     end
D 238     local inv = meta:get_inventory()
239     if inv:is_empty("src") then
11e43f 240         return msg_fuel_missing
bf58c7 241     end
D 242     local src_list = inv:get_list("src")
11e43f 243     local fuel_count = 0
bf58c7 244     for _, src_stack in pairs(src_list) do
D 245         if src_stack and src_stack:get_name() == fuel_type then
11e43f 246             fuel_count = fuel_count + 1
bf58c7 247         end
D 248     end
11e43f 249     -- Check that the has the correct fuel
CH 250     if fuel_count ~= correct_fuel_count then
251         return msg_fuel_missing
bf58c7 252     end
11e43f 253
CH 254     -- Check that the reactor is complete
255     if reactor_structure_badness(pos) ~= 0 then
256         return "Error: The power plant seems to be built incorrectly."
257     end
258
bf58c7 259     meta:set_int("burn_time", 1)
D 260     technic.swap_node(pos, "technic:hv_nuclear_reactor_core_active")
261     meta:set_int("HV_EU_supply", power_supply)
262     for idx, src_stack in pairs(src_list) do
263         src_stack:take_item()
264         inv:set_stack("src", idx, src_stack)
265     end
11e43f 266
CH 267     return nil
ee0765 268 end
67b90f 269
85a984 270
67b90f 271 minetest.register_abm({
78f16c 272     label = "Machines: reactor melt-down check",
67b90f 273     nodenames = {"technic:hv_nuclear_reactor_core_active"},
85a984 274     interval = 4,
67b90f 275     chance = 1,
Z 276     action = function (pos, node)
277         local meta = minetest.get_meta(pos)
278         local badness = reactor_structure_badness(pos)
279         local accum_badness = meta:get_int("structure_accumulated_badness")
280         if badness == 0 then
281             if accum_badness ~= 0 then
e05680 282                 meta:set_int("structure_accumulated_badness", math.max(accum_badness - 4, 0))
d59055 283                 siren_clear(pos, meta)
67b90f 284             end
Z 285         else
d59055 286             siren_danger(pos, meta)
67b90f 287             accum_badness = accum_badness + badness
85a984 288             if accum_badness >= 25 then
S 289                 melt_down_reactor(pos)
67b90f 290             else
Z 291                 meta:set_int("structure_accumulated_badness", accum_badness)
292             end
293         end
294     end,
295 })
ee0765 296
85a984 297 local function run(pos, node)
563a4c 298     local meta = minetest.get_meta(pos)
N 299     local burn_time = meta:get_int("burn_time") or 0
300     if burn_time >= burn_ticks or burn_time == 0 then
bf58c7 301         if digiline_remote_path and meta:get_int("HV_EU_supply") == power_supply then
D 302             digiline_remote.send_to_node(pos, meta:get_string("remote_channel"),
303                     "fuel used", 6, true)
304         end
305         if meta:get_string("autostart") == "true" then
11e43f 306             if not start_reactor(pos, meta) then
563a4c 307                 return
ee0765 308             end
S 309         end
563a4c 310         meta:set_int("HV_EU_supply", 0)
N 311         meta:set_int("burn_time", 0)
410e34 312         meta:set_string("infotext", S("@1 Idle", reactor_desc))
563a4c 313         technic.swap_node(pos, "technic:hv_nuclear_reactor_core")
67b90f 314         meta:set_int("structure_accumulated_badness", 0)
d59055 315         siren_clear(pos, meta)
563a4c 316     elseif burn_time > 0 then
N 317         burn_time = burn_time + 1
318         meta:set_int("burn_time", burn_time)
319         local percent = math.floor(burn_time / burn_ticks * 100)
85a984 320         meta:set_string("infotext", reactor_desc.." ("..percent.."%)")
563a4c 321         meta:set_int("HV_EU_supply", power_supply)
ee0765 322     end
563a4c 323 end
N 324
bf58c7 325 local nuclear_reactor_receive_fields = function(pos, formname, fields, sender)
D 326     local player_name = sender:get_player_name()
327     if minetest.is_protected(pos, player_name) then
328         minetest.chat_send_player(player_name, "You are not allowed to edit this!")
329         minetest.record_protection_violation(pos, player_name)
330         return
331     end
332     local meta = minetest.get_meta(pos)
333     local update_formspec = false
334     if fields.remote_channel then
335         meta:set_string("remote_channel", fields.remote_channel)
336     end
337     if fields.start then
11e43f 338         local start_error_msg = start_reactor(pos, meta)
CH 339         if not start_error_msg then
bf58c7 340             minetest.chat_send_player(player_name, "Start successful")
D 341         else
11e43f 342             minetest.chat_send_player(player_name, start_error_msg)
bf58c7 343         end
D 344     end
345     if fields.autostart then
346         meta:set_string("autostart", fields.autostart)
347         update_formspec = true
348     end
349     if fields.enable_digiline then
350         meta:set_string("enable_digiline", fields.enable_digiline)
351         update_formspec = true
352     end
353     if update_formspec then
354         meta:set_string("formspec", make_reactor_formspec(meta))
355     end
356 end
357
358 local digiline_remote_def = function(pos, channel, msg)
359     local meta = minetest.get_meta(pos)
360     if meta:get_string("enable_digiline") ~= "true" or
361             channel ~= meta:get_string("remote_channel") then
362         return
363     end
364     -- Convert string messages to tables:
365     local msgt = type(msg)
366     if msgt == "string" then
367         local smsg = msg:lower()
368         msg = {}
369         if smsg == "get" then
370             msg.command = "get"
371         elseif smsg:sub(1, 13) == "self_destruct" then
372             msg.command = "self_destruct"
373             msg.timer = tonumber(smsg:sub(15)) or 0
374         elseif smsg == "start" then
375             msg.command = "start"
376         end
377     elseif msgt ~= "table" then
378         return
379     end
380
381     if msg.command == "get" then
382         local inv = meta:get_inventory()
383         local invtable = {}
384         for i = 1, 6 do
385             local stack = inv:get_stack("src", i)
386             if stack:is_empty() then
387                 invtable[i] = 0
388             elseif stack:get_name() == fuel_type then
389                 invtable[i] = stack:get_count()
390             else
391                 invtable[i] = -stack:get_count()
392             end
393         end
394         digiline_remote.send_to_node(pos, channel, {
395             burn_time = meta:get_int("burn_time"),
396             enabled   = meta:get_int("HV_EU_supply") == power_supply,
397             siren     = meta:get_int("siren") == 1,
398             structure_accumulated_badness = meta:get_int("structure_accumulated_badness"),
399             rods = invtable
400         }, 6, true)
401     elseif digiline_meltdown and msg.command == "self_destruct" and
402             minetest.get_node(pos).name == "technic:hv_nuclear_reactor_core_active" then
403         if msg.timer ~= 0 and type(msg.timer) == "number" then
404             siren_danger(pos, meta)
405             minetest.after(msg.timer, melt_down_reactor, pos)
406         else
407             melt_down_reactor(pos)
408         end
409     elseif msg.command == "start" then
11e43f 410         local start_error_msg = start_reactor(pos, meta)
CH 411         if not start_error_msg then
bf58c7 412             digiline_remote.send_to_node(pos, channel, "Start successful", 6, true)
D 413         else
11e43f 414             digiline_remote.send_to_node(pos, channel, start_error_msg, 6, true)
bf58c7 415         end
D 416     end
417 end
418
563a4c 419 minetest.register_node("technic:hv_nuclear_reactor_core", {
85a984 420     description = reactor_desc,
1d20af 421     tiles = {
VE 422         "technic_hv_nuclear_reactor_core.png",
423         "technic_hv_nuclear_reactor_core.png"..cable_entry
424     },
425     drawtype = "mesh",
426     mesh = "technic_reactor.obj",
bf58c7 427     groups = {cracky = 1, technic_machine = 1, technic_hv = 1, digiline_remote_receive = 1},
563a4c 428     legacy_facedir_simple = true,
N 429     sounds = default.node_sound_wood_defaults(),
430     paramtype = "light",
1d20af 431     paramtype2 = "facedir",
563a4c 432     stack_max = 1,
bf58c7 433     on_receive_fields = nuclear_reactor_receive_fields,
563a4c 434     on_construct = function(pos)
N 435         local meta = minetest.get_meta(pos)
85a984 436         meta:set_string("infotext", reactor_desc)
bf58c7 437         meta:set_string("formspec", make_reactor_formspec(meta))
D 438         if digiline_remote_path then
439             meta:set_string("remote_channel",
440                     "nucelear_reactor"..minetest.pos_to_string(pos))
441         end
563a4c 442         local inv = meta:get_inventory()
N 443         inv:set_size("src", 6)
85a984 444     end,
bf58c7 445     _on_digiline_remote_receive = digiline_remote_def,
563a4c 446     can_dig = technic.machine_can_dig,
85a984 447     on_destruct = function(pos) siren_set_state(pos, SS_OFF) end,
563a4c 448     allow_metadata_inventory_put = technic.machine_inventory_put,
N 449     allow_metadata_inventory_take = technic.machine_inventory_take,
450     allow_metadata_inventory_move = technic.machine_inventory_move,
451     technic_run = run,
452 })
453
454 minetest.register_node("technic:hv_nuclear_reactor_core_active", {
1d20af 455     tiles = {
VE 456         "technic_hv_nuclear_reactor_core.png",
457         "technic_hv_nuclear_reactor_core.png"..cable_entry
458     },
459     drawtype = "mesh",
460     mesh = "technic_reactor.obj",
bf58c7 461     groups = {cracky = 1, technic_machine = 1, technic_hv = 1, radioactive = 4,
D 462         not_in_creative_inventory = 1, digiline_remote_receive = 1},
563a4c 463     legacy_facedir_simple = true,
N 464     sounds = default.node_sound_wood_defaults(),
85a984 465     drop = "technic:hv_nuclear_reactor_core",
S 466     light_source = 14,
563a4c 467     paramtype = "light",
1d20af 468     paramtype2 = "facedir",
bf58c7 469     on_receive_fields = nuclear_reactor_receive_fields,
D 470     _on_digiline_remote_receive = digiline_remote_def,
563a4c 471     can_dig = technic.machine_can_dig,
85a984 472     after_dig_node = melt_down_reactor,
S 473     on_destruct = function(pos) siren_set_state(pos, SS_OFF) end,
563a4c 474     allow_metadata_inventory_put = technic.machine_inventory_put,
N 475     allow_metadata_inventory_take = technic.machine_inventory_take,
476     allow_metadata_inventory_move = technic.machine_inventory_move,
477     technic_run = run,
1c617f 478     technic_on_disable = function(pos, node)
N 479         local timer = minetest.get_node_timer(pos)
a8daa4 480         timer:start(1)
1c617f 481         end,
N 482     on_timer = function(pos, node)
483         local meta = minetest.get_meta(pos)
85a984 484
1c617f 485         -- Connected back?
3252da 486         if meta:get_int("HV_EU_timeout") > 0 then return false end
85a984 487
1c617f 488         local burn_time = meta:get_int("burn_time") or 0
N 489
490         if burn_time >= burn_ticks or burn_time == 0 then
491             meta:set_int("HV_EU_supply", 0)
492             meta:set_int("burn_time", 0)
493             technic.swap_node(pos, "technic:hv_nuclear_reactor_core")
67b90f 494             meta:set_int("structure_accumulated_badness", 0)
d59055 495             siren_clear(pos, meta)
3252da 496             return false
1c617f 497         end
85a984 498
1c617f 499         meta:set_int("burn_time", burn_time + 1)
3252da 500         return true
1c617f 501     end,
ee0765 502 })
S 503
504 technic.register_machine("HV", "technic:hv_nuclear_reactor_core",        technic.producer)
505 technic.register_machine("HV", "technic:hv_nuclear_reactor_core_active", technic.producer)
506