Browse Source

Respect protected areas when spawning remote portals (#27)

* respect protected areas when spawning remote portals

* Use portal_shape to determine area to check for natural/protected

* Log warning on deprecated function call
pull/28/head
Treer GitHub 2 weeks ago
parent
commit
5b3b56ebec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 140 additions and 52 deletions
  1. +1
    -0
      .luacheckrc
  2. +5
    -5
      init.lua
  3. +12
    -6
      mapgen.lua
  4. +15
    -9
      mapgen_nobiomes.lua
  5. +89
    -23
      portal_api.lua
  6. +14
    -5
      portal_api.txt
  7. +4
    -4
      portal_examples.lua

+ 1
- 0
.luacheckrc View File

@@ -23,6 +23,7 @@ read_globals = {
"PseudoRandom",
"stairs",
"stairsplus",
"string.split",
table = { fields = { "copy", "getn" } },
"vector",
"VoxelArea",


+ 5
- 5
init.lua View File

@@ -58,7 +58,7 @@ nether.NETHER_REALM_ENABLED = minetest.settings:get_bool("nether_realm_ena
nether.DEPTH_CEILING = tonumber(minetest.settings:get("nether_depth_ymax") or nether.DEPTH_CEILING)
nether.DEPTH_FLOOR = tonumber(minetest.settings:get("nether_depth_ymin") or nether.DEPTH_FLOOR)

if nether.DEPTH_FLOOR + 1000 > nether.DEPTH_CEILING then
if nether.DEPTH_FLOOR + 1000 > nether.DEPTH_CEILING then
error("The lower limit of the Nether must be set at least 1000 lower than the upper limit, and more than 3000 is recommended. Set settingtypes.txt, or 'All Settings' -> 'Mods' -> 'nether' -> 'Nether depth'", 0)
end
nether.DEPTH = nether.DEPTH_CEILING -- Deprecated, use nether.DEPTH_CEILING instead.
@@ -101,7 +101,7 @@ The expedition parties have found no diamonds or gold, and after an experienced
return pos.y < nether.DEPTH_CEILING
end,

find_realm_anchorPos = function(surface_anchorPos)
find_realm_anchorPos = function(surface_anchorPos, player_name)
-- divide x and z by a factor of 8 to implement Nether fast-travel
local destination_pos = vector.divide(surface_anchorPos, nether.FASTTRAVEL_FACTOR)
destination_pos.x = math.floor(0.5 + destination_pos.x) -- round to int
@@ -116,12 +116,12 @@ The expedition parties have found no diamonds or gold, and after an experienced
return existing_portal_location, existing_portal_orientation
else
local start_y = nether.DEPTH_CEILING - math.random(500, 1500) -- Search starting altitude
destination_pos.y = nether.find_nether_ground_y(destination_pos.x, destination_pos.z, start_y)
destination_pos.y = nether.find_nether_ground_y(destination_pos.x, destination_pos.z, start_y, player_name)
return destination_pos
end
end,

find_surface_anchorPos = function(realm_anchorPos)
find_surface_anchorPos = function(realm_anchorPos, player_name)
-- 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.
@@ -140,7 +140,7 @@ The expedition parties have found no diamonds or gold, and after an experienced
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")
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, "nether_portal", player_name)
return destination_pos
end
end,


+ 12
- 6
mapgen.lua View File

@@ -465,10 +465,15 @@ end


-- use knowledge of the nether mapgen algorithm to return a suitable ground level for placing a portal.
function nether.find_nether_ground_y(target_x, target_z, start_y)
-- player_name is optional, allowing a player to spawn a remote portal in their own protected areas.
function nether.find_nether_ground_y(target_x, target_z, start_y, player_name)
local nobj_cave_point = minetest.get_perlin(np_cave)
local air = 0 -- Consecutive air nodes found

local minp_schem, maxp_schem = nether.get_schematic_volume({x = target_x, y = 0, z = target_z}, nil, "nether_portal")
local minp = {x = minp_schem.x, y = 0, z = minp_schem.z}
local maxp = {x = maxp_schem.x, y = 0, z = maxp_schem.z}

for y = start_y, math_max(NETHER_FLOOR + BLEND, start_y - 4096), -1 do
local nval_cave = nobj_cave_point:get3d({x = target_x, y = y, z = target_z})

@@ -476,13 +481,14 @@ function nether.find_nether_ground_y(target_x, target_z, start_y)
air = air + 1
else -- Not cavern, check if 4 nodes of space above
if air >= 4 then
local portal_y = y + 1
-- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = y , z = target_z - 2}
local maxp = {x = target_x + 2, y = y + 4, z = target_z + 2}
if nether.volume_is_natural(minp, maxp) then
return y + 1
minp.y = minp_schem.y + portal_y
maxp.y = maxp_schem.y + portal_y
if nether.volume_is_natural_and_unprotected(minp, maxp, player_name) then
return portal_y
else -- Restart search a little lower
nether.find_nether_ground_y(target_x, target_z, y - 16)
nether.find_nether_ground_y(target_x, target_z, y - 16, player_name)
end
else -- Not enough space, reset air to zero
air = 0


+ 15
- 9
mapgen_nobiomes.lua View File

@@ -201,24 +201,30 @@ end)


-- use knowledge of the nether mapgen algorithm to return a suitable ground level for placing a portal.
function nether.find_nether_ground_y(target_x, target_z, start_y)
-- player_name is optional, allowing a player to spawn a remote portal in their own protected areas.
function nether.find_nether_ground_y(target_x, target_z, start_y, player_name)
local nobj_cave_point = minetest.get_perlin(np_cave)
local air = 0 -- Consecutive air nodes found

for y = start_y, start_y - 4096, -1 do
local minp_schem, maxp_schem = nether.get_schematic_volume({x = target_x, y = 0, z = target_z}, nil, "nether_portal")
local minp = {x = minp_schem.x, y = 0, z = minp_schem.z}
local maxp = {x = maxp_schem.x, y = 0, z = maxp_schem.z}

for y = start_y, math.max(NETHER_FLOOR + BLEND, start_y - 4096), -1 do
local nval_cave = nobj_cave_point:get3d({x = target_x, y = y, z = target_z})

if nval_cave > TCAVE then -- Cavern
air = air + 1
else -- Not cavern, check if 4 nodes of space above
if air >= 4 then
local portal_y = y + 1
-- Check volume for non-natural nodes
local minp = {x = target_x - 1, y = y , z = target_z - 2}
local maxp = {x = target_x + 2, y = y + 4, z = target_z + 2}
if nether.volume_is_natural(minp, maxp) then
return y + 1
minp.y = minp_schem.y + portal_y
maxp.y = maxp_schem.y + portal_y
if nether.volume_is_natural_and_unprotected(minp, maxp, player_name) then
return portal_y
else -- Restart search a little lower
nether.find_nether_ground_y(target_x, target_z, y - 16)
nether.find_nether_ground_y(target_x, target_z, y - 16, player_name)
end
else -- Not enough space, reset air to zero
air = 0
@@ -226,5 +232,5 @@ function nether.find_nether_ground_y(target_x, target_z, start_y)
end
end

return start_y -- Fallback
end
return math.max(start_y, NETHER_FLOOR + BLEND) -- Fallback
end

+ 89
- 23
portal_api.lua View File

@@ -1303,8 +1303,9 @@ end


-- invoked when a player attempts to turn obsidian nodes into an open portal
-- player_name is optional, allowing a player to spawn a remote portal in their own protected area
-- ignition_node_name is optional
local function ignite_portal(ignition_pos, ignition_node_name)
local function ignite_portal(ignition_pos, player_name, 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
@@ -1354,9 +1355,9 @@ local function ignite_portal(ignition_pos, ignition_node_name)

local destination_anchorPos, destination_orientation
if portal_definition.is_within_realm(ignition_pos) then
destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos)
destination_anchorPos, destination_orientation = portal_definition.find_surface_anchorPos(anchorPos, player_name or "")
else
destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos)
destination_anchorPos, destination_orientation = portal_definition.find_realm_anchorPos(anchorPos, player_name or "")
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
@@ -1825,7 +1826,7 @@ function register_frame_node(frame_node_name)
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)
ignite_portal(pos, nil, node.name)
end,
action_off = function (pos, node)
if DEBUG then minetest.chat_send_all("portal frame material: mesecons action OFF") end
@@ -2081,7 +2082,7 @@ function nether.register_portal(name, portaldef)
end

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, player_name)

local destination_pos = {x = pos.x, y = 0, z = pos.z}
local existing_portal_location, existing_portal_orientation =
@@ -2089,7 +2090,7 @@ function nether.register_portal(name, portaldef)
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)
destination_pos.y = nether.find_surface_target_y(destination_pos.x, destination_pos.z, name, player_name)
return destination_pos
end
end
@@ -2153,10 +2154,10 @@ end
function nether.register_portal_ignition_item(item_name, ignition_failure_sound)

