nether/init.lua
Treer 614a3f91a7 Separate portal shape code from portal behavior code, and refactor
Allows dev and testing/debugging to focus on one (shape or behavior) without getting mixed up in the other, will also enable different portal shapes later on. Other adjustments:
* Reuse/reignite an existing portal or portal frame if there is one at the remote destination, rather than the area being overwritten by a slightly offset portal schematic.
* Create remote portals in a matching orientation to the local portal.
* Preserve player position & facing relative to portal when traveling through wormhole (even if portals are at 90°)
* Players no longer bounce several times after teleporting
* Player is not teleported until after the portal at the other end is confirmed/built
2020-02-15 10:55:37 +01:00

1015 lines
37 KiB
Lua

--[[
Nether mod for minetest
Copyright (C) 2013 PilzAdam
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE.
]]--
-- Parameters
local NETHER_DEPTH = -5000
local TCAVE = 0.6
local BLEND = 128
local DEBUG = true
-- 3D noise
local np_cave = {
offset = 0,
scale = 1,
spread = {x = 384, y = 128, z = 384}, -- squashed 3:1
seed = 59033,
octaves = 5,
persist = 0.7,
lacunarity = 2.0,
--flags = ""
}
-- Stuff
local yblmax = NETHER_DEPTH - BLEND * 2
netherportal = {} -- portal API
-- Functions
--[[
AnchorPos and WormholdPos are node locations in a portal defined as follows:
.
+--------+--------+--------+--------+
| | Frame | |
| | | | |
+--------+--------+--------+--------+
| | | |
| | | |
+--------+ + +--------+
| | Wormhole | |
| | | |
+--------+ + +--------+
| |Wormhole | |
| | Pos | |
+--------+--------+--------+--------+
| Anchor | | | |
| Pos | | | |
+--------+--------+--------+--------+
+X/East or +Z/North ----->
A better location for AnchorPos would be directly under WormholePos, as it's more centered
and you don't need to know the portal's orientation to find AnchorPos from the WormholePos
or vice-versa, however AnchorPos is in the bottom/south/west-corner to keep compatibility
with earlier versions of this mod (which only records portal corners p1 & p2 in the node metadata).
Orientation is 0 or 90, 0 meaning a portal that faces north/south - i.e. obsidian running
east/west.
]]
-- This object defines a portal's shape, segregating the shape logic code from portal physics.
-- You can create a new "PortalShape" definition object which implements the same
-- functions if you wish to register a custom shaped portal in register_portal().
-- Since it's symmetric, this PortalShape definition has only implemented orientations of 0 and 90
local TraditionalPortalShape = {
-- todo: use size to update is_portal_frame
size = vector.new(4, 5, 1), -- size of the portal, and not necessarily the size of the schematic, which may clear area around the portal.
schematic_filename = minetest.get_modpath("nether") .. "/schematics/nether_portal.mts",
-- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
get_schematicPos_from_anchorPos = function(anchorPos, orientation)
assert(orientation, "no orientation passed")
if orientation == 0 then
return {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 2}
else
return {x = anchorPos.x - 2, y = anchorPos.y, z = anchorPos.z }
end
end,
get_wormholePos_from_anchorPos = function(anchorPos, orientation)
assert(orientation, "no orientation passed")
if orientation == 0 then
return {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z }
else
return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
end
end,
get_anchorPos_from_wormholePos = function(wormholePos, orientation)
assert(orientation, "no orientation passed")
if orientation == 0 then
return {x = wormholePos.x - 1, y = wormholePos.y - 1, z = wormholePos.z }
else
return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z - 1}
end
end,
apply_func_to_frame_nodes = function(anchorPos, orientation, func)
local shortCircuited
if orientation == 0 then
-- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
shortCircuited =
func({x = anchorPos.x + 0, y = anchorPos.y, z = anchorPos.z}) or
func({x = anchorPos.x + 1, y = anchorPos.y, z = anchorPos.z}) or
func({x = anchorPos.x + 2, y = anchorPos.y, z = anchorPos.z}) or
func({x = anchorPos.x + 3, y = anchorPos.y, z = anchorPos.z}) or
func({x = anchorPos.x + 0, y = anchorPos.y + 4, z = anchorPos.z}) or
func({x = anchorPos.x + 1, y = anchorPos.y + 4, z = anchorPos.z}) or
func({x = anchorPos.x + 2, y = anchorPos.y + 4, z = anchorPos.z}) or
func({x = anchorPos.x + 3, y = anchorPos.y + 4, z = anchorPos.z}) or
func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z}) or
func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z}) or
func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z}) or
func({x = anchorPos.x + 3, y = anchorPos.y + 1, z = anchorPos.z}) or
func({x = anchorPos.x + 3, y = anchorPos.y + 2, z = anchorPos.z}) or
func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z})
else
shortCircuited =
func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 0}) or
func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 1}) or
func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 2}) or
func({x = anchorPos.x, y = anchorPos.y, z = anchorPos.z + 3}) or
func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 0}) or
func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 1}) or
func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 2}) or
func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or
func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z }) or
func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z }) or
func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z }) or
func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 3}) or
func({x = anchorPos.x, y = anchorPos.y + 2, z = anchorPos.z + 3}) or
func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z + 3})
end
return not shortCircuited
end,
apply_func_to_wormhole_nodes = function(anchorPos, orientation, func)
local shortCircuited
if orientation == 0 then
local wormholePos = {x = anchorPos.x + 1, y = anchorPos.y + 1, z = anchorPos.z}
-- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
shortCircuited =
func({x = wormholePos.x + 0, y = wormholePos.y + 0, z = wormholePos.z}) or
func({x = wormholePos.x + 1, y = wormholePos.y + 0, z = wormholePos.z}) or
func({x = wormholePos.x + 0, y = wormholePos.y + 1, z = wormholePos.z}) or
func({x = wormholePos.x + 1, y = wormholePos.y + 1, z = wormholePos.z}) or
func({x = wormholePos.x + 0, y = wormholePos.y + 2, z = wormholePos.z}) or
func({x = wormholePos.x + 1, y = wormholePos.y + 2, z = wormholePos.z})
else
local wormholePos = {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 1}
shortCircuited =
func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 0}) or
func({x = wormholePos.x, y = wormholePos.y + 0, z = wormholePos.z + 1}) or
func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 0}) or
func({x = wormholePos.x, y = wormholePos.y + 1, z = wormholePos.z + 1}) or
func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 0}) or
func({x = wormholePos.x, y = wormholePos.y + 2, z = wormholePos.z + 1})
end
return not shortCircuited
end,
-- Check for whether the portal is blocked in, and if so then provide a safe way
-- on one side for the player to step out of the portal. Suggest including a roof
-- incase the portal was blocked with lava flowing from above.
disable_portal_trap = function(anchorPos, orientation)
assert(orientation, "no orientation passed")
-- Not implemented yet. It may not need to be implemented because if you
-- wait in a portal long enough you teleport again. So a trap portal would have to link
-- to one of two blocked-in portals which link to each other - which is possible, but
-- quite extreme.
end
}
local registered_portals = {
["default:obsidian"] = {
shape = TraditionalPortalShape,
wormhole_node_name = "nether:portal",
frame_node_name = "default:obsidian",
find_realm_anchorPos = function(pos)
end,
find_surface_anchorPos = function(pos)
end
}
}
-- p1 and p2 are used to keep maps backwards compatible with earlier versions of this mod.
-- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner.
local function get_p1_and_p2_from_anchorPos(portal_shape, anchorPos, orientation)
assert(orientation, "no orientation passed")
local p1 = anchorPos
local p2
if orientation == 0 then
p2 = {x = p1.x + portal_shape.size.x - 1, y = p1.y + portal_shape.size.y - 1, z = p1.z }
else
p2 = {x = p1.x, y = p1.y + portal_shape.size.y - 1, z = p1.z + portal_shape.size.x - 1}
end
return p1, p2
end
-- orientation is the rotation degrees passed to place_schematic: 0, 90, 180, or 270
local function get_param2_from_orientation(param2, orientation)
return orientation / 90
end
local function get_orientation_from_param2(param2)
return param2 * 90
end
local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
-- p1 and p2 are used here to keep maps backwards compatible with earlier versions of this mod
-- (p2's value is the opposite corner of the portal frame to p1, according to the fixed portal shape of earlier versions of this mod)
local p1, p2 = get_p1_and_p2_from_anchorPos(portal_definition.shape, anchorPos, orientation)
local param2 = get_param2_from_orientation(0, orientation)
local updateFunc = function(pos)
if ignite and minetest.get_node(pos).name == "air" then
minetest.set_node(pos, {name = portal_definition.wormhole_node_name, param2 = param2})
end
local meta = minetest.get_meta(pos)
meta:set_string("p1", minetest.pos_to_string(p1))
meta:set_string("p2", minetest.pos_to_string(p2))
meta:set_string("target", minetest.pos_to_string(destination_wormholePos))
end
portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, updateFunc)
portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, updateFunc)
end
local function set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, true)
end
-- Checks pos, and if it's part of a portal or portal frame then three values are returned: anchorPos, orientation, is_ignited
-- where orientation is 0 or 90 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
local function is_portal_frame(portal_definition, pos)
local nodes_are_valid -- using closures as a way for the functions to return extra information - by setting this variable
local portal_is_ignited -- using closures as a way for the functions to return extra information - by setting this variable
local frame_node_name = portal_definition.frame_node_name
local check_frame_Func = function(check_pos)
if minetest.get_node(check_pos).name ~= frame_node_name then
nodes_are_valid = false
return true -- short-circuit the search
end
end
local wormhole_node_name = portal_definition.wormhole_node_name
local check_wormhole_Func = function(check_pos)
local node_name = minetest.get_node(check_pos).name
if node_name ~= wormhole_node_name then
portal_is_ignited = false;
if node_name ~= "air" then
nodes_are_valid = false
return true -- short-circuit the search
end
end
end
-- this function returns two bools: portal found, portal is lit
local is_portal_at_anchorPos = function(anchorPos, orientation)
nodes_are_valid = true
portal_is_ignited = true
portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, check_frame_Func)
if nodes_are_valid then
-- a valid frame exists at anchorPos, check the wormhole is either ignited or unobstructed
portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, check_wormhole_Func)
end
return nodes_are_valid, portal_is_ignited and nodes_are_valid -- returns two bools: portal was found, portal is lit
end
local width_minus_1 = portal_definition.shape.size.x - 1
local height_minus_1 = portal_definition.shape.size.y - 1
local depth_minus_1 = portal_definition.shape.size.z - 1
for d = -depth_minus_1, depth_minus_1 do
for w = -width_minus_1, width_minus_1 do
for y = -height_minus_1, height_minus_1 do
local testAnchorPos_x = {x = pos.x + w, y = pos.y + y, z = pos.z + d}
local portal_found, portal_lit = is_portal_at_anchorPos(testAnchorPos_x, 0)
if portal_found then
return testAnchorPos_x, 0, portal_lit
else
-- try orthogonal orientation
local testForAnchorPos_z = {x = pos.x + d, y = pos.y + y, z = pos.z + w}
portal_found, portal_lit = is_portal_at_anchorPos(testForAnchorPos_z, 90)
if portal_found then return testForAnchorPos_z, 90, portal_lit end
end
end
end
end
end
local function build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
minetest.place_schematic(
portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation),
portal_definition.shape.schematic_filename,
orientation,
nil,
true
)
if DEBUG then minetest.chat_send_all("Placed portal schematic at " .. minetest.pos_to_string(portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation)) .. ", orientation " .. orientation) end
set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos)
end
-- Used to find or build a remote twin after a portal is opened.
-- If a portal is found that is already lit then the destination_wormholePos argument is ignored - the anchorPos
-- of the portal that was found will be returned but its destination will be unchanged.
-- * suggested_anchorPos indicates where the portal should be built
-- * destination_wormholePos is the wormholePos of the destination portal this one will be linked to.
-- * suggested_orientation is the suggested schematic rotation: 0, 90, 180, 270 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
--
-- Returns the final (anchorPos, orientation), as they may differ from the anchorPos and orientation that was
-- specified if an existing portal was already found there.
local function locate_or_build_portal(portal_definition, suggested_anchorPos, suggested_orientation, destination_wormholePos)
if DEBUG then minetest.chat_send_all("locate_or_build_portal at " .. minetest.pos_to_string(suggested_anchorPos) .. ", targetted to " .. minetest.pos_to_string(destination_wormholePos) .. ", orientation " .. suggested_orientation) end
local result_anchorPos = suggested_anchorPos;
local result_orientation = suggested_orientation;
local place_new_portal = true
-- Searching for an existing portal at wormholePos seems better than at anchorPos, though isn't important
local suggested_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(suggested_anchorPos, suggested_orientation)
local found_anchorPos, found_orientation, is_ignited = is_portal_frame(portal_definition, suggested_wormholePos)
if found_anchorPos ~= nil then
-- A portal is already here, we don't have to build one, though we may need to ignite it
result_anchorPos = found_anchorPos
result_orientation = found_orientation
if is_ignited then
if DEBUG then minetest.chat_send_all("Build aborted: already a portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end
else
if DEBUG then minetest.chat_send_all("Build aborted: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end
-- ignite the portal
set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
end
else
build_portal(portal_definition, result_anchorPos, result_orientation, destination_wormholePos)
end
return result_anchorPos, result_orientation
end
local function volume_is_natural(minp, maxp)
local c_air = minetest.get_content_id("air")
local c_ignore = minetest.get_content_id("ignore")
local vm = minetest.get_voxel_manip()
local pos1 = {x = minp.x, y = minp.y, z = minp.z}
local pos2 = {x = maxp.x, y = maxp.y, z = maxp.z}
local emin, emax = vm:read_from_map(pos1, pos2)
local area = VoxelArea:new({MinEdge = emin, MaxEdge = emax})
local data = vm:get_data()
for z = pos1.z, pos2.z do
for y = pos1.y, pos2.y do
local vi = area:index(pos1.x, y, z)
for x = pos1.x, pos2.x do
local id = data[vi] -- Existing node
if id ~= c_air and id ~= c_ignore then -- These are natural
local name = minetest.get_name_from_content_id(id)
if not minetest.registered_nodes[name].is_ground_content then
return false
end
end
vi = vi + 1
end
end
end
return true
end
local function find_nether_target_y(target_x, target_z, start_y)
local nobj_cave_point = minetest.get_perlin(np_cave)
local air = 0 -- Consecutive air nodes found
for y = start_y, start_y - 4096, -1 do
local nval_cave = nobj_cave_point:get3d({x = target_x, y = y, z = target_z})
if nval_cave > TCAVE then -- Cavern
air = air + 1
else -- Not cavern, check if 4 nodes of space above
if air >= 4 then
-- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = y - 1, z = target_z - 2}
local maxp = {x = target_x + 2, y = y + 3, z = target_z + 2}
if volume_is_natural(minp, maxp) then
return y + 2
else -- Restart search a little lower
find_nether_target_y(target_x, target_z, y - 16)
end
else -- Not enough space, reset air to zero
air = 0
end
end
end
return start_y -- Fallback
end
--todo: find_surface_target_y() fails and selects locations under the surface
local function find_surface_target_y(target_x, target_z, start_y)
for y = start_y, start_y - 256, -16 do
-- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = y - 1, z = target_z - 2}
local maxp = {x = target_x + 2, y = y + 3, z = target_z + 2}
if volume_is_natural(minp, maxp) then
return y
end
end
return start_y - 256 -- Fallback
end
-- invoked when a player attempts to turn obsidian nodes into an open portal
local function ignite_portal(ignition_pos)
local ignition_node_name = minetest.get_node(ignition_pos).name
-- find out what sort of portals are made from the node that was clicked on
local portal_definition = registered_portals[ignition_node_name]
if portal_definition == nil then
return false -- no portals are made from the node at ignition_pos
end
-- check it was a portal frame that the player is trying to ignite
local anchorPos, orientation, is_ignited = is_portal_frame(portal_definition, ignition_pos)
if anchorPos == nil then
if DEBUG then minetest.chat_send_all("No portal frame found at " .. minetest.pos_to_string(ignition_pos)) end
return false -- no portal is here
elseif is_ignited then
if DEBUG then
local meta = minetest.get_meta(ignition_pos)
if meta ~= nil then minetest.chat_send_all("This portal links to " .. meta:get_string("target") .. ". p1=" .. meta:get_string("p1") .. " p2=" .. meta:get_string("p2")) end
end
return false -- portal is already ignited
end
if DEBUG then minetest.chat_send_all("Found portal frame. Looked at " .. minetest.pos_to_string(ignition_pos) .. ", found at " .. minetest.pos_to_string(anchorPos) .. " orientation " .. orientation) end
-- pick a destination
local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation)
if anchorPos.y < NETHER_DEPTH then
destination_wormholePos.y = find_surface_target_y(destination_wormholePos.x, destination_wormholePos.z, -16)
else
local start_y = NETHER_DEPTH - math.random(500, 1500) -- Search start
destination_wormholePos.y = find_nether_target_y(destination_wormholePos.x, destination_wormholePos.z, start_y)
end
if DEBUG then minetest.chat_send_all("Destinaton set to " .. minetest.pos_to_string(destination_wormholePos)) end
-- ignition/BURN_BABY_BURN
set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos)
return true
end
-- invoked when a player is standing in a portal
local function ensure_remote_portal_then_teleport(player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
-- check player is still standing in a portal
local playerPos = player:getpos()
playerPos.y = playerPos.y + 0.1 -- Fix some glitches at -8000
if minetest.get_node(playerPos).name ~= portal_definition.wormhole_node_name then
return -- the player has moved out of the portal
end
-- debounce - check player is still standing in the same portal that called this function
local meta = minetest.get_meta(playerPos)
if not vector.equals(local_anchorPos, minetest.string_to_pos(meta:get_string("p1"))) then
if DEBUG then minetest.chat_send_all("the player already teleported from " .. minetest.pos_to_string(local_anchorPos) .. ", and is now standing in a different portal - " .. meta:get_string("p1")) end
return -- the player already teleported, and is now standing in a different portal
end
local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, local_orientation)
local node = minetest.get_node_or_nil(destination_wormholePos)
if node == nil then
-- area not emerged yet, delay and retry
if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() could not find anything yet at " .. minetest.pos_to_string(destination_wormholePos)) end
minetest.after(1, ensure_remote_portal_then_teleport, player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
else
local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(local_anchorPos, local_orientation)
if node.name == portal_definition.wormhole_node_name then
-- portal exists
local destination_orientation = get_orientation_from_param2(node.param2)
portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation)
-- rotate the player if the destination portal is a different orientation
local rotation_angle = math.rad(destination_orientation - local_orientation)
local offset = vector.subtract(playerPos, local_wormholePos) -- preserve player's position in the portal
local rotated_offset = {x = math.cos(rotation_angle) * offset.x - math.sin(rotation_angle) * offset.z, y = offset.y, z = math.sin(rotation_angle) * offset.x + math.cos(rotation_angle) * offset.z}
player:setpos(vector.add(destination_wormholePos, rotated_offset))
player:set_look_horizontal(player:get_look_horizontal() + rotation_angle)
else
-- destination portal still needs to be built
if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() saw " .. node.name .. " at " .. minetest.pos_to_string(destination_wormholePos) .. " rather than a wormhole. Calling locate_or_build_portal()") end
local new_dest_anchorPos, new_dest_orientation = locate_or_build_portal(portal_definition, destination_anchorPos, local_orientation, local_wormholePos)
if local_orientation ~= new_dest_orientation or not vector.equals(destination_anchorPos, new_dest_anchorPos) then
-- Update the local portal's target to match where the existing remote portal was found
destination_anchorPos = new_dest_anchorPos
destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation)
if DEBUG then minetest.chat_send_all("update target to " .. minetest.pos_to_string(destination_wormholePos)) end
set_portal_metadata(
portal_definition,
local_anchorPos,
local_orientation,
destination_wormholePos
)
minetest.after(0.1, ensure_remote_portal_then_teleport, player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
end
end
end
end
-- ABMs
minetest.register_abm({
nodenames = {"nether:portal"},
interval = 1,
chance = 2,
action = function(pos, node)
minetest.add_particlespawner({
amount = 32,
time = 4,
minpos = {x = pos.x - 0.25, y = pos.y - 0.25, z = pos.z - 0.25},
maxpos = {x = pos.x + 0.25, y = pos.y + 0.25, z = pos.z + 0.25},
minvel = {x = -0.8, y = -0.8, z = -0.8},
maxvel = {x = 0.8, y = 0.8, z = 0.8},
minacc = {x = 0, y = 0, z = 0},
maxacc = {x = 0, y = 0, z = 0},
minexptime = 0.5,
maxexptime = 1,
minsize = 1,
maxsize = 2,
collisiondetection = false,
texture = "nether_particle.png"
})
for _, obj in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
if obj:is_player() then
local meta = minetest.get_meta(pos)
local destination_wormholePos = minetest.string_to_pos(meta:get_string("target"))
local local_p1 = minetest.string_to_pos(meta:get_string("p1"))
if destination_wormholePos ~= nil and local_p1 ~= nil then
-- find out what sort of portal we're in
local p1_node_name = minetest.get_node(local_p1).name
local portal_definition = registered_portals[p1_node_name]
if portal_definition == nil then
if p1_node_name ~= "ignore" then
-- I've seen cases where the p1_node_name temporarily returns "ignore", but it comes right - perhaps it happens when playerPos and anchorPos are in different chunks?
if DEBUG then minetest.chat_send_all("Weirdness: No portal with a \"" .. p1_node_name .. "\" frame is registered. Portal metadata at " .. minetest.pos_to_string(pos) .. " claims node ".. minetest.pos_to_string(local_p1) .. " is its portal corner (p1), but that location contains \"" .. p1_node_name .. "\"") end
else
minetest.log("error", "No portal with a \"" .. p1_node_name .. "\" frame is registered. Portal metadata at " .. minetest.pos_to_string(pos) .. " claims node ".. minetest.pos_to_string(local_p1) .. " is its portal corner (p1), but that location contains \"" .. p1_node_name .. "\"")
end
return
end
-- force emerge of target area
minetest.get_voxel_manip():read_from_map(destination_wormholePos, destination_wormholePos)
if not minetest.get_node_or_nil(destination_wormholePos) then
minetest.emerge_area(vector.subtract(destination_wormholePos, 4), vector.add(destination_wormholePos, 4))
end
local local_orientation = get_orientation_from_param2(node.param2)
minetest.after(
3, -- hopefully target area is emerged in 3 seconds
function()
ensure_remote_portal_then_teleport(
obj,
portal_definition,
local_p1,
local_orientation,
destination_wormholePos
)
end
)
end
end
end
end,
})
-- Nodes
minetest.register_node("nether:portal", {
description = "Nether Portal",
tiles = {
"nether_transparent.png",
"nether_transparent.png",
"nether_transparent.png",
"nether_transparent.png",
{
name = "nether_portal.png",
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 0.5,
},
},
{
name = "nether_portal.png",
animation = {
type = "vertical_frames",
aspect_w = 16,
aspect_h = 16,
length = 0.5,
},
},
},
drawtype = "nodebox",
paramtype = "light",
paramtype2 = "facedir",
sunlight_propagates = true,
use_texture_alpha = true,
walkable = false,
diggable = false,
pointable = false,
buildable_to = false,
is_ground_content = false,
drop = "",
light_source = 5,
post_effect_color = {a = 180, r = 128, g = 0, b = 128},
alpha = 192,
node_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.1, 0.5, 0.5, 0.1},
},
},
groups = {not_in_creative_inventory = 1}
})
minetest.register_node(":default:obsidian", {
description = "Obsidian",
tiles = {"default_obsidian.png"},
is_ground_content = false,
sounds = default.node_sound_stone_defaults(),
groups = {cracky = 1, level = 2},
on_destruct = function(pos)
local meta = minetest.get_meta(pos)
local p1 = minetest.string_to_pos(meta:get_string("p1"))
local p2 = minetest.string_to_pos(meta:get_string("p2"))
local target = minetest.string_to_pos(meta:get_string("target"))
if not p1 or not p2 then
return
end
for x = p1.x, p2.x do
for y = p1.y, p2.y do
for z = p1.z, p2.z do
local nn = minetest.get_node({x = x, y = y, z = z}).name
if nn == "default:obsidian" or nn == "nether:portal" then
if nn == "nether:portal" then
minetest.remove_node({x = x, y = y, z = z})
end
local m = minetest.get_meta({x = x, y = y, z = z})
m:set_string("p1", "")
m:set_string("p2", "")
m:set_string("target", "")
end
end
end
end
meta = minetest.get_meta(target)
if not meta then
return
end
p1 = minetest.string_to_pos(meta:get_string("p1"))
p2 = minetest.string_to_pos(meta:get_string("p2"))
if not p1 or not p2 then
return
end
for x = p1.x, p2.x do
for y = p1.y, p2.y do
for z = p1.z, p2.z do
local nn = minetest.get_node({x = x, y = y, z = z}).name
if nn == "default:obsidian" or nn == "nether:portal" then
if nn == "nether:portal" then
minetest.remove_node({x = x, y = y, z = z})
end
local m = minetest.get_meta({x = x, y = y, z = z})
m:set_string("p1", "")
m:set_string("p2", "")
m:set_string("target", "")
end
end
end
end
end,
})
minetest.register_node("nether:rack", {
description = "Netherrack",
tiles = {"nether_rack.png"},
is_ground_content = true,
groups = {cracky = 3, level = 2},
sounds = default.node_sound_stone_defaults(),
})
minetest.register_node("nether:sand", {
description = "Nethersand",
tiles = {"nether_sand.png"},
is_ground_content = true,
groups = {crumbly = 3, level = 2, falling_node = 1},
sounds = default.node_sound_gravel_defaults({
footstep = {name = "default_gravel_footstep", gain = 0.45},
}),
})
minetest.register_node("nether:glowstone", {
description = "Glowstone",
tiles = {"nether_glowstone.png"},
is_ground_content = true,
light_source = 14,
paramtype = "light",
groups = {cracky = 3, oddly_breakable_by_hand = 3},
sounds = default.node_sound_glass_defaults(),
})
minetest.register_node("nether:brick", {
description = "Nether Brick",
tiles = {"nether_brick.png"},
is_ground_content = false,
groups = {cracky = 2, level = 2},
sounds = default.node_sound_stone_defaults(),
})
local fence_texture =
"default_fence_overlay.png^nether_brick.png^default_fence_overlay.png^[makealpha:255,126,126"
minetest.register_node("nether:fence_nether_brick", {
description = "Nether Brick Fence",
drawtype = "fencelike",
tiles = {"nether_brick.png"},
inventory_image = fence_texture,
wield_image = fence_texture,
paramtype = "light",
sunlight_propagates = true,
is_ground_content = false,
selection_box = {
type = "fixed",
fixed = {-1/7, -1/2, -1/7, 1/7, 1/2, 1/7},
},
groups = {cracky = 2, level = 2},
sounds = default.node_sound_stone_defaults(),
})
-- Register stair and slab
stairs.register_stair_and_slab(
"nether_brick",
"nether:brick",
{cracky = 2, level = 2},
{"nether_brick.png"},
"nether stair",
"nether slab",
default.node_sound_stone_defaults()
)
-- StairsPlus
if minetest.get_modpath("moreblocks") then
stairsplus:register_all(
"nether", "brick", "nether:brick", {
description = "Nether Brick",
groups = {cracky = 2, level = 2},
tiles = {"nether_brick.png"},
sounds = default.node_sound_stone_defaults(),
})
end
-- Craftitems
minetest.override_item("default:mese_crystal_fragment", {
on_place = function(stack, _, pt)
if pt.under and minetest.get_node(pt.under).name == "default:obsidian" then
local done = ignite_portal(pt.under)
if done and not minetest.settings:get_bool("creative_mode") then
stack:take_item()
end
end
return stack
end,
})
-- Crafting
minetest.register_craft({
output = "nether:brick 4",
recipe = {
{"nether:rack", "nether:rack"},
{"nether:rack", "nether:rack"},
}
})
minetest.register_craft({
output = "nether:fence_nether_brick 6",
recipe = {
{"nether:brick", "nether:brick", "nether:brick"},
{"nether:brick", "nether:brick", "nether:brick"},
},
})
-- Mapgen
-- Initialize noise object, localise noise and data buffers
local nobj_cave = nil
local nbuf_cave = nil
local dbuf = nil
-- Content ids
local c_air = minetest.get_content_id("air")
--local c_stone_with_coal = minetest.get_content_id("default:stone_with_coal")
--local c_stone_with_iron = minetest.get_content_id("default:stone_with_iron")
local c_stone_with_mese = minetest.get_content_id("default:stone_with_mese")
local c_stone_with_diamond = minetest.get_content_id("default:stone_with_diamond")
local c_stone_with_gold = minetest.get_content_id("default:stone_with_gold")
--local c_stone_with_copper = minetest.get_content_id("default:stone_with_copper")
local c_mese = minetest.get_content_id("default:mese")
local c_gravel = minetest.get_content_id("default:gravel")
local c_dirt = minetest.get_content_id("default:dirt")
local c_sand = minetest.get_content_id("default:sand")
local c_cobble = minetest.get_content_id("default:cobble")
local c_mossycobble = minetest.get_content_id("default:mossycobble")
local c_stair_cobble = minetest.get_content_id("stairs:stair_cobble")
local c_lava_source = minetest.get_content_id("default:lava_source")
local c_lava_flowing = minetest.get_content_id("default:lava_flowing")
local c_water_source = minetest.get_content_id("default:water_source")
local c_water_flowing = minetest.get_content_id("default:water_flowing")
local c_glowstone = minetest.get_content_id("nether:glowstone")
local c_nethersand = minetest.get_content_id("nether:sand")
local c_netherbrick = minetest.get_content_id("nether:brick")
local c_netherrack = minetest.get_content_id("nether:rack")
-- On-generated function
minetest.register_on_generated(function(minp, maxp, seed)
if minp.y > NETHER_DEPTH then
return
end
local x1 = maxp.x
local y1 = maxp.y
local z1 = maxp.z
local x0 = minp.x
local y0 = minp.y
local z0 = minp.z
local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
local data = vm:get_data(dbuf)
local x11 = emax.x -- Limits of mapchunk plus mapblock shell
local y11 = emax.y
local z11 = emax.z
local x00 = emin.x
local y00 = emin.y
local z00 = emin.z
local ystride = x1 - x0 + 1
local zstride = ystride * ystride
local chulens = {x = ystride, y = ystride, z = ystride}
local minposxyz = {x = x0, y = y0, z = z0}
nobj_cave = nobj_cave or minetest.get_perlin_map(np_cave, chulens)
local nvals_cave = nobj_cave:get3dMap_flat(minposxyz, nbuf_cave)
for y = y00, y11 do -- Y loop first to minimise tcave calculations
local tcave
local in_chunk_y = false
if y >= y0 and y <= y1 then
if y > yblmax then
tcave = TCAVE + ((y - yblmax) / BLEND) ^ 2
else
tcave = TCAVE
end
in_chunk_y = true
end
for z = z00, z11 do
local vi = area:index(x00, y, z) -- Initial voxelmanip index
local ni
local in_chunk_yz = in_chunk_y and z >= z0 and z <= z1
for x = x00, x11 do
if in_chunk_yz and x == x0 then
-- Initial noisemap index
ni = (z - z0) * zstride + (y - y0) * ystride + 1
end
local in_chunk_yzx = in_chunk_yz and x >= x0 and x <= x1 -- In mapchunk
local id = data[vi] -- Existing node
-- Cave air, cave liquids and dungeons are overgenerated,
-- convert these throughout mapchunk plus shell
if id == c_air or -- Air and liquids to air
id == c_lava_source or
id == c_lava_flowing or
id == c_water_source or
id == c_water_flowing then
data[vi] = c_air
-- Dungeons are preserved so we don't need
-- to check for cavern in the shell
elseif id == c_cobble or -- Dungeons (preserved) to netherbrick
id == c_mossycobble or
id == c_stair_cobble then
data[vi] = c_netherbrick
end
if in_chunk_yzx then -- In mapchunk
if nvals_cave[ni] > tcave then -- Only excavate cavern in mapchunk
data[vi] = c_air
elseif id == c_mese then -- Mese block to lava
data[vi] = c_lava_source
elseif id == c_stone_with_gold or -- Precious ores to glowstone
id == c_stone_with_mese or
id == c_stone_with_diamond then
data[vi] = c_glowstone
elseif id == c_gravel or -- Blob ore to nethersand
id == c_dirt or
id == c_sand then
data[vi] = c_nethersand
else -- All else to netherstone
data[vi] = c_netherrack
end
ni = ni + 1 -- Only increment noise index in mapchunk
end
vi = vi + 1
end
end
end
vm:set_data(data)
vm:set_lighting({day = 0, night = 0})
vm:calc_lighting()
vm:update_liquids()
vm:write_to_map()
end)