Finish example portals

give nether_portal_circular.mts a flat floor - the new nether_portal_circular.mts allows its bottom nodes to be sunk into the ground.
Example portal basic behavior and book_of_portals_pagetext implemented.
Other changes are allowing portals corrupted by ABMs to be repaired, finding better ground level of surface portals.
This commit is contained in:
Treer 2020-01-04 15:38:13 +11:00 committed by SmallJoker
parent 6752964c96
commit a971e0359e
3 changed files with 155 additions and 130 deletions

View File

@ -1,6 +1,6 @@
-- see portal_api.txt for documentation -- see portal_api.txt for documentation
local DEBUG = false local DEBUG = true
local IGNORE_MODSTORAGE_PORTALS = false -- set true if you don't want portals to remember where they were linked - sometimes it's handy for debugging to have the portal always recalculate its target
nether.registered_portals = {} nether.registered_portals = {}
@ -581,17 +581,21 @@ end
-- Add portal information to mod storage, so new portals may find existing portals near the target location. -- 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 -- Do this whenever a portal is created or changes its ignition state
local function store_portal_location_info(portal_name, anchorPos, orientation, ignited) local function store_portal_location_info(portal_name, anchorPos, orientation, ignited)
if not IGNORE_MODSTORAGE_PORTALS then
mod_storage:set_string( mod_storage:set_string(
minetest.pos_to_string(anchorPos) .. " is " .. portal_name, minetest.pos_to_string(anchorPos) .. " is " .. portal_name,
minetest.serialize({orientation = orientation, active = ignited}) minetest.serialize({orientation = orientation, active = ignited})
) )
end end
end
-- Remove portal information from mod storage. -- Remove portal information from mod storage.
-- Do this if a portal frame is destroyed such that it cannot be ignited anymore. -- Do this if a portal frame is destroyed such that it cannot be ignited anymore.
local function remove_portal_location_info(portal_name, anchorPos) local function remove_portal_location_info(portal_name, anchorPos)
if not IGNORE_MODSTORAGE_PORTALS then
mod_storage:set_string(minetest.pos_to_string(anchorPos) .. " is " .. portal_name, "") mod_storage:set_string(minetest.pos_to_string(anchorPos) .. " is " .. portal_name, "")
end end
end
-- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage -- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage
-- data. -- data.
@ -607,6 +611,8 @@ local function list_closest_portals(portal_definition, anchorPos, distance_limit
local result = {} local result = {}
if not IGNORE_MODSTORAGE_PORTALS then
local isRealm = portal_definition.is_within_realm(anchorPos) local isRealm = portal_definition.is_within_realm(anchorPos)
if distance_limit == nil then distance_limit = -1 end if distance_limit == nil then distance_limit = -1 end
if y_factor == nil then y_factor = 1 end if y_factor == nil then y_factor = 1 end
@ -624,7 +630,7 @@ local function list_closest_portals(portal_definition, anchorPos, distance_limit
local distance = math.hypot(y * y_factor, math.hypot(x, z)) local distance = math.hypot(y * y_factor, math.hypot(x, z))
if distance <= distance_limit or distance_limit < 0 then if distance <= distance_limit or distance_limit < 0 then
local info = minetest.deserialize(value) or {} local info = minetest.deserialize(value) or {}
if DEBUG then minetest.chat_send_all("found " .. found_name .. " listed at distance " .. distance .. " from dest " .. minetest.pos_to_string(anchorPos) .. ", found: " .. minetest.pos_to_string(found_anchorPos) .. " orientation " .. info.orientation) end 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.anchorPos = found_anchorPos
info.distance = distance info.distance = distance
result[distance] = info result[distance] = info
@ -633,6 +639,7 @@ local function list_closest_portals(portal_definition, anchorPos, distance_limit
end end
end end
end end
end
return result return result
end end
@ -1020,7 +1027,7 @@ local function ignite_portal(ignition_pos, ignition_node_name)
local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name) local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name)
for _, portal_definition in ipairs(portal_definition_list) do for _, portal_definition in ipairs(portal_definition_list) do
local continue = false local continue = false -- WRT the for loop, since lua has no continue keyword
-- check it was a portal frame that the player is trying to ignite -- 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) local anchorPos, orientation, is_ignited = is_within_portal_frame(portal_definition, ignition_pos)
@ -1028,11 +1035,20 @@ local function ignite_portal(ignition_pos, ignition_node_name)
if DEBUG then minetest.chat_send_all("No " .. portal_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end if DEBUG then minetest.chat_send_all("No " .. portal_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end
continue = true -- no portal is here, but perhaps there more than one portal type we need to search for continue = true -- no portal is here, but perhaps there more than one portal type we need to search for
elseif is_ignited then elseif is_ignited then
if DEBUG then local repair = false
local meta = minetest.get_meta(ignition_pos) local meta = minetest.get_meta(ignition_pos)
if meta ~= nil then minetest.chat_send_all("This portal links to " .. meta:get_string("target") .. ". p1=" .. meta:get_string("p1") .. " p2=" .. meta:get_string("p2")) end if meta ~= nil then
if meta:get_string("p1") == "" then
-- metadata is missing, the portal frame node must have been removed without calling
-- 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
else
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
end end
return false -- portal is already ignited end
if not repair then return false end -- portal is already ignited
end end
if continue == false then if continue == false then
@ -1595,8 +1611,16 @@ function nether.register_portal(name, portaldef)
if portaldef.find_surface_anchorPos == nil then -- default to using find_surface_target_y() if portaldef.find_surface_anchorPos == nil then -- default to using find_surface_target_y()
portaldef.find_surface_anchorPos = function(pos) portaldef.find_surface_anchorPos = function(pos)
local surface_y = nether.find_surface_target_y(pos.x, pos.z, name)
return {x = pos.x, y = surface_y, z = pos.z} local destination_pos = {x = pos.x, y = 0, z = pos.z}
local existing_portal_location, existing_portal_orientation =
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
else
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, name)
return destination_pos
end
end end
end end
@ -1674,6 +1698,7 @@ end
-- use this when determining where to spawn a portal, to avoid overwriting player builds -- use this when determining where to spawn a portal, to avoid overwriting player builds
-- It checks the area for any nodes that aren't ground or trees. -- It checks the area for any nodes that aren't ground or trees.
-- (Water also fails this test, unless it is unemerged)
function nether.volume_is_natural(minp, maxp) function nether.volume_is_natural(minp, maxp)
local c_air = minetest.get_content_id("air") local c_air = minetest.get_content_id("air")
local c_ignore = minetest.get_content_id("ignore") local c_ignore = minetest.get_content_id("ignore")
@ -1691,13 +1716,14 @@ function nether.volume_is_natural(minp, maxp)
for x = pos1.x, pos2.x do for x = pos1.x, pos2.x do
local id = data[vi] -- Existing node local id = data[vi] -- Existing node
if DEBUG and id == nil then minetest.chat_send_all("nil block at index " .. vi) end if DEBUG and id == nil then minetest.chat_send_all("nil block at index " .. vi) end
if id ~= c_air and id ~= c_ignore and id ~= nil then -- These are natural or not emerged if id ~= c_air and id ~= c_ignore and id ~= nil then -- checked for common natural or not emerged
local name = minetest.get_name_from_content_id(id) local name = minetest.get_name_from_content_id(id)
local nodedef = minetest.registered_nodes[name] local nodedef = minetest.registered_nodes[name]
if not nodedef.is_ground_content then if not nodedef.is_ground_content then
-- trees are natural but not "ground content" -- trees are natural but not "ground content"
local node_groups = nodedef.groups local node_groups = nodedef.groups
if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil and node_groups.leafdecay == nil) then if node_groups == nil or (node_groups.tree == nil and node_groups.leaves == nil and node_groups.leafdecay == nil) then
if DEBUG then minetest.chat_send_all("volume_is_natural() found unnatural node " .. name) end
return false return false
end end
end end
@ -1707,6 +1733,7 @@ function nether.volume_is_natural(minp, maxp)
end end
end end
if DEBUG then minetest.chat_send_all("Volume is natural") end
return true return true
end end
@ -1718,26 +1745,20 @@ function nether.find_surface_target_y(target_x, target_z, portal_name)
if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
local surface_level = minetest.get_spawn_level(target_x, target_z) local surface_level = minetest.get_spawn_level(target_x, target_z)
if surface_level ~= nil then if surface_level ~= nil then
-- get_spawn_level() seems to err on the side of caution and sometimes spawn the player a
-- block higher than the ground level.
local shouldBeGroundPos = {x = target_x, y = surface_level - 1, z = target_z}
local groundNode = minetest.get_node_or_nil(shouldBeGroundPos)
if groundNode == nil then
-- force the area to be loaded - it's going to be loaded anyway by volume_is_natural()
minetest.get_voxel_manip():read_from_map(shouldBeGroundPos, shouldBeGroundPos)
groundNode = minetest.get_node(shouldBeGroundPos)
end
if not groundNode.is_ground_content then
if DEBUG then minetest.chat_send_all("find_surface_target_y dropped spawn_level by 1") end
surface_level = surface_level - 1
shouldBeGroundPos.y = shouldBeGroundPos.y - 1 -- get_spawn_level() tends to err on the side of caution and sometimes spawn the player a
groundNode = minetest.get_node_or_nil(shouldBeGroundPos) -- block higher than the ground level. The implementation is mapgen specific
if groundNode ~= nil and not groundNode.is_ground_content then -- and -2 seems to be the right amount for v6, v5, carpathian, valleys, and flat,
if DEBUG then minetest.chat_send_all("find_surface_target_y dropped spawn_level by 2") end -- but v7 only needs -1.
-- 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
-- will clear a suitable hole around them.
if minetest.get_mapgen_setting("mg_name") == "v7" then
surface_level = surface_level - 1 surface_level = surface_level - 1
else
surface_level = surface_level - 2
end end
end
-- Check volume for non-natural nodes -- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = surface_level - 1, z = target_z - 2} local minp = {x = target_x - 1, y = surface_level - 1, z = target_z - 2}
local maxp = {x = target_x + 2, y = surface_level + 3, z = target_z + 2} local maxp = {x = target_x + 2, y = surface_level + 3, z = target_z + 2}

View File

@ -22,15 +22,30 @@
SOFTWARE. SOFTWARE.
]]-- ]]--
local S = nether.get_translator
-- Sets how far a Surface Portal will travel, measured in cells along the Moore curve, -- 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 -- 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 -- 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. -- the Moore curve frequently doubles back upon itself.
local SURFACE_TRAVEL_DISTANCE = 15 -- 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
local S = nether.get_translator 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
nether.register_portal("floatlands_portal", { nether.register_portal("floatlands_portal", {
shape = nether.PortalShape_Platform, shape = nether.PortalShape_Platform,
@ -49,59 +64,40 @@ nether.register_portal("floatlands_portal", {
}, },
book_of_portals_pagetext = S([[ The Floatlands book_of_portals_pagetext = S([[ The Floatlands
Requiring 14 blocks of ice, but otherwise constructed the same as the portal to the Nether: 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
]] .. "\u{25A9}"), Side view (looking from either side)
two blocks deep
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),
is_within_realm = function(pos) -- return true if pos is inside the Nether is_within_realm = function(pos) -- return true if pos is inside the Nether
return pos.y < nether.DEPTH return pos.y > FLOATLAND_LEVEL - 200
end, end,
find_realm_anchorPos = function(surface_anchorPos) find_realm_anchorPos = function(surface_anchorPos)
-- divide x and z by a factor of 8 to implement Nether fast-travel -- TODO: Once paramat finishes adjusting the floatlands, implement a surface algorithm that finds land
local destination_pos = vector.divide(surface_anchorPos, nether.FASTTRAVEL_FACTOR) local destination_pos = {x = surface_anchorPos.x ,y = FLOATLAND_LEVEL + 2, z = surface_anchorPos.z}
destination_pos.x = math.floor(0.5 + destination_pos.x) -- round to int
destination_pos.z = math.floor(0.5 + destination_pos.z) -- round to int
destination_pos.y = nether.DEPTH - 1000 -- temp value so find_nearest_working_portal() returns nether portals
-- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Nether) -- 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, 8, 0) local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("floatlands_portal", destination_pos, 20, 0)
if existing_portal_location ~= nil then if existing_portal_location ~= nil then
return existing_portal_location, existing_portal_orientation return existing_portal_location, existing_portal_orientation
else else
local start_y = nether.DEPTH - math.random(500, 1500) -- Search starting altitude
destination_pos.y = nether.find_nether_ground_y(destination_pos.x, destination_pos.z, start_y)
return destination_pos return destination_pos
end end
end,
find_surface_anchorPos = function(realm_anchorPos)
-- A portal definition doesn't normally need to provide a find_surface_anchorPos() function,
-- since find_surface_target_y() will be used by default, but Nether portals also scale position
-- to create fast-travel. Defining a custom function also means we can look for existing nearby portals:
-- Multiply x and z by a factor of 8 to implement Nether fast-travel
local destination_pos = vector.multiply(realm_anchorPos, nether.FASTTRAVEL_FACTOR)
destination_pos.x = math.min(30900, math.max(-30900, destination_pos.x)) -- clip to world boundary
destination_pos.z = math.min(30900, math.max(-30900, destination_pos.z)) -- clip to world boundary
destination_pos.y = 0 -- temp value so find_nearest_working_portal() doesn't return nether portals
-- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are outside the Nether)
local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("floatlands_portal", destination_pos, 8 * nether.FASTTRAVEL_FACTOR, 0)
if existing_portal_location ~= nil then
return existing_portal_location, existing_portal_orientation
else
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "nether_portal")
return destination_pos
end end
end,
}) })
@ -112,21 +108,23 @@ local get_moore_coords -- will be function get_moore_coords(cell_count, distan
nether.register_portal("surface_portal", { nether.register_portal("surface_portal", {
shape = nether.PortalShape_Circular, shape = nether.PortalShape_Circular,
frame_node_name = "default:cobble", frame_node_name = "default:tinblock",
wormhole_node_color = 4, -- 4 is cyan wormhole_node_color = 4, -- 4 is cyan
book_of_portals_pagetext = S([[ Surface portal book_of_portals_pagetext = S([[ Surface portal
Requiring 16 blocks of tin, the frame must be constructed in the following fashion:
Stargate? seven blocks wide
seven blocks high
in a circular shape
standing vertically, like a doorway
These 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.
]] .. "\u{25A9}"), ]]),
is_within_realm = function(pos) is_within_realm = function(pos)
-- Always return true, because these portals always just take you around the surface -- Always return true, because these portals always just take you around the surface
@ -156,14 +154,25 @@ These
local distance = get_moore_distance(cellCount, math.floor(x / divisor + 0.5), math.floor(z / divisor + 0.5)) local distance = get_moore_distance(cellCount, math.floor(x / divisor + 0.5), math.floor(z / divisor + 0.5))
local destination_distance = (distance + SURFACE_TRAVEL_DISTANCE) % (cellCount * cellCount) local destination_distance = (distance + SURFACE_TRAVEL_DISTANCE) % (cellCount * cellCount)
local moore_pos = get_moore_coords(cellCount, destination_distance) local moore_pos = get_moore_coords(cellCount, destination_distance)
local target_x = moore_pos.x * divisor - maxDistFromOrigin local target_x = moore_pos.x * divisor - maxDistFromOrigin
local target_z = moore_pos.y * divisor - maxDistFromOrigin local target_z = moore_pos.y * divisor - maxDistFromOrigin
local search_radius = divisor / 2 - 5 -- any portal within this area will do
-- a y_factor of 0 makes the search ignore the altitude of the portals
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)
if existing_portal_location ~= nil then
-- use the existing portal that was found near target_x, target_z
return existing_portal_location, existing_portal_orientation
else
-- find a good location for the new portal
local adj_x, adj_z = 0, 0 local adj_x, adj_z = 0, 0
if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this 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 -- 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 -- us a surface height, since nether.find_surface_target_y() works *much* better when
-- it can use get_spawn_level() -- 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 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.x * 65732 +
@ -171,27 +180,22 @@ These
minetest.get_mapgen_setting("seed") * 3 minetest.get_mapgen_setting("seed") * 3
) )
local radius = divisor / 2 - 5 local attemptLimit = 12 -- how many attempts we'll make at finding a good location
local attemptLimit = 10 -- how many attempts we'll make to find a good location
for attempt = 1, attemptLimit do for attempt = 1, attemptLimit do
adj_x = math.floor(prng:rand_normal_dist(-radius, radius, 2) + 0.5) adj_x = math.floor(prng:rand_normal_dist(-search_radius, search_radius, 2) + 0.5)
adj_z = math.floor(prng:rand_normal_dist(-radius, 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) 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 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) -- 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") 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 break
end end
end end
end end
local destination_pos = {x = target_x + adj_x, y = 0, z = target_z + adj_z} local destination_pos = {x = target_x + adj_x, y = 0, z = target_z + adj_z}
-- a y_factor of 0 makes the search ignore the altitude of the portals
local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("surface_portal", destination_pos, radius, 0)
if existing_portal_location ~= nil then
return existing_portal_location, existing_portal_orientation
else
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "surface_portal") destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "surface_portal")
return destination_pos return destination_pos
end end
end end