From 6a6567e98c2b8281f8093d2dd245d76240ecc339 Mon Sep 17 00:00:00 2001 From: watilin Date: Sun, 16 Nov 2025 15:38:45 +0100 Subject: [PATCH 1/5] Make detector tube use a node timer instead of core.after --- tubes/signal.lua | 73 ++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/tubes/signal.lua b/tubes/signal.lua index 6e80dda..7724d2f 100644 --- a/tubes/signal.lua +++ b/tubes/signal.lua @@ -1,14 +1,7 @@ local S = core.get_translator("pipeworks") --- the core.after() calls below can sometimes trigger after a tube --- breaks, at which point item_exit() is no longer valid, so we have to make --- sure that there even IS a callback to run, first. - -local function after_break(pos) - local name = core.get_node(pos).name - if core.registered_nodes[name].item_exit then - core.registered_nodes[name].item_exit(pos) - end +local function detector_on_destruct(pos) + core.get_node_timer(pos):stop() end if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then @@ -18,52 +11,54 @@ if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then inventory_image = "pipeworks_detector_tube_inv.png", plain = { "pipeworks_detector_tube_plain.png" }, node_def = { - tube = {can_go = function(pos, node, velocity, stack) - local meta = core.get_meta(pos) - local nitems = meta:get_int("nitems")+1 - meta:set_int("nitems", nitems) - local saved_pos = vector.new(pos) - core.after(detector_tube_step, after_break, saved_pos) - return pipeworks.notvel(pipeworks.meseadjlist,velocity) - end}, + tube = { + can_go = function(pos, node, velocity, stack) + -- No need to count passing items, as starting a second node timer + -- simply overrides the previous one. If enough time is elapsed + -- without a new item coming in, the timer can expire and it then + -- means that there is currently no item in the tube. + local timer = core.get_node_timer(pos) + timer:start(detector_tube_step) + + return pipeworks.notvel(pipeworks.meseadjlist, velocity) + end, + }, groups = {mesecon = 2, not_in_creative_inventory = 1}, drop = "pipeworks:detector_tube_off_1", mesecons = {receptor = {state = "on", rules = pipeworks.mesecons_rules}}, - item_exit = function(pos) - local meta = core.get_meta(pos) - local nitems = meta:get_int("nitems")-1 + + on_timer = function(pos, elapsed) local node = core.get_node(pos) - local name = node.name - local fdir = node.param2 - if nitems == 0 then - core.set_node(pos, {name = string.gsub(name, "on", "off"), param2 = fdir}) - mesecon.receptor_off(pos, pipeworks.mesecons_rules) - else - meta:set_int("nitems", nitems) - end - end, - on_construct = function(pos) - local meta = core.get_meta(pos) - meta:set_int("nitems", 1) - core.after(detector_tube_step, after_break, pos) + node.name = string.gsub(node.name, "on", "off") + core.swap_node(pos, node) + mesecon.receptor_off(pos, pipeworks.mesecons_rules) end, + + on_destruct = detector_on_destruct, }, }) + pipeworks.register_tube("pipeworks:detector_tube_off", { description = S("Detecting Pneumatic Tube Segment"), inventory_image = "pipeworks_detector_tube_inv.png", plain = { "pipeworks_detector_tube_plain.png" }, node_def = { - tube = {can_go = function(pos, node, velocity, stack) - local node = core.get_node(pos) - local name = node.name - local fdir = node.param2 - core.set_node(pos,{name = string.gsub(name, "off", "on"), param2 = fdir}) + tube = { + can_go = function(pos, node, velocity, stack) + -- start a timer that will be handled by the "on" tube + local timer = core.get_node_timer(pos) + timer:start(detector_tube_step) + node.name = string.gsub(node.name, "off", "on") + core.swap_node(pos, node) mesecon.receptor_on(pos, pipeworks.mesecons_rules) + return pipeworks.notvel(pipeworks.meseadjlist, velocity) - end}, + end, + }, groups = {mesecon = 2}, mesecons = {receptor = {state = "off", rules = pipeworks.mesecons_rules }}, + + on_destruct = detector_on_destruct, }, }) From 9b5a6e6e2107ab6686be50f060f3c976d99e002c Mon Sep 17 00:00:00 2001 From: watilin Date: Fri, 28 Nov 2025 14:45:38 +0100 Subject: [PATCH 2/5] Replace node timers with globalstep and save pending timers in a file --- tubes/signal.lua | 117 +++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/tubes/signal.lua b/tubes/signal.lua index 7724d2f..5edf594 100644 --- a/tubes/signal.lua +++ b/tubes/signal.lua @@ -1,11 +1,66 @@ local S = core.get_translator("pipeworks") -local function detector_on_destruct(pos) - core.get_node_timer(pos):stop() -end - if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then local detector_tube_step = 5 * (tonumber(core.settings:get("dedicated_server_step")) or 0.09) + + -- this table stores ad-hoc timers (not node timers) for every detector tube + -- { [position_hash] = time } + local detector_timers = {} + + -- Persistency: load/save pending timers from/into a file across game restarts + -- this is basically a copypaste from mesecons code + local wpath = core.get_worldpath() + local filename = "detector_timers" + + local f = io.open(wpath..DIR_DELIM..filename, "r") + if f then + local t = f:read("*all") + f:close() + if t and t ~= "" then + detector_timers = core.deserialize(t) + end + end + + core.register_on_shutdown(function() + local f = io.open(wpath..DIR_DELIM..filename, "w") + f:write(core.serialize(detector_timers)) + f:close() + end) + + local function detector_set_timer(pos) + -- refresh timer if already set + detector_timers[core.hash_node_position(pos)] = detector_tube_step + end + + core.register_globalstep(function(dtime) + for hash,time in pairs(detector_timers) do + time = time - dtime + if time <= 0 then + local pos = core.get_position_from_hash(hash) + local node = core.get_node_or_nil(pos) + if node then + detector_timers[hash] = nil + if string.find(node.name, "pipeworks:detector_tube_on", 1, true) then + node.name = string.gsub(node.name, "on", "off") + core.swap_node(pos, node) + mesecon.receptor_off(pos, pipeworks.mesecons_rules) + end + end + -- in case the area wasn't loaded, do not remove the timer + else + detector_timers[hash] = time + end + end + end) + + -- cleanup metadata from previous versions + local function detector_cleanup_metadata(pos) + local meta = core.get_meta(pos) + if not meta then return end + -- an empty string deletes the key even if the previous value wasn't a string + meta:set_string("nitems", "") + end + pipeworks.register_tube("pipeworks:detector_tube_on", { description = S("Detecting Pneumatic Tube Segment on"), inventory_image = "pipeworks_detector_tube_inv.png", @@ -13,53 +68,35 @@ if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then node_def = { tube = { can_go = function(pos, node, velocity, stack) - -- No need to count passing items, as starting a second node timer - -- simply overrides the previous one. If enough time is elapsed - -- without a new item coming in, the timer can expire and it then - -- means that there is currently no item in the tube. - local timer = core.get_node_timer(pos) - timer:start(detector_tube_step) - + detector_cleanup_metadata(pos) + detector_set_timer(pos) return pipeworks.notvel(pipeworks.meseadjlist, velocity) end, }, groups = {mesecon = 2, not_in_creative_inventory = 1}, drop = "pipeworks:detector_tube_off_1", mesecons = {receptor = {state = "on", rules = pipeworks.mesecons_rules}}, - - on_timer = function(pos, elapsed) - local node = core.get_node(pos) - node.name = string.gsub(node.name, "on", "off") - core.swap_node(pos, node) - mesecon.receptor_off(pos, pipeworks.mesecons_rules) - end, - - on_destruct = detector_on_destruct, }, }) pipeworks.register_tube("pipeworks:detector_tube_off", { - description = S("Detecting Pneumatic Tube Segment"), - inventory_image = "pipeworks_detector_tube_inv.png", - plain = { "pipeworks_detector_tube_plain.png" }, - node_def = { - tube = { - can_go = function(pos, node, velocity, stack) - -- start a timer that will be handled by the "on" tube - local timer = core.get_node_timer(pos) - timer:start(detector_tube_step) - node.name = string.gsub(node.name, "off", "on") - core.swap_node(pos, node) - mesecon.receptor_on(pos, pipeworks.mesecons_rules) - - return pipeworks.notvel(pipeworks.meseadjlist, velocity) - end, - }, - groups = {mesecon = 2}, - mesecons = {receptor = {state = "off", rules = pipeworks.mesecons_rules }}, - - on_destruct = detector_on_destruct, + description = S("Detecting Pneumatic Tube Segment"), + inventory_image = "pipeworks_detector_tube_inv.png", + plain = {"pipeworks_detector_tube_plain.png"}, + node_def = { + tube = { + can_go = function(pos, node, velocity, stack) + detector_cleanup_metadata(pos) + node.name = string.gsub(node.name, "off", "on") + core.swap_node(pos, node) + mesecon.receptor_on(pos, pipeworks.mesecons_rules) + detector_set_timer(pos) + return pipeworks.notvel(pipeworks.meseadjlist, velocity) + end, }, + groups = {mesecon = 2}, + mesecons = {receptor = {state = "off", rules = pipeworks.mesecons_rules}}, + }, }) core.register_craft( { From a30a46e0e2b85926fa93771fe5511523c2b44a04 Mon Sep 17 00:00:00 2001 From: watilin Date: Fri, 28 Nov 2025 14:55:10 +0100 Subject: [PATCH 3/5] Make luacheck happy --- tubes/signal.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tubes/signal.lua b/tubes/signal.lua index 5edf594..ddc71d0 100644 --- a/tubes/signal.lua +++ b/tubes/signal.lua @@ -1,3 +1,5 @@ +-- luacheck: globals DIR_DELIM + local S = core.get_translator("pipeworks") if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then From d3df35e3fa3d49193611dc8ccf2e9c3e9522cb1c Mon Sep 17 00:00:00 2001 From: watilin Date: Mon, 1 Dec 2025 16:15:35 +0100 Subject: [PATCH 4/5] Use mod storage instead of a file --- tubes/signal.lua | 55 ++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/tubes/signal.lua b/tubes/signal.lua index ddc71d0..25fec7d 100644 --- a/tubes/signal.lua +++ b/tubes/signal.lua @@ -1,56 +1,57 @@ -- luacheck: globals DIR_DELIM local S = core.get_translator("pipeworks") +local storage = core.get_mod_storage() +local STORAGE_PREFIX = "detector_timer_" if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then local detector_tube_step = 5 * (tonumber(core.settings:get("dedicated_server_step")) or 0.09) - -- this table stores ad-hoc timers (not node timers) for every detector tube + -- in-memory table to avoid traversing the whole storage in globalstep -- { [position_hash] = time } local detector_timers = {} - -- Persistency: load/save pending timers from/into a file across game restarts - -- this is basically a copypaste from mesecons code - local wpath = core.get_worldpath() - local filename = "detector_timers" - - local f = io.open(wpath..DIR_DELIM..filename, "r") - if f then - local t = f:read("*all") - f:close() - if t and t ~= "" then - detector_timers = core.deserialize(t) + -- populate our in-memory table from storage + for key, val in pairs(storage:to_table().fields) do + local short_key = string.match(key, "^"..STORAGE_PREFIX.."(.+)") + if short_key then + local hash = tonumber(short_key) + local pos = core.get_position_from_hash(hash) + detector_timers[hash] = tonumber(val) end end - core.register_on_shutdown(function() - local f = io.open(wpath..DIR_DELIM..filename, "w") - f:write(core.serialize(detector_timers)) - f:close() - end) + local function detector_set_timer(pos, time) + local hash = core.hash_node_position(pos) + time = time or detector_tube_step + -- timer will be refreshed if already set + detector_timers[hash] = time + storage:set_float(string.format("%s%.0f", STORAGE_PREFIX, hash), time) + end - local function detector_set_timer(pos) - -- refresh timer if already set - detector_timers[core.hash_node_position(pos)] = detector_tube_step + local function detector_remove_timer(pos) + local hash = core.hash_node_position(pos) + detector_timers[hash] = nil + storage:set_string(string.format("%s%.0f", STORAGE_PREFIX, hash), "") end core.register_globalstep(function(dtime) - for hash,time in pairs(detector_timers) do + for hash, time in pairs(detector_timers) do + local pos = core.get_position_from_hash(hash) time = time - dtime if time <= 0 then - local pos = core.get_position_from_hash(hash) local node = core.get_node_or_nil(pos) if node then - detector_timers[hash] = nil - if string.find(node.name, "pipeworks:detector_tube_on", 1, true) then - node.name = string.gsub(node.name, "on", "off") + detector_remove_timer(pos) + if string.find(node.name, "pipeworks:detector_tube_on_", 1, true) then + node.name = string.gsub(node.name, "on", "off", 1) core.swap_node(pos, node) mesecon.receptor_off(pos, pipeworks.mesecons_rules) end end -- in case the area wasn't loaded, do not remove the timer else - detector_timers[hash] = time + detector_set_timer(pos, time) end end end) @@ -89,7 +90,7 @@ if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then tube = { can_go = function(pos, node, velocity, stack) detector_cleanup_metadata(pos) - node.name = string.gsub(node.name, "off", "on") + node.name = string.gsub(node.name, "off", "on", 1) core.swap_node(pos, node) mesecon.receptor_on(pos, pipeworks.mesecons_rules) detector_set_timer(pos) From 4f3bb4d50488275040409b1131070a6354121596 Mon Sep 17 00:00:00 2001 From: watilin Date: Mon, 1 Dec 2025 16:27:32 +0100 Subject: [PATCH 5/5] Minor oversight --- tubes/signal.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/tubes/signal.lua b/tubes/signal.lua index 25fec7d..4b25b1a 100644 --- a/tubes/signal.lua +++ b/tubes/signal.lua @@ -16,7 +16,6 @@ if core.get_modpath("mesecons") and pipeworks.enable_detector_tube then local short_key = string.match(key, "^"..STORAGE_PREFIX.."(.+)") if short_key then local hash = tonumber(short_key) - local pos = core.get_position_from_hash(hash) detector_timers[hash] = tonumber(val) end end