mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-08-26 03:47:25 +02:00
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.
876 lines
29 KiB
Lua
876 lines
29 KiB
Lua
local S = minetest.get_translator(minetest.get_current_modname())
|
|
|
|
local SCAN_2_MAP_CHUNKS = true -- slower but helps to find more suitable places
|
|
|
|
-- Localize functions for better performance
|
|
local abs = math.abs
|
|
local ceil = math.ceil
|
|
local floor = math.floor
|
|
local max = math.max
|
|
local min = math.min
|
|
local random = math.random
|
|
local dist = vector.distance
|
|
local add = vector.add
|
|
local mul = vector.multiply
|
|
local sub = vector.subtract
|
|
|
|
-- Setup
|
|
local W_MIN, W_MAX = 4, 23
|
|
local H_MIN, H_MAX = 5, 23
|
|
local N_MIN, N_MAX = 6, (W_MAX-2) * (H_MAX-2)
|
|
local TRAVEL_X, TRAVEL_Y, TRAVEL_Z = 8, 1, 8
|
|
local LIM_MIN, LIM_MAX = mcl_vars.mapgen_edge_min, mcl_vars.mapgen_edge_max
|
|
local PLAYER_COOLOFF, MOB_COOLOFF = 3, 14 -- for this many seconds they won't teleported again
|
|
local TOUCH_CHATTER_TIME = 1 -- prevent multiple teleportation attempts caused by multiple portal touches, for this number of seconds
|
|
local CHATTER_US = TOUCH_CHATTER_TIME * 1000000
|
|
local DELAY = 3 -- seconds before teleporting in Nether portal in Survival mode (4 minus ABM interval time)
|
|
local DISTANCE_MAX = 128
|
|
local PORTAL = "mcl_portals:portal"
|
|
local OBSIDIAN = "mcl_core:obsidian"
|
|
local O_Y_MIN, O_Y_MAX = max(mcl_vars.mg_overworld_min, -31), min(mcl_vars.mg_overworld_max, 2048)
|
|
local N_Y_MIN, N_Y_MAX = mcl_vars.mg_bedrock_nether_bottom_min, mcl_vars.mg_bedrock_nether_top_min - H_MIN
|
|
|
|
-- Alpha and particles
|
|
local node_particles_allowed = minetest.settings:get("mcl_node_particles") or "none"
|
|
local node_particles_levels = { none=0, low=1, medium=2, high=3 }
|
|
local PARTICLES = node_particles_levels[node_particles_allowed]
|
|
|
|
-- Table of objects (including players) which recently teleported by a
|
|
-- Nether portal. Those objects have a brief cooloff period before they
|
|
-- can teleport again. This prevents annoying back-and-forth teleportation.
|
|
local cooloff = {}
|
|
function mcl_portals.nether_portal_cooloff(object)
|
|
return cooloff[object]
|
|
end
|
|
|
|
local chatter = {}
|
|
|
|
local queue = {}
|
|
local chunks = {}
|
|
|
|
local storage = mcl_portals.storage
|
|
local exits = {}
|
|
local keys = minetest.deserialize(storage:get_string("nether_exits_keys") or "return {}") or {}
|
|
for _, key in pairs(keys) do
|
|
local n = tonumber(key)
|
|
if n then
|
|
exits[key] = minetest.deserialize(storage:get_string("nether_exits_"..key) or "return {}") or {}
|
|
end
|
|
end
|
|
minetest.register_on_shutdown(function()
|
|
local keys={}
|
|
for key, data in pairs(exits) do
|
|
storage:set_string("nether_exits_"..tostring(key), minetest.serialize(data))
|
|
keys[#keys+1] = key
|
|
end
|
|
storage:set_string("nether_exits_keys", minetest.serialize(keys))
|
|
end)
|
|
|
|
local get_node = mcl_vars.get_node
|
|
local set_node = minetest.set_node
|
|
local registered_nodes = minetest.registered_nodes
|
|
local is_protected = minetest.is_protected
|
|
local find_nodes_in_area = minetest.find_nodes_in_area
|
|
local find_nodes_in_area_under_air = minetest.find_nodes_in_area_under_air
|
|
local log = minetest.log
|
|
local pos_to_string = minetest.pos_to_string
|
|
local is_area_protected = minetest.is_area_protected
|
|
local get_us_time = minetest.get_us_time
|
|
|
|
local dimension_to_teleport = { nether = "overworld", overworld = "nether" }
|
|
|
|
local limits = {
|
|
nether = {
|
|
pmin = {x=LIM_MIN, y = N_Y_MIN, z = LIM_MIN},
|
|
pmax = {x=LIM_MAX, y = N_Y_MAX, z = LIM_MAX},
|
|
},
|
|
overworld = {
|
|
pmin = {x=LIM_MIN, y = O_Y_MIN, z = LIM_MIN},
|
|
pmax = {x=LIM_MAX, y = O_Y_MAX, z = LIM_MAX},
|
|
},
|
|
}
|
|
|
|
-- This function registers exits from Nether portals.
|
|
-- Incoming verification performed: two nodes must be portal nodes, and an obsidian below them.
|
|
-- If the verification passes - position adds to the table and saves to mod storage on exit.
|
|
local function add_exit(p)
|
|
if not p or not p.y or not p.z or not p.x then return end
|
|
local x, y, z = floor(p.x), floor(p.y), floor(p.z)
|
|
local p = {x = x, y = y, z = z}
|
|
if get_node({x=x,y=y-1,z=z}).name ~= OBSIDIAN or get_node(p).name ~= PORTAL or get_node({x=x,y=y+1,z=z}).name ~= PORTAL then return end
|
|
local k = floor(z/256) * 256 + floor(x/256)
|
|
if not exits[k] then
|
|
exits[k]={}
|
|
end
|
|
local e = exits[k]
|
|
for i = 1, #e do
|
|
local t = e[i]
|
|
if t and t.x == p.x and t.y == p.y and t.z == p.z then
|
|
return
|
|
end
|
|
end
|
|
e[#e+1] = p
|
|
log("action", "[mcl_portals] Exit added at " .. pos_to_string(p))
|
|
end
|
|
|
|
-- This function removes Nether portals exits.
|
|
local function remove_exit(p)
|
|
if not p or not p.y or not p.z or not p.x then return end
|
|
local x, y, z = floor(p.x), floor(p.y), floor(p.z)
|
|
local k = floor(z/256) * 256 + floor(x/256)
|
|
if not exits[k] then return end
|
|
local p = {x = x, y = y, z = z}
|
|
local e = exits[k]
|
|
if e then
|
|
for i, t in pairs(e) do
|
|
if t and t.x == x and t.y == y and t.z == z then
|
|
e[i] = nil
|
|
log("action", "[mcl_portals] Nether portal removed from " .. pos_to_string(p))
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- This functon searches Nether portal nodes whitin distance specified
|
|
local function find_exit(p, dx, dy, dz)
|
|
if not p or not p.y or not p.z or not p.x then return end
|
|
local dx, dy, dz = dx or DISTANCE_MAX, dy or DISTANCE_MAX, dz or DISTANCE_MAX
|
|
if dx < 1 or dy < 1 or dz < 1 then return false end
|
|
|
|
--y values aren't used
|
|
local x = floor(p.x)
|
|
--local y = floor(p.y)
|
|
local z = floor(p.z)
|
|
|
|
local x1 = x-dx+1
|
|
--local y1 = y-dy+1
|
|
local z1 = z-dz+1
|
|
|
|
local x2 = x+dx-1
|
|
--local y2 = y+dy-1
|
|
local z2 = z+dz-1
|
|
|
|
local k1x, k2x = floor(x1/256), floor(x2/256)
|
|
local k1z, k2z = floor(z1/256), floor(z2/256)
|
|
|
|
local t, d
|
|
for kx = k1x, k2x do for kz = k1z, k2z do
|
|
local k = kz*256 + kx
|
|
local e = exits[k]
|
|
if e then
|
|
for _, t0 in pairs(e) do
|
|
local d0 = dist(p, t0)
|
|
if not d or d>d0 then
|
|
d = d0
|
|
t = t0
|
|
if d==0 then return t end
|
|
end
|
|
end
|
|
end
|
|
end end
|
|
|
|
if t and abs(t.x-p.x) <= dx and abs(t.y-p.y) <= dy and abs(t.z-p.z) <= dz then
|
|
return t
|
|
end
|
|
end
|
|
|
|
|
|
-- Ping-Pong the coordinate for Fast Travelling, https://git.minetest.land/Wuzzy/MineClone2/issues/795#issuecomment-11058
|
|
local function ping_pong(x, m, l1, l2)
|
|
if x < 0 then
|
|
return l1 + abs(((x*m+l1) % (l1*4)) - (l1*2)), floor(x*m/l1/2) + ((ceil(x*m/l1)+1)%2) * ((x*m)%l1)/l1
|
|
end
|
|
return l2 - abs(((x*m+l2) % (l2*4)) - (l2*2)), floor(x*m/l2/2) + (floor(x*m/l2)%2) * ((x*m)%l2)/l2
|
|
end
|
|
|
|
local function get_target(p)
|
|
if p and p.y and p.x and p.z then
|
|
local x, z = p.x, p.z
|
|
local y, d = mcl_worlds.y_to_layer(p.y)
|
|
local o1, o2 -- y offset
|
|
if y then
|
|
if d=="nether" then
|
|
x, o1 = ping_pong(x, TRAVEL_X, LIM_MIN, LIM_MAX)
|
|
z, o2 = ping_pong(z, TRAVEL_Z, LIM_MIN, LIM_MAX)
|
|
y = floor(y * TRAVEL_Y + (o1+o2) / 16 * LIM_MAX)
|
|
y = min(max(y + O_Y_MIN, O_Y_MIN), O_Y_MAX)
|
|
elseif d=="overworld" then
|
|
x, y, z = floor(x / TRAVEL_X + 0.5), floor(y / TRAVEL_Y + 0.5), floor(z / TRAVEL_Z + 0.5)
|
|
y = min(max(y + N_Y_MIN, N_Y_MIN), N_Y_MAX)
|
|
end
|
|
return {x=x, y=y, z=z}, d
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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 destroying_portal then
|
|
return
|
|
end
|
|
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
|
|
|
|
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
|
|
if minetest.get_modpath("screwdriver") then
|
|
on_rotate = screwdriver.disallow
|
|
end
|
|
|
|
minetest.register_node(PORTAL, {
|
|
description = S("Nether Portal"),
|
|
_doc_items_longdesc = S("A Nether portal teleports creatures and objects to the hot and dangerous Nether dimension (and back!). Enter at your own risk!"),
|
|
_doc_items_usagehelp = S("Stand in the portal for a moment to activate the teleportation. Entering a Nether portal for the first time will also create a new portal in the other dimension. If a Nether portal has been built in the Nether, it will lead to the Overworld. A Nether portal is destroyed if the any of the obsidian which surrounds it is destroyed, or if it was caught in an explosion."),
|
|
|
|
tiles = {
|
|
"blank.png",
|
|
"blank.png",
|
|
"blank.png",
|
|
"blank.png",
|
|
{
|
|
name = "mcl_portals_portal.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1.25,
|
|
},
|
|
},
|
|
{
|
|
name = "mcl_portals_portal.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1.25,
|
|
},
|
|
},
|
|
},
|
|
drawtype = "nodebox",
|
|
paramtype = "light",
|
|
paramtype2 = "facedir",
|
|
sunlight_propagates = true,
|
|
use_texture_alpha = minetest.features.use_texture_alpha_string_modes and "blend" or true,
|
|
walkable = false,
|
|
buildable_to = false,
|
|
is_ground_content = false,
|
|
drop = "",
|
|
light_source = 11,
|
|
post_effect_color = {a = 180, r = 51, g = 7, b = 89},
|
|
node_box = {
|
|
type = "fixed",
|
|
fixed = {
|
|
{-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
|
|
},
|
|
},
|
|
groups = { creative_breakable = 1, portal = 1, not_in_creative_inventory = 1 },
|
|
sounds = mcl_sounds.node_sound_glass_defaults(),
|
|
after_destruct = destroy_nether_portal,
|
|
on_rotate = on_rotate,
|
|
|
|
_mcl_hardness = -1,
|
|
_mcl_blast_resistance = 0,
|
|
})
|
|
|
|
local function light_frame(x1, y1, z1, x2, y2, z2, name, node, node_frame)
|
|
local orientation = 0
|
|
if x1 == x2 then
|
|
orientation = 1
|
|
end
|
|
local pos = {}
|
|
local node = node or {name = PORTAL, param2 = orientation}
|
|
local node_frame = node_frame or {name = OBSIDIAN}
|
|
for x = x1 - 1 + orientation, x2 + 1 - orientation do
|
|
pos.x = x
|
|
for z = z1 - orientation, z2 + orientation do
|
|
pos.z = z
|
|
for y = y1 - 1, y2 + 1 do
|
|
pos.y = y
|
|
local frame = (x < x1) or (x > x2) or (y < y1) or (y > y2) or (z < z1) or (z > z2)
|
|
if frame then
|
|
set_node(pos, node_frame)
|
|
else
|
|
set_node(pos, node)
|
|
add_exit({x=pos.x, y=pos.y-1, z=pos.z})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--Build arrival portal
|
|
function build_nether_portal(pos, width, height, orientation, name, clear_before_build)
|
|
local width, height, orientation = width or W_MIN - 2, height or H_MIN - 2, orientation or random(0, 1)
|
|
|
|
if clear_before_build then
|
|
light_frame(pos.x, pos.y, pos.z, pos.x + (1 - orientation) * (width - 1), pos.y + height - 1, pos.z + orientation * (width - 1), name, {name="air"}, {name="air"})
|
|
end
|
|
light_frame(pos.x, pos.y, pos.z, pos.x + (1 - orientation) * (width - 1), pos.y + height - 1, pos.z + orientation * (width - 1), name)
|
|
|
|
-- Build obsidian platform:
|
|
for x = pos.x - orientation, pos.x + orientation + (width - 1) * (1 - orientation), 1 + orientation do
|
|
for z = pos.z - 1 + orientation, pos.z + 1 - orientation + (width - 1) * orientation, 2 - orientation do
|
|
local pp = {x = x, y = pos.y - 1, z = z}
|
|
local pp_1 = {x = x, y = pos.y - 2, z = z}
|
|
local nn = get_node(pp).name
|
|
local nn_1 = get_node(pp_1).name
|
|
if ((nn=="air" and nn_1 == "air") or not registered_nodes[nn].is_ground_content) and not is_protected(pp, name) then
|
|
set_node(pp, {name = OBSIDIAN})
|
|
end
|
|
end
|
|
end
|
|
|
|
log("action", "[mcl_portals] Destination Nether portal generated at "..pos_to_string(pos).."!")
|
|
|
|
return pos
|
|
end
|
|
|
|
function mcl_portals.spawn_nether_portal(pos, rot, pr, name)
|
|
if not pos then return end
|
|
local o = 0
|
|
if rot then
|
|
if rot == "270" or rot=="90" then
|
|
o = 1
|
|
elseif rot == "random" then
|
|
o = random(0,1)
|
|
end
|
|
end
|
|
build_nether_portal(pos, nil, nil, o, name, true)
|
|
end
|
|
|
|
-- Teleportation cooloff for some seconds, to prevent back-and-forth teleportation
|
|
local function stop_teleport_cooloff(o)
|
|
cooloff[o] = nil
|
|
chatter[o] = nil
|
|
end
|
|
|
|
local function teleport_cooloff(obj)
|
|
cooloff[obj] = true
|
|
if obj:is_player() then
|
|
minetest.after(PLAYER_COOLOFF, stop_teleport_cooloff, obj)
|
|
else
|
|
minetest.after(MOB_COOLOFF, stop_teleport_cooloff, obj)
|
|
end
|
|
end
|
|
|
|
local function finalize_teleport(obj, exit)
|
|
if not obj or not exit or not exit.x or not exit.y or not exit.z then return end
|
|
|
|
local objpos = obj:get_pos()
|
|
if not objpos then return end
|
|
|
|
local is_player = obj:is_player()
|
|
local name
|
|
if is_player then
|
|
name = obj:get_player_name()
|
|
end
|
|
local _, dim = mcl_worlds.y_to_layer(exit.y)
|
|
|
|
|
|
-- If player stands, player is at ca. something+0.5 which might cause precision problems, so we used ceil for objpos.y
|
|
objpos = {x = floor(objpos.x+0.5), y = ceil(objpos.y), z = floor(objpos.z+0.5)}
|
|
if get_node(objpos).name ~= PORTAL then return end
|
|
|
|
-- THIS IS A TEMPORATY CODE SECTION FOR COMPATIBILITY REASONS -- 1 of 2 -- TODO: Remove --
|
|
-- Old worlds have no exits indexed - adding the exit to return here:
|
|
add_exit(objpos)
|
|
-- TEMPORATY CODE SECTION ENDS HERE --
|
|
|
|
|
|
-- Enable teleportation cooloff for some seconds, to prevent back-and-forth teleportation
|
|
teleport_cooloff(obj)
|
|
|
|
-- Teleport
|
|
obj:set_pos(exit)
|
|
|
|
if is_player then
|
|
mcl_worlds.dimension_change(obj, dim)
|
|
minetest.sound_play("mcl_portals_teleport", {pos=exit, gain=0.5, max_hear_distance = 16}, true)
|
|
log("action", "[mcl_portals] player "..name.." teleported to Nether portal at "..pos_to_string(exit)..".")
|
|
else
|
|
log("action", "[mcl_portals] entity teleported to Nether portal at "..pos_to_string(exit)..".")
|
|
end
|
|
end
|
|
|
|
local function create_portal_2(pos1, name, obj)
|
|
local orientation = 0
|
|
local pos2 = {x = pos1.x + 3, y = pos1.y + 3, z = pos1.z + 3}
|
|
local nodes = find_nodes_in_area(pos1, pos2, {"air"})
|
|
if #nodes == 64 then
|
|
orientation = random(0,1)
|
|
else
|
|
pos2.x = pos2.x - 1
|
|
nodes = find_nodes_in_area(pos1, pos2, {"air"})
|
|
if #nodes == 48 then
|
|
orientation = 1
|
|
end
|
|
end
|
|
local exit = build_nether_portal(pos1, W_MIN-2, H_MIN-2, orientation, name)
|
|
finalize_teleport(obj, exit)
|
|
local cn = mcl_vars.get_chunk_number(pos1)
|
|
chunks[cn] = nil
|
|
if queue[cn] then
|
|
for next_obj, _ in pairs(queue[cn]) do
|
|
if next_obj ~= obj then
|
|
finalize_teleport(next_obj, exit)
|
|
end
|
|
end
|
|
queue[cn] = nil
|
|
end
|
|
end
|
|
|
|
local function get_lava_level(pos, pos1, pos2)
|
|
if pos.y > -1000 then
|
|
return max(min(mcl_vars.mg_lava_overworld_max, pos2.y-1), pos1.y+1)
|
|
end
|
|
return max(min(mcl_vars.mg_lava_nether_max, pos2.y-1), pos1.y+1)
|
|
end
|
|
|
|
local function ecb_scan_area_2(blockpos, action, calls_remaining, param)
|
|
if calls_remaining and calls_remaining > 0 then return end
|
|
local pos, pos1, pos2, name, obj = param.pos, param.pos1, param.pos2, param.name or "", param.obj
|
|
local pos0, distance
|
|
local lava = get_lava_level(pos, pos1, pos2)
|
|
|
|
-- THIS IS A TEMPORATY CODE SECTION FOR COMPATIBILITY REASONS -- 2 of 2 -- TODO: Remove --
|
|
-- Find portals for old worlds (new worlds keep them all in the table):
|
|
local portals = find_nodes_in_area(pos1, pos2, {PORTAL})
|
|
if portals and #portals>0 then
|
|
for _, p in pairs(portals) do
|
|
add_exit(p)
|
|
end
|
|
local exit = find_exit(pos)
|
|
if exit then
|
|
finalize_teleport(obj, exit)
|
|
end
|
|
return
|
|
end
|
|
-- TEMPORATY CODE SECTION ENDS HERE --
|
|
|
|
|
|
local nodes = find_nodes_in_area_under_air(pos1, pos2, {"group:building_block"})
|
|
if nodes then
|
|
local nc = #nodes
|
|
log("action", "[mcl_portals] Area for destination Nether portal emerged! Found " .. tostring(nc) .. " nodes under the air around "..pos_to_string(pos))
|
|
if nc > 0 then
|
|
for i=1,nc do
|
|
local node = nodes[i]
|
|
local node1 = {x=node.x, y=node.y+1, z=node.z }
|
|
local node2 = {x=node.x+2, y=node.y+3, z=node.z+2}
|
|
local nodes2 = find_nodes_in_area(node1, node2, {"air"})
|
|
if nodes2 then
|
|
local nc2 = #nodes2
|
|
if nc2 == 27 and not is_area_protected(node, node2, name) then
|
|
local distance0 = dist(pos, node)
|
|
if distance0 < 2 then
|
|
log("action", "[mcl_portals] found space at pos "..pos_to_string(node).." - creating a portal")
|
|
create_portal_2(node1, name, obj)
|
|
return
|
|
end
|
|
if not distance or (distance0 < distance) or (distance0 < distance-1 and node.y > lava and pos0.y < lava) then
|
|
log("verbose", "[mcl_portals] found distance "..tostring(distance0).." at pos "..pos_to_string(node))
|
|
distance = distance0
|
|
pos0 = {x=node1.x, y=node1.y, z=node1.z}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if distance then -- several nodes of air might be better than lava lake, right?
|
|
log("action", "[mcl_portals] using backup pos "..pos_to_string(pos0).." to create a portal")
|
|
create_portal_2(pos0, name, obj)
|
|
return
|
|
end
|
|
|
|
if param.next_chunk_1 and param.next_chunk_2 and param.next_pos then
|
|
local pos1, pos2, p = param.next_chunk_1, param.next_chunk_2, param.next_pos
|
|
if p.x >= pos1.x and p.x <= pos2.x and p.y >= pos1.y and p.y <= pos2.y and p.z >= pos1.z and p.z <= pos2.z then
|
|
log("action", "[mcl_portals] Making additional search in chunk below, because current one doesn't contain any air space for portal, target pos "..pos_to_string(p))
|
|
minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = p, pos1 = pos1, pos2 = pos2, name=name, obj=obj})
|
|
return
|
|
end
|
|
end
|
|
|
|
log("action", "[mcl_portals] found no space, reverting to target pos "..pos_to_string(pos).." - creating a portal")
|
|
if pos.y < lava then
|
|
pos.y = lava + 1
|
|
else
|
|
pos.y = pos.y + 1
|
|
end
|
|
create_portal_2(pos, name, obj)
|
|
end
|
|
|
|
local function create_portal(pos, limit1, limit2, name, obj)
|
|
local cn = mcl_vars.get_chunk_number(pos)
|
|
if chunks[cn] then
|
|
local q = queue[cn] or {}
|
|
q[obj] = true
|
|
queue[cn] = q
|
|
return
|
|
end
|
|
chunks[cn] = true
|
|
|
|
-- we need to emerge the area here, but currently (mt5.4/mcl20.71) map generation is slow
|
|
-- so we'll emerge single chunk only: 5x5x5 blocks, 80x80x80 nodes maximum
|
|
-- and maybe one more chunk from below if (SCAN_2_MAP_CHUNKS = true)
|
|
|
|
local pos1 = add(mul(mcl_vars.pos_to_chunk(pos), mcl_vars.chunk_size_in_nodes), mcl_vars.central_chunk_offset_in_nodes)
|
|
local pos2 = add(pos1, mcl_vars.chunk_size_in_nodes - 1)
|
|
|
|
if not SCAN_2_MAP_CHUNKS then
|
|
if limit1 and limit1.x and limit1.y and limit1.z then
|
|
pos1 = {x = max(min(limit1.x, pos.x), pos1.x), y = max(min(limit1.y, pos.y), pos1.y), z = max(min(limit1.z, pos.z), pos1.z)}
|
|
end
|
|
if limit2 and limit2.x and limit2.y and limit2.z then
|
|
pos2 = {x = min(max(limit2.x, pos.x), pos2.x), y = min(max(limit2.y, pos.y), pos2.y), z = min(max(limit2.z, pos.z), pos2.z)}
|
|
end
|
|
minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = vector.new(pos), pos1 = pos1, pos2 = pos2, name=name, obj=obj})
|
|
return
|
|
end
|
|
|
|
-- Basically the copy of code above, with minor additions to continue the search in single additional chunk below:
|
|
local next_chunk_1 = {x = pos1.x, y = pos1.y - mcl_vars.chunk_size_in_nodes, z = pos1.z}
|
|
local next_chunk_2 = add(next_chunk_1, mcl_vars.chunk_size_in_nodes - 1)
|
|
local next_pos = {x = pos.x, y=max(next_chunk_2.y, limit1.y), z = pos.z}
|
|
if limit1 and limit1.x and limit1.y and limit1.z then
|
|
pos1 = {x = max(min(limit1.x, pos.x), pos1.x), y = max(min(limit1.y, pos.y), pos1.y), z = max(min(limit1.z, pos.z), pos1.z)}
|
|
next_chunk_1 = {x = max(min(limit1.x, next_pos.x), next_chunk_1.x), y = max(min(limit1.y, next_pos.y), next_chunk_1.y), z = max(min(limit1.z, next_pos.z), next_chunk_1.z)}
|
|
end
|
|
if limit2 and limit2.x and limit2.y and limit2.z then
|
|
pos2 = {x = min(max(limit2.x, pos.x), pos2.x), y = min(max(limit2.y, pos.y), pos2.y), z = min(max(limit2.z, pos.z), pos2.z)}
|
|
next_chunk_2 = {x = min(max(limit2.x, next_pos.x), next_chunk_2.x), y = min(max(limit2.y, next_pos.y), next_chunk_2.y), z = min(max(limit2.z, next_pos.z), next_chunk_2.z)}
|
|
end
|
|
minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = vector.new(pos), pos1 = pos1, pos2 = pos2, name=name, obj=obj, next_chunk_1 = next_chunk_1, next_chunk_2 = next_chunk_2, next_pos = next_pos})
|
|
end
|
|
|
|
local function available_for_nether_portal(p)
|
|
local nn = get_node(p).name
|
|
local obsidian = nn == OBSIDIAN
|
|
if nn ~= "air" and minetest.get_item_group(nn, "fire") ~= 1 then
|
|
return false, obsidian
|
|
end
|
|
return true, obsidian
|
|
end
|
|
|
|
local function check_and_light_shape(pos, orientation)
|
|
local stack = {{x = pos.x, y = pos.y, z = pos.z}}
|
|
local node_list = {}
|
|
local index_list = {}
|
|
local node_counter = 0
|
|
-- Search most low node from the left (pos1) and most right node from the top (pos2)
|
|
local pos1 = {x = pos.x, y = pos.y, z = pos.z}
|
|
local pos2 = {x = pos.x, y = pos.y, z = pos.z}
|
|
|
|
local kx, ky, kz = pos.x - 1999, pos.y - 1999, pos.z - 1999
|
|
while #stack > 0 do
|
|
local i = #stack
|
|
local x, y, z = stack[i].x, stack[i].y, stack[i].z
|
|
local k = (x-kx)*16000000 + (y-ky)*4000 + z-kz
|
|
if index_list[k] then
|
|
stack[i] = nil -- Already checked, skip it
|
|
else
|
|
local good, obsidian = available_for_nether_portal(stack[i])
|
|
if obsidian then
|
|
stack[i] = nil
|
|
else
|
|
if (not good) or (node_counter >= N_MAX) then
|
|
return false
|
|
end
|
|
node_counter = node_counter + 1
|
|
node_list[node_counter] = {x = x, y = y, z = z}
|
|
index_list[k] = true
|
|
stack[i].y = y - 1
|
|
stack[i + 1] = {x = x, y = y + 1, z = z}
|
|
if orientation == 0 then
|
|
stack[i + 2] = {x = x - 1, y = y, z = z}
|
|
stack[i + 3] = {x = x + 1, y = y, z = z}
|
|
else
|
|
stack[i + 2] = {x = x, y = y, z = z - 1}
|
|
stack[i + 3] = {x = x, y = y, z = z + 1}
|
|
end
|
|
if (y < pos1.y) or (y == pos1.y and (x < pos1.x or z < pos1.z)) then
|
|
pos1 = {x = x, y = y, z = z}
|
|
end
|
|
if (x > pos2.x or z > pos2.z) or (x == pos2.x and z == pos2.z and y > pos2.y) then
|
|
pos2 = {x = x, y = y, z = z}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if node_counter < N_MIN then
|
|
return false
|
|
end
|
|
|
|
-- Limit rectangles width and height
|
|
if abs(pos2.x - pos1.x + pos2.z - pos1.z) + 3 > W_MAX or abs(pos2.y - pos1.y) + 3 > H_MAX then
|
|
return false
|
|
end
|
|
|
|
for i = 1, node_counter do
|
|
local node_pos = node_list[i]
|
|
minetest.set_node(node_pos, {name = PORTAL, param2 = orientation})
|
|
add_exit(node_pos)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Attempts to light a Nether portal at pos
|
|
-- Pos can be any of the inner part.
|
|
-- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
|
|
-- If no Nether portal can be lit, nothing happens.
|
|
-- Returns true if portal created
|
|
function mcl_portals.light_nether_portal(pos)
|
|
-- Only allow to make portals in Overworld and Nether
|
|
local dim = mcl_worlds.pos_to_dimension(pos)
|
|
if dim ~= "overworld" and dim ~= "nether" then
|
|
return false
|
|
end
|
|
local orientation = random(0, 1)
|
|
for orientation_iteration = 1, 2 do
|
|
if check_and_light_shape(pos, orientation) then
|
|
return true
|
|
end
|
|
orientation = 1 - orientation
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Teleport function
|
|
local function teleport_no_delay(obj, pos)
|
|
local is_player = obj:is_player()
|
|
if (not is_player and not obj:get_luaentity()) or cooloff[obj] then return end
|
|
|
|
local objpos = obj:get_pos()
|
|
if not objpos then return end
|
|
|
|
-- If player stands, player is at ca. something+0.5 which might cause precision problems, so we used ceil for objpos.y
|
|
objpos = {x = floor(objpos.x+0.5), y = ceil(objpos.y), z = floor(objpos.z+0.5)}
|
|
if get_node(objpos).name ~= PORTAL then return end
|
|
|
|
local target, dim = get_target(objpos)
|
|
if not target then return end
|
|
|
|
local name
|
|
if is_player then
|
|
name = obj:get_player_name()
|
|
end
|
|
|
|
local exit = find_exit(target)
|
|
if exit then
|
|
finalize_teleport(obj, exit)
|
|
else
|
|
dim = dimension_to_teleport[dim]
|
|
-- need to create arrival portal
|
|
create_portal(target, limits[dim].pmin, limits[dim].pmax, name, obj)
|
|
end
|
|
end
|
|
|
|
local function prevent_portal_chatter(obj)
|
|
local time_us = get_us_time()
|
|
local ch = chatter[obj] or 0
|
|
chatter[obj] = time_us
|
|
minetest.after(TOUCH_CHATTER_TIME, function(o)
|
|
if o and chatter[o] and get_us_time() - chatter[o] >= CHATTER_US then
|
|
chatter[o] = nil
|
|
end
|
|
end, obj)
|
|
return time_us - ch > CHATTER_US
|
|
end
|
|
|
|
local function animation(player, playername)
|
|
local ch = chatter[player] or 0
|
|
if cooloff[player] or get_us_time() - ch < CHATTER_US then
|
|
local pos = player:get_pos()
|
|
if not pos then
|
|
return
|
|
end
|
|
minetest.add_particlespawner({
|
|
amount = 1,
|
|
minpos = {x = pos.x - 0.1, y = pos.y + 1.4, z = pos.z - 0.1},
|
|
maxpos = {x = pos.x + 0.1, y = pos.y + 1.6, z = pos.z + 0.1},
|
|
minvel = 0,
|
|
maxvel = 0,
|
|
minacc = 0,
|
|
maxacc = 0,
|
|
minexptime = 0.1,
|
|
maxexptime = 0.2,
|
|
minsize = 5,
|
|
maxsize = 15,
|
|
collisiondetection = false,
|
|
texture = "mcl_particles_nether_portal_t.png",
|
|
playername = playername,
|
|
})
|
|
minetest.after(0.3, animation, player, playername)
|
|
end
|
|
end
|
|
|
|
local function teleport(obj, portal_pos)
|
|
local name = ""
|
|
if obj:is_player() then
|
|
name = obj:get_player_name()
|
|
animation(obj, name)
|
|
end
|
|
|
|
if cooloff[obj] then return end
|
|
|
|
if minetest.is_creative_enabled(name) then
|
|
teleport_no_delay(obj, portal_pos)
|
|
return
|
|
end
|
|
|
|
minetest.after(DELAY, teleport_no_delay, obj, portal_pos)
|
|
end
|
|
|
|
minetest.register_abm({
|
|
label = "Nether portal teleportation and particles",
|
|
nodenames = {PORTAL},
|
|
interval = 1,
|
|
chance = 1,
|
|
action = function(pos, node)
|
|
local o = node.param2 -- orientation
|
|
local d = random(0, 1) -- direction
|
|
local time = random() * 1.9 + 0.5
|
|
local velocity, acceleration
|
|
if o == 1 then
|
|
velocity = {x = random() * 0.7 + 0.3, y = random() - 0.5, z = random() - 0.5}
|
|
acceleration = {x = random() * 1.1 + 0.3, y = random() - 0.5, z = random() - 0.5}
|
|
else
|
|
velocity = {x = random() - 0.5, y = random() - 0.5, z = random() * 0.7 + 0.3}
|
|
acceleration = {x = random() - 0.5, y = random() - 0.5, z = random() * 1.1 + 0.3}
|
|
end
|
|
local distance = add(mul(velocity, time), mul(acceleration, time * time / 2))
|
|
if d == 1 then
|
|
if o == 1 then
|
|
distance.x = -distance.x
|
|
velocity.x = -velocity.x
|
|
acceleration.x = -acceleration.x
|
|
else
|
|
distance.z = -distance.z
|
|
velocity.z = -velocity.z
|
|
acceleration.z = -acceleration.z
|
|
end
|
|
end
|
|
distance = sub(pos, distance)
|
|
for _, obj in pairs(minetest.get_objects_inside_radius(pos, 15)) do
|
|
if obj:is_player() then
|
|
minetest.add_particlespawner({
|
|
amount = PARTICLES + 1,
|
|
minpos = distance,
|
|
maxpos = distance,
|
|
minvel = velocity,
|
|
maxvel = velocity,
|
|
minacc = acceleration,
|
|
maxacc = acceleration,
|
|
minexptime = time,
|
|
maxexptime = time,
|
|
minsize = 0.3,
|
|
maxsize = 1.8,
|
|
collisiondetection = false,
|
|
texture = "mcl_particles_nether_portal.png",
|
|
playername = obj:get_player_name(),
|
|
})
|
|
end
|
|
end
|
|
for _, obj in pairs(minetest.get_objects_inside_radius(pos, 1)) do --maikerumine added for objects to travel
|
|
local lua_entity = obj:get_luaentity() --maikerumine added for objects to travel
|
|
if (obj:is_player() or lua_entity) and prevent_portal_chatter(obj) then
|
|
teleport(obj, pos)
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
|
|
--[[ ITEM OVERRIDES ]]
|
|
|
|
local longdesc = registered_nodes[OBSIDIAN]._doc_items_longdesc
|
|
longdesc = longdesc .. "\n" .. S("Obsidian is also used as the frame of Nether portals.")
|
|
local usagehelp = S("To open a Nether portal, place an upright frame of obsidian with a width of at least 4 blocks and a height of 5 blocks, leaving only air in the center. After placing this frame, light a fire in the obsidian frame. Nether portals only work in the Overworld and the Nether.")
|
|
|
|
minetest.override_item(OBSIDIAN, {
|
|
_doc_items_longdesc = longdesc,
|
|
_doc_items_usagehelp = usagehelp,
|
|
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:
|
|
local portals_placed =
|
|
mcl_portals.light_nether_portal({x = x - 1, y = y, z = z}) or mcl_portals.light_nether_portal({x = x + 1, y = y, z = z}) or
|
|
mcl_portals.light_nether_portal({x = x, y = y - 1, z = z}) or mcl_portals.light_nether_portal({x = x, y = y + 1, z = z}) or
|
|
mcl_portals.light_nether_portal({x = x, y = y, z = z - 1}) or mcl_portals.light_nether_portal({x = x, y = y, z = z + 1})
|
|
if portals_placed then
|
|
log("action", "[mcl_portals] Nether portal activated at "..pos_to_string({x=x,y=y,z=z})..".")
|
|
if minetest.get_modpath("doc") then
|
|
doc.mark_entry_as_revealed(user:get_player_name(), "nodes", PORTAL)
|
|
|
|
-- Achievement for finishing a Nether portal TO the Nether
|
|
local dim = mcl_worlds.pos_to_dimension({x=x, y=y, z=z})
|
|
if minetest.get_modpath("awards") and dim ~= "nether" and user:is_player() then
|
|
awards.unlock(user:get_player_name(), "mcl:buildNetherPortal")
|
|
end
|
|
end
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end,
|
|
})
|