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.
This commit is contained in:
Elias Åström 2022-02-25 22:58:36 +01:00 committed by Gitea
parent f7d712543f
commit 3c10f0e970

@ -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: