From 5457e76636d166c6ecc50d424d1ba7df61655f2e Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 8 Aug 2024 00:55:55 +0300 Subject: [PATCH 01/16] Implement batched signals for Digiline Chests --- inventory.lua | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/inventory.lua b/inventory.lua index 0004954..624b9de 100644 --- a/inventory.lua +++ b/inventory.lua @@ -2,6 +2,11 @@ local S = digilines.S local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil +-- Signals which will be sent in a single batch +local batched_signals = {} +-- Maximum interval from the previous signal to include the current one into batch (in seconds) +local interval_to_batch = 0.1 + -- Sends a message onto the Digilines network. -- pos: the position of the Digilines chest node. -- action: the action string indicating what happened. @@ -10,7 +15,9 @@ local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil -- to_slot: the slot number that is put into (optional). -- side: which side of the chest the action occurred (optional). local function send_message(pos, action, stack, from_slot, to_slot, side) - local channel = minetest.get_meta(pos):get_string("channel") + local meta = minetest.get_meta(pos) + local channel = meta:get_string("channel") + local msg = { action = action, stack = stack and stack:to_table(), @@ -19,6 +26,20 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) -- Duplicate the vector in case the caller expects it not to change. side = side and vector.new(side) } + + -- Check if we need to include the current signal into batch + -- Store "prev_time" in metadata as a string to avoid integer overflow + local prev_time = tonumber(meta:get_string("prev_time")) + local cur_time = minetest.get_us_time() + meta:set_string("prev_time", tostring(cur_time)) + if cur_time - prev_time < 1000000 * interval_to_batch then + local pos_text = vector.to_string(pos) + batched_signals[pos_text] = batched_signals[pos_text] or {} + table.insert(batched_signals[pos_text], msg) + minetest.get_node_timer(pos):start(interval_to_batch) + return + end + digilines.receptor_send(pos, digilines.rules.default, channel, msg) end @@ -186,6 +207,9 @@ minetest.register_node("digilines:chest", { local inv = meta:get_inventory() inv:set_size("main", 8*4) end, + on_destruct = function(pos) + batched_signals[vector.to_string(pos)] = nil + end, after_place_node = tubescan, after_dig_node = tubescan, can_dig = function(pos) @@ -321,6 +345,20 @@ minetest.register_node("digilines:chest", { send_message(pos, "utake", stack, index) check_empty(pos) minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos)) + end, + on_timer = function(pos, elapsed) + local channel = minetest.get_meta(pos):get_string("channel") + local pos_text = vector.to_string(pos) + if #batched_signals[pos_text] == 1 then + -- If there is only one signal is the batch, don't send it in a batch + digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_text])) + else + digilines.receptor_send(pos, digilines.rules.default, channel, { + action = "batch", + signals = batched_signals[pos_text] + }) + end + batched_signals[pos_text] = nil end }) From faba1d8149d998d592703362733807e2fd47ad23 Mon Sep 17 00:00:00 2001 From: Andrii Date: Sat, 10 Aug 2024 19:15:33 +0300 Subject: [PATCH 02/16] Fix crash because of prev_time == nil --- inventory.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory.lua b/inventory.lua index 624b9de..617faa5 100644 --- a/inventory.lua +++ b/inventory.lua @@ -29,7 +29,7 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) -- Check if we need to include the current signal into batch -- Store "prev_time" in metadata as a string to avoid integer overflow - local prev_time = tonumber(meta:get_string("prev_time")) + local prev_time = tonumber(meta:get_string("prev_time") or "0") local cur_time = minetest.get_us_time() meta:set_string("prev_time", tostring(cur_time)) if cur_time - prev_time < 1000000 * interval_to_batch then From 385f41de9581069ed9901bdfa506b8ea14979585 Mon Sep 17 00:00:00 2001 From: Andrii Date: Sun, 18 Aug 2024 21:52:25 +0300 Subject: [PATCH 03/16] Restrict the maximum size of batch, finally fix the bug with prev_time == nil --- inventory.lua | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/inventory.lua b/inventory.lua index 617faa5..7963a7a 100644 --- a/inventory.lua +++ b/inventory.lua @@ -6,6 +6,25 @@ local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil local batched_signals = {} -- Maximum interval from the previous signal to include the current one into batch (in seconds) local interval_to_batch = 0.1 +-- Maximum number of signals in batch +local max_signals_in_batch = 100 + +-- Sends the current batch message of a Digiline chest +-- pos: the position of the Digilines chest node +-- channel: the channel to which the message will be sent +local function send_and_clear_batch(pos, channel) + local pos_text = vector.to_string(pos) + if #batched_signals[pos_text] == 1 then + -- If there is only one signal is the batch, don't send it in a batch + digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_text])) + else + digilines.receptor_send(pos, digilines.rules.default, channel, { + action = "batch", + signals = batched_signals[pos_text] + }) + end + batched_signals[pos_text] = nil +end -- Sends a message onto the Digilines network. -- pos: the position of the Digilines chest node. @@ -29,14 +48,21 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) -- Check if we need to include the current signal into batch -- Store "prev_time" in metadata as a string to avoid integer overflow - local prev_time = tonumber(meta:get_string("prev_time") or "0") + local prev_time = tonumber(meta:get_string("prev_time")) or 0 local cur_time = minetest.get_us_time() meta:set_string("prev_time", tostring(cur_time)) if cur_time - prev_time < 1000000 * interval_to_batch then local pos_text = vector.to_string(pos) batched_signals[pos_text] = batched_signals[pos_text] or {} table.insert(batched_signals[pos_text], msg) - minetest.get_node_timer(pos):start(interval_to_batch) + local node_timer = minetest.get_node_timer(pos) + if #batched_signals[pos_text] >= max_signals_in_batch then + -- Send the batch immediately if it's full + node_timer:stop() + send_and_clear_batch(pos, channel) + else + node_timer:start(interval_to_batch) + end return end @@ -347,18 +373,13 @@ minetest.register_node("digilines:chest", { minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos)) end, on_timer = function(pos, elapsed) - local channel = minetest.get_meta(pos):get_string("channel") - local pos_text = vector.to_string(pos) - if #batched_signals[pos_text] == 1 then - -- If there is only one signal is the batch, don't send it in a batch - digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_text])) - else - digilines.receptor_send(pos, digilines.rules.default, channel, { - action = "batch", - signals = batched_signals[pos_text] - }) + -- Send all the batched signals when enough time since the last signal passed + if not batched_signals[vector.to_string(pos)] then + return end - batched_signals[pos_text] = nil + local channel = minetest.get_meta(pos):get_string("channel") + send_and_clear_batch(pos, channel) + return false end }) From 9acd806c5c204eccaac6b94e439dc302f03f3119 Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 22 Aug 2024 00:04:03 +0300 Subject: [PATCH 04/16] Fix linter warning --- inventory.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory.lua b/inventory.lua index 7963a7a..988eb5c 100644 --- a/inventory.lua +++ b/inventory.lua @@ -372,7 +372,7 @@ minetest.register_node("digilines:chest", { check_empty(pos) minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos)) end, - on_timer = function(pos, elapsed) + on_timer = function(pos, _) -- Send all the batched signals when enough time since the last signal passed if not batched_signals[vector.to_string(pos)] then return From eb891e156b31870b3ca5efdac5eb4957acf0b727 Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 4 Sep 2024 22:52:26 +0300 Subject: [PATCH 05/16] Fix according to comments --- inventory.lua | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/inventory.lua b/inventory.lua index 988eb5c..aa8b1b9 100644 --- a/inventory.lua +++ b/inventory.lua @@ -8,22 +8,25 @@ local batched_signals = {} local interval_to_batch = 0.1 -- Maximum number of signals in batch local max_signals_in_batch = 100 +-- Time of last signal for each chest +local last_signal_time_for_chest = {} -- Sends the current batch message of a Digiline chest -- pos: the position of the Digilines chest node -- channel: the channel to which the message will be sent local function send_and_clear_batch(pos, channel) - local pos_text = vector.to_string(pos) - if #batched_signals[pos_text] == 1 then + local pos_hash = minetest.hash_node_position(pos) + if #batched_signals[pos_hash] == 1 then -- If there is only one signal is the batch, don't send it in a batch - digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_text])) + digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_hash])) else digilines.receptor_send(pos, digilines.rules.default, channel, { action = "batch", - signals = batched_signals[pos_text] + signals = batched_signals[pos_hash] }) end - batched_signals[pos_text] = nil + batched_signals[pos_hash] = nil + last_signal_time_for_chest[pos_hash] = nil end -- Sends a message onto the Digilines network. @@ -47,16 +50,15 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) } -- Check if we need to include the current signal into batch - -- Store "prev_time" in metadata as a string to avoid integer overflow - local prev_time = tonumber(meta:get_string("prev_time")) or 0 + local pos_hash = minetest.hash_node_position(pos) + local prev_time = last_signal_time_for_chest[pos_hash] or 0 local cur_time = minetest.get_us_time() - meta:set_string("prev_time", tostring(cur_time)) + last_signal_time_for_chest[pos_hash] = cur_time if cur_time - prev_time < 1000000 * interval_to_batch then - local pos_text = vector.to_string(pos) - batched_signals[pos_text] = batched_signals[pos_text] or {} - table.insert(batched_signals[pos_text], msg) + batched_signals[pos_hash] = batched_signals[pos_hash] or {} + table.insert(batched_signals[pos_hash], msg) local node_timer = minetest.get_node_timer(pos) - if #batched_signals[pos_text] >= max_signals_in_batch then + if #batched_signals[pos_hash] >= max_signals_in_batch then -- Send the batch immediately if it's full node_timer:stop() send_and_clear_batch(pos, channel) @@ -234,7 +236,7 @@ minetest.register_node("digilines:chest", { inv:set_size("main", 8*4) end, on_destruct = function(pos) - batched_signals[vector.to_string(pos)] = nil + batched_signals[minetest.hash_node_position(pos)] = nil end, after_place_node = tubescan, after_dig_node = tubescan, @@ -374,7 +376,7 @@ minetest.register_node("digilines:chest", { end, on_timer = function(pos, _) -- Send all the batched signals when enough time since the last signal passed - if not batched_signals[vector.to_string(pos)] then + if not batched_signals[minetest.hash_node_position(pos)] then return end local channel = minetest.get_meta(pos):get_string("channel") From 97f66f418153ed5425abf80c94bb82bdd7f992c7 Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 4 Sep 2024 22:59:14 +0300 Subject: [PATCH 06/16] Rename signal -> message --- inventory.lua | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/inventory.lua b/inventory.lua index aa8b1b9..029ec38 100644 --- a/inventory.lua +++ b/inventory.lua @@ -2,31 +2,31 @@ local S = digilines.S local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil --- Signals which will be sent in a single batch -local batched_signals = {} --- Maximum interval from the previous signal to include the current one into batch (in seconds) +-- Messages which will be sent in a single batch +local batched_messages = {} +-- Maximum interval from the previous message to include the current one into batch (in seconds) local interval_to_batch = 0.1 --- Maximum number of signals in batch -local max_signals_in_batch = 100 --- Time of last signal for each chest -local last_signal_time_for_chest = {} +-- Maximum number of messages in batch +local max_messages_in_batch = 100 +-- Time of the last message for each chest +local last_message_time_for_chest = {} -- Sends the current batch message of a Digiline chest -- pos: the position of the Digilines chest node -- channel: the channel to which the message will be sent local function send_and_clear_batch(pos, channel) local pos_hash = minetest.hash_node_position(pos) - if #batched_signals[pos_hash] == 1 then - -- If there is only one signal is the batch, don't send it in a batch - digilines.receptor_send(pos, digilines.rules.default, next(batched_signals[pos_hash])) + if #batched_messages[pos_hash] == 1 then + -- If there is only one message is the batch, don't send it in a batch + digilines.receptor_send(pos, digilines.rules.default, next(batched_messages[pos_hash])) else digilines.receptor_send(pos, digilines.rules.default, channel, { action = "batch", - signals = batched_signals[pos_hash] + messages = batched_messages[pos_hash] }) end - batched_signals[pos_hash] = nil - last_signal_time_for_chest[pos_hash] = nil + batched_messages[pos_hash] = nil + last_message_time_for_chest[pos_hash] = nil end -- Sends a message onto the Digilines network. @@ -49,16 +49,16 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) side = side and vector.new(side) } - -- Check if we need to include the current signal into batch + -- Check if we need to include the current message into batch local pos_hash = minetest.hash_node_position(pos) - local prev_time = last_signal_time_for_chest[pos_hash] or 0 + local prev_time = last_message_time_for_chest[pos_hash] or 0 local cur_time = minetest.get_us_time() - last_signal_time_for_chest[pos_hash] = cur_time + last_message_time_for_chest[pos_hash] = cur_time if cur_time - prev_time < 1000000 * interval_to_batch then - batched_signals[pos_hash] = batched_signals[pos_hash] or {} - table.insert(batched_signals[pos_hash], msg) + batched_messages[pos_hash] = batched_messages[pos_hash] or {} + table.insert(batched_messages[pos_hash], msg) local node_timer = minetest.get_node_timer(pos) - if #batched_signals[pos_hash] >= max_signals_in_batch then + if #batched_messages[pos_hash] >= max_messages_in_batch then -- Send the batch immediately if it's full node_timer:stop() send_and_clear_batch(pos, channel) @@ -236,7 +236,7 @@ minetest.register_node("digilines:chest", { inv:set_size("main", 8*4) end, on_destruct = function(pos) - batched_signals[minetest.hash_node_position(pos)] = nil + batched_messages[minetest.hash_node_position(pos)] = nil end, after_place_node = tubescan, after_dig_node = tubescan, @@ -375,8 +375,8 @@ minetest.register_node("digilines:chest", { minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos)) end, on_timer = function(pos, _) - -- Send all the batched signals when enough time since the last signal passed - if not batched_signals[minetest.hash_node_position(pos)] then + -- Send all the batched messages when enough time since the last message passed + if not batched_messages[minetest.hash_node_position(pos)] then return end local channel = minetest.get_meta(pos):get_string("channel") From ee3709066751784fdbf4dcc608cc0fe499134043 Mon Sep 17 00:00:00 2001 From: Andrii Date: Wed, 4 Sep 2024 23:55:57 +0300 Subject: [PATCH 07/16] Add information about batch signal into documentation --- docs/chest.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/chest.md b/docs/chest.md index 7632c19..04bbbd4 100644 --- a/docs/chest.md +++ b/docs/chest.md @@ -101,6 +101,16 @@ The message is sent when user takes `` from `` in the chest. ``` The message is sent when user puts `` to the chest to `` +---------------------------- + +``` +{ + action = "batch", + messages = +} +``` +The message contains an array of other messages (``) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amound of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. + ### Fields used within the messages | Field | Description | @@ -108,6 +118,7 @@ The message is sent when user puts `` to the chest to `` | `` | A table which contains data about the stack, and corresponds to the format returned by the :to_table() method of ItemStack (check the Minetest API documentation). | | ``, ``, ``, `` | The index of the corresponding slot starting from 1. | | `` | A vector represented as a table of format `{ x = , y = , z = }` which represent the direction from which the tube is connected to the chest. | +| `` | An array of messages which are emitted by Digiline chest in the same format. | ## Additional information From 4278dc572874e4aa4e008761abd10647ffd3bbea Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 5 Sep 2024 00:03:13 +0300 Subject: [PATCH 08/16] Return back getting "channel" in one line --- inventory.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inventory.lua b/inventory.lua index 029ec38..4eae613 100644 --- a/inventory.lua +++ b/inventory.lua @@ -37,8 +37,7 @@ end -- to_slot: the slot number that is put into (optional). -- side: which side of the chest the action occurred (optional). local function send_message(pos, action, stack, from_slot, to_slot, side) - local meta = minetest.get_meta(pos) - local channel = meta:get_string("channel") + local channel = minetest.get_meta(pos):get_string("channel") local msg = { action = action, From 785050d56148c3f339ae22deae40ec235198a75c Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 5 Sep 2024 00:14:01 +0300 Subject: [PATCH 09/16] Fix on_destruct() and missing 'channel' --- inventory.lua | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/inventory.lua b/inventory.lua index 4eae613..30b95b7 100644 --- a/inventory.lua +++ b/inventory.lua @@ -18,7 +18,8 @@ local function send_and_clear_batch(pos, channel) local pos_hash = minetest.hash_node_position(pos) if #batched_messages[pos_hash] == 1 then -- If there is only one message is the batch, don't send it in a batch - digilines.receptor_send(pos, digilines.rules.default, next(batched_messages[pos_hash])) + digilines.receptor_send(pos, digilines.rules.default, channel, + next(batched_messages[pos_hash])) else digilines.receptor_send(pos, digilines.rules.default, channel, { action = "batch", @@ -29,6 +30,14 @@ local function send_and_clear_batch(pos, channel) last_message_time_for_chest[pos_hash] = nil end +local function flush_batch_for_chest(pos) + if not batched_messages[minetest.hash_node_position(pos)] then + return + end + local channel = minetest.get_meta(pos):get_string("channel") + send_and_clear_batch(pos, channel) +end + -- Sends a message onto the Digilines network. -- pos: the position of the Digilines chest node. -- action: the action string indicating what happened. @@ -235,7 +244,7 @@ minetest.register_node("digilines:chest", { inv:set_size("main", 8*4) end, on_destruct = function(pos) - batched_messages[minetest.hash_node_position(pos)] = nil + flush_batch_for_chest(pos) end, after_place_node = tubescan, after_dig_node = tubescan, @@ -375,11 +384,7 @@ minetest.register_node("digilines:chest", { end, on_timer = function(pos, _) -- Send all the batched messages when enough time since the last message passed - if not batched_messages[minetest.hash_node_position(pos)] then - return - end - local channel = minetest.get_meta(pos):get_string("channel") - send_and_clear_batch(pos, channel) + flush_batch_for_chest(pos) return false end }) From 25d67cee6a4b3fe8d21a8673cb4d6a6bd334cf62 Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 5 Sep 2024 00:17:25 +0300 Subject: [PATCH 10/16] Add a comment --- inventory.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/inventory.lua b/inventory.lua index 30b95b7..6e0c6cc 100644 --- a/inventory.lua +++ b/inventory.lua @@ -30,7 +30,8 @@ local function send_and_clear_batch(pos, channel) last_message_time_for_chest[pos_hash] = nil end -local function flush_batch_for_chest(pos) +-- Send all the batched messages for the chest if present +local function send_batch_for_chest(pos) if not batched_messages[minetest.hash_node_position(pos)] then return end @@ -244,7 +245,7 @@ minetest.register_node("digilines:chest", { inv:set_size("main", 8*4) end, on_destruct = function(pos) - flush_batch_for_chest(pos) + send_batch_for_chest(pos) end, after_place_node = tubescan, after_dig_node = tubescan, @@ -384,7 +385,7 @@ minetest.register_node("digilines:chest", { end, on_timer = function(pos, _) -- Send all the batched messages when enough time since the last message passed - flush_batch_for_chest(pos) + send_batch_for_chest(pos) return false end }) From cdb3fe06dd53e9c3839f77849a3b9fb2d656bd46 Mon Sep 17 00:00:00 2001 From: Andrii Date: Thu, 5 Sep 2024 00:34:32 +0300 Subject: [PATCH 11/16] Fix for batches with single element --- inventory.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory.lua b/inventory.lua index 6e0c6cc..3692207 100644 --- a/inventory.lua +++ b/inventory.lua @@ -19,7 +19,7 @@ local function send_and_clear_batch(pos, channel) if #batched_messages[pos_hash] == 1 then -- If there is only one message is the batch, don't send it in a batch digilines.receptor_send(pos, digilines.rules.default, channel, - next(batched_messages[pos_hash])) + batched_messages[pos_hash][1]) else digilines.receptor_send(pos, digilines.rules.default, channel, { action = "batch", From 22b20b856658168e2fd289263bcf3a14782bc1d6 Mon Sep 17 00:00:00 2001 From: Andrii Date: Mon, 9 Sep 2024 23:52:25 +0300 Subject: [PATCH 12/16] Fix typo "amound" -> "amount" --- docs/chest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/chest.md b/docs/chest.md index 04bbbd4..0bfd3d8 100644 --- a/docs/chest.md +++ b/docs/chest.md @@ -109,7 +109,7 @@ The message is sent when user puts `` to the chest to `` messages = } ``` -The message contains an array of other messages (``) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amound of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. +The message contains an array of other messages (``) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amount of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. ### Fields used within the messages From 59d56a721c3a63f6bf7e5da95dc3170bd7b5f714 Mon Sep 17 00:00:00 2001 From: Andrii Date: Sat, 19 Oct 2024 22:55:17 +0300 Subject: [PATCH 13/16] Allow to batch only messages of specific types, send the batch even if the area was unloaded --- docs/chest.md | 2 +- inventory.lua | 68 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/docs/chest.md b/docs/chest.md index 0bfd3d8..938e930 100644 --- a/docs/chest.md +++ b/docs/chest.md @@ -109,7 +109,7 @@ The message is sent when user puts `` to the chest to `` messages = } ``` -The message contains an array of other messages (``) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amount of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. +The message contains an array of other messages (``) which were emitted within a very short interval, and for this reason merged into a batch and sent as a single message. This is needed to prevent burning of connected Lua controllers in case of a large amount of messages within a very short time. This can happen in case of using the feature which allows to move all items of the same type between inventories at once by taking one of them, and clicking on another one while holding Shift. Another theoretical scenario - sending a lot of requests to the server associated with inventory operations on a Digiline chest using a hacked client or custom software. The decision whether to include a message into a batch is made based on the interval from the previous message. For this reason, a batch message is always preceded by a single message which wasn't included into the batch because there was enough time from the previous message, and we cannot know in advance when the next message will be sent to include the current one into the batch too. Only messages with the following actions can be included into a batch: "uput", "utake", "uswap", "umove", "empty", "full"; all the other messages are related to tube events. This restriction was added because, when we send a batch, the area might become unloaded, so we need to load it back, and the possibility to include messages of all types into a batch might make a possibility to create mechanisms which keep the area loaded by causing a large amount of events to a Digiline chest. Messages with actions "empty" and "full" are included into a batch only if the batch is already not empty. If the chest tries to send a message which cannot be batched while its current batch is not empty, the batch is sent immediately to preserve the original order of messages. ### Fields used within the messages diff --git a/inventory.lua b/inventory.lua index 3692207..b121284 100644 --- a/inventory.lua +++ b/inventory.lua @@ -3,7 +3,7 @@ local S = digilines.S local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil -- Messages which will be sent in a single batch -local batched_messages = {} +local batches = {} -- Maximum interval from the previous message to include the current one into batch (in seconds) local interval_to_batch = 0.1 -- Maximum number of messages in batch @@ -11,28 +11,37 @@ local max_messages_in_batch = 100 -- Time of the last message for each chest local last_message_time_for_chest = {} +-- Messages which can be included into batch +local can_be_batched = { + ["empty"] = true, ["full"] = true, ["umove"] = true, + ["uswap"] = true, ["utake"] = true, ["uput"] = true +} + +-- Messages which shouldn't be included into batch when the batch is empty +local dont_batch_when_empty = { ["empty"] = true, ["full"] = true } + -- Sends the current batch message of a Digiline chest -- pos: the position of the Digilines chest node -- channel: the channel to which the message will be sent local function send_and_clear_batch(pos, channel) local pos_hash = minetest.hash_node_position(pos) - if #batched_messages[pos_hash] == 1 then + if #batches[pos_hash].messages == 1 then -- If there is only one message is the batch, don't send it in a batch digilines.receptor_send(pos, digilines.rules.default, channel, - batched_messages[pos_hash][1]) + batches[pos_hash].messages[1]) else digilines.receptor_send(pos, digilines.rules.default, channel, { action = "batch", - messages = batched_messages[pos_hash] + messages = batches[pos_hash].messages }) end - batched_messages[pos_hash] = nil + batches[pos_hash] = nil last_message_time_for_chest[pos_hash] = nil end -- Send all the batched messages for the chest if present local function send_batch_for_chest(pos) - if not batched_messages[minetest.hash_node_position(pos)] then + if not batches[minetest.hash_node_position(pos)] then return end local channel = minetest.get_meta(pos):get_string("channel") @@ -60,21 +69,31 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) -- Check if we need to include the current message into batch local pos_hash = minetest.hash_node_position(pos) - local prev_time = last_message_time_for_chest[pos_hash] or 0 - local cur_time = minetest.get_us_time() - last_message_time_for_chest[pos_hash] = cur_time - if cur_time - prev_time < 1000000 * interval_to_batch then - batched_messages[pos_hash] = batched_messages[pos_hash] or {} - table.insert(batched_messages[pos_hash], msg) - local node_timer = minetest.get_node_timer(pos) - if #batched_messages[pos_hash] >= max_messages_in_batch then - -- Send the batch immediately if it's full - node_timer:stop() + if can_be_batched[msg.action] and (batches[pos_hash] or not dont_batch_when_empty[msg.action]) then + local prev_time = last_message_time_for_chest[pos_hash] or 0 + local cur_time = minetest.get_us_time() + last_message_time_for_chest[pos_hash] = cur_time + if cur_time - prev_time < 1000000 * interval_to_batch or batches[pos_hash] then + batches[pos_hash] = batches[pos_hash] or { messages = {} } + table.insert(batches[pos_hash].messages, msg) + if batches[pos_hash].timer then + batches[pos_hash].timer:cancel() + end + if #batches[pos_hash].messages >= max_messages_in_batch then + -- Send the batch immediately if it's full + send_and_clear_batch(pos, channel) + else + batches[pos_hash].timer = minetest.after(interval_to_batch, send_batch_for_chest, pos) + end + + return + end + else + -- If the current message cannot be batched, flush the current batch to preserve order + if batches[pos_hash] then + batches[pos_hash].timer:cancel() send_and_clear_batch(pos, channel) - else - node_timer:start(interval_to_batch) end - return end digilines.receptor_send(pos, digilines.rules.default, channel, msg) @@ -245,6 +264,12 @@ minetest.register_node("digilines:chest", { inv:set_size("main", 8*4) end, on_destruct = function(pos) + local pos_hash = minetest.hash_node_position(pos) + if not batches[pos_hash] then + return + end + + batches[pos_hash].timer:cancel() send_batch_for_chest(pos) end, after_place_node = tubescan, @@ -382,11 +407,6 @@ minetest.register_node("digilines:chest", { send_message(pos, "utake", stack, index) check_empty(pos) minetest.log("action", player:get_player_name().." takes stuff from chest at "..minetest.pos_to_string(pos)) - end, - on_timer = function(pos, _) - -- Send all the batched messages when enough time since the last message passed - send_batch_for_chest(pos) - return false end }) From ec3448e2b56f9e155662b2db2a597e4bfd903958 Mon Sep 17 00:00:00 2001 From: Andrii Date: Sat, 19 Oct 2024 23:14:08 +0300 Subject: [PATCH 14/16] Check if the area is loaded when trying to send batched message on timer expiration --- inventory.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/inventory.lua b/inventory.lua index b121284..9dec8d1 100644 --- a/inventory.lua +++ b/inventory.lua @@ -39,11 +39,14 @@ local function send_and_clear_batch(pos, channel) last_message_time_for_chest[pos_hash] = nil end --- Send all the batched messages for the chest if present -local function send_batch_for_chest(pos) +-- Send all the batched messages on timer expiration for the chest if present +local function send_batch_on_timer(pos) if not batches[minetest.hash_node_position(pos)] then return end + if minetest.get_node(pos).name == "ignore" then + minetest.load_area(pos) + end local channel = minetest.get_meta(pos):get_string("channel") send_and_clear_batch(pos, channel) end @@ -83,7 +86,7 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) -- Send the batch immediately if it's full send_and_clear_batch(pos, channel) else - batches[pos_hash].timer = minetest.after(interval_to_batch, send_batch_for_chest, pos) + batches[pos_hash].timer = minetest.after(interval_to_batch, send_batch_on_timer, pos) end return @@ -270,7 +273,8 @@ minetest.register_node("digilines:chest", { end batches[pos_hash].timer:cancel() - send_batch_for_chest(pos) + local channel = minetest.get_meta(pos):get_string("channel") + send_and_clear_batch(pos, channel) end, after_place_node = tubescan, after_dig_node = tubescan, From 2ef5fe831d753dafa50fddb134b7f0c89eb4d703 Mon Sep 17 00:00:00 2001 From: Andrii Date: Sun, 20 Oct 2024 00:05:18 +0300 Subject: [PATCH 15/16] Don't reset timer when adding messages to batch --- inventory.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inventory.lua b/inventory.lua index 9dec8d1..8cbd36f 100644 --- a/inventory.lua +++ b/inventory.lua @@ -79,13 +79,10 @@ local function send_message(pos, action, stack, from_slot, to_slot, side) if cur_time - prev_time < 1000000 * interval_to_batch or batches[pos_hash] then batches[pos_hash] = batches[pos_hash] or { messages = {} } table.insert(batches[pos_hash].messages, msg) - if batches[pos_hash].timer then - batches[pos_hash].timer:cancel() - end if #batches[pos_hash].messages >= max_messages_in_batch then -- Send the batch immediately if it's full send_and_clear_batch(pos, channel) - else + elseif not batches[pos_hash].timer then batches[pos_hash].timer = minetest.after(interval_to_batch, send_batch_on_timer, pos) end From bcb4a972f21a32c54ba28e1aed4ac1f205bb07f4 Mon Sep 17 00:00:00 2001 From: Andrii Nemchenko <62670490+andriyndev@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:42:51 +0300 Subject: [PATCH 16/16] Update inventory.lua Co-authored-by: SmallJoker --- inventory.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/inventory.lua b/inventory.lua index 8cbd36f..2ffeed0 100644 --- a/inventory.lua +++ b/inventory.lua @@ -3,6 +3,16 @@ local S = digilines.S local pipeworks_enabled = minetest.get_modpath("pipeworks") ~= nil -- Messages which will be sent in a single batch +--[[ +Table format: +{ + [node pos hash] = { + messages = { msg1, msg2, ... } + timer = + }, + ... +} +]] local batches = {} -- Maximum interval from the previous message to include the current one into batch (in seconds) local interval_to_batch = 0.1