2019-08-06 23:23:06 +02:00
--[[
2019-07-27 12:33:19 +02:00
Nether mod portal examples for Minetest
2020-01-02 13:42:20 +01:00
These portal API examples are independent of the Nether .
To use this file , set nether.ENABLE_EXAMPLE_PORTALS to true in init.lua
2019-07-27 12:33:19 +02:00
Copyright ( C ) 2019 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 05:38:13 +01:00
local S = nether.get_translator
2019-07-27 12:33:19 +02:00
2020-01-02 13:42:20 +01:00
-- Sets how far a Surface Portal will travel, measured in cells along the Moore curve,
-- which are about 117 nodes square each. Larger numbers will generally mean further distance
-- as-the-crow-flies, but for small adjustments this will not always be true due to the how
-- the Moore curve frequently doubles back upon itself.
2020-01-04 05:38:13 +01:00
-- This doubling-back prevents the surface portal from taking players easily accross the
-- map - the curve is 262144 cells long!
local SURFACE_TRAVEL_DISTANCE = 26
2020-01-02 13:42:20 +01:00
2020-01-04 05:38:13 +01:00
local FLOATLANDS_ENABLED = false
local FLOATLAND_LEVEL = 1280
local floatlands_flavortext = " "
if minetest.get_mapgen_setting ( " mg_name " ) == " v7 " then
local mgv7_spflags = minetest.get_mapgen_setting ( " mgv7_spflags " )
FLOATLANDS_ENABLED = mgv7_spflags ~= nil and mgv7_spflags : find ( " floatlands " ) ~= nil and mgv7_spflags : find ( " nofloatlands " ) == nil
FLOATLAND_LEVEL = minetest.get_mapgen_setting ( " mgv7_floatland_level " ) or 1280
if FLOATLANDS_ENABLED then
floatlands_flavortext = " There is a floating land of hills and lakes and forests up there, the edges of which lead to a drop all the way back down to the surface. We have not found how far these strange lands extend. One day I may retire here. "
end
end
2019-07-27 12:33:19 +02:00
nether.register_portal ( " floatlands_portal " , {
2020-01-02 13:42:20 +01:00
shape = nether.PortalShape_Platform ,
2019-07-27 12:33:19 +02:00
frame_node_name = " default:ice " ,
wormhole_node_color = 7 , -- 2 is blue
2020-01-02 13:42:20 +01:00
wormhole_node_is_horizontal = true , -- indicate the wormhole surface is horizontal
2019-07-27 12:33:19 +02:00
particle_texture = {
name = " nether_particle_anim1.png " ,
animation = {
type = " vertical_frames " ,
aspect_w = 7 ,
aspect_h = 7 ,
length = 1 ,
} ,
scale = 1.5
} ,
book_of_portals_pagetext = S ( [ [ ─ ─ ═ ═ ♦ ♦ ♦ ◊ The Floatlands ◊ ♦ ♦ ♦ ═ ═ ─ ─
2020-01-04 05:38:13 +01:00
Requiring 21 blocks of ice , and constructed in the shape of a 3 × 3 platform with walls , or like a bowl :
┌ ─ ┬ ─ ┬ ─ ┐
┌ ─ ┼ ─ ┴ ─ ┴ ─ ┼ ─ ┐ Plan view ( looking down from above )
├ ─ ┤ ├ ─ ┤
├ ─ ┤ ├ ─ ┤ five blocks wide
└ ─ ┼ ─ ┬ ─ ┬ ─ ┼ ─ ┘ in both directions
└ ─ ┴ ─ ┴ ─ ┘
2019-07-27 12:33:19 +02:00
2020-01-04 05:38:13 +01:00
┌ ─ ┬ ─ ┬ ─ ┬ ─ ┬ ─ ┐ Side view ( looking from either side )
└ ─ ┼ ─ ┼ ─ ┼ ─ ┼ ─ ┘
└ ─ ┴ ─ ┴ ─ ┘ two blocks deep
2019-07-27 12:33:19 +02:00
2020-01-04 05:38:13 +01:00
This portal is different to the others , rather than acting akin to a doorway it appears to the eye more like a small pool of water which can be stepped into . Upon setting foot in the portal we found ourselves at a great altitude .
@ 1
] ] , floatlands_flavortext ) ,
2019-07-27 12:33:19 +02:00
2019-07-28 05:15:51 +02:00
is_within_realm = function ( pos ) -- return true if pos is inside the Nether
2020-01-04 05:38:13 +01:00
return pos.y > FLOATLAND_LEVEL - 200
2019-07-27 12:33:19 +02:00
end ,
find_realm_anchorPos = function ( surface_anchorPos )
2020-01-04 05:38:13 +01:00
-- TODO: Once paramat finishes adjusting the floatlands, implement a surface algorithm that finds land
local destination_pos = { x = surface_anchorPos.x , y = FLOATLAND_LEVEL + 2 , z = surface_anchorPos.z }
2019-07-27 12:33:19 +02:00
2020-01-04 05:38:13 +01:00
-- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Floatlands)
local existing_portal_location , existing_portal_orientation = nether.find_nearest_working_portal ( " floatlands_portal " , destination_pos , 20 , 0 )
2019-07-27 12:33:19 +02:00
if existing_portal_location ~= nil then
return existing_portal_location , existing_portal_orientation
2020-01-04 05:38:13 +01:00
else
2019-07-27 12:33:19 +02:00
return destination_pos
end
2020-01-04 05:38:13 +01:00
end
2019-07-27 12:33:19 +02:00
} )
2020-01-02 13:35:48 +01:00
2020-01-02 13:42:20 +01:00
-- These Moore Curve functions requred by surface_portal's find_surface_anchorPos() will
2020-01-02 13:35:48 +01:00
-- be assigned later in this file.
local get_moore_distance -- will be function get_moore_distance(cell_count, x, y): integer
local get_moore_coords -- will be function get_moore_coords(cell_count, distance): pos2d
2020-01-02 13:42:20 +01:00
nether.register_portal ( " surface_portal " , {
2019-07-27 12:33:19 +02:00
shape = nether.PortalShape_Circular ,
2020-01-04 05:38:13 +01:00
frame_node_name = " default:tinblock " ,
2019-07-27 12:33:19 +02:00
wormhole_node_color = 4 , -- 4 is cyan
2020-01-02 13:35:48 +01:00
book_of_portals_pagetext = S ( [ [ ─ ─ ═ ═ ♦ ♦ ♦ ◊ Surface portal ◊ ♦ ♦ ♦ ═ ═ ─ ─
2019-07-27 12:33:19 +02:00
2020-01-04 05:38:13 +01:00
Requiring 16 blocks of tin , the frame must be constructed in the following fashion :
2019-07-27 12:33:19 +02:00
┌ ═ ╤ ═ ╤ ═ ╗
┌ ═ ┼ ─ ┴ ─ ┴ ─ ┼ ═ ╗
┌ ═ ┼ ─ ┘ └ ─ ┼ ═ ╗
├ ─ ╢ ├ ─ ╢
2020-01-04 05:38:13 +01:00
├ ─ ╢ ├ ─ ╢ seven blocks wide
└ ─ ╚ ═ ╗ ┌ ═ ╡ ─ ┘ seven blocks high
└ ─ ╚ ═ ╤ ═ ╤ ═ ┼ ─ ┘ in a circular shape
└ ─ ┴ ─ ┴ ─ ┘ standing vertically , like a doorway
2019-07-27 12:33:19 +02:00
2020-01-04 05:38:13 +01:00
These travel a distance along the ground , and even when constructed deep underground they link back up to the surface , but we were never able to predict where the matching twin portal would appear . Coudreau believes it works in epicycles , but I am not convinced .
] ] ) ,
2019-07-27 12:33:19 +02:00
2020-01-02 13:35:48 +01:00
is_within_realm = function ( pos )
-- Always return true, because these portals always just take you around the surface
-- rather than taking you to a realm
return true
2019-07-27 12:33:19 +02:00
end ,
find_realm_anchorPos = function ( surface_anchorPos )
2020-01-02 13:35:48 +01:00
-- This function isn't needed, since this type of portal always goes to the surface
minecraft.log ( " error " , " find_realm_anchorPos called for surface portal " )
return { x = 0 , y = 0 , z = 0 }
2019-07-27 12:33:19 +02:00
end ,
find_surface_anchorPos = function ( realm_anchorPos )
-- A portal definition doesn't normally need to provide a find_surface_anchorPos() function,
2020-01-02 13:35:48 +01:00
-- since find_surface_target_y() will be used by default, but these portals travel around the
-- surface (following a Moore curve) so will be using a different x and z to realm_anchorPos.
2019-07-27 12:33:19 +02:00
2020-01-02 13:35:48 +01:00
local cellCount = 512
local maxDistFromOrigin = 30000 -- the world edges are at X=30927, X=− 30912, Z=30927 and Z=− 30912
2019-07-27 12:33:19 +02:00
2020-01-02 13:35:48 +01:00
-- clip realm_anchorPos to maxDistFromOrigin, and move the origin so that all values are positive
local x = math.min ( maxDistFromOrigin , math.max ( - maxDistFromOrigin , realm_anchorPos.x ) ) + maxDistFromOrigin
local z = math.min ( maxDistFromOrigin , math.max ( - maxDistFromOrigin , realm_anchorPos.z ) ) + maxDistFromOrigin
local divisor = math.ceil ( maxDistFromOrigin * 2 / cellCount )
local distance = get_moore_distance ( cellCount , math.floor ( x / divisor + 0.5 ) , math.floor ( z / divisor + 0.5 ) )
2020-01-02 13:42:20 +01:00
local destination_distance = ( distance + SURFACE_TRAVEL_DISTANCE ) % ( cellCount * cellCount )
2020-01-02 13:35:48 +01:00
local moore_pos = get_moore_coords ( cellCount , destination_distance )
local target_x = moore_pos.x * divisor - maxDistFromOrigin
local target_z = moore_pos.y * divisor - maxDistFromOrigin
2020-01-04 05:38:13 +01:00
local search_radius = divisor / 2 - 5 -- any portal within this area will do
2020-01-02 13:35:48 +01:00
-- a y_factor of 0 makes the search ignore the altitude of the portals
2020-01-04 05:38:13 +01:00
local existing_portal_location , existing_portal_orientation =
nether.find_nearest_working_portal ( " surface_portal " , { x = target_x , y = 0 , z = target_z } , search_radius , 0 )
2019-07-27 12:33:19 +02:00
if existing_portal_location ~= nil then
2020-01-04 05:38:13 +01:00
-- use the existing portal that was found near target_x, target_z
2019-07-27 12:33:19 +02:00
return existing_portal_location , existing_portal_orientation
else
2020-01-04 05:38:13 +01:00
-- find a good location for the new portal
local adj_x , adj_z = 0 , 0
if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
-- Deterministically look for a location in the cell where get_spawn_level() can give
-- us a surface height, since nether.find_surface_target_y() works *much* better when
-- it can use get_spawn_level()
local prng = PcgRandom ( -- seed the prng so that all portals for these Moore Curve coords will use the same random location
moore_pos.x * 65732 +
moore_pos.y * 729 +
minetest.get_mapgen_setting ( " seed " ) * 3
)
local attemptLimit = 12 -- how many attempts we'll make at finding a good location
for attempt = 1 , attemptLimit do
adj_x = math.floor ( prng : rand_normal_dist ( - search_radius , search_radius , 2 ) + 0.5 )
adj_z = math.floor ( prng : rand_normal_dist ( - search_radius , search_radius , 2 ) + 0.5 )
minetest.chat_send_all ( attempt .. " : x " .. target_x + adj_x .. " , z " .. target_z + adj_z )
if minetest.get_spawn_level ( target_x + adj_x , target_z + adj_z ) ~= nil then
-- found a location which will be at ground level (unless a player has built there)
minetest.chat_send_all ( " x " .. target_x + adj_x .. " , z " .. target_z + adj_z .. " is suitable. Within " .. search_radius .. " of " .. target_x .. " , " .. target_z )
break
end
end
end
local destination_pos = { x = target_x + adj_x , y = 0 , z = target_z + adj_z }
2020-01-02 13:42:20 +01:00
destination_pos.y = nether.find_surface_target_y ( destination_pos.x , destination_pos.z , " surface_portal " )
2020-01-04 05:38:13 +01:00
2019-07-27 12:33:19 +02:00
return destination_pos
end
end
} )
2020-01-02 13:35:48 +01:00
--=========================================--
-- Hilbert curve and Moore curve functions --
--=========================================--
2020-01-02 13:42:20 +01:00
-- These are space-filling curves, used by the surface_portal example as a way to determine where
2020-01-02 13:35:48 +01:00
-- to place portals. https://en.wikipedia.org/wiki/Moore_curve
-- Flip a quadrant on its diagonal axis
-- cell_count is the number of cells across the square is split into, and must be a power of 2
-- if flip_twice is true then pos does not change (any even numbers of flips would cancel out)
-- if flip_direction is true then the position is flipped along the \ diagonal
-- if flip_direction is false then the position is flipped along the / diagonal
local function hilbert_flip ( cell_count , pos , flip_direction , flip_twice )
if not flip_twice then
if flip_direction then
pos.x = ( cell_count - 1 ) - pos.x ;
pos.y = ( cell_count - 1 ) - pos.y ;
end
local temp_x = pos.x ;
pos.x = pos.y ;
pos.y = temp_x ;
end
end
local function test_bit ( cell_count , value , flag )
local bit_value = cell_count / 2
while bit_value > flag and bit_value >= 1 do
if value >= bit_value then value = value - bit_value end
bit_value = bit_value / 2
end
return value >= bit_value
end
-- Converts (x,y) to distance
-- starts at bottom left corner, i.e. (0, 0)
-- ends at bottom right corner, i.e. (cell_count - 1, 0)
local function get_hilbert_distance ( cell_count , x , y )
local distance = 0
local pos = { x = x , y = y }
local rx , ry
local s = cell_count / 2
while s > 0 do
if test_bit ( cell_count , pos.x , s ) then rx = 1 else rx = 0 end
if test_bit ( cell_count , pos.y , s ) then ry = 1 else ry = 0 end
local rx_XOR_ry = rx
if ry == 1 then rx_XOR_ry = 1 - rx_XOR_ry end -- XOR'd ry against rx
distance = distance + s * s * ( 2 * rx + rx_XOR_ry )
hilbert_flip ( cell_count , pos , rx > 0 , ry > 0 ) ;
s = math.floor ( s / 2 )
end
return distance ;
end
-- Converts distance to (x,y)
local function get_hilbert_coords ( cell_count , distance )
local pos = { x = 0 , y = 0 }
local rx , ry
local s = 1
while s < cell_count do
rx = math.floor ( distance / 2 ) % 2
ry = distance % 2
if rx == 1 then ry = 1 - ry end -- XOR ry with rx
hilbert_flip ( s , pos , rx > 0 , ry > 0 ) ;
pos.x = pos.x + s * rx
pos.y = pos.y + s * ry
distance = math.floor ( distance / 4 )
s = s * 2
end
return pos
end
-- Converts (x,y) to distance
-- A Moore curve is a variation of the Hilbert curve that has the start and
-- end next to each other.
-- Top middle point is the start/end location
get_moore_distance = function ( cell_count , x , y )
local quadLength = cell_count / 2
local quadrant = 1 - math.floor ( y / quadLength )
if math.floor ( x / quadLength ) == 1 then quadrant = 3 - quadrant end
local flipDirection = x < quadLength
local pos = { x = x % quadLength , y = y % quadLength }
hilbert_flip ( quadLength , pos , flipDirection , false )
return ( quadrant * quadLength * quadLength ) + get_hilbert_distance ( quadLength , pos.x , pos.y )
end
-- Converts distance to (x,y)
-- A Moore curve is a variation of the Hilbert curve that has the start and
-- end next to each other.
-- Top middle point is the start/end location
get_moore_coords = function ( cell_count , distance )
local quadLength = cell_count / 2
local quadDistance = quadLength * quadLength
local quadrant = math.floor ( distance / quadDistance )
local flipDirection = distance * 2 < cell_count * cell_count
local pos = get_hilbert_coords ( quadLength , distance % quadDistance )
hilbert_flip ( quadLength , pos , flipDirection , false )
if quadrant >= 2 then pos.x = pos.x + quadLength end
if quadrant % 3 == 0 then pos.y = pos.y + quadLength end
return pos
end