minetest.override_item(item_name, {
on_place = function(stack, _, pt)
on_place = function(stack, placer, pt)
local done = false
if pt.under and nether.is_frame_node[minetest.get_node(pt.under).name] then
done = ignite_portal(pt.under)
done = ignite_portal(pt.under, placer:get_player_name())
if done and not minetest.settings:get_bool("creative_mode") then
stack:take_item()
end
@@ -2175,22 +2176,22 @@ end

-- 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.
-- player_name is optional, allowing a player to spawn a remote portal in their own protected areas.
-- (Water also fails this test, unless it is unemerged)
function nether.volume_is_natural(minp, maxp)
function nether.volume_is_natural_and_unprotected(minp, maxp, player_name)

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 emin, emax = vm:read_from_map(minp, maxp)
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
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local vi = area:index(minp.x, y, z)
for x = minp.x, maxp.x do
local id = data[vi] -- Existing node
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 -- checked for common natural or not emerged
@@ -2200,7 +2201,7 @@ function nether.volume_is_natural(minp, maxp)
-- trees are natural but not "ground content"
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 DEBUG then minetest.chat_send_all("volume_is_natural() found unnatural node " .. name) end
if DEBUG then minetest.chat_send_all("volume_is_natural_and_unprotected() found unnatural node " .. name) end
return false
end
end
@@ -2210,13 +2211,73 @@ function nether.volume_is_natural(minp, maxp)
end
end

if DEBUG then minetest.chat_send_all("Volume is natural") end
if minetest.is_area_protected(minp, maxp, player_name or "") then
if DEBUG then minetest.chat_send_all("Volume is protected " .. minetest.pos_to_string(minp) .. "-" .. minetest.pos_to_string(maxp)) end
return false;
end

if DEBUG then minetest.chat_send_all("Volume is natural and unprotected for player '" .. (player_name or "") .. "', " .. minetest.pos_to_string(minp) .. "-" .. minetest.pos_to_string(maxp)) end
return true
end

-- Deprecated, use nether.volume_is_natural_and_unprotected() instead.
function nether.volume_is_natural(minp, maxp)

if nether.deprecation_warning_volume_is_natural == nil then
local stack = debug.traceback("", 2);
local calling_func = (string.split(stack, "\n", false, 2, false)[2] or ""):trim()
minetest.log("warning",
"Deprecated function \"nether.volume_is_natural()\" invoked. Use \"nether.volume_is_natural_and_unprotected()\" instead. " ..
calling_func)
nether.deprecation_warning_volume_is_natural = true;
end

return nether.volume_is_natural_and_unprotected(minp, maxp)
end

-- Gets the volume that may be altered if a portal is placed at the anchor_pos
-- orientation is optional, but specifying it will reduce the volume returned
-- portal_name is optional, but specifying it will reduce the volume returned
-- returns minp, maxp
function nether.get_schematic_volume(anchor_pos, orientation, portal_name)

if orientation == nil then
-- Return a volume large enough for any orientation
local minp0, maxp0 = nether.get_schematic_volume(anchor_pos, 0, portal_name)
local minp1, maxp1 = nether.get_schematic_volume(anchor_pos, 1, portal_name)

-- ToDo: If an asymmetric portal is used with an anchor not at the center of the
-- schematic then we will also need to check orientations 3 and 4.
-- (The currently existing portal-shapes are not affected)
return
{x = math.min(minp0.x, minp1.x), y = math.min(minp0.y, minp1.y), z = math.min(minp0.z, minp1.z)},
{x = math.max(maxp0.x, maxp1.x), y = math.max(maxp0.y, maxp1.y), z = math.max(maxp0.z, maxp1.z)}
end

-- Assume the largest possible portal shape unless we know it's a smaller one.
local shape_defintion = nether.PortalShape_Circular
if portal_name ~= nil and nether.registered_portals[portal_name] ~= nil then
shape_defintion = nether.registered_portals[portal_name].shape
end

local size = shape_defintion.schematic.size
local minp = shape_defintion.get_schematicPos_from_anchorPos(anchor_pos, orientation);
local maxp

if (orientation % 2) == 0 then
maxp = {x = minp.x + size.x - 1, y = minp.y + size.y - 1, z = minp.z + size.z - 1}
else
maxp = {x = minp.x + size.z - 1, y = minp.y + size.y - 1, z = minp.z + size.x - 1}
end
return minp, maxp
end


-- Can be used when implementing custom find_surface_anchorPos() functions
function nether.find_surface_target_y(target_x, target_z, portal_name)
-- portal_name is optional, providing it allows existing portals on the surface to be reused, and
-- a potentially smaller volume to be checked by volume_is_natural_and_unprotected().
-- player_name is optional, allowing a player to spawn a remote portal in their own protected areas.
function nether.find_surface_target_y(target_x, target_z, portal_name, player_name)

assert(target_x ~= nil and target_z ~= nil, "Arguments `target_x` and `target_z` cannot be nil when calling find_surface_target_y()")

@@ -2246,18 +2307,22 @@ function nether.find_surface_target_y(target_x, target_z, portal_name)
end
end

local minp_schem, maxp_schem = nether.get_schematic_volume({x = target_x, y = 0, z = target_z}, nil, portal_name)
local minp = {x = minp_schem.x, y = 0, z = minp_schem.z}
local maxp = {x = maxp_schem.x, y = 0, z = maxp_schem.z}

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
minp.y = minp_schem.y + y
maxp.y = maxp_schem.y + y
if nether.volume_is_natural_and_unprotected(minp, maxp, player_name) 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
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
if DEBUG then minetest.chat_send_all("volume_is_natural_and_unprotected check failed, but a portal frame is here " .. minetest.pos_to_string(anchorPos) .. ", so this is still a good target y level") end
return y
end
end


+ 14
- 5
portal_api.txt View File

@@ -49,17 +49,22 @@ surface.
Helper functions
----------------

* `nether.volume_is_natural(minp, maxp)`: returns a boolean
* `nether.volume_is_natural_and_unprotected(minp, maxp, player_name)`: 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.
Water will fail this test, unless it is unemerged.
* player_name is optional, providing it allows the player's own protected
areas to be treated as unprotected.

* `nether.find_surface_target_y(target_x, target_z, portal_name)`: returns a
suitable anchorPos
* `nether.find_surface_target_y(target_x, target_z, portal_name, player_name)`:
returns a suitable anchorPos
* 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.
* player_name is optional, providing it prevents the exclusion of surface
target areas which are protected by the player.

* `nether.find_nearest_working_portal(portal_name, anchorPos, distance_limit, y_factor)`: returns
(anchorPos, orientation), or nil if no portal was found within the
@@ -208,7 +213,7 @@ Used by `nether.register_portal`.
-- Ideally implementations are fast, as this function can be used to
-- sift through a list of portals.

find_realm_anchorPos = function(surface_anchorPos),
find_realm_anchorPos = function(surface_anchorPos, player_name),
-- Required. Return a position in the realm that a portal created at
-- surface_anchorPos will link to.
-- Return an anchorPos or (anchorPos, orientation)
@@ -218,8 +223,10 @@ Used by `nether.register_portal`.
-- orientation, otherwise the existing portal could be overwritten by
-- a new one with the orientation of the surface portal.
-- Return nil to prevent the portal from igniting.
-- player_name may be "", e.g. if the portal was ignited by a mesecon,
-- and is provided for use with volume_is_natural_and_unprotected() etc.

find_surface_anchorPos = function(realm_anchorPos),
find_surface_anchorPos = function(realm_anchorPos, player_name),
-- Optional. If you don't implement this then a position near the
-- surface will be picked.
-- Return an anchorPos or (anchorPos, orientation)
@@ -233,6 +240,8 @@ Used by `nether.register_portal`.
-- orientation, otherwise the existing portal could be overwritten by
-- a new one with the orientation of the realm portal.
-- Return nil to prevent the portal from igniting.
-- player_name may be "", e.g. if the portal was ignited by a mesecon,
-- and is provided for use with volume_is_natural_and_unprotected() etc.

on_run_wormhole = function(portalDef, anochorPos, orientation),
-- invoked once per second per portal


+ 4
- 4
portal_examples.lua View File

@@ -85,7 +85,7 @@ This portal is different to the others, rather than acting akin to a doorway it
return pos.y > FLOATLAND_LEVEL - 200
end,

find_realm_anchorPos = function(surface_anchorPos)
find_realm_anchorPos = function(surface_anchorPos, player_name)
-- 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}

@@ -131,13 +131,13 @@ Due to such difficulties, we never learned what determines the direction and dis
return true
end,

find_realm_anchorPos = function(surface_anchorPos)
find_realm_anchorPos = function(surface_anchorPos, player_name)
-- This function isn't needed, since this type of portal always goes to the surface
minetest.log("error" , "find_realm_anchorPos called for surface portal")
return {x=0, y=0, z=0}
end,

find_surface_anchorPos = function(realm_anchorPos)
find_surface_anchorPos = function(realm_anchorPos, player_name)
-- 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 these portals travel around the
-- surface (following a Moore curve) so will be calculating a different x and z to realm_anchorPos.
@@ -192,7 +192,7 @@ Due to such difficulties, we never learned what determines the direction and dis
end

local destination_pos = {x = target_x + adj_x, y = 0, z = target_z + adj_z}
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", player_name)

return destination_pos
end


Loading…
Cancel
Save