From b6e2a3335a53ef2d10dbd2f8e0bfffd7abe9a25c Mon Sep 17 00:00:00 2001 From: Treer Date: Fri, 5 Jul 2019 23:22:22 +1000 Subject: [PATCH] provide Portal API --- .luacheckrc | 4 + init.lua | 1305 +---------------- mapgen.lua | 225 +++ nodes.lua | 165 +++ portal_api.lua | 1069 ++++++++++++++ portal_api.txt | 84 ++ ...palette.png => nether_portals_palette.png} | Bin 7 files changed, 1593 insertions(+), 1259 deletions(-) create mode 100644 mapgen.lua create mode 100644 nodes.lua create mode 100644 portal_api.lua create mode 100644 portal_api.txt rename textures/{portal_palette.png => nether_portals_palette.png} (100%) diff --git a/.luacheckrc b/.luacheckrc index cc73fb0..f3bb235 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,6 +1,10 @@ unused_args = false allow_defined_top = true +globals = { + "nether" +} + read_globals = { "core", "default", diff --git a/init.lua b/init.lua index be5542a..93ac9ec 100644 --- a/init.lua +++ b/init.lua @@ -19,1280 +19,67 @@ ]]-- +-- Global Nether namespace +nether = {} +nether.path = minetest.get_modpath("nether") --- Parameters - -local DEBUG = false -local NETHER_DEPTH = -5000 -local TCAVE = 0.6 -local BLEND = 128 +-- Settings +nether.DEPTH = -5000 +nether.FASTTRAVEL_FACTOR = 8 -- 10 could be better value for Minetest, since there's no sprint, but ex-Minecraft players will be mathing for 8 --- 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 = "" -} +-- Load files +dofile(nether.path .. "/portal_api.lua") +dofile(nether.path .. "/nodes.lua") +dofile(nether.path .. "/mapgen.lua") --- Stuff - -local yblmax = NETHER_DEPTH - BLEND * 2 +-- Portals are ignited by right-clicking with a mese crystal fragment +nether.register_portal_ignition_item("default:mese_crystal_fragment") -netherportal = {} -- portal API +-- Use the Portal API to add a portal type which goes to the Nether +-- See portal_api.txt for documentation +nether.register_portal("nether_portal", { + shape = nether.PortalShape_Traditional, + frame_node_name = "default:obsidian", + wormhole_node_name = "nether:portal", + wormhole_node_color = 0, + sound_ambient = "nether_portal_hum", + sound_ignite = "", + sound_extinguish = "", + sound_teleport = "", --- Functions - - ---[[ - For this TraditionalPortalShape implementation, anchorPos and wormholdPos are defined as follows: - . - +--------+--------+--------+--------+ - | | Frame | | - | | | | p2 | - +--------+--------+--------+--------+ - | | | | - | | | | - +--------+ + +--------+ - | | Wormhole | | - | | | | - +--------+ + +--------+ - | |Wormhole | | - | | Pos | | - +--------+--------+--------+--------+ - AnchorPos| Node | | | - | p1 | Timer | | | - +--------+--------+--------+--------+ - - +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 nether 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 behaviour code. --- 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 = { - 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 + within_realm = function(pos) -- return true if pos is inside the Nether + return pos.y < nether.DEPTH 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 + find_realm_anchorPos = function(surface_pos) + -- divide x and z by a factor of 8 to implement Nether fast-travel + local destination_pos = vector.divide(surface_pos, nether.FASTTRAVEL_FACTOR) + destination_pos.x = math.floor(0.5 + destination_pos.x) -- round to int + destination_pos.z = math.floor(0.5 + destination_pos.z) -- round to int + + local start_y = nether.DEPTH - math.random(500, 1500) -- Search start + destination_pos.y = nether.find_nether_ground_y(destination_pos.x, destination_pos.z, start_y) + return destination_pos 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, - - -- p1 and p2 are used to keep maps compatible with earlier versions of this mod. - -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together - -- they define the bounding volume for the portal. - get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation) - assert(orientation, "no orientation passed") - local p1 = anchorPos -- TraditionalPortalShape puts the anchorPos at p1 for backwards&forwards compatibility - local p2 - - if orientation == 0 then - p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z } - else - p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1} - end - return p1, p2 - end, - - get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2) - if p1.z == p2.z then - return p1, 0 - elseif p1.x == p2.x then - return p1, 90 - else - -- this KISS implementation will break you've made a 3D PortalShape definition - minetest.log("error", "get_anchorPos_and_orientation_from_p1_and_p2 failed on p1=" .. meta:get_string("p1") .. " p2=" .. meta:get_string("p2")) - end - end, - - apply_func_to_frame_nodes = function(anchorPos, orientation, func) - -- a 4x5 portal is small enough that hardcoded positions is simpler that procedural code - 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. - -- If portal can appear in mid-air then can also check for that and add a platform. - 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 -} ---=====================================================-- ---======== End of TraditionalPortalShape class ========-- ---=====================================================-- - - -local registered_portals = { - ["netherportal"] = { - name = "netherportal", - shape = TraditionalPortalShape, - wormhole_node_name = "nether:portal", - wormhole_node_color = 0, - frame_node_name = "default:obsidian", - sound_ambient = "portal_hum", - sound_ignite = "", - sound_extinguish = "", - sound_teleport = "", - - - find_realm_anchorPos = function(pos) - end, - - find_surface_anchorPos = function(pos) - end - - -- on_run_wormhole - -- on_ignite - -- on_extinguish - -- on_player_teleported - -- on_created - } -} - - -local function get_timerPos_from_p1_and_p2(p1, p2) - -- Pick a frame node for the portal's timer. - -- - -- The timer event will need to know the portal definition, which can be determined by - -- what the portal frame is made from, so the timer node should be on the frame. - -- The timer event will also need to know its portal orientation, but unless someone - -- makes a cubic portal shape, orientation can be determined from p1 and p2 in the node's - -- metadata (frame nodes don't have orientation set in param2 like wormhole nodes do). - -- - -- We shouldn't pick p1 (or p2) as it's possible for two orthogonal portals to share - -- the same p1, etc. - -- - -- I'll pick the bottom center node of the portal, since that works for rectangular portals - -- and if someone want to make a circular portal then that positon will still likely be part - -- of the frame. - return { - x = math.floor((p1.x + p2.x) / 2), - y = p1.y, - z = math.floor((p1.z + p2.z) / 2), - } -end - --- orientation is the rotation degrees passed to place_schematic: 0, 90, 180, or 270 --- color is a value from 0 to 7 corresponding to the pixels in portal_palette.png -local function get_param2_from_color_and_orientation(color, orientation) - assert(orientation, "no orientation passed") - - -- wormhole nodes have a paramtype2 of colorfacedir, which means the - -- high 3 bits are palette, followed by 3 direction bits and 2 rotation bits. - -- We set the palette bits and rotation - return (orientation / 90) + color * 32 -end - -local function get_orientation_from_param2(param2) - -- Strip off the top 6 bits, unfortunately MT lua has no bitwise '&' - -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits) - if param2 >= 128 then param2 = param2 - 128 end - if param2 >= 64 then param2 = param2 - 64 end - if param2 >= 32 then param2 = param2 - 32 end - if param2 >= 16 then param2 = param2 - 16 end - if param2 >= 8 then param2 = param2 - 8 end - - return param2 * 90 -end - --- Combining frame_node_name, p1, and p2 will always be enough to uniquely identify a portal_defintion --- WITHOUT needing to inspect the world. register_portal() will enforce this. --- This function does not require the portal to be in a loaded chunk. --- Returns nil if no portal_defintion matches the arguments -local function get_portal_defintion(frame_node_name, p1, p2) - - local size = vector.add(vector.subtract(p2, p1), 1) - local rotated_size = {x = size.z, y = size.y, z = size.x} - - for _, portal_def in pairs(registered_portals) do - if portal_def.frame_node_name == frame_node_name then - if vector.equals(size, portal_def.shape.size) or vector.equals(rotated_size, portal_def.shape.size) then - return portal_def - end - end - end - return nil -end - --- Returns a list of all portal_defintions with a frame made of frame_node_name. --- Ideally no two portal types will be built from the same frame material so this call might be enough --- to uniquely identify a portal_defintion without needing to inspect the world, HOWEVER we shouldn't --- cramp anyone's style and prohibit non-nether use of obsidian to make portals, so it returns a list. --- If the list contains more than one item then routines like ignite_portal() will have to search twice --- for a portal and take twice the CPU. -local function list_portal_definitions_for_frame_node(frame_node_name) - local result = {} - for _, portal_def in pairs(registered_portals) do - if portal_def.frame_node_name == frame_node_name then table.insert(result, portal_def) end - end - return result -end - - -local extinguish_portal -- the function will be assigned to this further down, rather than having to define the function before other functions can use it. - -local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite) - - - -- Portal position is stored in metadata as p1 and p2 to keep maps compatible with earlier versions of this mod. - -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together - -- they define the bounding volume for the portal. - local p1, p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation) - local p1_string, p2_string = minetest.pos_to_string(p1), minetest.pos_to_string(p2) - local param2 = get_param2_from_color_and_orientation(portal_definition.wormhole_node_color, orientation) - - local update_aborted-- using closures to allow the updateFunc to return extra information - by setting this variable - - local updateFunc = function(pos) - - local meta = minetest.get_meta(pos) - - if ignite then - local node_name = minetest.get_node(pos).name - if node_name == "air" then - minetest.set_node(pos, {name = portal_definition.wormhole_node_name, param2 = param2}) - end - - local existing_p1 = meta:get_string("p1") - if existing_p1 ~= "" then - local existing_p2 = meta:get_string("p2") - if DEBUG then minetest.chat_send_all("existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string) end - if existing_p1 ~= p1_string or existing_p2 ~= p2_string then - -- this node is already part of another portal, so extinguish that, because nodes only - -- contain a link in the metadata to one portal, and being part of two allows a slew of bugs - extinguish_portal(pos, node_name) - - -- clear the metadata to avoid causing a loop if extinguish_portal() fails on this node (e.g. it only works on frame nodes) - meta:set_string("p1", nil) - meta:set_string("p2", nil) - meta:set_string("target", nil) - meta:set_string("frame_node_name", nil) - - update_aborted = true - return true -- short-circuit the update - end - end - end - - 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)) - -- including "frame_node_name" in the metadata lets us know which kind of portal this is. - -- It's not strictly necessary for TraditionalPortalShape as we know that p1 is part of - -- the frame, and legacy portals don't have this extra metadata - indicating obsidian, - -- but p1 isn't always loaded so reading this from the metadata saves an extra call to - -- minetest.getnode(). - meta:set_string("frame_node_name", portal_definition.frame_node_name) - end - - repeat - update_aborted = false - portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, updateFunc) - portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, updateFunc) - until not update_aborted - - local timerPos = get_timerPos_from_p1_and_p2(p1, p2) - minetest.get_node_timer(timerPos):start(1) -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 to allow the check functions to return extra information - by setting this variable - local portal_is_ignited -- using closures to allow the check 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) -- check_frame_Func affects nodes_are_valid, portal_is_ignited - - 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) -- check_wormhole_Func affects nodes_are_valid, portal_is_ignited - 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 the 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 - - --- use this when determining where to spawn a portal, to avoid overwriting player builds -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) - local nodedef = minetest.registered_nodes[name] - if not nodedef.is_ground_content then - -- trees are natural but not "ground content" - local node_groups = nodedef.groups - if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil) then - return false - end - 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 - - ---[[ -"The normal realm portal has a particular X, Z, it searches downwards for a suitable Y. -It can't be placed in mid-air, and for performance the test for a suitable placement position cannot move downwards in -1 node steps, instead it moves downwards in 16 node steps, so it will almost always be placed buried in solid nodes. - -The portal cannot be placed in any volume that contains non-natural nodes (is_ground_content = false) to not grief -player builds. This makes it even more likely the portal will be a little way underground. - -The portal is placed with air nodes around it to create a space so it isn't embedded in stone. -It is expected that the player has a pickaxe to dig their way out, this is highly likely if they have built a portal -and are exploring the nether. The player will not be trapped. - -Note that MC also often places portals embedded in stone. - -The code could be [edit: has been] altered to first try to find a surface position, but if this surface position is -unsuitable due to being near player builds, the portal will still move downwards into the ground, so this is -unavoidable. - -Any search for a suitable resting-on-surface or resting-on-cave-surface position will be somewhat complex, to avoid -placement on a tiny floating island or narrow spike etc. which would be impractical or deadly to the player. - -A portal room embedded underground is the safest and the most accessible for the player. - -So i decided to start the placement position search at y = -16 as that, or a little below, is the most likely suitable -position: Ground is almost always present there, it's below any lakes or seas, below most player builds. - -Also, the search for non-natural nodes doesn't actually guarantee avoiding player builds, as a player build can be -composed of only natural nodes (is_ground_content = true). So even more good reason to start the search a little way -underground where player builds are more unlikely. Y = -16 seemed a reasonable compromise between safety and distance -from surface. - -Each placement position search has to search a volume of nodes for non-natural nodes, this is not lightweight, and many -searches may happen if there a lot of underground player builds present. So the code has been written to avoid -intensive procedures." --- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676 -]] -local function find_surface_target_y(portal_definition, target_x, target_z) - - -- try to spawn on surface first - if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this - local surface_level = minetest.get_spawn_level(target_x, target_z) - if surface_level ~= nil then - -- Check volume for non-natural nodes - local minp = {x = target_x - 1, y = surface_level - 1, z = target_z - 2} - local maxp = {x = target_x + 2, y = surface_level + 3, z = target_z + 2} - if volume_is_natural(minp, maxp) then - return surface_level - end - end - end - - -- fallback to underground search - local start_y = -16 - 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 - else - -- players have built here - don't grief. - -- but reigniting existing portals in portal rooms is fine - desirable even. - local anchorPos, orientation, is_ignited = is_portal_frame(portal_definition, {x = target_x, y = y, z = target_z}) - if anchorPos ~= nil then - return y - end - 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 which sort of portals are made from the node that was clicked on - local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name) - - for _, portal_definition in ipairs(portal_definition_list) do - local continue = false - - -- 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_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end - continue = true -- no portal is here, but perhaps there more than one portal type we need to search for - 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 continue == false then - 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(portal_definition, destination_wormholePos.x, destination_wormholePos.z) - 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 - end -end - - --- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos -extinguish_portal = function(pos, node_name) -- assigned rather than declared because extinguish_portal is already declared, for use by earlier functions in the file. - - -- mesecons seems to invoke action_off() 6 times every time you place a block? - if DEBUG then minetest.chat_send_all("extinguish_portal" .. minetest.pos_to_string(pos) .. " " .. node_name) end - - 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 p1 == nil or p2 == nil then - return - end - - local portal_definition = get_portal_defintion(node_name, p1, p2) - if portal_definition == nil then - minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name) - return -- no portal frames are made from this type of node - end - local frame_node_name = portal_definition.frame_node_name - local wormhole_node_name = portal_definition.wormhole_node_name - - minetest.get_node_timer(get_timerPos_from_p1_and_p2(p1, p2)):stop() - - 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 == frame_node_name or nn == wormhole_node_name then - if nn == wormhole_node_name 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", "") - m:set_string("frame_node_name", "") - end - end - end - end - - if target ~= nil then extinguish_portal(target, node_name) end -end - - --- Sometimes after a portal is placed, concurrent mapgen routines overwrite it. --- Make portals immortal for ~20 seconds after creation -local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orientation, destination_wormholePos) - - local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) - local wormhole_node = minetest.get_node_or_nil(wormholePos) - - if wormhole_node == nil or wormhole_node.name ~= portal_definition.wormhole_node_name then - -- ruh roh - local message = "Newly created portal at " .. minetest.pos_to_string(anchorPos) .. " was overwritten. Attempting to recreate. Issue spotted after " .. elapsed .. " seconds" - minetest.log("warning", message) - if DEBUG then minetest.chat_send_all("!!! " .. message) end - - -- A pre-existing portal frame wouldn't have been immediately overwritten, so no need to check for one, just place the portal. - build_portal(portal_definition, anchorPos, orientation, destination_wormholePos) - end - - if elapsed < 20 then -- stop checking after 20 seconds - local delay = elapsed * 2 - minetest.after(delay, remote_portal_checkup, elapsed + delay, portal_definition, anchorPos, orientation, destination_wormholePos) - end -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 dest_wormhole_node = minetest.get_node_or_nil(destination_wormholePos) - - if dest_wormhole_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 dest_wormhole_node.name == portal_definition.wormhole_node_name then - -- portal exists - local destination_orientation = get_orientation_from_param2(dest_wormhole_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 " .. dest_wormhole_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 - ) - end - minetest.after(0.1, ensure_remote_portal_then_teleport, player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos) - - -- make sure portal isn't overwritten by ongoing generation/emerge - minetest.after(2, remote_portal_checkup, 2, portal_definition, new_dest_anchorPos, new_dest_orientation, local_wormholePos) - end - end -end - - --- run_wormhole() is invoked once per second per portal, handling teleportation and particle effects. --- See get_timerPos_from_p1_and_p2() for an explanation of where pos will be -function run_wormhole(pos, time_elapsed) - - local portal_definition -- will be used inside run_wormhole_node_func() - - local run_wormhole_node_func = function(pos) - - if math.random(2) == 1 then -- lets run only 3 particlespawners instead of 6 per portal - minetest.add_particlespawner({ - amount = 16, - time = 2, - 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.5, - minsize = 0.5, - maxsize = 1.5, - collisiondetection = false, - texture = "nether_particle.png^[colorize:#808:alpha", - glow = 5 - }) - end - - 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 - - -- 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(minetest.get_node(pos).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 - - local p1, p2, frame_node_name - local meta = minetest.get_meta(pos) - if meta ~= nil then - p1 = minetest.string_to_pos(meta:get_string("p1")) - p2 = minetest.string_to_pos(meta:get_string("p2")) - --frame_node_name = minetest.string_to_pos(meta:get_string("frame_node_name")) don't rely on this yet until you're sure everything works with old portals that don't have this set - end - if p1 ~= nil and p2 ~= nil then - -- look up the portal shape by what it's built from, so we know where the wormhole nodes will be located - if frame_node_name == nil then frame_node_name = minetest.get_node(pos).name end -- pos should be a frame node - portal_definition = get_portal_defintion(frame_node_name, p1, p2) - if portal_definition == nil then - minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(pos) .. " was invoked but that location contains \"" .. frame_node_name .. "\"") - else - local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) - portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, run_wormhole_node_func) - end - end -end - - -minetest.register_lbm({ - label = "Start portal timer", - name = "nether:start_portal_timer", - nodenames = {"nether:portal"}, - run_at_every_load = false, - action = function(pos, node) - local p1, p2 - local meta = minetest.get_meta(pos) - if meta ~= nil then - p1 = minetest.string_to_pos(meta:get_string("p1")) - p2 = minetest.string_to_pos(meta:get_string("p1")) - end - if p1 ~= nil and p2 ~= nil then - local timerPos = get_timerPos_from_p1_and_p2(p1, p2) - local timer = minetest.get_node_timer(timerPos) - if timer ~= nil then - timer:start(1) - elseif DEBUG then - minetest.chat_send_all("get_node_timer" .. minetest.pos_to_string(timerPos) .. " returned null") - end - end + find_surface_anchorPos = function(realm_pos) + -- A portal definition doesn't normally need to provide a find_surface_anchorPos() function, + -- since find_surface_target_y() will be used by default, but Nether portals also scale position + -- to create fast-travel: + + -- Multiply x and z by a factor of 8 to implement Nether fast-travel + local destination_pos = vector.multiply(realm_pos, nether.FASTTRAVEL_FACTOR) + destination_pos.x = math.min(30900, math.max(-30900, destination_pos.x)) -- clip to world boundary + destination_pos.z = math.min(30900, math.max(-30900, destination_pos.z)) -- clip to world boundary + + destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "nether_portal") + return destination_pos 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 = "colorfacedir", - palette = "portal_palette.png", - post_effect_color = {a = 180, r = 128, g = 0, b = 128}, - sunlight_propagates = true, - use_texture_alpha = true, - walkable = false, - diggable = false, - pointable = false, - buildable_to = false, - is_ground_content = false, - drop = "", - light_source = 5, - 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}, - - mesecons = {effector = { - action_on = function (pos, node) - ignite_portal(pos, node.name) - end, - action_off = function (pos, node) - extinguish_portal(pos, node.name) - end - }}, - on_destruct = function(pos) - extinguish_portal(pos, "default:obsidian") - end, - on_timer = function(pos, elapsed) - run_wormhole(pos, elapsed) - return true - 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) diff --git a/mapgen.lua b/mapgen.lua new file mode 100644 index 0000000..653d8f0 --- /dev/null +++ b/mapgen.lua @@ -0,0 +1,225 @@ +--[[ + + 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 = nether.DEPTH +local TCAVE = 0.6 +local BLEND = 128 + + +-- 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 + + + + +-- 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) + + +-- use knowledge of the nether mapgen algorithm to return a suitable ground level for placing a portal. +function nether.find_nether_ground_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 , z = target_z - 2} + local maxp = {x = target_x + 2, y = y + 4, z = target_z + 2} + if nether.volume_is_natural(minp, maxp) then + return y + 1 + else -- Restart search a little lower + nether.find_nether_ground_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 \ No newline at end of file diff --git a/nodes.lua b/nodes.lua new file mode 100644 index 0000000..1d495ad --- /dev/null +++ b/nodes.lua @@ -0,0 +1,165 @@ +-- Portal node + +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 = "colorfacedir", + palette = "nether_portals_palette.png", + post_effect_color = { + -- post_effect_color can't be changed dynamically in Minetest like the portal colour is. + -- If you need a different post_effect_color then create a custom node and set it as the + -- wormhole_node_name in your portaldef. + -- Hopefully this colour is close enough to magenta to work with the traditional magenta + -- portals, close enough to red to work for a red portal, and also close enough to red to + -- work with blue & cyan portals - since blue portals are sometimes portrayed as being red + -- from the opposite side / from the inside. + a = 160, r = 128, g = 0, b = 80 + }, + sunlight_propagates = true, + use_texture_alpha = true, + walkable = false, + diggable = false, + pointable = false, + buildable_to = false, + is_ground_content = false, + drop = "", + light_source = 5, + 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} +}) + + +-- Nether nodes + +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 + + +-- 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"}, + }, +}) + + diff --git a/portal_api.lua b/portal_api.lua new file mode 100644 index 0000000..5ff01fd --- /dev/null +++ b/portal_api.lua @@ -0,0 +1,1069 @@ +-- see portal_api.txt for documentation +local DEBUG = false + + +nether.registered_portals = {} + +-- gives the colour values in nether_portals_palette.png that are used by the wormhole colorfacedir +-- hardware colouring. +nether.portals_palette = { + [0] = {r = 128, g = 0, b = 128}, -- traditional/magenta + [1] = {r = 0, g = 0, b = 0}, -- black + [2] = {r = 19, g = 19, b = 255}, -- blue + [3] = {r = 55, g = 168, b = 0}, -- green + [4] = {r = 141, g = 237, b = 255}, -- cyan + [5] = {r = 221, g = 0, b = 0}, -- red + [6] = {r = 255, g = 240, b = 0}, -- yellow + [7] = {r = 255, g = 255, b = 255} -- white +} + + +--[[ + +Portal shapes +============= + + + For this PortalShape_Traditional implementation, anchorPos and wormholdPos are defined as follows: + . + +--------+--------+--------+--------+ + | | Frame | | + | | | | p2 | + +--------+--------+--------+--------+ + | | | | + | | | | + +--------+ + +--------+ + | | Wormhole | | + | | | | + +--------+ + +--------+ + | |Wormhole | | + | | Pos | | + +--------+--------+--------+--------+ + AnchorPos| Node | | | + | p1 | Timer | | | + +--------+--------+--------+--------+ + + +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 nether 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 behaviour code. +-- 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 +nether.PortalShape_Traditional = { + 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, + + -- p1 and p2 are used to keep maps compatible with earlier versions of this mod. + -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together + -- they define the bounding volume for the portal. + get_p1_and_p2_from_anchorPos = function(self, anchorPos, orientation) + assert(orientation, "no orientation passed") + local p1 = anchorPos -- PortalShape_Traditional puts the anchorPos at p1 for backwards&forwards compatibility + local p2 + + if orientation == 0 then + p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z } + else + p2 = {x = p1.x, y = p1.y + self.size.y - 1, z = p1.z + self.size.x - 1} + end + return p1, p2 + end, + + get_anchorPos_and_orientation_from_p1_and_p2 = function(p1, p2) + if p1.z == p2.z then + return p1, 0 + elseif p1.x == p2.x then + return p1, 90 + else + -- this KISS implementation will break you've made a 3D PortalShape definition + minetest.log("error", "get_anchorPos_and_orientation_from_p1_and_p2 failed on p1=" .. minetest.pos_to_string(p1) .. " p2=" .. minetest.pos_to_string(p2)) + end + end, + + apply_func_to_frame_nodes = function(anchorPos, orientation, func) + -- a 4x5 portal is small enough that hardcoded positions is simpler that procedural code + 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. + -- If portal can appear in mid-air then can also check for that and add a platform. + 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 +} -- End of PortalShape_Traditional class + + +--====================================================-- +--======== End of PortalShape implementations ========-- +--====================================================-- + + + + +-- Portal implementation functions -- +-- =============================== -- + +-- list of node names that are used as frame nodes by registered portals +local is_frame_node = {} + +-- this is a function that will be assigned to further down, allowing functions to use it +-- that are defined before it is. +local extinguish_portal + + +local function get_timerPos_from_p1_and_p2(p1, p2) + -- Pick a frame node for the portal's timer. + -- + -- The timer event will need to know the portal definition, which can be determined by + -- what the portal frame is made from, so the timer node should be on the frame. + -- The timer event will also need to know its portal orientation, but unless someone + -- makes a cubic portal shape, orientation can be determined from p1 and p2 in the node's + -- metadata (frame nodes don't have orientation set in param2 like wormhole nodes do). + -- + -- We shouldn't pick p1 (or p2) as it's possible for two orthogonal portals to share + -- the same p1, etc. Or at least it was - there's code to try to stop that now. + -- + -- I'll pick the bottom center node of the portal, since that works for rectangular portals + -- and if someone want to make a circular portal then that positon will still likely be part + -- of the frame. + return { + x = math.floor((p1.x + p2.x) / 2), + y = p1.y, + z = math.floor((p1.z + p2.z) / 2), + } +end + +-- orientation is the rotation degrees passed to place_schematic: 0, 90, 180, or 270 +-- color is a value from 0 to 7 corresponding to the pixels in portals_palette.png +local function get_param2_from_color_and_orientation(color, orientation) + assert(orientation, "no orientation passed") + + -- wormhole nodes have a paramtype2 of colorfacedir, which means the + -- high 3 bits are palette, followed by 3 direction bits and 2 rotation bits. + -- We set the palette bits and rotation + return (orientation / 90) + color * 32 +end + +local function get_orientation_from_param2(param2) + -- Strip off the top 6 bits, unfortunately MT lua has no bitwise '&' + -- (high 3 bits are palette, followed by 3 direction bits then 2 rotation bits) + if param2 >= 128 then param2 = param2 - 128 end + if param2 >= 64 then param2 = param2 - 64 end + if param2 >= 32 then param2 = param2 - 32 end + if param2 >= 16 then param2 = param2 - 16 end + if param2 >= 8 then param2 = param2 - 8 end + + return param2 * 90 +end + +-- Combining frame_node_name, p1, and p2 will always be enough to uniquely identify a portal_definition +-- WITHOUT needing to inspect the world. register_portal() will enforce this. +-- This function does not require the portal to be in a loaded chunk. +-- Returns nil if no portal_definition matches the arguments +local function get_portal_definition(frame_node_name, p1, p2) + + local size = vector.add(vector.subtract(p2, p1), 1) + local rotated_size = {x = size.z, y = size.y, z = size.x} + + for _, portal_def in pairs(nether.registered_portals) do + if portal_def.frame_node_name == frame_node_name then + if vector.equals(size, portal_def.shape.size) or vector.equals(rotated_size, portal_def.shape.size) then + return portal_def + end + end + end + return nil +end + +-- Returns a list of all portal_definitions with a frame made of frame_node_name. +-- Ideally no two portal types will be built from the same frame material so this call might be enough +-- to uniquely identify a portal_definition without needing to inspect the world, HOWEVER we shouldn't +-- cramp anyone's style and prohibit non-nether use of obsidian to make portals, so it returns a list. +-- If the list contains more than one item then routines like ignite_portal() will have to search twice +-- for a portal and take twice the CPU. +local function list_portal_definitions_for_frame_node(frame_node_name) + local result = {} + for _, portal_def in pairs(nether.registered_portals) do + if portal_def.frame_node_name == frame_node_name then table.insert(result, portal_def) end + end + return result +end + + +local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite) + + + -- Portal position is stored in metadata as p1 and p2 to keep maps compatible with earlier versions of this mod. + -- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together + -- they define the bounding volume for the portal. + local p1, p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(anchorPos, orientation) + local p1_string, p2_string = minetest.pos_to_string(p1), minetest.pos_to_string(p2) + local param2 = get_param2_from_color_and_orientation(portal_definition.wormhole_node_color, orientation) + + local update_aborted-- using closures to allow the updateFunc to return extra information - by setting this variable + + local updateFunc = function(pos) + + local meta = minetest.get_meta(pos) + + if ignite then + local node_name = minetest.get_node(pos).name + if node_name == "air" then + minetest.set_node(pos, {name = portal_definition.wormhole_node_name, param2 = param2}) + end + + local existing_p1 = meta:get_string("p1") + if existing_p1 ~= "" then + local existing_p2 = meta:get_string("p2") + if DEBUG then minetest.chat_send_all("existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string) end + if existing_p1 ~= p1_string or existing_p2 ~= p2_string then + -- this node is already part of another portal, so extinguish that, because nodes only + -- contain a link in the metadata to one portal, and being part of two allows a slew of bugs + extinguish_portal(pos, node_name) + + -- clear the metadata to avoid causing a loop if extinguish_portal() fails on this node (e.g. it only works on frame nodes) + meta:set_string("p1", nil) + meta:set_string("p2", nil) + meta:set_string("target", nil) + meta:set_string("frame_node_name", nil) + + update_aborted = true + return true -- short-circuit the update + end + end + end + + 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)) + -- including "frame_node_name" in the metadata lets us know which kind of portal this is. + -- It's not strictly necessary for PortalShape_Traditional as we know that p1 is part of + -- the frame, and legacy portals don't have this extra metadata - indicating obsidian, + -- but p1 isn't always loaded so reading this from the metadata saves an extra call to + -- minetest.getnode(). + meta:set_string("frame_node_name", portal_definition.frame_node_name) + end + + repeat + update_aborted = false + portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, updateFunc) + portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, updateFunc) + until not update_aborted + + local timerPos = get_timerPos_from_p1_and_p2(p1, p2) + minetest.get_node_timer(timerPos):start(1) +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 to allow the check functions to return extra information - by setting this variable + local portal_is_ignited -- using closures to allow the check 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) -- check_frame_Func affects nodes_are_valid, portal_is_ignited + + 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) -- check_wormhole_Func affects nodes_are_valid, portal_is_ignited + 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 the 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; + + -- 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 + + +-- use this when determining where to spawn a portal, to avoid overwriting player builds +function nether.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) + local nodedef = minetest.registered_nodes[name] + if not nodedef.is_ground_content then + -- trees are natural but not "ground content" + local node_groups = nodedef.groups + if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil) then + return false + end + end + end + vi = vi + 1 + end + end + end + + return true +end + + +--[[ +"The normal realm portal has a particular X, Z, it searches downwards for a suitable Y. +It can't be placed in mid-air, and for performance the test for a suitable placement position cannot move downwards in +1 node steps, instead it moves downwards in 16 node steps, so it will almost always be placed buried in solid nodes. + +The portal cannot be placed in any volume that contains non-natural nodes (is_ground_content = false) to not grief +player builds. This makes it even more likely the portal will be a little way underground. + +The portal is placed with air nodes around it to create a space so it isn't embedded in stone. +It is expected that the player has a pickaxe to dig their way out, this is highly likely if they have built a portal +and are exploring the nether. The player will not be trapped. + +Note that MC also often places portals embedded in stone. + +The code could be [edit: has been] altered to first try to find a surface position, but if this surface position is +unsuitable due to being near player builds, the portal will still move downwards into the ground, so this is +unavoidable. + +Any search for a suitable resting-on-surface or resting-on-cave-surface position will be somewhat complex, to avoid +placement on a tiny floating island or narrow spike etc. which would be impractical or deadly to the player. + +A portal room embedded underground is the safest and the most accessible for the player. + +So i decided to start the placement position search at y = -16 as that, or a little below, is the most likely suitable +position: Ground is almost always present there, it's below any lakes or seas, below most player builds. + +Also, the search for non-natural nodes doesn't actually guarantee avoiding player builds, as a player build can be +composed of only natural nodes (is_ground_content = true). So even more good reason to start the search a little way +underground where player builds are more unlikely. Y = -16 seemed a reasonable compromise between safety and distance +from surface. + +Each placement position search has to search a volume of nodes for non-natural nodes, this is not lightweight, and many +searches may happen if there a lot of underground player builds present. So the code has been written to avoid +intensive procedures." +-- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676 +]] +function nether.find_surface_target_y(target_x, target_z, portal_name) + + -- try to spawn on surface first + if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this + local surface_level = minetest.get_spawn_level(target_x, target_z) + if surface_level ~= nil then + -- Check volume for non-natural nodes + local minp = {x = target_x - 1, y = surface_level - 1, z = target_z - 2} + local maxp = {x = target_x + 2, y = surface_level + 3, z = target_z + 2} + if nether.volume_is_natural(minp, maxp) then + return surface_level + end + end + end + + -- fallback to underground search + local start_y = -16 + 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 nether.volume_is_natural(minp, maxp) then + return y + elseif portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then + -- players have built here - don't grief. + -- but reigniting existing portals in portal rooms is fine - desirable even. + local anchorPos, orientation, is_ignited = is_portal_frame(nether.registered_portals[portal_name], {x = target_x, y = y, z = target_z}) + if anchorPos ~= nil then + return y + end + end + end + + return start_y - 256 -- Fallback +end + + +-- invoked when a player attempts to turn obsidian nodes into an open portal +-- ignition_node_name is optional +local function ignite_portal(ignition_pos, ignition_node_name) + + if ignition_node_name == nil then ignition_node_name = minetest.get_node(ignition_pos).name end + + -- find which sort of portals are made from the node that was clicked on + local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name) + + for _, portal_definition in ipairs(portal_definition_list) do + local continue = false + + -- 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_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end + continue = true -- no portal is here, but perhaps there more than one portal type we need to search for + 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 continue == false then + 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 + + local destination_anchorPos + if portal_definition.within_realm(ignition_pos) then + destination_anchorPos = portal_definition.find_surface_anchorPos(anchorPos) + else + destination_anchorPos = portal_definition.find_realm_anchorPos(anchorPos) + end + + local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, orientation) + if DEBUG then minetest.chat_send_all("Destinaton set to " .. minetest.pos_to_string(destination_anchorPos)) end + + -- ignition/BURN_BABY_BURN + set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos) + + return true + end + end +end + + +-- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos +extinguish_portal = function(pos, node_name) -- assigned rather than declared because extinguish_portal is already declared, for use by earlier functions in the file. + + -- mesecons seems to invoke action_off() 6 times every time you place a block? + if DEBUG then minetest.chat_send_all("extinguish_portal" .. minetest.pos_to_string(pos) .. " " .. node_name) end + + 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 p1 == nil or p2 == nil then + return + end + + local portal_definition = get_portal_definition(node_name, p1, p2) + if portal_definition == nil then + minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name) + return -- no portal frames are made from this type of node + end + local frame_node_name = portal_definition.frame_node_name + local wormhole_node_name = portal_definition.wormhole_node_name + + minetest.get_node_timer(get_timerPos_from_p1_and_p2(p1, p2)):stop() + + 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 == frame_node_name or nn == wormhole_node_name then + if nn == wormhole_node_name 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", "") + m:set_string("frame_node_name", "") + end + end + end + end + + if target ~= nil then extinguish_portal(target, node_name) end +end + + +-- Sometimes after a portal is placed, concurrent mapgen routines overwrite it. +-- Make portals immortal for ~20 seconds after creation +local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orientation, destination_wormholePos) + + local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) + local wormhole_node = minetest.get_node_or_nil(wormholePos) + + if wormhole_node == nil or wormhole_node.name ~= portal_definition.wormhole_node_name then + -- ruh roh + local message = "Newly created portal at " .. minetest.pos_to_string(anchorPos) .. " was overwritten. Attempting to recreate. Issue spotted after " .. elapsed .. " seconds" + minetest.log("warning", message) + if DEBUG then minetest.chat_send_all("!!! " .. message) end + + -- A pre-existing portal frame wouldn't have been immediately overwritten, so no need to check for one, just place the portal. + build_portal(portal_definition, anchorPos, orientation, destination_wormholePos) + end + + if elapsed < 20 then -- stop checking after 20 seconds + local delay = elapsed * 2 + minetest.after(delay, remote_portal_checkup, elapsed + delay, portal_definition, anchorPos, orientation, destination_wormholePos) + end +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 dest_wormhole_node = minetest.get_node_or_nil(destination_wormholePos) + + if dest_wormhole_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 dest_wormhole_node.name == portal_definition.wormhole_node_name then + -- portal exists + local destination_orientation = get_orientation_from_param2(dest_wormhole_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 " .. dest_wormhole_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 + ) + end + minetest.after(0.1, ensure_remote_portal_then_teleport, player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos) + + -- make sure portal isn't overwritten by ongoing generation/emerge + minetest.after(2, remote_portal_checkup, 2, portal_definition, new_dest_anchorPos, new_dest_orientation, local_wormholePos) + end + end +end + + +-- run_wormhole() is invoked once per second per portal, handling teleportation and particle effects. +-- See get_timerPos_from_p1_and_p2() for an explanation of where pos will be +function run_wormhole(pos, time_elapsed) + + local portal_definition -- will be used inside run_wormhole_node_func() + + local run_wormhole_node_func = function(pos) + + if math.random(2) == 1 then -- lets run only 3 particlespawners instead of 6 per portal + minetest.add_particlespawner({ + amount = 16, + time = 2, + 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.5, + minsize = 0.5, + maxsize = 1.5, + collisiondetection = false, + texture = portal_definition.particle_texture_colored, + glow = 5 + }) + end + + 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 + + -- 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(minetest.get_node(pos).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 + + local p1, p2, frame_node_name + local meta = minetest.get_meta(pos) + if meta ~= nil then + p1 = minetest.string_to_pos(meta:get_string("p1")) + p2 = minetest.string_to_pos(meta:get_string("p2")) + --frame_node_name = minetest.string_to_pos(meta:get_string("frame_node_name")) don't rely on this yet until you're sure everything works with old portals that don't have this set + end + if p1 ~= nil and p2 ~= nil then + -- look up the portal shape by what it's built from, so we know where the wormhole nodes will be located + if frame_node_name == nil then frame_node_name = minetest.get_node(pos).name end -- pos should be a frame node + portal_definition = get_portal_definition(frame_node_name, p1, p2) + if portal_definition == nil then + minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(pos) .. " was invoked but that location contains \"" .. frame_node_name .. "\"") + else + local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) + portal_definition.shape.apply_func_to_wormhole_nodes(anchorPos, orientation, run_wormhole_node_func) + end + end +end + + +minetest.register_lbm({ + label = "Start portal timer", + name = "nether:start_portal_timer", + nodenames = {"nether:portal"}, + run_at_every_load = false, + action = function(pos, node) + local p1, p2 + local meta = minetest.get_meta(pos) + if meta ~= nil then + p1 = minetest.string_to_pos(meta:get_string("p1")) + p2 = minetest.string_to_pos(meta:get_string("p1")) + end + if p1 ~= nil and p2 ~= nil then + local timerPos = get_timerPos_from_p1_and_p2(p1, p2) + local timer = minetest.get_node_timer(timerPos) + if timer ~= nil then + timer:start(1) + elseif DEBUG then + minetest.chat_send_all("get_node_timer" .. minetest.pos_to_string(timerPos) .. " returned null") + end + end + end +}) + + +function register_frame_node(frame_node_name) + + -- copy the existing node definition + local node = minetest.registered_nodes[frame_node_name] + local extended_node_def = {} + for key, value in pairs(node) do extended_node_def[key] = value end + + extended_node_def.replaced_by_portalapi = {} + + -- add portal portal functionality + extended_node_def.replaced_by_portalapi.mesecons = extended_node_def.mesecons + extended_node_def.mesecons = {effector = { + action_on = function (pos, node) + ignite_portal(pos, node.name) + end, + action_off = function (pos, node) + extinguish_portal(pos, node.name) + end + }} + extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct + extended_node_def.on_destruct = function(pos) + extinguish_portal(pos, frame_node_name) + end + extended_node_def.replaced_by_portalapi.on_timer = extended_node_def.on_timer + extended_node_def.on_timer = function(pos, elapsed) + run_wormhole(pos, elapsed) + return true + end + + -- replace the node with the new extended definition + minetest.register_node(":" .. frame_node_name, extended_node_def) +end + +function unregister_frame_node(frame_node_name) + + -- copy the existing node definition + local node = minetest.registered_nodes[frame_node_name] + local restored_node_def = {} + for key, value in pairs(node) do restored_node_def[key] = value end + + -- remove portal portal functionality + restored_node_def.mesecons = nil + restored_node_def.on_destruct = nil + restored_node_def.on_timer = nil + restored_node_def.replaced_by_portalapi = nil + + if node.replaced_by_portalapi ~= nil then + for key, value in pairs(node.replaced_by_portalapi) do restored_node_def[key] = value end + end + + -- replace the node with the restored definition + minetest.register_node(":" .. frame_node_name, restored_node_def) +end + + +-- check for mistakes people might make in custom shape definitions +function test_shapedef_is_valid(shape_defintion) + assert(shape_defintion ~= nil, "shape definition cannot be nil") + + local result = true + + -- todo + + return result +end + + + +-- check for mistakes people might make in portal definitions +function test_portaldef_is_valid(portal_definition) + + local result = test_shapedef_is_valid(portal_definition.shape) + + assert(portal_definition.wormhole_node_color >= 0 and portal_definition.wormhole_node_color < 8, "portaldef.wormhole_node_color must be between 0 and 7 (inclusive)") + assert(portal_definition.within_realm ~= nil, "portaldef.within_realm() must be implemented") + assert(portal_definition.find_realm_anchorPos ~= nil, "portaldef.find_realm_anchorPos() must be implemented") + -- todo + + return result +end + + +-- Portal API functions -- +-- ==================== -- + + +local portaldef_default = { + shape = PortalShape_Traditional, + wormhole_node_name = "nether:portal", + wormhole_node_color = 0, + frame_node_name = "default:obsidian", + particle_texture = "nether_particle.png", + sound_ambient = "nether_portal_hum", + sound_ignite = "", + sound_extinguish = "", + sound_teleport = "", +} + + + +function nether.register_portal(name, portaldef) + + assert(name ~= nil, "Unable to register portal: Name is nil") + assert(portaldef ~= nil, "Unable to register portal ''" .. name .. "'': portaldef is nil") + if nether.registered_portals[name] ~= nil then + minetest.log("error", "Unable to register portal: '" .. name .. "' is already in use") + return false; + end + + portaldef.name = name + + -- use portaldef_default for any values missing from portaldef + setmetatable(portaldef, {__index = portaldef_default}) + + if portaldef.particle_color == nil then + -- default the particle colours to be the same as the wormhole colour + assert(portaldef.wormhole_node_color >= 0 and portaldef.wormhole_node_color < 8, "portaldef.wormhole_node_color must be between 0 and 7 (inclusive)") + local rgb = nether.portals_palette[portaldef.wormhole_node_color] + portaldef.particle_color = minetest.rgba(rgb.r, rgb.g, rgb.b) + end + if portaldef.particle_texture_colored == nil then + -- Combine the particle texture with the particle color unless a colored particle texture was specified. + portaldef.particle_texture_colored = portaldef.particle_texture .. "^[colorize:" .. portaldef.particle_color .. ":alpha" + end + + if portaldef.find_surface_anchorPos == nil then -- default to using find_surface_target_y() + portaldef.find_surface_anchorPos = function(pos) + local surface_y = nether.find_surface_target_y(pos.x, pos.z, name) + return {x = pos.x, y = surface_y, z = pos.z} + end + end + + if test_portaldef_is_valid(portaldef) then + nether.registered_portals[portaldef.name] = portaldef + + if not is_frame_node[portaldef.frame_node_name] then + register_frame_node(portaldef.frame_node_name) + is_frame_node[portaldef.frame_node_name] = true + end + return true + end +end + +function nether.unregister_portal(name) + + assert(name ~= nil, "Cannot unregister portal: Name is nil") + + local portaldef = nether.registered_portals[name] + local result = portaldef ~= nil + + if portaldef ~= nil then + nether.registered_portals[name] = nil + + local portals_still_using_frame_node = list_portal_definitions_for_frame_node(portaldef.frame_node_name) + if next(portals_still_using_frame_node) == nil then + -- no portals are using this frame node any more + unregister_frame_node(portaldef.frame_node_name) + is_frame_node[portaldef.frame_node_name] = nil + end + end + + return result +end + +function nether.register_portal_ignition_item(item_name) + + minetest.override_item(item_name, { + on_place = function(stack, _, pt) + if pt.under and is_frame_node[minetest.get_node(pt.under).name] 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, + }) + +end \ No newline at end of file diff --git a/portal_api.txt b/portal_api.txt new file mode 100644 index 0000000..770ed5e --- /dev/null +++ b/portal_api.txt @@ -0,0 +1,84 @@ +Portal API Reference +==================== + +The portal system used to get to the Nether can be used to create portals +to other realms. + + + +Helper functions +---------------- + +* `nether.volume_is_natural(minp, maxp)` + * use this when determining where to spawn a portal, to avoid overwriting + player builds. It checks the area for any nodes that aren't ground or + trees. + +* `nether.find_surface_target_y(target_x, target_z, portal_name)` + * Can be used to implement custom find_surface_anchorPos() functions + * Providing the portal_name allows existing portals on the surface to be reused. + + + + +API functions +------------- + +Call these functions only at load time! + +* `nether.register_portal(name, portal definition)` + * Returns true on success. Can return false if the portal definition + clashes with a portal already registered by another mod, e.g. if the size + and frame node is not unique. +* `nether.unregister_portal(name)` + * Unregisters the portal from the engine, and deletes the entry with key + `name` from `nether.registered_portals` and associated internal tables. + * Returns true on success +* register_portal_ignition_item(name) + + + +Portal definition +----------------- + +Used by `nether.register_portal`. + + { + shape = PortalShape_Traditional, + -- optional. + + wormhole_node_name = "nether:portal", + -- optional. Allows a custom wormhole node to be specified. + -- Useful if you want the portals to have a different post_effect_color + -- or texture. + + wormhole_node_color = 0, + + particle_color = "#808", + -- Optional. Will default to the same colour as wormhole_node_color if + -- not specified. + + frame_node_name = "default:obsidian", + -- required + + + sound_ambient = "portal_hum", + sound_ignite = "", + sound_extinguish = "", + sound_teleport = "", + + within_realm = function(pos) + end, + + find_realm_anchorPos = function(pos) + end, + + find_surface_anchorPos = function(pos) + end, + + on_run_wormhole, + on_ignite, + on_extinguish, + on_player_teleported, + on_created + } diff --git a/textures/portal_palette.png b/textures/nether_portals_palette.png similarity index 100% rename from textures/portal_palette.png rename to textures/nether_portals_palette.png