From 1a6d15def301d6dcce1e88d36e94f8f409656875 Mon Sep 17 00:00:00 2001 From: Treer Date: Sat, 13 Jul 2019 17:22:17 +1000 Subject: [PATCH] Portals connect to nearby targets Records portal positions. More testing required --- init.lua | 32 +++++--- portal_api.lua | 206 ++++++++++++++++++++++++++++++++++++++++--------- portal_api.txt | 38 +++++---- 3 files changed, 218 insertions(+), 58 deletions(-) diff --git a/init.lua b/init.lua index d61526f..f05eae1 100644 --- a/init.lua +++ b/init.lua @@ -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 }) diff --git a/portal_api.lua b/portal_api.lua index dc49c5a..ee04200 100644 --- a/portal_api.lua +++ b/portal_api.lua @@ -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 \ No newline at end of file +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 diff --git a/portal_api.txt b/portal_api.txt index 770ed5e..2c51a36 100644 --- a/portal_api.txt +++ b/portal_api.txt @@ -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,