2020-01-08 10:06:00 +01:00
--[[
Portal API for Minetest
See portal_api.txt for documentation
--
Copyright ( C ) 2020 Treer
Permission to use , copy , modify , and / or distribute this software for
any purpose with or without fee is hereby granted , provided that the
above copyright notice and this permission notice appear in all copies .
THE SOFTWARE IS PROVIDED " AS IS " AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS . IN NO EVENT SHALL THE AUTHOR
BE LIABLE FOR ANY SPECIAL , DIRECT , INDIRECT , OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE , DATA OR PROFITS ,
WHETHER IN AN ACTION OF CONTRACT , NEGLIGENCE OR OTHER TORTIOUS ACTION ,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
SOFTWARE .
] ] --
2020-01-04 15:33:56 +01:00
local DEBUG = false
2020-01-08 10:06:00 +01:00
local DEBUG_IGNORE_MODSTORAGE = false -- setting true prevents portals from knowing where other portals are, forcing find_realm_anchorpos() etc. to be executed every time
2019-08-06 23:23:06 +02:00
nether.registered_portals = { }
2020-02-01 03:35:59 +01:00
nether.registered_portals_count = 0
-- Exposes a list of node names that are used as frame nodes by registered portals
nether.is_frame_node = { }
2019-08-06 23:23:06 +02:00
-- gives the colour values in nether_portals_palette.png that are used by the wormhole colorfacedir
-- hardware colouring.
nether.portals_palette = {
2020-01-06 12:21:15 +01:00
[ 0 ] = { r = 128 , g = 0 , b = 128 , asString = " #800080 " } , -- traditional/magenta
[ 1 ] = { r = 0 , g = 0 , b = 0 , asString = " #000000 " } , -- black
[ 2 ] = { r = 19 , g = 19 , b = 255 , asString = " #1313FF " } , -- blue
[ 3 ] = { r = 55 , g = 168 , b = 0 , asString = " #37A800 " } , -- green
[ 4 ] = { r = 141 , g = 237 , b = 255 , asString = " #8DEDFF " } , -- cyan
[ 5 ] = { r = 221 , g = 0 , b = 0 , asString = " #DD0000 " } , -- red
[ 6 ] = { r = 255 , g = 240 , b = 0 , asString = " #FFF000 " } , -- yellow
[ 7 ] = { r = 255 , g = 255 , b = 255 , asString = " #FFFFFF " } -- white
2019-08-06 23:23:06 +02:00
}
2020-01-08 11:37:31 +01:00
if minetest.get_mod_storage == nil then
error ( nether.modname .. " does not support Minetest versions earlier than 0.4.16 " , 0 )
2020-01-08 10:06:00 +01:00
end
2019-08-06 23:23:06 +02:00
--[[
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 .
2020-01-02 13:42:20 +01:00
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
2019-08-06 23:23:06 +02:00
2020-01-08 11:37:31 +01:00
WormholePos The location of the node that a portal ' s target is set to, and a player is teleported
2019-08-06 23:23:06 +02:00
to . It can also be used to test whether a portal is active .
2020-01-08 11:37:31 +01:00
AnchorPos Introduced by the portal_api . Coordinates for portals are normally given in terms of
2020-01-02 13:42:20 +01:00
the AnchorPos . The AnchorPos does not change with portal orientation - portals rotate
around the AnchorPos . Ideally an AnchorPos would be near the bottom center of a portal
2020-01-08 11:37:31 +01:00
shape , but this is not the case with PortalShape_Traditional to keep comptaibility with
2020-01-02 13:42:20 +01:00
earlier versions of the nether mod .
Usually an orientation is required with an AnchorPos .
2020-01-08 11:37:31 +01:00
Orientation is yaw , either 0 or 90 , 0 meaning a portal that faces north / south - i.e . obsidian
2020-01-06 14:08:27 +01:00
running east / west .
2020-01-08 11:37:31 +01:00
TimerPos The portal_api replaces ABMs with a single node timer per portal , and the TimerPos is the
2020-01-02 13:42:20 +01:00
node in which that timer is located . Extra metadata is also kept in the TimerPos node .
2019-08-06 23:23:06 +02:00
Portal shapes
=============
2020-01-08 11:37:31 +01:00
For the PortalShape_Traditional implementation , p1 , p2 , anchorPos , wormholdPos and TimerPos are defined
2019-08-06 23:23:06 +02:00
as follows :
.
+--------+--------+--------+--------+
| | Frame | |
| | | | p2 |
+--------+--------+--------+--------+
| | | |
| | | |
+--------+ + +--------+
| | Wormhole | |
| | | |
+--------+ + +--------+
| | Wormhole | |
| | Pos | |
+--------+--------+--------+--------+
2020-01-02 13:42:20 +01:00
AnchorPos | TimerPos | | |
| p1 | | | |
2019-08-06 23:23:06 +02:00
+--------+--------+--------+--------+
+ 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 ) .
] ]
-- 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
2020-01-02 13:42:20 +01:00
-- functions if you wish to register a custom shaped portal in register_portal(). Examples of other
-- shapes follow after PortalShape_Traditional.
2019-08-06 23:23:06 +02:00
-- 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.
2020-02-01 06:51:58 +01:00
schematic_filename = nether.path .. " /schematics/nether_portal.mts " ,
is_horizontal = false , -- whether the wormhole is a vertical or horizontal surface
2020-02-02 05:49:13 +01:00
diagram_image = {
image = " nether_book_diagram_traditional.png " , -- The diagram to be shown in the Book of Portals
width = 144 ,
height = 305
} ,
2019-08-06 23:23:06 +02:00
-- 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.
2020-02-01 06:51:58 +01:00
schematic_filename = nether.path .. " /schematics/nether_portal_circular.mts " ,
is_horizontal = false , -- whether the wormhole is a vertical or horizontal surface
2020-02-02 05:49:13 +01:00
diagram_image = {
image = " nether_book_diagram_circular.png " , -- The diagram to be shown in the Book of Portals
width = 150 ,
height = 243
} ,
2019-08-06 23:23:06 +02:00
-- 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() " )
2020-01-08 11:31:10 +01:00
local p1
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
zRange = 2
2019-08-06 23:23:06 +02:00
end
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
yEdge = y == 1 or y == 5
2019-08-06 23:23:06 +02:00
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
2020-01-02 13:42:20 +01:00
-- Example alternative PortalShape
-- This platform shape is symmetrical around the y-axis, so the orientation value never matters.
nether.PortalShape_Platform = {
name = " Platform " ,
size = vector.new ( 5 , 2 , 5 ) , -- size of the portal, and not necessarily the size of the schematic,
-- which may clear area around the portal.
2020-02-01 06:51:58 +01:00
schematic_filename = nether.path .. " /schematics/nether_portal_platform.mts " ,
is_horizontal = true , -- whether the wormhole is a vertical or horizontal surface
2020-02-02 05:49:13 +01:00
diagram_image = {
image = " nether_book_diagram_platform.png " , -- The diagram to be shown in the Book of Portals
width = 200 ,
height = 130
} ,
2020-01-02 13:42:20 +01:00
-- returns the coords for minetest.place_schematic() that will place the schematic on the anchorPos
get_schematicPos_from_anchorPos = function ( anchorPos , orientation )
return { x = anchorPos.x - 2 , y = anchorPos.y , z = anchorPos.z - 2 }
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 ( self ~= nil and self.name == nether.PortalShape_Platform . name , " Must pass self as first argument, or use shape:func() instead of shape.func() " )
local p1 = { x = anchorPos.x - 2 , y = anchorPos.y , z = anchorPos.z - 2 }
local p2 = { x = anchorPos.x + 2 , y = anchorPos.y + 1 , z = anchorPos.z + 2 }
return p1 , p2
end ,
get_anchorPos_and_orientation_from_p1_and_p2 = function ( p1 , p2 )
return { x = p1.x + 2 , y = p1.y , z = p1.z + 2 } , 0
end ,
apply_func_to_frame_nodes = function ( anchorPos , orientation , func )
local shortCircuited
local yPlus1 = anchorPos.y + 1
-- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
shortCircuited =
func ( { x = anchorPos.x - 2 , y = yPlus1 , z = anchorPos.z - 1 } ) or func ( { x = anchorPos.x + 2 , y = yPlus1 , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x - 2 , y = yPlus1 , z = anchorPos.z } ) or func ( { x = anchorPos.x + 2 , y = yPlus1 , z = anchorPos.z } ) or
func ( { x = anchorPos.x - 2 , y = yPlus1 , z = anchorPos.z + 1 } ) or func ( { x = anchorPos.x + 2 , y = yPlus1 , z = anchorPos.z + 1 } ) or
func ( { x = anchorPos.x - 1 , y = yPlus1 , z = anchorPos.z - 2 } ) or func ( { x = anchorPos.x - 1 , y = yPlus1 , z = anchorPos.z + 2 } ) or
func ( { x = anchorPos.x , y = yPlus1 , z = anchorPos.z - 2 } ) or func ( { x = anchorPos.x , y = yPlus1 , z = anchorPos.z + 2 } ) or
func ( { x = anchorPos.x + 1 , y = yPlus1 , z = anchorPos.z - 2 } ) or func ( { x = anchorPos.x + 1 , y = yPlus1 , z = anchorPos.z + 2 } ) or
func ( { x = anchorPos.x - 1 , y = anchorPos.y , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x - 1 , y = anchorPos.y , z = anchorPos.z } ) or
func ( { x = anchorPos.x - 1 , y = anchorPos.y , z = anchorPos.z + 1 } ) or
func ( { x = anchorPos.x , y = anchorPos.y , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x , y = anchorPos.y , z = anchorPos.z } ) or
func ( { x = anchorPos.x , y = anchorPos.y , z = anchorPos.z + 1 } ) or
func ( { x = anchorPos.x + 1 , y = anchorPos.y , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x + 1 , y = anchorPos.y , z = anchorPos.z } ) or
func ( { x = anchorPos.x + 1 , y = anchorPos.y , z = anchorPos.z + 1 } )
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
local yPlus1 = anchorPos.y + 1
-- use short-circuiting of boolean evaluation to allow func() to cause an abort by returning true
shortCircuited =
func ( { x = anchorPos.x - 1 , y = yPlus1 , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x - 1 , y = yPlus1 , z = anchorPos.z } ) or
func ( { x = anchorPos.x - 1 , y = yPlus1 , z = anchorPos.z + 1 } ) or
func ( { x = anchorPos.x , y = yPlus1 , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x , y = yPlus1 , z = anchorPos.z } ) or
func ( { x = anchorPos.x , y = yPlus1 , z = anchorPos.z + 1 } ) or
func ( { x = anchorPos.x + 1 , y = yPlus1 , z = anchorPos.z - 1 } ) or
func ( { x = anchorPos.x + 1 , y = yPlus1 , z = anchorPos.z } ) or
func ( { x = anchorPos.x + 1 , y = yPlus1 , z = anchorPos.z + 1 } )
return not shortCircuited
end ,
-- Check for suffocation
disable_portal_trap = function ( anchorPos , orientation )
-- Not implemented.
end
} -- End of PortalShape_Platform class
2019-08-06 23:23:06 +02:00
--====================================================--
--======== End of PortalShape implementations ========--
--====================================================--
-- Portal implementation functions --
-- =============================== --
local ignition_item_name
local S = nether.get_translator
local mod_storage = minetest.get_mod_storage ( )
local malleated_filenames = { }
2020-01-12 07:07:09 +01:00
local meseconsAvailable = minetest.get_modpath ( " mesecon " ) ~= nil and minetest.global_exists ( " mesecon " )
2020-02-02 05:49:13 +01:00
local book_added_as_treasure = false
2019-08-06 23:23:06 +02:00
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
2020-01-02 13:42:20 +01:00
-- orientation is the yaw rotation degrees passed to place_schematic: 0, 90, 180, or 270
2019-08-06 23:23:06 +02:00
-- color is a value from 0 to 7 corresponding to the color of pixels in nether_portals_palette.png
2020-01-02 13:42:20 +01:00
-- portal_is_horizontal is a bool indicating whether the portal lies flat or stands vertically
local function get_colorfacedir_from_color_and_orientation ( color , orientation , portal_is_horizontal )
2019-08-06 23:23:06 +02:00
assert ( orientation , " no orientation passed " )
2020-01-02 13:42:20 +01:00
local axis_direction , rotation
local dir = math.floor ( ( orientation % 360 ) / 90 + 0.5 )
-- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
-- will set the node's rotation.
-- if the portal is horizontal then the node axis direction reflects the yaw orientation and
-- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
if portal_is_horizontal then
if dir == 0 then axis_direction = 1 end -- North
if dir == 1 then axis_direction = 3 end -- East
if dir == 2 then axis_direction = 2 end -- South
if dir == 3 then axis_direction = 4 end -- West
rotation = math.floor ( axis_direction / 2 ) ; -- a rotation is only needed if axis_direction is east or west
else
axis_direction = 0 -- 0 is up, or +Y
rotation = dir
end
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
-- 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
2020-01-02 13:42:20 +01:00
return rotation + axis_direction * 4 + color * 32
2019-08-06 23:23:06 +02:00
end
2020-01-02 13:42:20 +01:00
local function get_orientation_from_colorfacedir ( param2 )
local axis_direction = 0
-- Strip off the top 6 bits to leave the 2 rotation bits, unfortunately MT lua has no bitwise '&'
2019-08-06 23:23:06 +02:00
-- (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
2020-01-02 13:42:20 +01:00
if param2 >= 16 then param2 = param2 - 16 ; axis_direction = axis_direction + 4 end
if param2 >= 8 then param2 = param2 - 8 ; axis_direction = axis_direction + 2 end
if param2 >= 4 then param2 = param2 - 4 ; axis_direction = axis_direction + 1 end
-- if the portal is vertical then node axis direction will be +Y (up) and portal orientation
-- will set the node's rotation.
-- if the portal is horizontal then the node axis direction reflects the yaw orientation and
-- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
if axis_direction == 0 or axis_direction == 5 then
-- portal is vertical
return param2 * 90
else
if axis_direction == 1 then return 0 end
if axis_direction == 3 then return 90 end
if axis_direction == 2 then return 180 end
if axis_direction == 4 then return 270 end
end
2019-08-06 23:23:06 +02:00
end
2020-01-08 11:37:31 +01:00
-- We want wormhole nodes to only emit mesecon energy orthogonally to the
-- wormhole surface so that the wormhole will not send power to the frame,
2020-01-06 14:08:27 +01:00
-- this allows the portal frame to listen for mesecon energy from external switches/wires etc.
function get_mesecon_emission_rules_from_colorfacedir ( param2 )
local axis_direction = 0
-- Strip off the top 6 bits to leave the 2 rotation 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 ; axis_direction = axis_direction + 4 end
if param2 >= 8 then param2 = param2 - 8 ; axis_direction = axis_direction + 2 end
if param2 >= 4 then param2 = param2 - 4 ; axis_direction = axis_direction + 1 end
-- if the portal is vertical then node axis_direction will be +Y (up) and node rotation
-- will reflect the portal's yaw orientation.
-- If the portal is horizontal then the node axis direction reflects the yaw orientation and
-- the node's rotation will be whatever's needed to keep the texture horizontal (either 0 or 1)
local rules
if axis_direction == 0 or axis_direction == 5 then
-- portal is vertical
rules = { { x = 0 , y = 0 , z = 1 } , { x = 0 , y = 0 , z = - 1 } }
if param2 % 2 ~= 0 then
rules = mesecon.rotate_rules_right ( rules )
end
else
-- portal is horizontal, only emit up
rules = { { x = 0 , y = 1 , z = 0 } }
end
return rules
end
nether.get_mesecon_emission_rules_from_colorfacedir = get_mesecon_emission_rules_from_colorfacedir -- make the function available to nodes.lua
2019-08-06 23:23:06 +02:00
-- 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 )
2020-01-08 11:37:31 +01:00
if not DEBUG_IGNORE_MODSTORAGE then
2020-01-12 06:47:05 +01:00
local key = minetest.pos_to_string ( anchorPos ) .. " is " .. portal_name
if DEBUG then minetest.chat_send_all ( " Adding/updating portal in mod_storage: " .. key ) end
2020-01-04 05:38:13 +01:00
mod_storage : set_string (
2020-01-12 06:47:05 +01:00
key ,
2020-01-04 05:38:13 +01:00
minetest.serialize ( { orientation = orientation , active = ignited } )
)
end
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
if not DEBUG_IGNORE_MODSTORAGE then
2020-01-12 06:47:05 +01:00
local key = minetest.pos_to_string ( anchorPos ) .. " is " .. portal_name
if DEBUG then minetest.chat_send_all ( " Removing portal from mod_storage: " .. key ) end
mod_storage : set_string ( key , " " )
2020-01-04 05:38:13 +01:00
end
2019-08-06 23:23:06 +02:00
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 = { }
2020-01-08 11:37:31 +01:00
if not DEBUG_IGNORE_MODSTORAGE then
2020-01-04 05:38:13 +01:00
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 )
2020-01-08 11:37:31 +01:00
if closingBrace ~= nil then
2020-01-04 05:38:13 +01:00
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 .. " (within " .. distance_limit .. " ) 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
2019-08-06 23:23:06 +02:00
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 } )
2020-01-08 11:37:31 +01:00
if timerNodeMeta ~= nil then
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
-- 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
2020-01-12 06:47:05 +01:00
-- Returns true if a portal was found to extinguish
2019-08-06 23:23:06 +02:00
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
2020-01-12 06:47:05 +01:00
return false
2019-08-06 23:23:06 +02:00
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 )
2020-01-12 06:47:05 +01:00
return false -- no portal frames are made from this type of node
2019-08-06 23:23:06 +02:00
end
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
if frame_was_destroyed then
2019-08-06 23:23:06 +02:00
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
2020-01-06 14:08:27 +01:00
local clearPos = { x = x , y = y , z = z }
local nn = minetest.get_node ( clearPos ) . name
2019-08-06 23:23:06 +02:00
if nn == frame_node_name or nn == wormhole_node_name then
if nn == wormhole_node_name then
2020-01-06 14:08:27 +01:00
minetest.remove_node ( clearPos )
2020-01-12 07:07:09 +01:00
if meseconsAvailable then mesecon.receptor_off ( clearPos ) end
2019-08-06 23:23:06 +02:00
end
2020-01-06 14:08:27 +01:00
local m = minetest.get_meta ( clearPos )
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
extinguish_portal ( target , node_name )
2019-08-06 23:23:06 +02:00
end
2020-01-08 11:37:31 +01:00
if portal_definition.on_extinguish ~= nil then
2019-08-06 23:23:06 +02:00
portal_definition.on_extinguish ( portal_definition , anchorPos , orientation )
end
2020-01-12 06:47:05 +01:00
return true
2019-08-06 23:23:06 +02:00
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 )
2020-01-04 15:33:56 +01:00
local param2 = get_colorfacedir_from_color_and_orientation ( portal_definition.wormhole_node_color , orientation , portal_definition.shape . is_horizontal )
2020-01-06 14:08:27 +01:00
local mesecon_rules
2020-01-12 07:07:09 +01:00
if ignite and meseconsAvailable then mesecon_rules = get_mesecon_emission_rules_from_colorfacedir ( param2 ) end
2019-08-06 23:23:06 +02:00
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 } )
2020-01-12 07:07:09 +01:00
if meseconsAvailable then mesecon.receptor_on ( pos , mesecon_rules ) end
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
[ " default:obsidian " ] = portal_definition.frame_node_name ,
2019-08-06 23:23:06 +02:00
[ " 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 ,
2020-01-04 15:33:56 +01:00
param2 = get_colorfacedir_from_color_and_orientation ( portal_definition.wormhole_node_color , orientation , portal_definition.shape . is_horizontal )
2019-08-06 23:23:06 +02:00
}
portal_definition.shape . apply_func_to_wormhole_nodes (
2020-01-08 11:37:31 +01:00
anchorPos ,
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
-- that restriction, but it would require maintaining an accurate list of every portal that links to a portal so they
2019-08-06 23:23:06 +02:00
-- 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
2020-01-04 05:38:13 +01:00
local continue = false -- WRT the for loop, since lua has no continue keyword
2019-08-06 23:23:06 +02:00
-- 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
2020-01-05 03:19:46 +01:00
continue = true -- no portal is here, but perhaps there's more than one portal type we need to search for
2019-08-06 23:23:06 +02:00
elseif is_ignited then
2020-01-05 03:19:46 +01:00
-- Found a portal, check its metadata and timer is healthy.
2020-01-04 05:38:13 +01:00
local repair = false
local meta = minetest.get_meta ( ignition_pos )
2020-01-05 03:19:46 +01:00
if meta ~= nil then
local p1 , p2 , target = meta : get_string ( " p1 " ) , meta : get_string ( " p2 " ) , meta : get_string ( " target " )
if p1 == " " or p2 == " " or target == " " then
2020-01-08 11:37:31 +01:00
-- metadata is missing, the portal frame node must have been removed without calling
2020-01-04 05:38:13 +01:00
-- on_destruct - perhaps by an ABM, then replaced - presumably by a player.
-- allowing reigniting will repair the portal
if DEBUG then minetest.chat_send_all ( " Broken portal detected, allowing reignition/repair " ) end
repair = true
2020-01-08 11:37:31 +01:00
else
2020-01-04 05:38:13 +01:00
if DEBUG then minetest.chat_send_all ( " This portal links to " .. meta : get_string ( " target " ) .. " . p1= " .. meta : get_string ( " p1 " ) .. " p2= " .. meta : get_string ( " p2 " ) ) end
2020-01-08 11:37:31 +01:00
2020-01-05 03:19:46 +01:00
-- Check the portal's timer is running, and fix if it's not.
2020-01-08 11:37:31 +01:00
-- A portal's timer can stop running if the game is played without that portal type being
2020-01-05 03:19:46 +01:00
-- registered, e.g. enabling one of the example portals then later disabling it, then enabling it again.
-- (if this is a frequent problem, then change the value of "run_at_every_load" in the lbm)
local timer = minetest.get_node_timer ( get_timerPos_from_p1_and_p2 ( minetest.string_to_pos ( p1 ) , minetest.string_to_pos ( p2 ) ) )
if timer ~= nil and timer : get_timeout ( ) == 0 then
if DEBUG then minetest.chat_send_all ( " Portal timer was not running: restarting the timer. " ) end
2020-01-08 11:37:31 +01:00
timer : start ( 1 )
2020-01-05 03:19:46 +01:00
end
2020-01-04 05:38:13 +01:00
end
2019-08-06 23:23:06 +02:00
end
2020-01-05 03:19:46 +01:00
if not repair then return false end -- portal is already ignited (or timer has been fixed)
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
return true
end
end
end
-- invoked when a player is standing in a portal
2020-01-12 05:46:37 +01:00
local function ensure_remote_portal_then_teleport ( playerName , portal_definition , local_anchorPos , local_orientation , destination_wormholePos )
2019-08-06 23:23:06 +02:00
2020-01-12 05:46:37 +01:00
local player = minetest.get_player_by_name ( playerName )
if player == nil then return end -- player quit the game while teleporting
2020-01-05 10:14:30 +01:00
local playerPos = player : get_pos ( )
2020-01-12 05:46:37 +01:00
if playerPos == nil then return end -- player quit the game while teleporting
-- check player is still standing in a portal
2019-08-06 23:23:06 +02:00
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
2020-01-12 05:46:37 +01:00
minetest.after ( 1 , ensure_remote_portal_then_teleport , playerName , portal_definition , local_anchorPos , local_orientation , destination_wormholePos )
2019-08-06 23:23:06 +02:00
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
2020-01-02 13:42:20 +01:00
local destination_orientation = get_orientation_from_colorfacedir ( dest_wormhole_node.param2 )
2019-08-06 23:23:06 +02:00
local destination_anchorPos = portal_definition.shape . get_anchorPos_from_wormholePos ( destination_wormholePos , destination_orientation )
portal_definition.shape . disable_portal_trap ( destination_anchorPos , destination_orientation )
2020-01-08 11:37:31 +01:00
-- if the portal is already linked to a different portal then extinguish the other portal and
2019-08-06 23:23:06 +02:00
-- 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
2020-01-12 05:46:37 +01:00
minetest.sound_play ( portal_definition.sounds . teleport , { to_player = playerName } )
2019-08-06 23:23:06 +02:00
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 )
2020-01-05 10:14:30 +01:00
player : set_pos ( new_playerPos )
2019-08-06 23:23:06 +02:00
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.
2020-01-08 11:37:31 +01:00
-- 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
2019-08-06 23:23:06 +02:00
-- 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.
2020-01-08 11:37:31 +01:00
-- I don't think this is worth preventing, but I document it incase someone describes entering a portal
2019-08-06 23:23:06 +02:00
-- 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
2020-01-12 05:46:37 +01:00
minetest.after ( 0.1 , ensure_remote_portal_then_teleport , playerName , portal_definition , local_anchorPos , local_orientation , destination_wormholePos )
2019-08-06 23:23:06 +02:00
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 )
2020-01-12 05:46:37 +01:00
local playerName = obj : get_player_name ( )
2019-08-06 23:23:06 +02:00
minetest.after (
3 , -- hopefully target area is emerged in 3 seconds
function ( )
ensure_remote_portal_then_teleport (
2020-01-12 05:46:37 +01:00
playerName ,
2019-08-06 23:23:06 +02:00
portal_definition ,
local_anchorPos ,
local_orientation ,
destination_wormholePos
)
end
)
end
end
end
end
2020-01-06 12:21:15 +01:00
local p1 , p2 , portal_name
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
if portal_definition.on_run_wormhole ~= nil then
2019-08-06 23:23:06 +02:00
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
2020-02-01 06:51:58 +01:00
local function create_book ( item_name , inventory_description , inventory_image , title , author , chapters )
2019-08-06 23:23:06 +02:00
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] " ..
2020-02-01 06:51:58 +01:00
" background[0,0;18,11;nether_book_background.png;true] " ..
" image_button_exit[17.3,0;0.8,0.8;nether_book_close.png;;] " ..
2019-08-06 23:23:06 +02:00
" label[3.1,0.5; " .. minetest.formspec_escape ( title ) .. " ] " ..
2020-02-01 06:51:58 +01:00
" label[3.6,0.9; " .. author .. " ] "
local image_x_adj = - 0.4
local image_width = 1.6
local image_padding = 0.06
for i , chapter in ipairs ( chapters ) do
2020-02-02 05:49:13 +01:00
local left_margin = 0.9
local top_margin = 1.7
2020-02-01 06:51:58 +01:00
local width = 7.9
local height = 12.0
local item_number = i
local items_on_page = math.floor ( # chapters / 2 )
if i > items_on_page then
-- page 2
2020-02-02 05:49:13 +01:00
left_margin = 10.1
top_margin = 0.8
2020-02-01 06:51:58 +01:00
height = 12.9
item_number = i - items_on_page
items_on_page = # chapters - items_on_page
end
2019-08-06 23:23:06 +02:00
2020-02-02 05:49:13 +01:00
local available_height = ( height - top_margin ) / items_on_page
local y = top_margin + ( item_number - 1 ) * available_height
2019-08-06 23:23:06 +02:00
2020-02-01 06:51:58 +01:00
-- add chapter title
local title_height = 0
if chapter.title ~= nil then
title_height = 0.6
2020-02-02 05:49:13 +01:00
formspec = formspec .. " label[ " .. left_margin + 1.5 .. " , "
2020-02-01 06:51:58 +01:00
.. y .. " ; ──══♦♦♦◊ " .. minetest.formspec_escape ( chapter.title ) .. " ◊♦♦♦══──] "
end
-- add chapter image
local x_offset = 0
if chapter.image ~= nil then
x_offset = image_width + image_x_adj + image_padding
2020-02-02 05:49:13 +01:00
local image_height = image_width / chapter.image . width * chapter.image . height
formspec = formspec .. " image[ " .. left_margin + image_x_adj .. " , " .. y + title_height .. " ; " .. image_width .. " , "
.. image_height .. " ; " .. chapter.image . image .. " ] "
2020-02-01 06:51:58 +01:00
end
-- add chapter text
2020-02-02 05:49:13 +01:00
formspec = formspec .. " textarea[ " .. left_margin + x_offset .. " , " .. y + title_height .. " ; " .. width - x_offset .. " , "
2020-02-01 06:51:58 +01:00
.. available_height - title_height .. " ;; " .. minetest.formspec_escape ( chapter.text ) .. " ;] "
end
2019-08-06 23:23:06 +02:00
minetest.show_formspec ( player_name , item_name , formspec )
end
minetest.register_craftitem ( item_name , {
2020-02-02 05:49:13 +01:00
description = inventory_description ,
inventory_image = inventory_image ,
groups = { book = 1 } ,
on_use = display_book ,
_doc_items_hidden = true ,
_doc_items_longdesc =
S ( " A guidebook for how to build portals to other realms. It can sometimes be found in dungeon chests, however a copy of this book is not needed as its contents are included in this Encyclopedia. " ) .. " \n " ..
S ( " Refer: \" Help \" > \" Basics \" > \" Building Portals \" " ) ,
2019-08-06 23:23:06 +02:00
} )
end
2020-01-06 12:21:15 +01:00
local function add_book_as_treasure ( )
2020-02-02 05:49:13 +01:00
book_added_as_treasure = true
2020-01-06 12:21:15 +01:00
if minetest.get_modpath ( " loot " ) then
loot.register_loot ( {
2020-02-02 05:49:13 +01:00
weights = { generic = nether.PORTAL_BOOK_LOOT_WEIGHTING * 1000 ,
2020-01-06 12:21:15 +01:00
books = 100 } ,
payload = { stack = " nether:book_of_portals " }
} )
end
if minetest.get_modpath ( " dungeon_loot " ) then
2020-02-02 05:49:13 +01:00
dungeon_loot.register ( { name = " nether:book_of_portals " , chance = nether.PORTAL_BOOK_LOOT_WEIGHTING } )
2020-01-06 12:21:15 +01:00
end
-- todo: add to Treasurer mod TRMP https://github.com/poikilos/trmp_minetest_game
2020-02-02 05:49:13 +01:00
end
-- Returns true if the Help-modpack was installed and Portal instructions were added to it
-- Help-modpack details can be found at https://forum.minetest.net/viewtopic.php?t=15912
local function add_book_to_help_modpack ( chapters )
local result = false
if minetest.get_modpath ( " doc " ) ~= nil and minetest.global_exists ( " doc " ) then
if minetest.get_modpath ( " doc_basics " ) ~= nil then
local text = S ( " Portals to other realms can be opened by building a frame in the right shape with the right blocks, then using an item to activate it. A local copy of the guidebook to portals is published below. \n --- \n \n " )
local images = { }
for i , chapter in ipairs ( chapters ) do
if chapter.image ~= nil then
-- Portal chapters have images (from their portalDef.shape)
text = text .. " \n \n \n " .. ( i - 1 ) .. " ) " .. chapter.title .. " \n \n "
local aspect_3_to_2_width = chapter.image . width
local aspect_3_to_2_height = aspect_3_to_2_width / 3 * 2
if chapter.image . height > aspect_3_to_2_height then
aspect_3_to_2_height = chapter.image . height
aspect_3_to_2_width = aspect_3_to_2_height / 2 * 3
end
local image_conveted_to_3_2_ratio =
" [combine: " .. aspect_3_to_2_width .. " x " .. aspect_3_to_2_height .. " :0,0= " .. chapter.image . image
images [ # images + 1 ] = { image = image_conveted_to_3_2_ratio }
end
text = text .. chapter.text
end
result = doc.add_entry ( " basics " , " portals_api " , {
name = S ( " Building Portals " ) ,
data = {
text = text ,
images = images ,
aspect_ratio = .5
}
} )
end
end
return result
2020-01-06 12:21:15 +01:00
end
2019-08-06 23:23:06 +02:00
-- 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 ( )
2020-02-01 06:51:58 +01:00
local chapters = { }
2019-08-06 23:23:06 +02:00
2020-02-01 06:51:58 +01:00
local intro_text
2019-08-06 23:23:06 +02:00
-- tell the player how many portal types there are
2020-02-01 03:35:59 +01:00
if nether.registered_portals_count == 1 then
2020-02-01 06:51:58 +01:00
intro_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. " )
2019-08-06 23:23:06 +02:00
else
2020-02-01 06:51:58 +01:00
intro_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. " , nether.registered_portals_count )
2019-08-06 23:23:06 +02:00
end
-- tell the player how to ignite portals
local ignition_item_description = " <error - ignition item not set> "
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
2020-02-01 06:51:58 +01:00
intro_text = intro_text ..
S ( " \n \n The 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. " , string.lower ( ignition_item_description ) )
chapters [ # chapters + 1 ] = { text = intro_text }
2019-08-06 23:23:06 +02:00
-- 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).
2020-02-01 06:51:58 +01:00
local portalDefs_in_order = { }
2019-08-06 23:23:06 +02:00
if nether.registered_portals [ " nether_portal " ] then
2020-02-01 06:51:58 +01:00
portalDefs_in_order [ # portalDefs_in_order + 1 ] = nether.registered_portals [ " nether_portal " ]
2019-08-06 23:23:06 +02:00
end
for portalName , portalDef in pairs ( nether.registered_portals ) do
if portalName ~= " nether_portal " then
2020-02-01 06:51:58 +01:00
portalDefs_in_order [ # portalDefs_in_order + 1 ] = portalDef
2019-08-06 23:23:06 +02:00
end
end
2020-02-01 06:51:58 +01:00
for _ , portalDef in ipairs ( portalDefs_in_order ) do
chapters [ # chapters + 1 ] = {
2020-02-02 05:49:13 +01:00
text = portalDef.book_of_portals_pagetext ,
image = portalDef.shape . diagram_image ,
title = portalDef.title
2020-02-01 06:51:58 +01:00
}
end
2019-08-06 23:23:06 +02:00
2020-01-06 12:21:15 +01:00
2019-08-06 23:23:06 +02:00
create_book (
" nether:book_of_portals " ,
S ( " Book of Portals " ) ,
" nether_book_of_portals.png " ,
2020-02-01 06:51:58 +01:00
S ( " A definitive guide to Rifts and Portals " ) ,
2019-08-06 23:23:06 +02:00
" Riccard F. Burton " , -- perhaps a Richard F. Burton of an alternate universe
2020-02-01 06:51:58 +01:00
chapters
2019-08-06 23:23:06 +02:00
)
2020-01-06 12:21:15 +01:00
2020-02-02 05:49:13 +01:00
local using_helpModpack = add_book_to_help_modpack ( chapters )
if not using_helpModpack and not book_added_as_treasure and nether.PORTAL_BOOK_LOOT_WEIGHTING > 0 then
-- Only place the Book of Portals in chests if there are non-Nether (i.e. novel) portals
-- which players need a way to find out about.
if nether.registered_portals_count > 1 or ( nether.registered_portals_count == 1 and nether.registered_portals [ " nether_portal " ] == nil ) then
add_book_as_treasure ( )
end
2020-01-06 12:21:15 +01:00
end
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:31:10 +01:00
local node_def = minetest.registered_nodes [ frame_node_name ]
2019-08-06 23:23:06 +02:00
local extended_node_def = { }
2020-01-08 11:31:10 +01:00
for key , value in pairs ( node_def ) do extended_node_def [ key ] = value end
2019-08-06 23:23:06 +02:00
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
2020-01-12 06:47:05 +01:00
extended_node_def.replaced_by_portalapi . on_blast = extended_node_def.on_blast
extended_node_def.on_blast = function ( pos , intensity )
if DEBUG then minetest.chat_send_all ( " portal frame material: blast " ) end
extinguish_portal ( pos , frame_node_name , extended_node_def.replaced_by_portalapi . on_blast == nil )
2020-02-01 06:51:58 +01:00
if extended_node_def.replaced_by_portalapi . on_blast ~= nil then
2020-01-12 06:47:05 +01:00
extended_node_def.replaced_by_portalapi . on_blast ( pos , intensity )
else
minetest.remove_node ( pos )
end
end
2019-08-06 23:23:06 +02:00
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 " ) )
2020-01-05 03:19:46 +01:00
p2 = minetest.string_to_pos ( meta : get_string ( " p2 " ) )
2019-08-06 23:23:06 +02:00
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 )
2020-01-05 03:19:46 +01:00
if DEBUG then minetest.chat_send_all ( " LBM started portal timer " .. minetest.pos_to_string ( timerPos ) ) end
2019-08-06 23:23:06 +02:00
elseif DEBUG then
minetest.chat_send_all ( " get_node_timer " .. minetest.pos_to_string ( timerPos ) .. " returned null " )
end
end
end
} )
-- Portal API functions --
-- ==================== --
2020-01-08 10:06:00 +01:00
-- the fallback defaults for wormhole nodedefs
local wormhole_nodedef_default = {
description = S ( " Portal wormhole " ) ,
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.
2020-01-08 11:37:31 +01:00
-- If you need a different post_effect_color then use register_wormhole_node() to create
-- another wormhole node with the right post_effect_color and set it as the wormhole_node_name
2020-01-08 10:06:00 +01:00
-- 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 } ,
2020-01-08 11:37:31 +01:00
mesecons = { receptor = {
2020-01-08 10:06:00 +01:00
state = " on " ,
2020-01-08 11:37:31 +01:00
rules = function ( node )
2020-01-08 10:06:00 +01:00
return nether.get_mesecon_emission_rules_from_colorfacedir ( node.param2 )
end
} }
}
-- Call only at load time
function nether . register_wormhole_node ( name , nodedef )
assert ( name ~= nil , " Unable to register wormhole node: Name is nil " )
assert ( nodedef ~= nil , " Unable to register wormhole node '' " .. name .. " '': nodedef is nil " )
2020-01-08 11:37:31 +01:00
for key , value in pairs ( wormhole_nodedef_default ) do
2020-01-08 10:06:00 +01:00
if nodedef [ key ] == nil then nodedef [ key ] = value end
end
2020-01-08 11:37:31 +01:00
minetest.register_node ( name , nodedef )
2020-01-08 10:06:00 +01:00
end
2019-08-06 23:23:06 +02:00
-- The fallback defaults for registered portaldef tables
local portaldef_default = {
2020-02-01 06:51:58 +01:00
title = S ( " Untitled portal " ) ,
book_of_portals_pagetext = S ( " We know almost nothing about this portal " ) ,
2020-01-06 12:21:15 +01:00
shape = nether.PortalShape_Traditional ,
2020-01-02 13:42:20 +01:00
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 ,
2019-08-06 23:23:06 +02:00
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) " )
2020-01-06 12:21:15 +01:00
portaldef.particle_color = nether.portals_palette [ portaldef.wormhole_node_color ] . asString
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
2020-01-04 05:38:13 +01:00
local destination_pos = { x = pos.x , y = 0 , z = pos.z }
2020-01-08 11:37:31 +01:00
local existing_portal_location , existing_portal_orientation =
2020-01-04 05:38:13 +01:00
nether.find_nearest_working_portal ( name , destination_pos , 10 , 0 ) -- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are outside the realm)
if existing_portal_location ~= nil then
return existing_portal_location , existing_portal_orientation
2020-01-08 11:37:31 +01:00
else
2020-01-04 05:38:13 +01:00
destination_pos.y = nether.find_surface_target_y ( destination_pos.x , destination_pos.z , name )
return destination_pos
end
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
minetest.log ( " error " ,
portaldef.mod_name .. " tried to register a portal ' " .. portaldef.name .. " ' made of " .. portaldef.frame_node_name ..
2019-08-06 23:23:06 +02:00
" , 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
2020-02-01 03:35:59 +01:00
-- Update registered_portals_count
local portalCount = 0
for _ in pairs ( nether.registered_portals ) do portalCount = portalCount + 1 end
nether.registered_portals_count = portalCount
2019-08-06 23:23:06 +02:00
create_book_of_portals ( )
2020-02-01 03:35:59 +01:00
if not nether.is_frame_node [ portaldef.frame_node_name ] then
2019-08-06 23:23:06 +02:00
-- add portal functions to the nodedef being used for the portal frame
register_frame_node ( portaldef.frame_node_name )
2020-02-01 03:35:59 +01:00
nether.is_frame_node [ portaldef.frame_node_name ] = true
2019-08-06 23:23:06 +02:00
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 )
2020-02-01 03:35:59 +01:00
nether.is_frame_node [ portaldef.frame_node_name ] = nil
2019-08-06 23:23:06 +02:00
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
2020-02-01 03:35:59 +01:00
if pt.under and nether.is_frame_node [ minetest.get_node ( pt.under ) . name ] then
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
-- It checks the area for any nodes that aren't ground or trees.
2020-01-04 05:38:13 +01:00
-- (Water also fails this test, unless it is unemerged)
2019-08-06 23:23:06 +02:00
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
2020-01-02 13:42:20 +01:00
if DEBUG and id == nil then minetest.chat_send_all ( " nil block at index " .. vi ) end
2020-01-04 05:38:13 +01:00
if id ~= c_air and id ~= c_ignore and id ~= nil then -- checked for common natural or not emerged
2019-08-06 23:23:06 +02:00
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
2020-01-02 13:42:20 +01:00
if node_groups == nil or ( node_groups.tree == nil and node_groups.leaves == nil and node_groups.leafdecay == nil ) then
2020-01-04 05:38:13 +01:00
if DEBUG then minetest.chat_send_all ( " volume_is_natural() found unnatural node " .. name ) end
2019-08-06 23:23:06 +02:00
return false
end
end
end
vi = vi + 1
end
end
end
2020-01-04 05:38:13 +01:00
if DEBUG then minetest.chat_send_all ( " Volume is natural " ) end
2019-08-06 23:23:06 +02:00
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 )
2020-01-08 11:37:31 +01:00
-- default to starting the search at -16 (probably underground) if we don't know the
2020-01-05 03:19:46 +01:00
-- surface, like paramat's original code from before get_spawn_level() was available:
-- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676
local start_y = - 16
2019-08-06 23:23:06 +02:00
-- try to spawn on surface first
if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
2020-01-08 11:31:10 +01:00
local surface_level = minetest.get_spawn_level ( target_x , target_z )
2020-01-08 11:37:31 +01:00
if surface_level ~= nil then -- test this since get_spawn_level() can return nil over water or steep/high terrain
2020-01-02 13:42:20 +01:00
2020-01-08 11:37:31 +01:00
-- get_spawn_level() tends to err on the side of caution and spawns the player a
-- block higher than the ground level. The implementation is mapgen specific
2020-01-05 03:19:46 +01:00
-- and -2 seems to be the right correction for v6, v5, carpathian, valleys, and flat,
2020-01-04 05:38:13 +01:00
-- but v7 only needs -1.
2020-01-08 11:37:31 +01:00
-- Perhaps this was not always the case, and -2 may be too much in older versions
-- of minetest, but half-buried portals are perferable to floating ones, and they
2020-01-05 03:19:46 +01:00
-- will clear a suitable hole around themselves.
2020-01-08 11:37:31 +01:00
if minetest.get_mapgen_setting ( " mg_name " ) == " v7 " then
2020-01-04 05:38:13 +01:00
surface_level = surface_level - 1
else
surface_level = surface_level - 2
2019-08-06 23:23:06 +02:00
end
2020-01-05 03:19:46 +01:00
start_y = surface_level
2019-08-06 23:23:06 +02:00
end
end
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
2020-01-05 03:19:46 +01:00
if DEBUG then minetest.chat_send_all ( " Volume_is_natural check failed, but a portal frame is here " .. minetest.pos_to_string ( anchorPos ) .. " , so this is still a good target y level " ) end
2019-08-06 23:23:06 +02:00
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. " )
2020-01-08 11:37:31 +01:00
local contenders = list_closest_portals ( portal_definition , anchorPos , distance_limit , y_factor )
2019-08-06 23:23:06 +02:00
-- sort by distance
local dist_list = { }
for dist , _ in pairs ( contenders ) do table.insert ( dist_list , dist ) end
table.sort ( dist_list )
2020-01-08 11:37:31 +01:00
for _ , dist in ipairs ( dist_list ) do
2019-08-06 23:23:06 +02:00
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
2020-01-08 11:37:31 +01:00
-- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been
2019-08-06 23:23:06 +02:00
-- destroyed, so check the portal is still there
2020-01-08 11:31:10 +01:00
local portalFound , portalIsActive = is_portal_at_anchorPos ( portal_definition , portal_info.anchorPos , portal_info.orientation , true )
2019-08-06 23:23:06 +02:00
if portalFound then
return portal_info.anchorPos , portal_info.orientation
else
2020-01-02 13:42:20 +01:00
if DEBUG then minetest.chat_send_all ( " Portal wasn't found, removing portal from mod_storage at " .. minetest.pos_to_string ( portal_info.anchorPos ) .. " orientation " .. portal_info.orientation ) end
2019-08-06 23:23:06 +02:00
-- The portal at that location must have been destroyed
remove_portal_location_info ( portal_name , portal_info.anchorPos )
end
end
return nil
2020-01-06 12:21:15 +01:00
end