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