diff --git a/technic/machines/creative_generator.lua b/technic/machines/creative_generator.lua new file mode 100644 index 00000000..07d67704 --- /dev/null +++ b/technic/machines/creative_generator.lua @@ -0,0 +1,192 @@ +-- technic/technic/register/creative_generator.lua +-- Creative-only node that feeds power into the grid for free +-- Copyright (c) 2026 Technic Developers +-- SPDX-License-Identifier: LGPL-2.1-or-later + +local S = technic.getter +local digilines_exists = core.get_modpath("digilines") and true or false + +local function set_creative_generator_formspec(meta) + local formspec = "size[5,2.25]" .. + "field[0.3,0.5;2,1;power;" .. S("Output Power") .. ";${power}]" + if digilines_exists then + formspec = formspec .. + "field[2.3,0.5;3,1;channel;" .. S("Digiline Channel") .. ";${channel}]" + end + -- The names for these toggle buttons are explicit about which + -- state they'll switch to, so that multiple presses (arising + -- from the ambiguity between lag and a missed press) only make + -- the single change that the user expects. + if meta:get_int("mesecon_mode") == 0 then + formspec = formspec .. "button[0,1;5,1;mesecon_mode_1;" .. S("Ignoring Mesecon Signal") .. "]" + else + formspec = formspec .. "button[0,1;5,1;mesecon_mode_0;" .. S("Controlled by Mesecon Signal") .. "]" + end + if meta:get_int("enabled") == 0 then + formspec = formspec .. "button[0,1.75;5,1;enable;" .. S("@1 Disabled", S("Creative Generator")) .. "]" + else + formspec = formspec .. "button[0,1.75;5,1;disable;" .. S("@1 Enabled", S("Creative Generator")) .. "]" + end + meta:set_string("formspec", formspec) +end + +local def = { + description = S("Creative Generator"), + tiles = { + "technic_hv_generator_top.png", + "technic_machine_bottom.png^technic_cable_connection_overlay.png", + "technic_hv_generator_side.png^technic_creative_generator_overlay.png", + "technic_hv_generator_side.png^technic_creative_generator_overlay.png", + "technic_hv_generator_side.png^technic_creative_generator_overlay.png", + "technic_hv_generator_side.png^technic_creative_generator_overlay.png" + }, + groups = { + snappy = 2, + choppy = 2, + oddly_breakable_by_hand = 2, + technic_machine = 1, + technic_all_tiers = 1, + axey = 2, + handy = 1, + }, + connect_sides = { "bottom" }, + sounds = technic.sounds.node_sound_wood_defaults(), + drop = "", +} + +def.on_construct = function(pos) + local meta = core.get_meta(pos) + meta:set_string("infotext", S("Creative Generator")) + meta:set_int("power", 100000) + meta:set_int("enabled", 1) + meta:set_int("mesecon_mode", 0) + meta:set_int("mesecon_effect", 0) + set_creative_generator_formspec(meta) +end + +def.on_receive_fields = function(pos, formname, fields, sender) + if not sender or core.is_protected(pos, sender:get_player_name()) then + return + end + local meta = core.get_meta(pos) + local power = nil + if fields.power then + power = tonumber(fields.power) or 0 + power = math.max(power, 0) + power = math.min(power, 2147483647) + power = math.floor(power) + if power == meta:get_int("power") then power = nil end + end + if power then meta:set_int("power", power) end + if fields.channel then meta:set_string("channel", fields.channel) end + if fields.enable then meta:set_int("enabled", 1) end + if fields.disable then meta:set_int("enabled", 0) end + if fields.mesecon_mode_0 then meta:set_int("mesecon_mode", 0) end + if fields.mesecon_mode_1 then meta:set_int("mesecon_mode", 1) end + set_creative_generator_formspec(meta) +end + +if core.get_modpath("mesecons") then + def.mesecons = { + effector = { + action_on = function(pos, node) + core.get_meta(pos):set_int("mesecon_effect", 1) + end, + action_off = function(pos, node) + core.get_meta(pos):set_int("mesecon_effect", 0) + end + } + } +end + +if digilines_exists then + def.digilines = { + receptor = { + rules = technic.digilines.rules, + action = function() end + }, + effector = { + rules = technic.digilines.rules, + action = function(pos, node, channel, msg) + if type(msg) ~= "string" then + return + end + local meta = core.get_meta(pos) + if channel ~= meta:get_string("channel") then + return + end + msg = msg:lower() + if msg == "get" then + digilines.receptor_send(pos, technic.digilines.rules, channel, { + enabled = meta:get_int("enabled"), + power = meta:get_int("power"), + mesecon_mode = meta:get_int("mesecon_mode") + }) + return + elseif msg == "off" then + meta:set_int("enabled", 0) + elseif msg == "on" then + meta:set_int("enabled", 1) + elseif msg == "toggle" then + local onn = meta:get_int("enabled") + onn = 1 - onn -- Mirror onn with pivot 0.5, so switch between 1 and 0. + meta:set_int("enabled", onn) + elseif msg:sub(1, 5) == "power" then + local power = tonumber(msg:sub(7)) + if not power then + return + end + power = math.max(power, 0) + power = math.min(power, 2147483647) + power = math.floor(power) + meta:set_int("power", power) + elseif msg:sub(1, 12) == "mesecon_mode" then + meta:set_int("mesecon_mode", tonumber(msg:sub(14))) + else + return + end + set_creative_generator_formspec(meta) + end + }, + } +end + +def.technic_run = function(pos, node, run_stage) + -- run only in producer stage. + if run_stage == technic.receiver then + return + end + + -- Machine information + local machine_name = S("Creative Generator") + local meta = core.get_meta(pos) + local enabled = meta:get_int("enabled") == 1 and + (meta:get_int("mesecon_mode") == 0 or meta:get_int("mesecon_effect") ~= 0) + + local demand = enabled and meta:get_int("power") or 0 + + local pos_down = vector.offset(pos, 0, -1, 0) + local name_down = core.get_node(pos_down).name + + local to = technic.get_cable_tier(name_down) + + if to then + meta:set_int(to .. "_EU_demand", 0) + meta:set_int(to .. "_EU_supply", demand) + meta:set_string("infotext", S("@1 (@2 @3)", machine_name, + technic.EU_string(demand), to)) + else + meta:set_string("infotext", S("@1 Has Bad Cabling", machine_name)) + if to then + meta:set_int(to .. "_EU_supply", 0) + end + return + end +end +def.technic_on_disable = def.technic_run + +core.register_node("technic:creative_generator", def) + +for tier, machines in pairs(technic.machines) do + technic.register_machine(tier, "technic:creative_generator", technic.producer) +end diff --git a/technic/machines/init.lua b/technic/machines/init.lua index a26c510c..28e49731 100644 --- a/technic/machines/init.lua +++ b/technic/machines/init.lua @@ -41,6 +41,7 @@ dofile(path.."/switching_station_globalstep.lua") dofile(path.."/power_monitor.lua") dofile(path.."/supply_converter.lua") +dofile(path.."/creative_generator.lua") dofile(path.."/other/init.lua") diff --git a/technic/spec/creative_generator_spec.lua b/technic/spec/creative_generator_spec.lua new file mode 100644 index 00000000..acb456e0 --- /dev/null +++ b/technic/spec/creative_generator_spec.lua @@ -0,0 +1,79 @@ +require("mineunit") +--[[ + Technic network unit tests. + Execute mineunit at technic source directory. +--]] + +-- Load complete technic mod +fixture("technic") +sourcefile("init") + +describe("Creative Generator using", function() + + -- Execute on mods loaded callbacks to finish loading. + mineunit:mods_loaded() + -- Tell mods that 1 minute passed already to execute all weird core.after hacks. + mineunit:execute_globalstep(60) + world.set_default_node("air") + + -- Execute multiple globalsteps: run_network(times = 1, dtime = 1) + local run_network = spec_utility.run_globalsteps + + describe("network building", function() + + local player = Player("SX") + local gen_pos = {x=101,y=951,z=100} + local def = core.registered_items["technic:creative_generator"] + local on_receive_fields = def.on_receive_fields + + world.layout({ + {{x=100,y=950,z=100}, "technic:hv_cable"}, + {{x=101,y=950,z=100}, "technic:hv_cable"}, + {{x=100,y=951,z=100}, "technic:switching_station"}, + {gen_pos, "technic:creative_generator"}, + }) + + -- Build network + local id = technic.pos2network(gen_pos) + local net = technic.networks[id] + + it("generates energy", function() + -- Build the network so it includes the creative generator and + -- execute one globalstep to collect supply values from machines. + run_network(1) + assert.equals(100000, net.supply) + end) + + it("generates more energy on request", function() + local meta = core.get_meta(gen_pos) + meta:set_int("power", 20260617) + run_network(1) + assert.equals(20260617, net.supply) + end) + + it("pauses on formspec request", function() + on_receive_fields(gen_pos, "", { disable = true }, player) + run_network(1) + assert.equals(0, net.supply) -- no power sources + end) + + it("resumes on formspec request", function() + on_receive_fields(gen_pos, "", { enable = true }, player) + run_network(1) + assert.equals(20260617, net.supply) -- back to normal + end) + + it("maintains a minimum power", function() + on_receive_fields(gen_pos, "", { power = "-1" }, player) + run_network(1) + assert.equals(0, net.supply) -- math.max(power, 0) + end) + + it("maintains a maximum power", function() + on_receive_fields(gen_pos, "", { power = tostring(2147483647 + 10) }, player) + run_network(1) + assert.equals(2147483647, net.supply) -- math.min(power, 2147483647), s32 limit + end) + + end) +end) \ No newline at end of file diff --git a/technic/textures/technic_creative_generator_overlay.png b/technic/textures/technic_creative_generator_overlay.png new file mode 100644 index 00000000..f226284b Binary files /dev/null and b/technic/textures/technic_creative_generator_overlay.png differ