rearrange code

eliminate need to assign extinguish_portal() to a local var, and group the helper functions together in the API section
This commit is contained in:
Treer 2019-07-21 14:45:10 +10:00 committed by SmallJoker
parent e2666146ca
commit 835ad9686d
1 changed files with 195 additions and 230 deletions

View File

@ -218,10 +218,6 @@ 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.
local extinguish_portal
local function get_timerPos_from_p1_and_p2(p1, p2)
-- Pick a frame node for the portal's timer.
@ -361,6 +357,108 @@ local function list_closest_portals(portal_definition, anchorPos, distance_limit
end
-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
-- see also ambient_sound_stop()
function ambient_sound_play(portal_definition, soundPos, timerNodeMeta)
if portal_definition.sounds.ambient ~= nil then
local soundLength = portal_definition.sounds.ambient.length
if soundLength == nil then soundLength = 3 end
local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played")
-- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed
if os.time() >= lastPlayed + soundLength then
local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 6})
if timerNodeMeta ~= nil then
timerNodeMeta:set_int("ambient_sound_handle", soundHandle)
timerNodeMeta:set_int("ambient_sound_last_played", os.time())
end
end
end
end
-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
-- see also ambient_sound_play()
function ambient_sound_stop(timerNodeMeta)
if timerNodeMeta ~= nil then
local soundHandle = timerNodeMeta:get_int("ambient_sound_handle")
minetest.sound_stop(soundHandle)
end
end
-- WARNING - this is invoked by on_destruct, so you can't assume there's an accesible node at pos
function extinguish_portal(pos, node_name, frame_was_destroyed)
-- 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
local meta = minetest.get_meta(pos)
local p1 = minetest.string_to_pos(meta:get_string("p1"))
local p2 = minetest.string_to_pos(meta:get_string("p2"))
local target = minetest.string_to_pos(meta:get_string("target"))
if p1 == nil or p2 == nil then
if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end
return
end
local portal_definition = get_portal_definition(node_name, p1, p2)
if portal_definition == nil then
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
if portal_definition.sounds.extinguish ~= nil then
minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1})
end
-- stop timer and ambient sound
local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
minetest.get_node_timer(timerPos):stop()
ambient_sound_stop(minetest.get_meta(timerPos))
-- 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
for x = p1.x, p2.x do
for y = p1.y, p2.y do
for z = p1.z, p2.z do
local nn = minetest.get_node({x = x, y = y, z = z}).name
if nn == frame_node_name or nn == wormhole_node_name then
if nn == wormhole_node_name then
minetest.remove_node({x = x, y = y, z = z})
end
local m = minetest.get_meta({x = x, y = y, z = z})
m:set_string("p1", "")
m:set_string("p2", "")
m:set_string("target", "")
m:set_string("frame_node_name", "")
end
end
end
end
if target ~= nil then
if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end
extinguish_portal(target, node_name)
end
if portal_definition.on_extinguish ~= nil then
portal_definition.on_extinguish(portal_definition, anchorPos, orientation)
end
end
-- Note: will extinguish any portal using the same nodes that are being set
local function set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos, ignite)
@ -608,128 +706,6 @@ local function locate_or_build_portal(portal_definition, suggested_wormholePos,
end
-- use this when determining where to spawn a portal, to avoid overwriting player builds
function nether.volume_is_natural(minp, maxp)
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 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
local id = data[vi] -- Existing node
if id ~= c_air and id ~= c_ignore then -- These are natural
local name = minetest.get_name_from_content_id(id)
local nodedef = minetest.registered_nodes[name]
if not nodedef.is_ground_content then
-- 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) then
return false
end
end
end
vi = vi + 1
end
end
end
return true
end
--[[
"The normal realm portal has a particular X, Z, it searches downwards for a suitable Y.
It can't be placed in mid-air, and for performance the test for a suitable placement position cannot move downwards in
1 node steps, instead it moves downwards in 16 node steps, so it will almost always be placed buried in solid nodes.
The portal cannot be placed in any volume that contains non-natural nodes (is_ground_content = false) to not grief
player builds. This makes it even more likely the portal will be a little way underground.
The portal is placed with air nodes around it to create a space so it isn't embedded in stone.
It is expected that the player has a pickaxe to dig their way out, this is highly likely if they have built a portal
and are exploring the nether. The player will not be trapped.
Note that MC also often places portals embedded in stone.
The code could be [edit: has been] altered to first try to find a surface position, but if this surface position is
unsuitable due to being near player builds, the portal will still move downwards into the ground, so this is
unavoidable.
Any search for a suitable resting-on-surface or resting-on-cave-surface position will be somewhat complex, to avoid
placement on a tiny floating island or narrow spike etc. which would be impractical or deadly to the player.
A portal room embedded underground is the safest and the most accessible for the player.
So i decided to start the placement position search at y = -16 as that, or a little below, is the most likely suitable
position: Ground is almost always present there, it's below any lakes or seas, below most player builds.
Also, the search for non-natural nodes doesn't actually guarantee avoiding player builds, as a player build can be
composed of only natural nodes (is_ground_content = true). So even more good reason to start the search a little way
underground where player builds are more unlikely. Y = -16 seemed a reasonable compromise between safety and distance
from surface.
Each placement position search has to search a volume of nodes for non-natural nodes, this is not lightweight, and many
searches may happen if there a lot of underground player builds present. So the code has been written to avoid
intensive procedures."
-- https://github.com/minetest-mods/nether/issues/5#issuecomment-506983676
]]
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)
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
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}
if nether.volume_is_natural(minp, maxp) then
return surface_level
end
end
end
-- fallback to underground search
local start_y = -16
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
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
return y
end
end
end
return start_y - 256 -- Fallback
end
-- invoked when a player attempts to turn obsidian nodes into an open portal
-- ignition_node_name is optional
local function ignite_portal(ignition_pos, ignition_node_name)
@ -788,106 +764,6 @@ local function ignite_portal(ignition_pos, ignition_node_name)
end
end
-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
-- see also ambient_sound_stop()
function ambient_sound_play(portal_definition, soundPos, timerNodeMeta)
if portal_definition.sounds.ambient ~= nil then
local soundLength = portal_definition.sounds.ambient.length
if soundLength == nil then soundLength = 3 end
local lastPlayed = timerNodeMeta:get_int("ambient_sound_last_played")
-- Using "os.time() % soundLength == 0" is lightweight but means delayed starts, so trying a stored lastPlayed
if os.time() >= lastPlayed + soundLength then
local soundHandle = minetest.sound_play(portal_definition.sounds.ambient, {pos = soundPos, max_hear_distance = 6})
if timerNodeMeta ~= nil then
timerNodeMeta:set_int("ambient_sound_handle", soundHandle)
timerNodeMeta:set_int("ambient_sound_last_played", os.time())
end
end
end
end
-- the timerNode is used to keep the metadata as that node already needs to be known any time a portal is stopped or run
-- see also ambient_sound_play()
function ambient_sound_stop(timerNodeMeta)
if timerNodeMeta ~= nil then
local soundHandle = timerNodeMeta:get_int("ambient_sound_handle")
minetest.sound_stop(soundHandle)
end
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, 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
local meta = minetest.get_meta(pos)
local p1 = minetest.string_to_pos(meta:get_string("p1"))
local p2 = minetest.string_to_pos(meta:get_string("p2"))
local target = minetest.string_to_pos(meta:get_string("target"))
if p1 == nil or p2 == nil then
if DEBUG then minetest.chat_send_all(" no active portal found to extinguish") end
return
end
local portal_definition = get_portal_definition(node_name, p1, p2)
if portal_definition == nil then
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
if portal_definition.sounds.extinguish ~= nil then
minetest.sound_play(portal_definition.sounds.extinguish, {pos = p1})
end
-- stop timer and ambient sound
local timerPos = get_timerPos_from_p1_and_p2(p1, p2)
minetest.get_node_timer(timerPos):stop()
ambient_sound_stop(minetest.get_meta(timerPos))
-- 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
for x = p1.x, p2.x do
for y = p1.y, p2.y do
for z = p1.z, p2.z do
local nn = minetest.get_node({x = x, y = y, z = z}).name
if nn == frame_node_name or nn == wormhole_node_name then
if nn == wormhole_node_name then
minetest.remove_node({x = x, y = y, z = z})
end
local m = minetest.get_meta({x = x, y = y, z = z})
m:set_string("p1", "")
m:set_string("p2", "")
m:set_string("target", "")
m:set_string("frame_node_name", "")
end
end
end
end
if target ~= nil then
if DEBUG then minetest.chat_send_all(" attempting to also extinguish target with wormholePos " .. minetest.pos_to_string(target)) end
extinguish_portal(target, node_name)
end
if portal_definition.on_extinguish ~= nil then
portal_definition.on_extinguish(portal_definition, anchorPos, orientation)
end
end
-- invoked when a player is standing in a portal
local function ensure_remote_portal_then_teleport(player, portal_definition, local_anchorPos, local_orientation, destination_wormholePos)
@ -917,6 +793,7 @@ local function ensure_remote_portal_then_teleport(player, portal_definition, loc
if dest_wormhole_node.name == portal_definition.wormhole_node_name then
-- portal exists
local destination_orientation = get_orientation_from_param2(dest_wormhole_node.param2)
local destination_anchorPos = portal_definition.shape.get_anchorPos_from_wormholePos(destination_wormholePos, destination_orientation)
portal_definition.shape.disable_portal_trap(destination_anchorPos, destination_orientation)
@ -957,9 +834,9 @@ local function ensure_remote_portal_then_teleport(player, portal_definition, loc
portal_definition.on_player_teleported(portal_definition, player, playerPos, new_playerPos)
end
else
-- no wormhole node at destination - destination portal either needs to be built or ignited
-- A very rare edge-case that takes time to set up:
-- If the destination portal is unlit and shares a node with a lit portal that is linked to this
-- no wormhole node at destination - destination portal either needs to be built or ignited.
-- Note: A very rare edge-case that is difficult to set up:
-- If the destination portal is unlit and its frame shares a node with a lit portal that is linked to this
-- portal (but has not been travelled through, thus not linking this portal back to it), then igniting
-- the destination portal will extinguish the portal it's touching, which will extinguish this portal
-- which will leave a confused player.
@ -1230,7 +1107,6 @@ function test_shapedef_is_valid(shape_defintion)
end
-- check for mistakes people might make in portal definitions
function test_portaldef_is_valid(portal_definition)
@ -1245,6 +1121,7 @@ function test_portaldef_is_valid(portal_definition)
end
-- convert portals made with old ABM version of nether mod to use the timer instead
minetest.register_lbm({
label = "Start portal timer",
name = "nether:start_portal_timer",
@ -1406,6 +1283,94 @@ function nether.register_portal_ignition_item(item_name)
ignition_item_name = item_name
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.
function nether.volume_is_natural(minp, maxp)
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 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
local id = data[vi] -- Existing node
if id ~= c_air and id ~= c_ignore then -- These are natural
local name = minetest.get_name_from_content_id(id)
local nodedef = minetest.registered_nodes[name]
if not nodedef.is_ground_content then
-- 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) then
return false
end
end
end
vi = vi + 1
end
end
end
return true
end
-- 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.
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)
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
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}
if nether.volume_is_natural(minp, maxp) then
return surface_level
end
end
end
-- fallback to underground search
local start_y = -16
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
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
return y
end
end
end
return start_y - 256 -- Fallback
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.