From 3c10f0e970d063448f9ebad190a32203b5542bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20=C3=85str=C3=B6m?= Date: Fri, 25 Feb 2022 22:58:36 +0100 Subject: [PATCH] Rewrite portal removal to avoid stack overflow This solves a problem were nether portal removal would trigger deep recursion through node callbacks. For large portals this could result in stack overflow crashes on LuaJIT. The issue is solved by rewriting the portal removal to avoid recursion and removing the portal in one operation using minetest.bulk_set_node. --- mods/ITEMS/mcl_portals/portal_nether.lua | 87 ++++++++++++++++-------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/mods/ITEMS/mcl_portals/portal_nether.lua b/mods/ITEMS/mcl_portals/portal_nether.lua index 46025619c..0d93e11d1 100644 --- a/mods/ITEMS/mcl_portals/portal_nether.lua +++ b/mods/ITEMS/mcl_portals/portal_nether.lua @@ -204,37 +204,52 @@ local function get_target(p) end end --- Destroy portal if pos (portal frame or portal node) got destroyed +-- Destroy a nether portal. Connected portal nodes are searched and removed +-- using 'bulk_set_node'. This function is called from 'after_destruct' of +-- nether portal nodes. The flag 'destroying_portal' is used to avoid this +-- function being called recursively through callbacks in 'bulk_set_node'. +local destroying_portal = false local function destroy_nether_portal(pos, node) - if not node then return end - local nn, orientation = node.name, node.param2 - local obsidian = nn == OBSIDIAN - - local function check_remove(pos, orientation) - local node = get_node(pos) - if node and (node.name == PORTAL and (orientation == nil or (node.param2 == orientation))) then - minetest.remove_node(pos) - remove_exit(pos) - end - end - if obsidian then -- check each of 6 sides of it and destroy every portal: - check_remove({x = pos.x - 1, y = pos.y, z = pos.z}, 0) - check_remove({x = pos.x + 1, y = pos.y, z = pos.z}, 0) - check_remove({x = pos.x, y = pos.y, z = pos.z - 1}, 1) - check_remove({x = pos.x, y = pos.y, z = pos.z + 1}, 1) - check_remove({x = pos.x, y = pos.y - 1, z = pos.z}) - check_remove({x = pos.x, y = pos.y + 1, z = pos.z}) + if destroying_portal then return end - if orientation == 0 then - check_remove({x = pos.x - 1, y = pos.y, z = pos.z}, 0) - check_remove({x = pos.x + 1, y = pos.y, z = pos.z}, 0) - else - check_remove({x = pos.x, y = pos.y, z = pos.z - 1}, 1) - check_remove({x = pos.x, y = pos.y, z = pos.z + 1}, 1) + destroying_portal = true + + local orientation = node.param2 + local checked_tab = { [minetest.hash_node_position(pos)] = true } + local nodes = { pos } + + local function check_remove(pos) + local h = minetest.hash_node_position(pos) + if checked_tab[h] then + return + end + + local node = minetest.get_node(pos) + if node and node.name == PORTAL and (orientation == nil or node.param2 == orientation) then + table.insert(nodes, pos) + checked_tab[h] = true + end end - check_remove({x = pos.x, y = pos.y - 1, z = pos.z}) - check_remove({x = pos.x, y = pos.y + 1, z = pos.z}) + + local i = 1 + while i <= #nodes do + pos = nodes[i] + if orientation == 0 then + check_remove({x = pos.x - 1, y = pos.y, z = pos.z}) + check_remove({x = pos.x + 1, y = pos.y, z = pos.z}) + else + check_remove({x = pos.x, y = pos.y, z = pos.z - 1}) + check_remove({x = pos.x, y = pos.y, z = pos.z + 1}) + end + check_remove({x = pos.x, y = pos.y - 1, z = pos.z}) + check_remove({x = pos.x, y = pos.y + 1, z = pos.z}) + remove_exit(pos) + i = i + 1 + end + + minetest.bulk_set_node(nodes, { name = "air" }) + destroying_portal = false end local on_rotate @@ -817,7 +832,23 @@ local usagehelp = S("To open a Nether portal, place an upright frame of obsidian minetest.override_item(OBSIDIAN, { _doc_items_longdesc = longdesc, _doc_items_usagehelp = usagehelp, - after_destruct = destroy_nether_portal, + after_destruct = function(pos, node) + local function check_remove(pos, orientation) + local node = get_node(pos) + if node and node.name == PORTAL then + minetest.remove_node(pos) + end + end + + -- check each of 6 sides of it and destroy every portal + check_remove({x = pos.x - 1, y = pos.y, z = pos.z}) + check_remove({x = pos.x + 1, y = pos.y, z = pos.z}) + check_remove({x = pos.x, y = pos.y, z = pos.z - 1}) + check_remove({x = pos.x, y = pos.y, z = pos.z + 1}) + check_remove({x = pos.x, y = pos.y - 1, z = pos.z}) + check_remove({x = pos.x, y = pos.y + 1, z = pos.z}) + end, + _on_ignite = function(user, pointed_thing) local x, y, z = pointed_thing.under.x, pointed_thing.under.y, pointed_thing.under.z -- Check empty spaces around obsidian and light all frames found: