Portals connect to nearby targets

Records portal positions. More testing required
This commit is contained in:
Treer 2019-07-13 17:22:17 +10:00 committed by SmallJoker
parent 43b37e96b2
commit 1a6d15def3
3 changed files with 218 additions and 58 deletions

View File

@ -75,28 +75,42 @@ The expedition parties have found no diamonds or gold, and after an experienced
return pos.y < nether.DEPTH
end,
find_realm_anchorPos = function(surface_pos)
find_realm_anchorPos = function(surface_anchorPos)
-- divide x and z by a factor of 8 to implement Nether fast-travel
local destination_pos = vector.divide(surface_pos, nether.FASTTRAVEL_FACTOR)
local destination_pos = vector.divide(surface_anchorPos, nether.FASTTRAVEL_FACTOR)
destination_pos.x = math.floor(0.5 + destination_pos.x) -- round to int
destination_pos.z = math.floor(0.5 + destination_pos.z) -- round to int
destination_pos.y = nether.DEPTH - 1000 -- temp value so find_nearest_working_portal() returns nether portals
local start_y = nether.DEPTH - math.random(500, 1500) -- Search start
destination_pos.y = nether.find_nether_ground_y(destination_pos.x, destination_pos.z, start_y)
return destination_pos
-- a y_factor of 0 makes the search ignore the altitude of the portals (as long as they are in the Nether)
local existing_portal_location, existing_portal_orientation = nether.find_nearest_working_portal("nether_portal", destination_pos, 8, 0)
if existing_portal_location ~= nil then
return existing_portal_location, existing_portal_orientation
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
end
end,
find_surface_anchorPos = function(realm_pos)
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:
-- Multiply x and z by a factor of 8 to implement Nether fast-travel
local destination_pos = vector.multiply(realm_pos, nether.FASTTRAVEL_FACTOR)
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
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "nether_portal")
return destination_pos
-- 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("nether_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
})

View File

@ -1,5 +1,5 @@
-- see portal_api.txt for documentation
local DEBUG = false
local DEBUG = true
nether.registered_portals = {}
@ -214,8 +214,9 @@ nether.PortalShape_Traditional = {
-- list of node names that are used as frame nodes by registered portals
local is_frame_node = {}
local S = nether.get_translator
local ignition_item_name
local S = nether.get_translator
local mod_storage = minetest.get_mod_storage()
-- this is a function that will be assigned to further down, allowing functions to use it
-- that are defined before it is.
@ -231,8 +232,8 @@ local function get_timerPos_from_p1_and_p2(p1, p2)
-- 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.
-- 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
@ -245,7 +246,7 @@ local function get_timerPos_from_p1_and_p2(p1, p2)
end
-- orientation is the rotation degrees passed to place_schematic: 0, 90, 180, or 270
-- color is a value from 0 to 7 corresponding to the pixels in portals_palette.png
-- color is a value from 0 to 7 corresponding to the color of pixels in nether_portals_palette.png
local function get_param2_from_color_and_orientation(color, orientation)
assert(orientation, "no orientation passed")
@ -301,8 +302,66 @@ local function list_portal_definitions_for_frame_node(frame_node_name)
end
local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
-- Add portal information to mod storage, so new portals may find existing portals near the target location.
-- Do this whenever a portal is created or changes its ignition state
local function store_portal_location_info(portal_name, anchorPos, orientation, ignited)
mod_storage:set_string(
minetest.pos_to_string(anchorPos) .. " is " .. portal_name,
minetest.serialize({orientation = orientation, active = ignited})
)
end
-- Remove portal information from mod storage.
-- Do this if a portal frame is destroyed such that it cannot be ignited anymore.
local function remove_portal_location_info(portal_name, anchorPos)
mod_storage:set_string(minetest.pos_to_string(anchorPos) .. " is " .. portal_name, "")
end
-- Returns a table of the nearest portals to anchorPos indexed by distance, based on mod_storage
-- data.
-- Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0.
-- WARNING: Portals are not checked, and inactive portals especially may have been damaged without
-- being removed from the mod_storage data. Check these portals still exist before using them, and
-- invoke remove_portal_location_info() on any found to no longer exist.
--
-- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
-- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
-- Pass a nil or negative distance_limit to indicate no distance limit
local function list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
local result = {}
local isRealm = portal_definition.within_realm(anchorPos)
if distance_limit == nil then distance_limit = -1 end
if y_factor == nil then y_factor = 1 end
for key, value in pairs(mod_storage:to_table().fields) do
local closingBrace = key:find(")", 6, true)
if closingBrace ~= nil then
local found_anchorPos = minetest.string_to_pos(key:sub(0, closingBrace))
if found_anchorPos ~= nil and portal_definition.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 at distance " .. distance .. " from dest " .. minetest.pos_to_string(anchorPos) .. ", found: " .. minetest.pos_to_string(found_anchorPos) .. " orientation " .. info.orientation) end
info.anchorPos = found_anchorPos
info.distance = distance
result[distance] = info
end
end
end
end
end
return result
end
local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
-- Portal position is stored in metadata as p1 and p2 to keep maps compatible with earlier versions of this mod.
-- p1 is the bottom/west/south corner of the portal, and p2 is the opposite corner, together
@ -363,24 +422,40 @@ local function set_portal_metadata(portal_definition, anchorPos, orientation, de
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
-- Checks pos, and if it's part of a portal or portal frame then three values are returned: anchorPos, orientation, is_ignited
-- where orientation is 0 or 90 (0 meaning a portal that faces north/south - i.e. obsidian running east/west)
local function is_portal_frame(portal_definition, pos)
-- 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)
if minetest.get_node(check_pos).name ~= frame_node_name then
nodes_are_valid = false
return true -- short-circuit the search
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
@ -396,21 +471,22 @@ local function is_portal_frame(portal_definition, pos)
end
end
-- this function returns two bools: portal found, portal is lit
local is_portal_at_anchorPos = function(anchorPos, orientation)
nodes_are_valid = true
portal_is_ignited = true
portal_definition.shape.apply_func_to_frame_nodes(anchorPos, orientation, check_frame_Func) -- check_frame_Func affects nodes_are_valid, portal_is_ignited
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
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_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
@ -420,14 +496,14 @@ local function is_portal_frame(portal_definition, pos)
for y = -height_minus_1, height_minus_1 do
local testAnchorPos_x = {x = pos.x + w, y = pos.y + y, z = pos.z + d}
local portal_found, portal_lit = is_portal_at_anchorPos(testAnchorPos_x, 0)
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(testForAnchorPos_z, 90)
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
@ -568,8 +644,20 @@ function nether.find_surface_target_y(target_x, target_z, portal_name)
-- try to spawn on surface first
if minetest.get_spawn_level ~= nil then -- older versions of Minetest don't have this
local surface_level = minetest.get_spawn_level(target_x, target_z)
local surface_level = minetest.get_spawn_level(target_x, target_z)
if surface_level ~= nil then
-- get_spawn_level() seems to err on the side of caution and sometimes spawn the player a
-- block higher than the ground level.
local shouldBeGroundPos = {x = target_x, y = surface_level - 1, z = target_z}
local groundNode = minetest.get_node(shouldBeGroundPos)
if groundNode.Name = 'ignore' 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)
local groundNode = minetest.get_node(shouldBeGroundPos)
end
if not groundNode.is_ground_content then
surface_level = surface_level - 1
end
-- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = surface_level - 1, z = target_z - 2}
local maxp = {x = target_x + 2, y = surface_level + 3, z = target_z + 2}
@ -629,14 +717,16 @@ local function ignite_portal(ignition_pos, ignition_node_name)
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
local destination_anchorPos, destination_orientation
if portal_definition.within_realm(ignition_pos) then
destination_anchorPos = portal_definition.find_surface_anchorPos(anchorPos)
destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos)
else
destination_anchorPos = portal_definition.find_realm_anchorPos(anchorPos)
destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos)
end
local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, orientation)
if DEBUG and destination_orientation == nil then minetest.chat_send_all("No destination_orientation given") end
if destination_orientation == nil then destination_orientation = orientation end
local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation)
if DEBUG then minetest.chat_send_all("Destinaton set to " .. minetest.pos_to_string(destination_anchorPos)) end
-- ignition/BURN_BABY_BURN
@ -649,7 +739,7 @@ end
-- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos
extinguish_portal = function(pos, node_name) -- assigned rather than declared because extinguish_portal is already declared, for use by earlier functions in the file.
extinguish_portal = function(pos, node_name, frame_was_destroyed) -- assigned rather than declared because extinguish_portal is already declared, for use by earlier functions in the file.
-- mesecons seems to invoke action_off() 6 times every time you place a block?
if DEBUG then minetest.chat_send_all("extinguish_portal" .. minetest.pos_to_string(pos) .. " " .. node_name) end
@ -667,6 +757,15 @@ extinguish_portal = function(pos, node_name) -- assigned rather than declared be
minetest.log("error", "extinguish_portal() invoked on " .. node_name .. " but no registered portal is constructed from " .. node_name)
return -- no portal frames are made from this type of node
end
-- update the ignition state in the portal location info
local anchorPos, orientation = portal_definition.shape.get_anchorPos_and_orientation_from_p1_and_p2(p1, p2)
if frame_was_destroyed then
remove_portal_location_info(portal_definition.name, anchorPos)
else
store_portal_location_info(portal_definition.name, anchorPos, orientation, false)
end
local frame_node_name = portal_definition.frame_node_name
local wormhole_node_name = portal_definition.wormhole_node_name
@ -698,6 +797,8 @@ end
-- 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)
@ -711,7 +812,7 @@ local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orie
build_portal(portal_definition, anchorPos, orientation, destination_wormholePos)
end
if elapsed < 20 then -- stop checking after 20 seconds
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
@ -970,7 +1071,7 @@ function register_frame_node(frame_node_name)
}}
extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct
extended_node_def.on_destruct = function(pos)
extinguish_portal(pos, frame_node_name)
extinguish_portal(pos, frame_node_name, true)
end
extended_node_def.replaced_by_portalapi.on_timer = extended_node_def.on_timer
extended_node_def.on_timer = function(pos, elapsed)
@ -1186,4 +1287,37 @@ function nether.register_portal_ignition_item(item_name)
})
ignition_item_name = item_name
end
end
-- Returns the anchorPos, orientation of the nearest portal, or nil.
-- A y_factor of 0 means y does not affect the distance_limit, a y_factor of 1 means y is included,
-- and a y_factor of 2 would squash the search-sphere by a factor of 2 on the y-axis, etc.
-- Pass a negative distance_limit to indicate no distance limit
function nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)
local portal_definition = nether.registered_portals[portal_name]
assert(portal_definition ~= nil, "find_nearest_working_portal() called with portal_name '" .. portal_name .. "', but no portal is registered with that name.")
local contenders = list_closest_portals(portal_definition, anchorPos, distance_limit, y_factor)
-- sort by distance
local dist_list = {}
for dist, _ in pairs(contenders) do table.insert(dist_list, dist) end
table.sort(dist_list)
for _, dist in ipairs(dist_list) do
local portal_info = contenders[dist]
if DEBUG then minetest.chat_send_all("checking portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end
local portalFound, portalActive = is_portal_at_anchorPos(portal_definition, portal_info.anchorPos, portal_info.orientation, true)
if portalFound then
return portal_info.anchorPos, portal_info.orientation
else
if DEBUG then minetest.chat_send_all("removing portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end
-- The portal at that location must have been destroyed
remove_portal_location_info(portal_name, portal_info.anchorPos)
end
end
return nil
end

View File

@ -9,16 +9,23 @@ to other realms.
Helper functions
----------------
* `nether.volume_is_natural(minp, maxp)`
* `nether.volume_is_natural(minp, maxp)`: returns a boolean
* use this when determining where to spawn a portal, to avoid overwriting
player builds. It checks the area for any nodes that aren't ground or
trees.
* `nether.find_surface_target_y(target_x, target_z, portal_name)`
* `nether.find_surface_target_y(target_x, target_z, portal_name)`: returns a suitable anchorPos
* Can be used to implement custom find_surface_anchorPos() functions
* Providing the portal_name allows existing portals on the surface to be reused.
* portal_name is optional, and providing it allows existing portals on the
surface to be reused.
* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns
an anchorPos, or nil if no portal was found within the the distance_limit.
* A y_factor of 0 means y does not affect the distance_limit, a y_factor
of 1 means y is included, and a y_factor of 2 would squash the
search-sphere by a factor of 2 on the y-axis, etc.
* Only portals in the same realm as the anchorPos will be returned, even if y_factor is 0.
* Pass a nil or negative distance_limit to indicate no distance limit
API functions
@ -53,28 +60,33 @@ Used by `nether.register_portal`.
-- or texture.
wormhole_node_color = 0,
-- A value from 0 to 7 corresponding to the color of pixels in
-- nether_portals_palette.png
particle_color = "#808",
-- Optional. Will default to the same colour as wormhole_node_color if
-- not specified.
frame_node_name = "default:obsidian",
-- required
-- Required. For best results, have your portal constructed of a
-- material nobody else is using.
sound_ambient = "portal_hum",
sound_ambient = "nether_portal_hum",
sound_ignite = "",
sound_extinguish = "",
sound_teleport = "",
within_realm = function(pos)
end,
within_realm = function(pos),
-- Required. Return true if a portal at pos is in the realm, rather
than the surface world.
find_realm_anchorPos = function(pos)
end,
find_realm_anchorPos = function(surface_anchorPos)
-- Required. Return a position in the realm that a portal created at
-- surface_anchorPos will link to.
find_surface_anchorPos = function(pos)
end,
find_surface_anchorPos = function(realm_anchorPos),
-- Optional. If you don't use this then a position near the surface
-- will be picked.
on_run_wormhole,
on_ignite,