diff --git a/nodes.lua b/nodes.lua index 1d495ad..e48a4f9 100644 --- a/nodes.lua +++ b/nodes.lua @@ -1,165 +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"}, - }, -}) - - +-- 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 index fa4aad3..6ed7b20 100644 --- a/portal_api.lua +++ b/portal_api.lua @@ -1,1671 +1,1671 @@ --- see portal_api.txt for documentation -local DEBUG = true - - -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 -} - - ---[[ - -Positions -========= - -p1 & p2 p1 and p2 is the system used by earlier versions of the nether mod, which the portal_api - is forwards and backwards compatible with. - 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. - The value of p1 and p2 is kept in the metadata of every node in the portal - -WormholePos The location of the node a portal's target is set to, and a player is teleported - to. It can also be used to test whether a portal is active. - -AnchorPos Introduced by the portal_api, this should - Usually an orientation is required with an AnchorPos - -TimerPos The portal_api replaces ABMs with a single node timer per portal, and the TimerPos is the - node in which that timer is located. Extra metadata is also kept in the TimerNode. - - -Portal shapes -============= - - - For the PortalShape_Traditional implementation, anchorPos, wormholdPos and TimerPos 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(). Examples follow --- after PortalShape_Traditional. --- Since it's symmetric, this PortalShape definition has only implemented orientations of 0 and 90 -nether.PortalShape_Traditional = { - name = "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 = nether.path .. "/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") - assert(self ~= nil and self.name == nether.PortalShape_Traditional.name, "Must pass self as first argument, or use shape:func() instead of shape.func()") - 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, - - -- returns true if function was applied to all frame nodes - 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, - - -- returns true if function was applied to all wormhole nodes - 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 - - --- Example alternative PortalShape -nether.PortalShape_Circular = { - name = "Circular", - size = vector.new(7, 7, 1), -- size of the portal, and not necessarily the size of the schematic, - -- which may clear area around the portal. - schematic_filename = nether.path .. "/schematics/nether_portal_circular.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 - 3, y = anchorPos.y, z = anchorPos.z - 3} - else - return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3 } - end - end, - - get_wormholePos_from_anchorPos = function(anchorPos, orientation) - -- wormholePos is the node above anchorPos - return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z} - end, - - get_anchorPos_from_wormholePos = function(wormholePos, orientation) - -- wormholePos is the node above anchorPos - return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z} - 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") - assert(self ~= nil and self.name == nether.PortalShape_Circular.name, "Must pass self as first argument, or use shape:func() instead of shape.func()") - local p1 = anchorPos -- PortalShape_Traditional puts the anchorPos at p1 for backwards&forwards compatibility - local p2 - - if orientation == 0 then - p1 = {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z } - p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z } - else - p1 = {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 3 } - 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 {x= p1.x + 3, y = p1.y, z = p1.z }, 0 - elseif p1.x == p2.x then - return {x= p1.x, y = p1.y, z = p1.z + 3}, 90 - end - end, - - apply_func_to_frame_nodes = function(anchorPos, orientation, func) - local shortCircuited - if orientation == 0 then - -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true - shortCircuited = - func({x = anchorPos.x + 0, y = anchorPos.y + 0, z = anchorPos.z}) or - func({x = anchorPos.x + 1, y = anchorPos.y + 0, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 0, z = anchorPos.z}) or - func({x = anchorPos.x + 2, y = anchorPos.y + 1, z = anchorPos.z}) or func({x = anchorPos.x - 2, 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 + 2, z = anchorPos.z}) or - func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z}) or func({x = anchorPos.x - 3, y = anchorPos.y + 3, z = anchorPos.z}) or - func({x = anchorPos.x + 3, 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 + 2, y = anchorPos.y + 5, z = anchorPos.z}) or func({x = anchorPos.x - 2, y = anchorPos.y + 5, z = anchorPos.z}) or - func({x = anchorPos.x + 1, y = anchorPos.y + 6, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 6, z = anchorPos.z}) or - func({x = anchorPos.x + 0, y = anchorPos.y + 6, z = anchorPos.z}) - else - shortCircuited = - func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 0}) or - func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z - 1}) or - func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z - 2}) or - func({x = anchorPos.x, y = anchorPos.y + 2, 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}) or func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z - 3}) or - func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z - 3}) or - func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z - 2}) or - func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z - 1}) or - func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 0}) - end - return not shortCircuited - end, - - -- returns true if function was applied to all wormhole nodes - apply_func_to_wormhole_nodes = function(anchorPos, orientation, func) - local xRange = 2 - local zRange = 0 - if orientation ~= 0 then - xRange = 0 - zRange = 2 - end - - local xEdge, yEdge, zEdge - local pos = {} - for x = -xRange, xRange do - pos.x = anchorPos.x + x - xEdge = x == -xRange or x == xRange - for z = -zRange, zRange do - zEdge = z == -zRange or z == zRange - pos.z = anchorPos.z + z - for y = 1, 5 do - yEdge = y == 1 or y == 5 - if not (yEdge and xEdge and zEdge) then - pos.y = anchorPos.y + y - if func(pos) then - -- func() caused an abort by returning true - return false - end - end - end - end - end - - return true - 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. - end -} -- End of PortalShape_Circular 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 = {} -local ignition_item_name -local S = nether.get_translator -local mod_storage = minetest.get_mod_storage() -local malleated_filenames = {} - - -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 color of pixels in nether_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 - - --- Add portal information to mod storage, so new portals may find existing portals near the target location. --- Do this whenever a portal is created or changes its ignition state -local function store_portal_location_info(portal_name, anchorPos, orientation, ignited) - mod_storage:set_string( - minetest.pos_to_string(anchorPos) .. " is " .. portal_name, - minetest.serialize({orientation = orientation, active = ignited}) - ) -end - --- Remove portal information from mod storage. --- Do this if a portal frame is destroyed such that it cannot be ignited anymore. -local function remove_portal_location_info(portal_name, anchorPos) - mod_storage:set_string(minetest.pos_to_string(anchorPos) .. " is " .. portal_name, "") -end - --- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage --- data. --- Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0. --- WARNING: Portals are not checked, and inactive portals especially may have been damaged without --- being removed from the mod_storage data. Check these portals still exist before using them, and --- invoke remove_portal_location_info() on any found to no longer exist. --- --- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included, --- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. --- Pass a nil or negative distance_limit to indicate no distance limit -local function list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor) - - local result = {} - - local isRealm = portal_definition.is_within_realm(anchorPos) - if distance_limit == nil then distance_limit = -1 end - if y_factor == nil then y_factor = 1 end - - for key, value in pairs(mod_storage:to_table().fields) do - local closingBrace = key:find(")", 6, true) - if closingBrace ~= nil then - local found_anchorPos = minetest.string_to_pos(key:sub(0, closingBrace)) - if found_anchorPos ~= nil and portal_definition.is_within_realm(found_anchorPos) == isRealm then - local found_name = key:sub(closingBrace + 5) - if found_name == portal_definition.name then - local x = anchorPos.x - found_anchorPos.x - local y = anchorPos.y - found_anchorPos.y - local z = anchorPos.z - found_anchorPos.z - local distance = math.hypot(y * y_factor, math.hypot(x, z)) - if distance <= distance_limit or distance_limit < 0 then - local info = minetest.deserialize(value) or {} - if DEBUG then minetest.chat_send_all("found " .. found_name .. " listed at distance " .. distance .. " from dest " .. minetest.pos_to_string(anchorPos) .. ", found: " .. minetest.pos_to_string(found_anchorPos) .. " orientation " .. info.orientation) end - info.anchorPos = found_anchorPos - info.distance = distance - result[distance] = info - end - end - end - end - end - return result -end - - --- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run --- see also ambient_sound_stop() -function ambient_sound_play(portal_definition, soundPos, timerNodeMeta) - if portal_definition.sounds.ambient ~= nil then - local soundLength = portal_definition.sounds.ambient.length - if soundLength == nil then soundLength = 3 end - local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played") - - -- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed - if os.time() >= lastPlayed + soundLength then - local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 8}) - if timerNodeMeta ~= nil then - timerNodeMeta:set_int("ambient_sound_handle", soundHandle) - timerNodeMeta:set_int("ambient_sound_last_played", os.time()) - end - end - end -end - --- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run --- see also ambient_sound_play() -function ambient_sound_stop(timerNodeMeta) - if timerNodeMeta ~= nil then - local soundHandle = timerNodeMeta:get_int("ambient_sound_handle") - minetest.sound_fade(soundHandle, -3, 0) - - -- clear the metadata - timerNodeMeta:set_string("ambient_sound_handle", "") - timerNodeMeta:set_string("ambient_sound_last_played", "") - end -end - - --- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos -function extinguish_portal(pos, node_name, frame_was_destroyed) - - -- 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 - if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end - 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 - - if portal_definition.sounds.extinguish ~= nil then - minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1}) - end - - -- stop timer and ambient sound - local timerPos = get_timerPos_from_p1_and_p2(p1, p2) - minetest.get_node_timer(timerPos):stop() - ambient_sound_stop(minetest.get_meta(timerPos)) - - -- update the ignition state in the portal location info - local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) - if frame_was_destroyed then - remove_portal_location_info(portal_definition.name, anchorPos) - else - store_portal_location_info(portal_definition.name, anchorPos, orientation, false) - end - - local frame_node_name = portal_definition.frame_node_name - local wormhole_node_name = portal_definition.wormhole_node_name - - - 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("portal_type", "") - end - end - end - end - - if target ~= nil then - if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end - extinguish_portal(target, node_name) - end - - if portal_definition.on_extinguish ~= nil then - portal_definition.on_extinguish(portal_definition, anchorPos, orientation) - end -end - - - - --- Note: will extinguish any portal using the same nodes that are being set -local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite) - - if DEBUG then minetest.chat_send_all("set_portal_metadata(ignite=" .. tostring(ignite) .. ") at " .. minetest.pos_to_string(anchorPos) .. " orient " .. orientation .. ", setting to target " .. minetest.pos_to_string(destination_wormholePos)) end - - -- 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 existing_p1 ~= p1_string or existing_p2 ~= p2_string then - if DEBUG then minetest.chat_send_all("set_portal_metadata() found existing metadata from another portal: existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string .. ", will existinguish existing portal...") end - -- 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, false) - - -- 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("portal_type", 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)) - - if portal_definition.name ~= "nether_portal" then - -- Legacy portals won't have this extra metadata, so don't rely on it. - -- It's not strictly necessary for PortalShape_Traditional as we know that p1 is part of - -- the frame and we can look up the portal type from p1, p2, and frame node name. - -- Being able to read this from the metadata means other portal shapes needn't have their - -- frame at the timerPos, it may handle unloaded nodes better, and it saves an extra call - -- to minetest.getnode(). - meta:set_string("portal_type", portal_definition.name) - end - 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) - - store_portal_location_info(portal_definition.name, anchorPos, orientation, true) -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 - - --- this function returns two bools: portal found, portal is lit -local function is_portal_at_anchorPos(portal_definition, anchorPos, orientation, force_chunk_load) - - 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) - local foundName = minetest.get_node(check_pos).name - if foundName ~= frame_node_name then - - if force_chunk_load and foundName == "ignore" then - -- area isn't loaded, force loading/emerge of check area - minetest.get_voxel_manip():read_from_map(check_pos, check_pos) - foundName = minetest.get_node(check_pos).name - if DEBUG then minetest.chat_send_all("Forced loading of 'ignore' node at " .. minetest.pos_to_string(check_pos) .. ", got " .. foundName) end - - if foundName ~= frame_node_name then - nodes_are_valid = false - return true -- short-circuit the search - end - else - nodes_are_valid = false - return true -- short-circuit the search - end - 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 - - 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 - --- 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_within_portal_frame(portal_definition, pos) - - 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(portal_definition, testAnchorPos_x, 0, true) - - 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(portal_definition, testForAnchorPos_z, 90, true) - - 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.schematic_filename, - orientation, - { -- node replacements - ["default:obsidian"] = portal_definition.frame_node_name, - ["nether:portal"] = portal_definition.wormhole_node_name - }, - true - ) - -- set the param2 on wormhole nodes to ensure they are the right color - local wormholeNode = { - name = portal_definition.wormhole_node_name, - param2 = get_param2_from_color_and_orientation(portal_definition.wormhole_node_color, orientation) - } - portal_definition.shape.apply_func_to_wormhole_nodes( - anchorPos, - orientation, - function(pos) minetest.swap_node(pos, wormholeNode) end - ) - - if DEBUG then minetest.chat_send_all("Placed " .. portal_definition.name .. " 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) - - if portal_definition.on_created ~= nil then - portal_definition.on_created(portal_definition, anchorPos, orientation) - 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) - - if DEBUG then minetest.chat_send_all("portal checkup at " .. elapsed .. " seconds") end - - 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 < 10 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 - - --- Used to find or build the remote twin after a portal is opened. --- If a portal is found that is already lit then it will be extinguished first and its destination_wormholePos updated, --- this is to enforce that portals only link together in mutual pairs. It would be better for gameplay if I didn't apply --- that restriction, but it would require maintaining an accurate list of every portal that links to a portal so they --- could be updated if the portal is destroyed. To keep the code simple I'm going to limit portals to only being the --- destination of one lit portal at a time. --- * suggested_wormholePos indicates where the portal should be built - note this not an anchorPos! --- * suggested_orientation is the suggested schematic rotation to use if no useable portal is found at suggested_wormholePos: --- 0, 90, 180, 270 (0 meaning a portal that faces north/south - i.e. obsidian running east/west) --- * destination_wormholePos is the wormholePos of the destination portal this one will be linked to. --- --- 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_wormholePos, suggested_orientation, destination_wormholePos) - - if DEBUG then minetest.chat_send_all("locate_or_build_portal() called at wormholePos" .. minetest.pos_to_string(suggested_wormholePos) .. " with suggested orient " .. suggested_orientation .. ", targetted to " .. minetest.pos_to_string(destination_wormholePos)) end - - local result_anchorPos; - local result_orientation; - - -- Searching for an existing portal at wormholePos seems better than at anchorPos, though isn't important - local found_anchorPos, found_orientation, is_ignited = is_within_portal_frame(portal_definition, suggested_wormholePos) -- can be optimized - check for portal at exactly suggested_wormholePos first - - 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 unnecessary: already a lit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation .. ". Extinguishing...") end - extinguish_portal(found_anchorPos, portal_definition.frame_node_name, false) - else - if DEBUG then minetest.chat_send_all(" Build unnecessary: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end - end - -- ignite the portal - set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos) - - else - result_orientation = suggested_orientation - result_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(suggested_wormholePos, result_orientation) - build_portal(portal_definition, result_anchorPos, result_orientation, destination_wormholePos) - -- make sure portal isn't overwritten by ongoing generation/emerge - minetest.after(2, remote_portal_checkup, 2, portal_definition, result_anchorPos, result_orientation, destination_wormholePos) - end - return result_anchorPos, result_orientation -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 - if DEBUG then minetest.chat_send_all("IGNITE the " .. ignition_node_name .. " at " .. minetest.pos_to_string(ignition_pos)) 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_within_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, destination_orientation - if portal_definition.is_within_realm(ignition_pos) then - destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos) - else - destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos) - end - if DEBUG and destination_orientation == nil then minetest.chat_send_all("No destination_orientation given") end - if destination_orientation == nil then destination_orientation = orientation end - - local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation) - if DEBUG then minetest.chat_send_all("Destination set to " .. minetest.pos_to_string(destination_anchorPos)) end - - -- ignition/BURN_BABY_BURN - set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos) - - if portal_definition.sounds.ignite ~= nil then - local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) - minetest.sound_play(portal_definition.sounds.ignite, {pos = local_wormholePos, max_hear_distance = 20}) - end - - if portal_definition.on_ignite ~= nil then - portal_definition.on_ignite(portal_definition, anchorPos, orientation) - end - - return true - end - 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) - local local_p1, local_p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(local_anchorPos, local_orientation) - local p1_at_playerPos = minetest.string_to_pos(meta:get_string("p1")) - if p1_at_playerPos == nil or not vector.equals(local_p1, p1_at_playerPos) 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 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) - local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, destination_orientation) - portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation) - - -- if the portal is already linked to a different portal then extinguish the other portal and - -- update the target portal to point back at this one. - local remoteMeta = minetest.get_meta(destination_wormholePos) - local remoteTarget = minetest.string_to_pos(remoteMeta:get_string("target")) - if remoteTarget == nil then - if DEBUG then minetest.chat_send_all("Failed to test whether target portal links back to this one") end - elseif not vector.equals(remoteTarget, local_wormholePos) then - if DEBUG then minetest.chat_send_all("Target portal is already linked, extinguishing then relighting to point back at this one") end - extinguish_portal(remoteTarget, portal_definition.frame_node_name, false) - set_portal_metadata_and_ignite( - portal_definition, - destination_anchorPos, - destination_orientation, - local_wormholePos - ) - end - - if DEBUG then minetest.chat_send_all("Teleporting player from wormholePos" .. minetest.pos_to_string(local_wormholePos) .. " to wormholePos" .. minetest.pos_to_string(destination_wormholePos)) end - - -- play the teleport sound - if portal_definition.sounds.teleport ~= nil then - minetest.sound_play(portal_definition.sounds.teleport, {to_player = player.name}) - end - - -- 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} - local new_playerPos = vector.add(destination_wormholePos, rotated_offset) - player:setpos(new_playerPos) - player:set_look_horizontal(player:get_look_horizontal() + rotation_angle) - - if portal_definition.on_player_teleported ~= nil then - portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos) - end - else - -- no wormhole node at destination - destination portal either needs to be built or ignited. - -- Note: A very rare edge-case that is difficult to set up: - -- If the destination portal is unlit and its frame shares a node with a lit portal that is linked to this - -- portal (but has not been travelled through, thus not linking this portal back to it), then igniting - -- the destination portal will extinguish the portal it's touching, which will extinguish this portal - -- which will leave a confused player. - -- I don't think this is worth preventing, but I document it incase someone describes entering a portal - -- and then the portal turning off. - 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_wormholePos, local_orientation, local_wormholePos) - local new_dest_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation) - - if not vector.equals(destination_wormholePos, new_dest_wormholePos) then - -- Update the local portal's target to match where the existing remote portal was found - destination_wormholePos = new_dest_wormholePos - if DEBUG then minetest.chat_send_all(" updating target to where remote portal was found - " .. 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) - 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 the timerPos location -function run_wormhole(timerPos, 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.7, - minsize = 0.5 * portal_definition.particle_texture_scale, - maxsize = 1.5 * portal_definition.particle_texture_scale, - collisiondetection = false, - texture = portal_definition.particle_texture_colored, - animation = portal_definition.particle_texture_animation, - 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")) - local local_p2 = minetest.string_to_pos(meta:get_string("p2")) - if destination_wormholePos ~= nil and local_p1 ~= nil and local_p2 ~= nil then - - -- force emerge of target area - minetest.get_voxel_manip():read_from_map(destination_wormholePos, destination_wormholePos) -- force load - if minetest.get_node_or_nil(destination_wormholePos) == nil then - minetest.emerge_area(vector.subtract(destination_wormholePos, 4), vector.add(destination_wormholePos, 4)) - end - - local local_anchorPos, local_orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(local_p1, local_p2) - minetest.after( - 3, -- hopefully target area is emerged in 3 seconds - function() - ensure_remote_portal_then_teleport( - obj, - portal_definition, - local_anchorPos, - local_orientation, - destination_wormholePos - ) - end - ) - end - end - end - end - - local p1, p2, frame_node_name - local meta = minetest.get_meta(timerPos) - if meta ~= nil then - p1 = minetest.string_to_pos(meta:get_string("p1")) - p2 = minetest.string_to_pos(meta:get_string("p2")) - portal_name = minetest.string_to_pos(meta:get_string("portal_type")) -- 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 - -- figure out the portal shape so we know where the wormhole nodes will be located - local frame_node_name - if portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then - portal_definition = nether.registered_portals[portal_name] - else - frame_node_name = minetest.get_node(timerPos).name -- timerPos should be a frame node if the shape is traditionalPortalShape - portal_definition = get_portal_definition(frame_node_name, p1, p2) - end - - if portal_definition == nil then - minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(timerPos) .. " 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) - - if portal_definition.on_run_wormhole ~= nil then - portal_definition.on_run_wormhole(portal_definition, anchorPos, orientation) - end - - local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) - ambient_sound_play(portal_definition, wormholePos, meta) - end - end -end - - -local function create_book(item_name, inventory_description, inventory_image, title, author, page1_text, page2_text) - - local display_book = function(itemstack, user, pointed_thing) - local player_name = user:get_player_name() - - minetest.sound_play("nether_book_open", {to_player = player_name, gain = 0.25}) - - local formspec = - "size[18,12.122]" .. - - "label[3.1,0.5;" .. minetest.formspec_escape(title) .. "]" .. - "label[3.6,0.9;" .. author .. "]" .. - - "textarea[ 0.9,1.7;7.9,12.0;;" .. minetest.formspec_escape(page1_text) .. ";]" .. - "textarea[10.1,0.8;7.9,12.9;;" .. minetest.formspec_escape(page2_text) .. ";]" .. - - "background[0,0;18,11;nether_book_background.png;true]".. - "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]" - - minetest.show_formspec(player_name, item_name, formspec) - end - - minetest.register_craftitem(item_name, { - description = inventory_description, - inventory_image = inventory_image, - groups = {book = 1}, - on_use = display_book - }) -end - --- Updates nether:book_of_portals --- A book the player can read to lean how to build the different portals -local function create_book_of_portals() - - local page1_text - local page2_text = "" - - -- tell the player how many portal types there are - local portalCount = 0 - for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end - if portalCount == 1 then - page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only one can I confirm as being more than merely a story.") - else - page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only @1 can I confirm as being more than merely stories.", portalCount) - end - - -- tell the player how to ignite portals - local ignition_item_description = "" - if ignition_item_name ~= nil and minetest.registered_items[ignition_item_name] ~= nil then - ignition_item_description = minetest.registered_items[ignition_item_name].description - end - page1_text = page1_text .. - S("\n\nThe key to opening such a doorway is to strike the frame with a @1, at which point the very air inside begins to crackle and glow.\n\n\n", string.lower(ignition_item_description)) - - -- Describe how to create each type of portal, or perhaps just give clues or flavor text, - -- but ensure the Nether is always listed first on the first page so other definitions can - -- refer to it (pairs() returns order based on a random hash). - local i = 1 - if nether.registered_portals["nether_portal"] then - page1_text = page1_text .. nether.registered_portals["nether_portal"].book_of_portals_pagetext .. "\n\n\n" - i = i + 1 - end - for portalName, portalDef in pairs(nether.registered_portals) do - if portalName ~= "nether_portal" then - if i <= portalCount / 2 then - page1_text = page1_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" - else - page2_text = page2_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" - end - i = i + 1 - end - end - - create_book( - "nether:book_of_portals", - S("Book of Portals"), - "nether_book_of_portals.png", - S("A treatise on Rifts and Portals"), - "Riccard F. Burton", -- perhaps a Richard F. Burton of an alternate universe - page1_text, - page2_text - ) -end - --- This is hack to work around how place_schematic() never invalidates its cache. --- A unique schematic filename is generated for each unique set of node replacements -function get_malleated_schematic_filename(portal_definition) - - local result - - if portal_definition.shape ~= nil and portal_definition.shape.schematic_filename ~= nil then - - local schematicFileName = portal_definition.shape.schematic_filename - local uniqueId = portal_definition.frame_node_name .. " " .. portal_definition.wormhole_node_name - - if malleated_filenames[schematicFileName] == nil then malleated_filenames[schematicFileName] = {} end - local filenamesForSchematic = malleated_filenames[schematicFileName] - - -- Split the schematic's filename into the path and filename - local lastSlashPos, _ = schematicFileName:find("/[^/]+$") -- find the rightmost slash - local lastBackslashPos, _ = schematicFileName:find("\\[^\\]+$") -- find the rightmost backslash - if lastSlashPos == nil then lastSlashPos = -1 end - if lastBackslashPos ~= nil then lastSlashPos = math.max(lastSlashPos, lastBackslashPos) end - local part_path = schematicFileName:sub(0, math.max(0, lastSlashPos - 1)) - local part_filename = schematicFileName:sub(lastSlashPos + 1) - - if filenamesForSchematic[uniqueId] == nil then - - local malleationCount = 0 - for _ in pairs(filenamesForSchematic) do malleationCount = malleationCount + 1 end - - local malleatedFilename = part_path .. DIR_DELIM - for i = 1, malleationCount do - malleatedFilename = malleatedFilename .. '.' .. DIR_DELIM -- should work on both Linux and Windows - end - malleatedFilename = malleatedFilename .. part_filename - filenamesForSchematic[uniqueId] = malleatedFilename - end - result = filenamesForSchematic[uniqueId] - end - - return result -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 = {} -- allows chaining or restoration of original functions, if necessary - - -- 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) - if DEBUG then minetest.chat_send_all("portal frame material: mesecons action ON") end - ignite_portal(pos, node.name) - end, - action_off = function (pos, node) - if DEBUG then minetest.chat_send_all("portal frame material: mesecons action OFF") end - extinguish_portal(pos, node.name, false) - end - }} - extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct - extended_node_def.on_destruct = function(pos) - if DEBUG then minetest.chat_send_all("portal frame material: destruct") end - extinguish_portal(pos, frame_node_name, true) - 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") - assert(shape_defintion.name ~= nil, "shape definition must have a name") - - local result = true - - local origin = vector.new() - local p1, p2 = shape_defintion:get_p1_and_p2_from_anchorPos(origin, 0) - assert(vector.equals(shape_defintion.size, vector.add(vector.subtract(p2, p1), 1)), "p1 and p2 of shape definition '" .. shape_defintion.name .. "' don't match shapeDef.size") - - -- 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.is_within_realm ~= nil, "portaldef.is_within_realm() must be implemented") - assert(portal_definition.find_realm_anchorPos ~= nil, "portaldef.find_realm_anchorPos() must be implemented") - -- todo - - return result -end - - --- convert portals made with old ABM version of nether mod to use the timer instead -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 -}) - -minetest.register_on_mods_loaded(function() - - -- Make the Book of Portals available as treasure/loot - if nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 and minetest.registered_items["nether:book_of_portals"] ~= nil then - - -- All portals should be registered now. - -- If the Nether is the only registered portal then lower the amount of these books - -- found as treasure, since many players already know the shape of a Nether portal - -- and what to build one out of, so would probably prefer other treasures. - local portalCount = 0 - for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end - local weight_adjust = 1 - if portalCount <= 1 then weight_adjust = 0.5 end - - if minetest.get_modpath("loot") then - loot.register_loot({ - weights = { generic = nether.PORTAL_BOOK_LOOT_WEIGHTING * 1000 * weight_adjust, - books = 100 }, - payload = { stack = "nether:book_of_portals" } - }) - end - - if minetest.get_modpath("dungeon_loot") then - dungeon_loot.register({name = "nether:book_of_portals", chance = nether.PORTAL_BOOK_LOOT_WEIGHTING * weight_adjust}) - end - - -- todo: add to Treasurer mod TRMP https://github.com/poikilos/trmp_minetest_game - -- todo: add to help modpack https://forum.minetest.net/viewtopic.php?t=15912 - end -end) - - --- Portal API functions -- --- ==================== -- - - --- The fallback defaults for registered portaldef tables -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", - particle_texture_animation = nil, - particle_texture_scale = 1, - sounds = { - ambient = {name = "nether_portal_ambient", gain = 0.6, length = 3}, - ignite = {name = "nether_portal_ignite", gain = 0.7}, - extinguish = {name = "nether_portal_extinguish", gain = 0.6}, - teleport = {name = "nether_portal_teleport", gain = 0.3} - } -} - - -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 - portaldef.mod_name = minetest.get_current_modname() - - -- use portaldef_default for any values missing from portaldef or portaldef.sounds - if portaldef.sounds ~= nil then setmetatable(portaldef.sounds, {__index = portaldef_default.sounds}) end - setmetatable(portaldef, {__index = portaldef_default}) - - portaldef.schematic_filename = get_malleated_schematic_filename(portaldef) - - 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 particle_texture_colored was specified. - if type(portaldef.particle_texture) == "table" and portaldef.particle_texture.animation ~= nil then - portaldef.particle_texture_colored = portaldef.particle_texture.name .. "^[colorize:" .. portaldef.particle_color .. ":alpha" - portaldef.particle_texture_animation = portaldef.particle_texture.animation - portaldef.particle_texture_scale = portaldef.particle_texture.scale or 1 - else - portaldef.particle_texture_colored = portaldef.particle_texture .. "^[colorize:" .. portaldef.particle_color .. ":alpha" - end - 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 - - -- check whether the portal definition clashes with anyone else's portal - local p1, p2 = portaldef.shape:get_p1_and_p2_from_anchorPos(vector.new(), 0) - local existing_portaldef = get_portal_definition(portaldef.frame_node_name, p1, p2) - if existing_portaldef ~= nil then - minetest.log("error", - portaldef.mod_name .." tried to register a portal '" .. portaldef.name .. "' made of " .. portaldef.frame_node_name .. - ", but it is the same material and shape as the portal '" .. existing_portaldef.name .. "' already registered by " .. existing_portaldef.mod_name .. - ". Edit the values one of those mods uses in its call to nether.register_portal() if you wish to resolve this clash.") - else - -- the new portaldef is good - nether.registered_portals[portaldef.name] = portaldef - create_book_of_portals() - - if not is_frame_node[portaldef.frame_node_name] then - -- add portal functions to the nodedef being used for the portal frame - register_frame_node(portaldef.frame_node_name) - is_frame_node[portaldef.frame_node_name] = true - end - - return true - end - end - - return false -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, ignition_failure_sound) - - minetest.override_item(item_name, { - on_place = function(stack, _, pt) - local done = false - if pt.under and is_frame_node[minetest.get_node(pt.under).name] then - done = ignite_portal(pt.under) - if done and not minetest.settings:get_bool("creative_mode") then - stack:take_item() - end - end - if not done and ignition_failure_sound ~= nil then - minetest.sound_play(ignition_failure_sound, {pos = pt.under, max_hear_distance = 10}) - end - - - return stack - end, - }) - - ignition_item_name = item_name -end - --- 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. -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 - --- Can be used when implementing custom find_surface_anchorPos() functions --- portal_name is optional, providing it allows existing portals on the surface to be reused. -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 - -- get_spawn_level() seems to err on the side of caution and sometimes spawn the player a - -- block higher than the ground level. - local shouldBeGroundPos = {x = target_x, y = surface_level - 1, z = target_z} - local groundNode = minetest.get_node_or_nil(shouldBeGroundPos) - if groundNode == nil then - -- force the area to be loaded - it's going to be loaded anyway by volume_is_natural() - minetest.get_voxel_manip():read_from_map(shouldBeGroundPos, shouldBeGroundPos) - groundNode = minetest.get_node(shouldBeGroundPos) - end - if not groundNode.is_ground_content then - surface_level = surface_level - 1 - end - -- 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_within_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 - - --- Returns the anchorPos, orientation of the nearest portal, or nil. --- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included, --- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. --- Pass a negative distance_limit to indicate no distance limit -function nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor) - - local portal_definition = nether.registered_portals[portal_name] - assert(portal_definition ~= nil, "find_nearest_working_portal() called with portal_name '" .. portal_name .. "', but no portal is registered with that name.") - - local contenders = list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor) - - -- sort by distance - local dist_list = {} - for dist, _ in pairs(contenders) do table.insert(dist_list, dist) end - table.sort(dist_list) - - for _, dist in ipairs(dist_list) do - local portal_info = contenders[dist] - if DEBUG then minetest.chat_send_all("checking portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end - - -- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been - -- destroyed, so check the portal is still there - local portalFound, portalActive = is_portal_at_anchorPos(portal_definition, portal_info.anchorPos, portal_info.orientation, true) - - if portalFound then - return portal_info.anchorPos, portal_info.orientation - else - if DEBUG then minetest.chat_send_all("removing portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end - -- The portal at that location must have been destroyed - remove_portal_location_info(portal_name, portal_info.anchorPos) - end - end - return nil -end +-- 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 +} + + +--[[ + +Positions +========= + +p1 & p2 p1 and p2 is the system used by earlier versions of the nether mod, which the portal_api + is forwards and backwards compatible with. + 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. + The value of p1 and p2 is kept in the metadata of every node in the portal + +WormholePos The location of the node a portal's target is set to, and a player is teleported + to. It can also be used to test whether a portal is active. + +AnchorPos Introduced by the portal_api, this should + Usually an orientation is required with an AnchorPos + +TimerPos The portal_api replaces ABMs with a single node timer per portal, and the TimerPos is the + node in which that timer is located. Extra metadata is also kept in the TimerNode. + + +Portal shapes +============= + + + For the PortalShape_Traditional implementation, anchorPos, wormholdPos and TimerPos 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(). Examples follow +-- after PortalShape_Traditional. +-- Since it's symmetric, this PortalShape definition has only implemented orientations of 0 and 90 +nether.PortalShape_Traditional = { + name = "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 = nether.path .. "/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") + assert(self ~= nil and self.name == nether.PortalShape_Traditional.name, "Must pass self as first argument, or use shape:func() instead of shape.func()") + 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, + + -- returns true if function was applied to all frame nodes + 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, + + -- returns true if function was applied to all wormhole nodes + 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 + + +-- Example alternative PortalShape +nether.PortalShape_Circular = { + name = "Circular", + size = vector.new(7, 7, 1), -- size of the portal, and not necessarily the size of the schematic, + -- which may clear area around the portal. + schematic_filename = nether.path .. "/schematics/nether_portal_circular.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 - 3, y = anchorPos.y, z = anchorPos.z - 3} + else + return {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z - 3 } + end + end, + + get_wormholePos_from_anchorPos = function(anchorPos, orientation) + -- wormholePos is the node above anchorPos + return {x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z} + end, + + get_anchorPos_from_wormholePos = function(wormholePos, orientation) + -- wormholePos is the node above anchorPos + return {x = wormholePos.x, y = wormholePos.y - 1, z = wormholePos.z} + 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") + assert(self ~= nil and self.name == nether.PortalShape_Circular.name, "Must pass self as first argument, or use shape:func() instead of shape.func()") + local p1 = anchorPos -- PortalShape_Traditional puts the anchorPos at p1 for backwards&forwards compatibility + local p2 + + if orientation == 0 then + p1 = {x = anchorPos.x - 3, y = anchorPos.y, z = anchorPos.z } + p2 = {x = p1.x + self.size.x - 1, y = p1.y + self.size.y - 1, z = p1.z } + else + p1 = {x = anchorPos.x, y = anchorPos.y, z = anchorPos.z - 3 } + 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 {x= p1.x + 3, y = p1.y, z = p1.z }, 0 + elseif p1.x == p2.x then + return {x= p1.x, y = p1.y, z = p1.z + 3}, 90 + end + end, + + apply_func_to_frame_nodes = function(anchorPos, orientation, func) + local shortCircuited + if orientation == 0 then + -- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true + shortCircuited = + func({x = anchorPos.x + 0, y = anchorPos.y + 0, z = anchorPos.z}) or + func({x = anchorPos.x + 1, y = anchorPos.y + 0, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 0, z = anchorPos.z}) or + func({x = anchorPos.x + 2, y = anchorPos.y + 1, z = anchorPos.z}) or func({x = anchorPos.x - 2, 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 + 2, z = anchorPos.z}) or + func({x = anchorPos.x + 3, y = anchorPos.y + 3, z = anchorPos.z}) or func({x = anchorPos.x - 3, y = anchorPos.y + 3, z = anchorPos.z}) or + func({x = anchorPos.x + 3, 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 + 2, y = anchorPos.y + 5, z = anchorPos.z}) or func({x = anchorPos.x - 2, y = anchorPos.y + 5, z = anchorPos.z}) or + func({x = anchorPos.x + 1, y = anchorPos.y + 6, z = anchorPos.z}) or func({x = anchorPos.x - 1, y = anchorPos.y + 6, z = anchorPos.z}) or + func({x = anchorPos.x + 0, y = anchorPos.y + 6, z = anchorPos.z}) + else + shortCircuited = + func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 0}) or + func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 0, z = anchorPos.z - 1}) or + func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 1, z = anchorPos.z - 2}) or + func({x = anchorPos.x, y = anchorPos.y + 2, 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}) or func({x = anchorPos.x, y = anchorPos.y + 3, z = anchorPos.z - 3}) or + func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z + 3}) or func({x = anchorPos.x, y = anchorPos.y + 4, z = anchorPos.z - 3}) or + func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z + 2}) or func({x = anchorPos.x, y = anchorPos.y + 5, z = anchorPos.z - 2}) or + func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 1}) or func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z - 1}) or + func({x = anchorPos.x, y = anchorPos.y + 6, z = anchorPos.z + 0}) + end + return not shortCircuited + end, + + -- returns true if function was applied to all wormhole nodes + apply_func_to_wormhole_nodes = function(anchorPos, orientation, func) + local xRange = 2 + local zRange = 0 + if orientation ~= 0 then + xRange = 0 + zRange = 2 + end + + local xEdge, yEdge, zEdge + local pos = {} + for x = -xRange, xRange do + pos.x = anchorPos.x + x + xEdge = x == -xRange or x == xRange + for z = -zRange, zRange do + zEdge = z == -zRange or z == zRange + pos.z = anchorPos.z + z + for y = 1, 5 do + yEdge = y == 1 or y == 5 + if not (yEdge and xEdge and zEdge) then + pos.y = anchorPos.y + y + if func(pos) then + -- func() caused an abort by returning true + return false + end + end + end + end + end + + return true + 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. + end +} -- End of PortalShape_Circular 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 = {} +local ignition_item_name +local S = nether.get_translator +local mod_storage = minetest.get_mod_storage() +local malleated_filenames = {} + + +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 color of pixels in nether_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 + + +-- Add portal information to mod storage, so new portals may find existing portals near the target location. +-- Do this whenever a portal is created or changes its ignition state +local function store_portal_location_info(portal_name, anchorPos, orientation, ignited) + mod_storage:set_string( + minetest.pos_to_string(anchorPos) .. " is " .. portal_name, + minetest.serialize({orientation = orientation, active = ignited}) + ) +end + +-- Remove portal information from mod storage. +-- Do this if a portal frame is destroyed such that it cannot be ignited anymore. +local function remove_portal_location_info(portal_name, anchorPos) + mod_storage:set_string(minetest.pos_to_string(anchorPos) .. " is " .. portal_name, "") +end + +-- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage +-- data. +-- Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0. +-- WARNING: Portals are not checked, and inactive portals especially may have been damaged without +-- being removed from the mod_storage data. Check these portals still exist before using them, and +-- invoke remove_portal_location_info() on any found to no longer exist. +-- +-- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included, +-- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. +-- Pass a nil or negative distance_limit to indicate no distance limit +local function list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor) + + local result = {} + + local isRealm = portal_definition.is_within_realm(anchorPos) + if distance_limit == nil then distance_limit = -1 end + if y_factor == nil then y_factor = 1 end + + for key, value in pairs(mod_storage:to_table().fields) do + local closingBrace = key:find(")", 6, true) + if closingBrace ~= nil then + local found_anchorPos = minetest.string_to_pos(key:sub(0, closingBrace)) + if found_anchorPos ~= nil and portal_definition.is_within_realm(found_anchorPos) == isRealm then + local found_name = key:sub(closingBrace + 5) + if found_name == portal_definition.name then + local x = anchorPos.x - found_anchorPos.x + local y = anchorPos.y - found_anchorPos.y + local z = anchorPos.z - found_anchorPos.z + local distance = math.hypot(y * y_factor, math.hypot(x, z)) + if distance <= distance_limit or distance_limit < 0 then + local info = minetest.deserialize(value) or {} + if DEBUG then minetest.chat_send_all("found " .. found_name .. " listed at distance " .. distance .. " from dest " .. minetest.pos_to_string(anchorPos) .. ", found: " .. minetest.pos_to_string(found_anchorPos) .. " orientation " .. info.orientation) end + info.anchorPos = found_anchorPos + info.distance = distance + result[distance] = info + end + end + end + end + end + return result +end + + +-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run +-- see also ambient_sound_stop() +function ambient_sound_play(portal_definition, soundPos, timerNodeMeta) + if portal_definition.sounds.ambient ~= nil then + local soundLength = portal_definition.sounds.ambient.length + if soundLength == nil then soundLength = 3 end + local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played") + + -- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed + if os.time() >= lastPlayed + soundLength then + local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 8}) + if timerNodeMeta ~= nil then + timerNodeMeta:set_int("ambient_sound_handle", soundHandle) + timerNodeMeta:set_int("ambient_sound_last_played", os.time()) + end + end + end +end + +-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run +-- see also ambient_sound_play() +function ambient_sound_stop(timerNodeMeta) + if timerNodeMeta ~= nil then + local soundHandle = timerNodeMeta:get_int("ambient_sound_handle") + minetest.sound_fade(soundHandle, -3, 0) + + -- clear the metadata + timerNodeMeta:set_string("ambient_sound_handle", "") + timerNodeMeta:set_string("ambient_sound_last_played", "") + end +end + + +-- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos +function extinguish_portal(pos, node_name, frame_was_destroyed) + + -- 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 + if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end + 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 + + if portal_definition.sounds.extinguish ~= nil then + minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1}) + end + + -- stop timer and ambient sound + local timerPos = get_timerPos_from_p1_and_p2(p1, p2) + minetest.get_node_timer(timerPos):stop() + ambient_sound_stop(minetest.get_meta(timerPos)) + + -- update the ignition state in the portal location info + local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2) + if frame_was_destroyed then + remove_portal_location_info(portal_definition.name, anchorPos) + else + store_portal_location_info(portal_definition.name, anchorPos, orientation, false) + end + + local frame_node_name = portal_definition.frame_node_name + local wormhole_node_name = portal_definition.wormhole_node_name + + + 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("portal_type", "") + end + end + end + end + + if target ~= nil then + if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end + extinguish_portal(target, node_name) + end + + if portal_definition.on_extinguish ~= nil then + portal_definition.on_extinguish(portal_definition, anchorPos, orientation) + end +end + + + + +-- Note: will extinguish any portal using the same nodes that are being set +local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite) + + if DEBUG then minetest.chat_send_all("set_portal_metadata(ignite=" .. tostring(ignite) .. ") at " .. minetest.pos_to_string(anchorPos) .. " orient " .. orientation .. ", setting to target " .. minetest.pos_to_string(destination_wormholePos)) end + + -- 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 existing_p1 ~= p1_string or existing_p2 ~= p2_string then + if DEBUG then minetest.chat_send_all("set_portal_metadata() found existing metadata from another portal: existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string .. ", will existinguish existing portal...") end + -- 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, false) + + -- 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("portal_type", 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)) + + if portal_definition.name ~= "nether_portal" then + -- Legacy portals won't have this extra metadata, so don't rely on it. + -- It's not strictly necessary for PortalShape_Traditional as we know that p1 is part of + -- the frame and we can look up the portal type from p1, p2, and frame node name. + -- Being able to read this from the metadata means other portal shapes needn't have their + -- frame at the timerPos, it may handle unloaded nodes better, and it saves an extra call + -- to minetest.getnode(). + meta:set_string("portal_type", portal_definition.name) + end + 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) + + store_portal_location_info(portal_definition.name, anchorPos, orientation, true) +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 + + +-- this function returns two bools: portal found, portal is lit +local function is_portal_at_anchorPos(portal_definition, anchorPos, orientation, force_chunk_load) + + 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) + local foundName = minetest.get_node(check_pos).name + if foundName ~= frame_node_name then + + if force_chunk_load and foundName == "ignore" then + -- area isn't loaded, force loading/emerge of check area + minetest.get_voxel_manip():read_from_map(check_pos, check_pos) + foundName = minetest.get_node(check_pos).name + if DEBUG then minetest.chat_send_all("Forced loading of 'ignore' node at " .. minetest.pos_to_string(check_pos) .. ", got " .. foundName) end + + if foundName ~= frame_node_name then + nodes_are_valid = false + return true -- short-circuit the search + end + else + nodes_are_valid = false + return true -- short-circuit the search + end + 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 + + 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 + +-- 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_within_portal_frame(portal_definition, pos) + + 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(portal_definition, testAnchorPos_x, 0, true) + + 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(portal_definition, testForAnchorPos_z, 90, true) + + 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.schematic_filename, + orientation, + { -- node replacements + ["default:obsidian"] = portal_definition.frame_node_name, + ["nether:portal"] = portal_definition.wormhole_node_name + }, + true + ) + -- set the param2 on wormhole nodes to ensure they are the right color + local wormholeNode = { + name = portal_definition.wormhole_node_name, + param2 = get_param2_from_color_and_orientation(portal_definition.wormhole_node_color, orientation) + } + portal_definition.shape.apply_func_to_wormhole_nodes( + anchorPos, + orientation, + function(pos) minetest.swap_node(pos, wormholeNode) end + ) + + if DEBUG then minetest.chat_send_all("Placed " .. portal_definition.name .. " 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) + + if portal_definition.on_created ~= nil then + portal_definition.on_created(portal_definition, anchorPos, orientation) + 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) + + if DEBUG then minetest.chat_send_all("portal checkup at " .. elapsed .. " seconds") end + + 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 < 10 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 + + +-- Used to find or build the remote twin after a portal is opened. +-- If a portal is found that is already lit then it will be extinguished first and its destination_wormholePos updated, +-- this is to enforce that portals only link together in mutual pairs. It would be better for gameplay if I didn't apply +-- that restriction, but it would require maintaining an accurate list of every portal that links to a portal so they +-- could be updated if the portal is destroyed. To keep the code simple I'm going to limit portals to only being the +-- destination of one lit portal at a time. +-- * suggested_wormholePos indicates where the portal should be built - note this not an anchorPos! +-- * suggested_orientation is the suggested schematic rotation to use if no useable portal is found at suggested_wormholePos: +-- 0, 90, 180, 270 (0 meaning a portal that faces north/south - i.e. obsidian running east/west) +-- * destination_wormholePos is the wormholePos of the destination portal this one will be linked to. +-- +-- 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_wormholePos, suggested_orientation, destination_wormholePos) + + if DEBUG then minetest.chat_send_all("locate_or_build_portal() called at wormholePos" .. minetest.pos_to_string(suggested_wormholePos) .. " with suggested orient " .. suggested_orientation .. ", targetted to " .. minetest.pos_to_string(destination_wormholePos)) end + + local result_anchorPos; + local result_orientation; + + -- Searching for an existing portal at wormholePos seems better than at anchorPos, though isn't important + local found_anchorPos, found_orientation, is_ignited = is_within_portal_frame(portal_definition, suggested_wormholePos) -- can be optimized - check for portal at exactly suggested_wormholePos first + + 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 unnecessary: already a lit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation .. ". Extinguishing...") end + extinguish_portal(found_anchorPos, portal_definition.frame_node_name, false) + else + if DEBUG then minetest.chat_send_all(" Build unnecessary: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end + end + -- ignite the portal + set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos) + + else + result_orientation = suggested_orientation + result_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(suggested_wormholePos, result_orientation) + build_portal(portal_definition, result_anchorPos, result_orientation, destination_wormholePos) + -- make sure portal isn't overwritten by ongoing generation/emerge + minetest.after(2, remote_portal_checkup, 2, portal_definition, result_anchorPos, result_orientation, destination_wormholePos) + end + return result_anchorPos, result_orientation +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 + if DEBUG then minetest.chat_send_all("IGNITE the " .. ignition_node_name .. " at " .. minetest.pos_to_string(ignition_pos)) 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_within_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, destination_orientation + if portal_definition.is_within_realm(ignition_pos) then + destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos) + else + destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos) + end + if DEBUG and destination_orientation == nil then minetest.chat_send_all("No destination_orientation given") end + if destination_orientation == nil then destination_orientation = orientation end + + local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation) + if DEBUG then minetest.chat_send_all("Destination set to " .. minetest.pos_to_string(destination_anchorPos)) end + + -- ignition/BURN_BABY_BURN + set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos) + + if portal_definition.sounds.ignite ~= nil then + local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) + minetest.sound_play(portal_definition.sounds.ignite, {pos = local_wormholePos, max_hear_distance = 20}) + end + + if portal_definition.on_ignite ~= nil then + portal_definition.on_ignite(portal_definition, anchorPos, orientation) + end + + return true + end + 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) + local local_p1, local_p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(local_anchorPos, local_orientation) + local p1_at_playerPos = minetest.string_to_pos(meta:get_string("p1")) + if p1_at_playerPos == nil or not vector.equals(local_p1, p1_at_playerPos) 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 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) + local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, destination_orientation) + portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation) + + -- if the portal is already linked to a different portal then extinguish the other portal and + -- update the target portal to point back at this one. + local remoteMeta = minetest.get_meta(destination_wormholePos) + local remoteTarget = minetest.string_to_pos(remoteMeta:get_string("target")) + if remoteTarget == nil then + if DEBUG then minetest.chat_send_all("Failed to test whether target portal links back to this one") end + elseif not vector.equals(remoteTarget, local_wormholePos) then + if DEBUG then minetest.chat_send_all("Target portal is already linked, extinguishing then relighting to point back at this one") end + extinguish_portal(remoteTarget, portal_definition.frame_node_name, false) + set_portal_metadata_and_ignite( + portal_definition, + destination_anchorPos, + destination_orientation, + local_wormholePos + ) + end + + if DEBUG then minetest.chat_send_all("Teleporting player from wormholePos" .. minetest.pos_to_string(local_wormholePos) .. " to wormholePos" .. minetest.pos_to_string(destination_wormholePos)) end + + -- play the teleport sound + if portal_definition.sounds.teleport ~= nil then + minetest.sound_play(portal_definition.sounds.teleport, {to_player = player.name}) + end + + -- 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} + local new_playerPos = vector.add(destination_wormholePos, rotated_offset) + player:setpos(new_playerPos) + player:set_look_horizontal(player:get_look_horizontal() + rotation_angle) + + if portal_definition.on_player_teleported ~= nil then + portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos) + end + else + -- no wormhole node at destination - destination portal either needs to be built or ignited. + -- Note: A very rare edge-case that is difficult to set up: + -- If the destination portal is unlit and its frame shares a node with a lit portal that is linked to this + -- portal (but has not been travelled through, thus not linking this portal back to it), then igniting + -- the destination portal will extinguish the portal it's touching, which will extinguish this portal + -- which will leave a confused player. + -- I don't think this is worth preventing, but I document it incase someone describes entering a portal + -- and then the portal turning off. + 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_wormholePos, local_orientation, local_wormholePos) + local new_dest_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation) + + if not vector.equals(destination_wormholePos, new_dest_wormholePos) then + -- Update the local portal's target to match where the existing remote portal was found + destination_wormholePos = new_dest_wormholePos + if DEBUG then minetest.chat_send_all(" updating target to where remote portal was found - " .. 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) + 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 the timerPos location +function run_wormhole(timerPos, 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.7, + minsize = 0.5 * portal_definition.particle_texture_scale, + maxsize = 1.5 * portal_definition.particle_texture_scale, + collisiondetection = false, + texture = portal_definition.particle_texture_colored, + animation = portal_definition.particle_texture_animation, + 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")) + local local_p2 = minetest.string_to_pos(meta:get_string("p2")) + if destination_wormholePos ~= nil and local_p1 ~= nil and local_p2 ~= nil then + + -- force emerge of target area + minetest.get_voxel_manip():read_from_map(destination_wormholePos, destination_wormholePos) -- force load + if minetest.get_node_or_nil(destination_wormholePos) == nil then + minetest.emerge_area(vector.subtract(destination_wormholePos, 4), vector.add(destination_wormholePos, 4)) + end + + local local_anchorPos, local_orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(local_p1, local_p2) + minetest.after( + 3, -- hopefully target area is emerged in 3 seconds + function() + ensure_remote_portal_then_teleport( + obj, + portal_definition, + local_anchorPos, + local_orientation, + destination_wormholePos + ) + end + ) + end + end + end + end + + local p1, p2, frame_node_name + local meta = minetest.get_meta(timerPos) + if meta ~= nil then + p1 = minetest.string_to_pos(meta:get_string("p1")) + p2 = minetest.string_to_pos(meta:get_string("p2")) + portal_name = minetest.string_to_pos(meta:get_string("portal_type")) -- 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 + -- figure out the portal shape so we know where the wormhole nodes will be located + local frame_node_name + if portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then + portal_definition = nether.registered_portals[portal_name] + else + frame_node_name = minetest.get_node(timerPos).name -- timerPos should be a frame node if the shape is traditionalPortalShape + portal_definition = get_portal_definition(frame_node_name, p1, p2) + end + + if portal_definition == nil then + minetest.log("error", "No portal with a \"" .. frame_node_name .. "\" frame is registered. run_wormhole" .. minetest.pos_to_string(timerPos) .. " 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) + + if portal_definition.on_run_wormhole ~= nil then + portal_definition.on_run_wormhole(portal_definition, anchorPos, orientation) + end + + local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) + ambient_sound_play(portal_definition, wormholePos, meta) + end + end +end + + +local function create_book(item_name, inventory_description, inventory_image, title, author, page1_text, page2_text) + + local display_book = function(itemstack, user, pointed_thing) + local player_name = user:get_player_name() + + minetest.sound_play("nether_book_open", {to_player = player_name, gain = 0.25}) + + local formspec = + "size[18,12.122]" .. + + "label[3.1,0.5;" .. minetest.formspec_escape(title) .. "]" .. + "label[3.6,0.9;" .. author .. "]" .. + + "textarea[ 0.9,1.7;7.9,12.0;;" .. minetest.formspec_escape(page1_text) .. ";]" .. + "textarea[10.1,0.8;7.9,12.9;;" .. minetest.formspec_escape(page2_text) .. ";]" .. + + "background[0,0;18,11;nether_book_background.png;true]".. + "image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;]" + + minetest.show_formspec(player_name, item_name, formspec) + end + + minetest.register_craftitem(item_name, { + description = inventory_description, + inventory_image = inventory_image, + groups = {book = 1}, + on_use = display_book + }) +end + +-- Updates nether:book_of_portals +-- A book the player can read to lean how to build the different portals +local function create_book_of_portals() + + local page1_text + local page2_text = "" + + -- tell the player how many portal types there are + local portalCount = 0 + for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end + if portalCount == 1 then + page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only one can I confirm as being more than merely a story.") + else + page1_text = S("In all my travels, and time spent in the Great Libraries, I have encountered no shortage of legends surrounding preternatural doorways said to open into other worlds, yet only @1 can I confirm as being more than merely stories.", portalCount) + end + + -- tell the player how to ignite portals + local ignition_item_description = "" + if ignition_item_name ~= nil and minetest.registered_items[ignition_item_name] ~= nil then + ignition_item_description = minetest.registered_items[ignition_item_name].description + end + page1_text = page1_text .. + S("\n\nThe key to opening such a doorway is to strike the frame with a @1, at which point the very air inside begins to crackle and glow.\n\n\n", string.lower(ignition_item_description)) + + -- Describe how to create each type of portal, or perhaps just give clues or flavor text, + -- but ensure the Nether is always listed first on the first page so other definitions can + -- refer to it (pairs() returns order based on a random hash). + local i = 1 + if nether.registered_portals["nether_portal"] then + page1_text = page1_text .. nether.registered_portals["nether_portal"].book_of_portals_pagetext .. "\n\n\n" + i = i + 1 + end + for portalName, portalDef in pairs(nether.registered_portals) do + if portalName ~= "nether_portal" then + if i <= portalCount / 2 then + page1_text = page1_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" + else + page2_text = page2_text .. portalDef.book_of_portals_pagetext .. "\n\n\n" + end + i = i + 1 + end + end + + create_book( + "nether:book_of_portals", + S("Book of Portals"), + "nether_book_of_portals.png", + S("A treatise on Rifts and Portals"), + "Riccard F. Burton", -- perhaps a Richard F. Burton of an alternate universe + page1_text, + page2_text + ) +end + +-- This is hack to work around how place_schematic() never invalidates its cache. +-- A unique schematic filename is generated for each unique set of node replacements +function get_malleated_schematic_filename(portal_definition) + + local result + + if portal_definition.shape ~= nil and portal_definition.shape.schematic_filename ~= nil then + + local schematicFileName = portal_definition.shape.schematic_filename + local uniqueId = portal_definition.frame_node_name .. " " .. portal_definition.wormhole_node_name + + if malleated_filenames[schematicFileName] == nil then malleated_filenames[schematicFileName] = {} end + local filenamesForSchematic = malleated_filenames[schematicFileName] + + -- Split the schematic's filename into the path and filename + local lastSlashPos, _ = schematicFileName:find("/[^/]+$") -- find the rightmost slash + local lastBackslashPos, _ = schematicFileName:find("\\[^\\]+$") -- find the rightmost backslash + if lastSlashPos == nil then lastSlashPos = -1 end + if lastBackslashPos ~= nil then lastSlashPos = math.max(lastSlashPos, lastBackslashPos) end + local part_path = schematicFileName:sub(0, math.max(0, lastSlashPos - 1)) + local part_filename = schematicFileName:sub(lastSlashPos + 1) + + if filenamesForSchematic[uniqueId] == nil then + + local malleationCount = 0 + for _ in pairs(filenamesForSchematic) do malleationCount = malleationCount + 1 end + + local malleatedFilename = part_path .. DIR_DELIM + for i = 1, malleationCount do + malleatedFilename = malleatedFilename .. '.' .. DIR_DELIM -- should work on both Linux and Windows + end + malleatedFilename = malleatedFilename .. part_filename + filenamesForSchematic[uniqueId] = malleatedFilename + end + result = filenamesForSchematic[uniqueId] + end + + return result +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 = {} -- allows chaining or restoration of original functions, if necessary + + -- 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) + if DEBUG then minetest.chat_send_all("portal frame material: mesecons action ON") end + ignite_portal(pos, node.name) + end, + action_off = function (pos, node) + if DEBUG then minetest.chat_send_all("portal frame material: mesecons action OFF") end + extinguish_portal(pos, node.name, false) + end + }} + extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct + extended_node_def.on_destruct = function(pos) + if DEBUG then minetest.chat_send_all("portal frame material: destruct") end + extinguish_portal(pos, frame_node_name, true) + 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") + assert(shape_defintion.name ~= nil, "shape definition must have a name") + + local result = true + + local origin = vector.new() + local p1, p2 = shape_defintion:get_p1_and_p2_from_anchorPos(origin, 0) + assert(vector.equals(shape_defintion.size, vector.add(vector.subtract(p2, p1), 1)), "p1 and p2 of shape definition '" .. shape_defintion.name .. "' don't match shapeDef.size") + + -- 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.is_within_realm ~= nil, "portaldef.is_within_realm() must be implemented") + assert(portal_definition.find_realm_anchorPos ~= nil, "portaldef.find_realm_anchorPos() must be implemented") + -- todo + + return result +end + + +-- convert portals made with old ABM version of nether mod to use the timer instead +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 +}) + +minetest.register_on_mods_loaded(function() + + -- Make the Book of Portals available as treasure/loot + if nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 and minetest.registered_items["nether:book_of_portals"] ~= nil then + + -- All portals should be registered now. + -- If the Nether is the only registered portal then lower the amount of these books + -- found as treasure, since many players already know the shape of a Nether portal + -- and what to build one out of, so would probably prefer other treasures. + local portalCount = 0 + for _ in pairs(nether.registered_portals) do portalCount = portalCount + 1 end + local weight_adjust = 1 + if portalCount <= 1 then weight_adjust = 0.5 end + + if minetest.get_modpath("loot") then + loot.register_loot({ + weights = { generic = nether.PORTAL_BOOK_LOOT_WEIGHTING * 1000 * weight_adjust, + books = 100 }, + payload = { stack = "nether:book_of_portals" } + }) + end + + if minetest.get_modpath("dungeon_loot") then + dungeon_loot.register({name = "nether:book_of_portals", chance = nether.PORTAL_BOOK_LOOT_WEIGHTING * weight_adjust}) + end + + -- todo: add to Treasurer mod TRMP https://github.com/poikilos/trmp_minetest_game + -- todo: add to help modpack https://forum.minetest.net/viewtopic.php?t=15912 + end +end) + + +-- Portal API functions -- +-- ==================== -- + + +-- The fallback defaults for registered portaldef tables +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", + particle_texture_animation = nil, + particle_texture_scale = 1, + sounds = { + ambient = {name = "nether_portal_ambient", gain = 0.6, length = 3}, + ignite = {name = "nether_portal_ignite", gain = 0.7}, + extinguish = {name = "nether_portal_extinguish", gain = 0.6}, + teleport = {name = "nether_portal_teleport", gain = 0.3} + } +} + + +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 + portaldef.mod_name = minetest.get_current_modname() + + -- use portaldef_default for any values missing from portaldef or portaldef.sounds + if portaldef.sounds ~= nil then setmetatable(portaldef.sounds, {__index = portaldef_default.sounds}) end + setmetatable(portaldef, {__index = portaldef_default}) + + portaldef.schematic_filename = get_malleated_schematic_filename(portaldef) + + 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 particle_texture_colored was specified. + if type(portaldef.particle_texture) == "table" and portaldef.particle_texture.animation ~= nil then + portaldef.particle_texture_colored = portaldef.particle_texture.name .. "^[colorize:" .. portaldef.particle_color .. ":alpha" + portaldef.particle_texture_animation = portaldef.particle_texture.animation + portaldef.particle_texture_scale = portaldef.particle_texture.scale or 1 + else + portaldef.particle_texture_colored = portaldef.particle_texture .. "^[colorize:" .. portaldef.particle_color .. ":alpha" + end + 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 + + -- check whether the portal definition clashes with anyone else's portal + local p1, p2 = portaldef.shape:get_p1_and_p2_from_anchorPos(vector.new(), 0) + local existing_portaldef = get_portal_definition(portaldef.frame_node_name, p1, p2) + if existing_portaldef ~= nil then + minetest.log("error", + portaldef.mod_name .." tried to register a portal '" .. portaldef.name .. "' made of " .. portaldef.frame_node_name .. + ", but it is the same material and shape as the portal '" .. existing_portaldef.name .. "' already registered by " .. existing_portaldef.mod_name .. + ". Edit the values one of those mods uses in its call to nether.register_portal() if you wish to resolve this clash.") + else + -- the new portaldef is good + nether.registered_portals[portaldef.name] = portaldef + create_book_of_portals() + + if not is_frame_node[portaldef.frame_node_name] then + -- add portal functions to the nodedef being used for the portal frame + register_frame_node(portaldef.frame_node_name) + is_frame_node[portaldef.frame_node_name] = true + end + + return true + end + end + + return false +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, ignition_failure_sound) + + minetest.override_item(item_name, { + on_place = function(stack, _, pt) + local done = false + if pt.under and is_frame_node[minetest.get_node(pt.under).name] then + done = ignite_portal(pt.under) + if done and not minetest.settings:get_bool("creative_mode") then + stack:take_item() + end + end + if not done and ignition_failure_sound ~= nil then + minetest.sound_play(ignition_failure_sound, {pos = pt.under, max_hear_distance = 10}) + end + + + return stack + end, + }) + + ignition_item_name = item_name +end + +-- 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. +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 + +-- Can be used when implementing custom find_surface_anchorPos() functions +-- portal_name is optional, providing it allows existing portals on the surface to be reused. +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 + -- get_spawn_level() seems to err on the side of caution and sometimes spawn the player a + -- block higher than the ground level. + local shouldBeGroundPos = {x = target_x, y = surface_level - 1, z = target_z} + local groundNode = minetest.get_node_or_nil(shouldBeGroundPos) + if groundNode == nil then + -- force the area to be loaded - it's going to be loaded anyway by volume_is_natural() + minetest.get_voxel_manip():read_from_map(shouldBeGroundPos, shouldBeGroundPos) + groundNode = minetest.get_node(shouldBeGroundPos) + end + if not groundNode.is_ground_content then + surface_level = surface_level - 1 + end + -- 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_within_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 + + +-- Returns the anchorPos, orientation of the nearest portal, or nil. +-- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included, +-- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. +-- Pass a negative distance_limit to indicate no distance limit +function nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor) + + local portal_definition = nether.registered_portals[portal_name] + assert(portal_definition ~= nil, "find_nearest_working_portal() called with portal_name '" .. portal_name .. "', but no portal is registered with that name.") + + local contenders = list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor) + + -- sort by distance + local dist_list = {} + for dist, _ in pairs(contenders) do table.insert(dist_list, dist) end + table.sort(dist_list) + + for _, dist in ipairs(dist_list) do + local portal_info = contenders[dist] + if DEBUG then minetest.chat_send_all("checking portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end + + -- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been + -- destroyed, so check the portal is still there + local portalFound, portalActive = is_portal_at_anchorPos(portal_definition, portal_info.anchorPos, portal_info.orientation, true) + + if portalFound then + return portal_info.anchorPos, portal_info.orientation + else + if DEBUG then minetest.chat_send_all("removing portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end + -- The portal at that location must have been destroyed + remove_portal_location_info(portal_name, portal_info.anchorPos) + end + end + return nil +end diff --git a/portal_api.txt b/portal_api.txt index 93b19d4..2ee5a33 100644 --- a/portal_api.txt +++ b/portal_api.txt @@ -1,184 +1,184 @@ -Portal API Reference -==================== - -The portal system used to get to the Nether can be used to create portals -to other realms. - -Pick a node type to have your portals built from, a shape in which the -portals must be built, and provide 3 functions for portals to find their -destination with: - * `find_realm_anchorPos(surface_anchorPos)` - * `find_surface_anchorPos(realm_anchorPos)` - * `is_within_realm(pos)` - -Optionally decorate by choosing portal colors, particles, media etc. - -See `init.lua` and `portal_examples.lua` for examples of 3 different portals. - -Portal code is more efficient when each type of portal uses a different type -of node to build its frame out of, however it is possible to register more than -one kind of portal with the same frame material — such as obsidian — provided -the size of the PortalShape is distinct from any other type of portal that is -using the same node as its frame, and portal sizes remain small. - -Realms ------- - -This API uses the concept of a "realm" for each type of portal. If a portal is -outside its realm then it links to a portal inside the realm, if a portal is -inside its realm then it links to the outside. - -You get to decide what consitutes your realm by implementing the function -`is_within_realm(position)`. - -The Nether realm, for example, is defined as existing at a certain depth. - - -Helper functions ----------------- - -* `nether.volume_is_natural(minp, maxp)`: returns a boolean - * 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)`: returns a - suitable anchorPos - * Can be used when implementing custom find_surface_anchorPos() functions - * portal_name is optional, providing it allows existing portals on the - surface to be reused. - -* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns - an anchorPos, or nil if no portal was found within the the distance_limit. - * A y_factor of 0 means y does not affect the distance_limit, a y_factor - of 1 means y is included (the default if y_factor is nil), and a y_factor - of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. - * Only portals in the same realm as the anchorPos will be returned, even if - y_factor is 0. - * Pass a nil (or negative) distance_limit to indicate no distance limit - - -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. - A false return value should be handled, you could: - * Fall back to using a secondary material for portals to be built with. - * Use error() to exit lua with a message explaining how two mods are - clashing and how it can be resolved. - * Continue without a portal (the reason will be logged for the user). -* `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, ignition_failure_sound) - * ignition_failure_sound is optional, it plays any time an attempt to use - the item occurs if a portal is not ignited. - - - -Portal definition ------------------ - -Used by `nether.register_portal`. - - { - shape = PortalShape_Traditional, - -- Optional. Defaults to PortalShape_Traditional - - 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, - -- Optional. Defaults to 0/magenta. - -- A value from 0 to 7 corresponding to the color of pixels in - -- nether_portals_palette.png: - -- 0 traditional/magenta - -- 1 black - -- 2 blue - -- 3 green - -- 4 cyan - -- 5 red - -- 6 yellow - -- 7 white - - particle_color = "#808", - -- Optional. Will default to a colour matching the wormhole_node_color - -- if not specified. - - particle_texture = "image.png", - -- Optional. Hardware colouring (i.e. particle_color) is applied to - -- this texture, use particle_texture_colored instead if you want to - -- use the colors of the image. - -- Animation and particle scale may also be specified, e.g: - -- particle_texture = { - -- name = "nether_particle_anim1.png", - -- animation = { - -- type = "vertical_frames", - -- aspect_w = 7, - -- aspect_h = 7, - -- length = 1, - -- }, - -- scale = 1.5 - -- }, - -- See lua_api.txt for Tile Animation definition - - frame_node_name = "default:obsidian", - -- Required. For best results, have your portal constructed of a - -- material nobody else is using. - - sounds = { - ambient = , - -- if the ambient SimpleSoundSpec is a table it can also contain a - -- "length" int, which is the number of seconds to wait before - -- repeating the ambient sound. Default is 3. - ignite = , - extinguish = , - teleport = , - } - -- sounds is optional - - within_realm = function(pos), - -- Required. Return true if a portal at pos is in the realm, rather - than the surface world. - - find_realm_anchorPos = function(surface_anchorPos), - -- Required. Return a position in the realm that a portal created at - -- surface_anchorPos will link to. - -- Return an anchorPos or anchorPos, orientation - -- If orientation is not specified then the orientation of the surface - -- portal will be used. - -- If the location of an existing portal is returned then include the - -- orientation, otherwise the existing portal could be overwritten by - -- a new one with the orientation of the surface portal. - - find_surface_anchorPos = function(realm_anchorPos), - -- Optional. If you don't implement this then a position near the - -- surface will be picked. - -- Return an anchorPos or (anchorPos, orientation) - -- If orientation is not specified then the orientation of the realm - -- portal will be used. - -- If the location of an existing portal is returned then include the - -- orientation, otherwise the existing portal could be overwritten by - -- a new one with the orientation of the realm portal. - - on_run_wormhole = function(portalDef, anochorPos, orientation), - -- invoked once per second per portal - on_extinguish = function(portalDef, anochorPos, orientation), - -- invoked when a portal is extinguished, including when the portal - -- it connected to was extinguished. - on_player_teleported = function(portalDef, player, oldPos, newPos), - -- invoked immediately after a player is teleported - on_ignite = function(portalDef, anochorPos, orientation) - -- invoked when a player or mesecon ignites a portal - on_created = function(portalDef, anochorPos, orientation) - -- invoked when a portal creates a remote twin, usually when a - -- player travels through a portal for the first time. - } +Portal API Reference +==================== + +The portal system used to get to the Nether can be used to create portals +to other realms. + +Pick a node type to have your portals built from, a shape in which the +portals must be built, and provide 3 functions for portals to find their +destination with: + * `find_realm_anchorPos(surface_anchorPos)` + * `find_surface_anchorPos(realm_anchorPos)` + * `is_within_realm(pos)` + +Optionally decorate by choosing portal colors, particles, media etc. + +See `init.lua` and `portal_examples.lua` for examples of 3 different portals. + +Portal code is more efficient when each type of portal uses a different type +of node to build its frame out of, however it is possible to register more than +one kind of portal with the same frame material — such as obsidian — provided +the size of the PortalShape is distinct from any other type of portal that is +using the same node as its frame, and portal sizes remain small. + +Realms +------ + +This API uses the concept of a "realm" for each type of portal. If a portal is +outside its realm then it links to a portal inside the realm, if a portal is +inside its realm then it links to the outside. + +You get to decide what consitutes your realm by implementing the function +`is_within_realm(position)`. + +The Nether realm, for example, is defined as existing at a certain depth. + + +Helper functions +---------------- + +* `nether.volume_is_natural(minp, maxp)`: returns a boolean + * 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)`: returns a + suitable anchorPos + * Can be used when implementing custom find_surface_anchorPos() functions + * portal_name is optional, providing it allows existing portals on the + surface to be reused. + +* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns + an anchorPos, or nil if no portal was found within the the distance_limit. + * A y_factor of 0 means y does not affect the distance_limit, a y_factor + of 1 means y is included (the default if y_factor is nil), and a y_factor + of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc. + * Only portals in the same realm as the anchorPos will be returned, even if + y_factor is 0. + * Pass a nil (or negative) distance_limit to indicate no distance limit + + +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. + A false return value should be handled, you could: + * Fall back to using a secondary material for portals to be built with. + * Use error() to exit lua with a message explaining how two mods are + clashing and how it can be resolved. + * Continue without a portal (the reason will be logged for the user). +* `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, ignition_failure_sound) + * ignition_failure_sound is optional, it plays any time an attempt to use + the item occurs if a portal is not ignited. + + + +Portal definition +----------------- + +Used by `nether.register_portal`. + + { + shape = PortalShape_Traditional, + -- Optional. Defaults to PortalShape_Traditional + + 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, + -- Optional. Defaults to 0/magenta. + -- A value from 0 to 7 corresponding to the color of pixels in + -- nether_portals_palette.png: + -- 0 traditional/magenta + -- 1 black + -- 2 blue + -- 3 green + -- 4 cyan + -- 5 red + -- 6 yellow + -- 7 white + + particle_color = "#808", + -- Optional. Will default to a colour matching the wormhole_node_color + -- if not specified. + + particle_texture = "image.png", + -- Optional. Hardware colouring (i.e. particle_color) is applied to + -- this texture, use particle_texture_colored instead if you want to + -- use the colors of the image. + -- Animation and particle scale may also be specified, e.g: + -- particle_texture = { + -- name = "nether_particle_anim1.png", + -- animation = { + -- type = "vertical_frames", + -- aspect_w = 7, + -- aspect_h = 7, + -- length = 1, + -- }, + -- scale = 1.5 + -- }, + -- See lua_api.txt for Tile Animation definition + + frame_node_name = "default:obsidian", + -- Required. For best results, have your portal constructed of a + -- material nobody else is using. + + sounds = { + ambient = , + -- if the ambient SimpleSoundSpec is a table it can also contain a + -- "length" int, which is the number of seconds to wait before + -- repeating the ambient sound. Default is 3. + ignite = , + extinguish = , + teleport = , + } + -- sounds is optional + + within_realm = function(pos), + -- Required. Return true if a portal at pos is in the realm, rather + than the surface world. + + find_realm_anchorPos = function(surface_anchorPos), + -- Required. Return a position in the realm that a portal created at + -- surface_anchorPos will link to. + -- Return an anchorPos or anchorPos, orientation + -- If orientation is not specified then the orientation of the surface + -- portal will be used. + -- If the location of an existing portal is returned then include the + -- orientation, otherwise the existing portal could be overwritten by + -- a new one with the orientation of the surface portal. + + find_surface_anchorPos = function(realm_anchorPos), + -- Optional. If you don't implement this then a position near the + -- surface will be picked. + -- Return an anchorPos or (anchorPos, orientation) + -- If orientation is not specified then the orientation of the realm + -- portal will be used. + -- If the location of an existing portal is returned then include the + -- orientation, otherwise the existing portal could be overwritten by + -- a new one with the orientation of the realm portal. + + on_run_wormhole = function(portalDef, anochorPos, orientation), + -- invoked once per second per portal + on_extinguish = function(portalDef, anochorPos, orientation), + -- invoked when a portal is extinguished, including when the portal + -- it connected to was extinguished. + on_player_teleported = function(portalDef, player, oldPos, newPos), + -- invoked immediately after a player is teleported + on_ignite = function(portalDef, anochorPos, orientation) + -- invoked when a player or mesecon ignites a portal + on_created = function(portalDef, anochorPos, orientation) + -- invoked when a portal creates a remote twin, usually when a + -- player travels through a portal for the first time. + } diff --git a/portal_examples.lua b/portal_examples.lua index 985fae6..67681ce 100644 --- a/portal_examples.lua +++ b/portal_examples.lua @@ -1,4 +1,4 @@ ---[[ +--[[ Nether mod portal examples for Minetest