diff --git a/.luacheckrc b/.luacheckrc index 5ed2386..99f4685 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -23,6 +23,7 @@ read_globals = { "PseudoRandom", "stairs", "stairsplus", + "string.split", table = { fields = { "copy", "getn" } }, "vector", "VoxelArea", diff --git a/init.lua b/init.lua index 6f493a9..8f704e4 100644 --- a/init.lua +++ b/init.lua @@ -19,6 +19,13 @@ ]]-- +-- Set DEBUG_FLAGS to determine the behavior of nether.debug(): +-- 0 = off +-- 1 = print(...) +-- 2 = minetest.chat_send_all(...) +-- 4 = minetest.log("info", ...) +local DEBUG_FLAGS = 0 + local S if minetest.get_translator ~= nil then S = minetest.get_translator("nether") @@ -58,11 +65,53 @@ 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. + +-- A debug-print function that understands vectors etc. and does not +-- evaluate when debugging is turned off. +-- Works like string.format(), treating the message as a format string. +-- nils, tables, and vectors passed as arguments to nether.debug() are +-- converted to strings and can be included inside the message with %s +function nether.debug(message, ...) + + local args = {...} + local argCount = select("#", ...) + + for i = 1, argCount do + local arg = args[i] + if arg == nil then + -- convert nils to strings + args[i] = '' + elseif type(arg) == "table" then + local tableCount = 0 + for _,_ in pairs(arg) do tableCount = tableCount + 1 end + if tableCount == 3 and arg.x ~= nil and arg.y ~= nil and arg.z ~= nil then + -- convert vectors to strings + args[i] = minetest.pos_to_string(arg) + else + -- convert tables to strings + -- (calling function can use dump() if a multi-line listing is desired) + args[i] = string.gsub(dump(arg, ""), "\n", " ") + end + end + end + + local composed_message = string.format(message, unpack(args)) + + if math.floor(DEBUG_FLAGS / 1) % 2 == 1 then print(composed_message) end + if math.floor(DEBUG_FLAGS / 2) % 2 == 1 then minetest.chat_send_all(composed_message) end + if math.floor(DEBUG_FLAGS / 4) % 2 == 1 then minetest.log("info", composed_message) end +end +if DEBUG_FLAGS == 0 then + -- do as little evaluation as possible + nether.debug = function() end +end + + -- Load files dofile(nether.path .. "/portal_api.lua") dofile(nether.path .. "/nodes.lua") @@ -101,7 +150,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 +165,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 +189,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, diff --git a/mapgen.lua b/mapgen.lua index c340e14..ecadcf8 100644 --- a/mapgen.lua +++ b/mapgen.lua @@ -113,7 +113,7 @@ override_underground_biomes() -- nether:native_mapgen is used to prevent ores and decorations being generated according -- to landforms created by the native mapgen. --- Ores and decorations are registered against "nether:rack" instead, and the lua +-- Ores and decorations can be registered against "nether:rack" instead, and the lua -- on_generate() callback will carve the Nether with nether:rack before invoking -- generate_decorations and generate_ores. minetest.register_node("nether:native_mapgen", {}) @@ -160,7 +160,7 @@ minetest.register_ore({ ore_type = "scatter", ore = "default:lava_source", wherein = "nether:rack", - clust_scarcity = 32 * 32 * 32, + clust_scarcity = 36 * 36 * 36, clust_num_ores = 4, clust_size = 2, y_max = NETHER_CEILING, @@ -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 diff --git a/mapgen_decorations.lua b/mapgen_decorations.lua index b210db8..5b7d7e3 100644 --- a/mapgen_decorations.lua +++ b/mapgen_decorations.lua @@ -113,7 +113,7 @@ minetest.register_decoration({ deco_type = "schematic", place_on = "nether:rack", sidelen = 80, - fill_ratio = 0.0004, + fill_ratio = 0.0003, biomes = {"nether_caverns"}, y_max = nether.DEPTH_CEILING, -- keep compatibility with mapgen_nobiomes.lua y_min = nether.DEPTH_FLOOR, @@ -127,7 +127,7 @@ minetest.register_decoration({ deco_type = "schematic", place_on = "nether:rack", sidelen = 80, - fill_ratio = 0.0007, + fill_ratio = 0.0008, biomes = {"nether_caverns"}, y_max = nether.DEPTH_CEILING, -- keep compatibility with mapgen_nobiomes.lua y_min = nether.DEPTH_FLOOR, diff --git a/mapgen_nobiomes.lua b/mapgen_nobiomes.lua index 2d1093c..103537d 100644 --- a/mapgen_nobiomes.lua +++ b/mapgen_nobiomes.lua @@ -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 \ No newline at end of file + return math.max(start_y, NETHER_FLOOR + BLEND) -- Fallback +end diff --git a/portal_api.lua b/portal_api.lua index 20862fe..4f28c7f 100644 --- a/portal_api.lua +++ b/portal_api.lua @@ -23,8 +23,9 @@ ]]-- -local DEBUG = false -local DEBUG_IGNORE_MODSTORAGE = false -- setting true prevents portals from knowing where other portals are, forcing find_realm_anchorpos() etc. to be executed every time +-- setting DEBUG_IGNORE_MODSTORAGE true prevents portals from knowing where other +-- portals are, forcing find_realm_anchorpos() etc. to be executed every time. +local DEBUG_IGNORE_MODSTORAGE = false nether.registered_portals = {} nether.registered_portals_count = 0 @@ -662,8 +663,9 @@ nether.PortalShape_Platform = { -- Portal implementation functions -- -- =============================== -- +local S = nether.get_translator +local debugf = nether.debug local ignition_item_name -local S = nether.get_translator local mod_storage = minetest.get_mod_storage() local meseconsAvailable = minetest.get_modpath("mesecon") ~= nil and minetest.global_exists("mesecon") local book_added_as_treasure = false @@ -821,7 +823,7 @@ end local function store_portal_location_info(portal_name, anchorPos, orientation, ignited) if not DEBUG_IGNORE_MODSTORAGE then local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name - if DEBUG then minetest.chat_send_all("Adding/updating portal in mod_storage: " .. key) end + debugf("Adding/updating portal in mod_storage: " .. key) mod_storage:set_string( key, minetest.serialize({orientation = orientation, active = ignited}) @@ -834,7 +836,7 @@ end local function remove_portal_location_info(portal_name, anchorPos) if not DEBUG_IGNORE_MODSTORAGE then local key = minetest.pos_to_string(anchorPos) .. " is " .. portal_name - if DEBUG then minetest.chat_send_all("Removing portal from mod_storage: " .. key) end + debugf("Removing portal from mod_storage: " .. key) mod_storage:set_string(key, "") end end @@ -872,7 +874,7 @@ local function list_closest_portals(portal_definition, anchorPos, distance_limit 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 " .. 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 + debugf("found %s listed at distance %.2f (within %.2f) from dest %s, found: %s orientation %s", found_name, distance, distance_limit, anchorPos, found_anchorPos, info.orientation) info.anchorPos = found_anchorPos info.distance = distance result[distance] = info @@ -924,14 +926,14 @@ end 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 + debugf("extinguish_portal %s %s", pos, node_name) 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 + debugf(" no active portal found to extinguish") return false end @@ -983,7 +985,7 @@ function extinguish_portal(pos, node_name, frame_was_destroyed) 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 + debugf(" attempting to also extinguish target with wormholePos %s", target) extinguish_portal(target, node_name) end @@ -1000,7 +1002,8 @@ 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) - if DEBUG then minetest.chat_send_all("set_portal_metadata(ignite=" .. tostring(ignite) .. ") at " .. minetest.pos_to_string(anchorPos) .. " orient " .. orientation .. ", setting to target " .. minetest.pos_to_string(destination_wormholePos)) end + ignite = ignite or false; + debugf("set_portal_metadata(ignite=%s) at %s orient %s, setting to target %s", ignite, anchorPos, orientation, destination_wormholePos) -- 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 @@ -1028,7 +1031,7 @@ local function set_portal_metadata(portal_definition, anchorPos, orientation, de if existing_p1 ~= "" then local existing_p2 = meta:get_string("p2") if existing_p1 ~= p1_string or existing_p2 ~= p2_string then - if DEBUG then minetest.chat_send_all("set_portal_metadata() found existing metadata from another portal: existing_p1 " .. existing_p1 .. ", existing_p2" .. existing_p2 .. ", p1 " .. p1_string .. ", p2 " .. p2_string .. ", will existinguish existing portal...") end + debugf("set_portal_metadata() found existing metadata from another portal: existing_p1 %s, existing_p2 %s, p1 %s, p2 %s, will extinguish existing portal...", existing_p1, existing_p2, p1_string, p2_string) -- this node is already part of another portal, so extinguish that, because nodes only -- contain a link in the metadata to one portal, and being part of two allows a slew of bugs extinguish_portal(pos, node_name, false) @@ -1092,7 +1095,7 @@ local function is_portal_at_anchorPos(portal_definition, anchorPos, orientation, -- 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 + debugf("Forced loading of 'ignore' node at %s, got %s", check_pos, foundName) if foundName ~= frame_node_name then nodes_are_valid = false @@ -1202,7 +1205,7 @@ local function build_portal(portal_definition, anchorPos, orientation, destinati function(pos) minetest.swap_node(pos, wormholeNode) end ) - if DEBUG then minetest.chat_send_all("Placed " .. portal_definition.name .. " portal schematic at " .. minetest.pos_to_string(portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation)) .. ", orientation " .. orientation) end + debugf("Placed %s portal schematic at %s, orientation %s", portal_definition.name, portal_definition.shape.get_schematicPos_from_anchorPos(anchorPos, orientation), orientation) set_portal_metadata(portal_definition, anchorPos, orientation, destination_wormholePos) @@ -1216,7 +1219,7 @@ 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 + debugf("portal checkup at %d seconds", elapsed) local wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(anchorPos, orientation) local wormhole_node = minetest.get_node_or_nil(wormholePos) @@ -1231,7 +1234,7 @@ local function remote_portal_checkup(elapsed, portal_definition, anchorPos, orie -- ruh roh local message = "Newly created portal at " .. minetest.pos_to_string(anchorPos) .. " was overwritten. Attempting to recreate. Issue spotted after " .. elapsed .. " seconds" minetest.log("warning", message) - if DEBUG then minetest.chat_send_all("!!! " .. message) end + debugf("!!! " .. message) -- A pre-existing portal frame wouldn't have been immediately overwritten, so no need to check for one, just place the portal. build_portal(portal_definition, anchorPos, orientation, destination_wormholePos) @@ -1259,7 +1262,7 @@ end -- specified if an existing portal was already found there. local function locate_or_build_portal(portal_definition, suggested_wormholePos, suggested_orientation, destination_wormholePos) - if DEBUG then minetest.chat_send_all("locate_or_build_portal() called at wormholePos" .. minetest.pos_to_string(suggested_wormholePos) .. " with suggested orient " .. suggested_orientation .. ", targetted to " .. minetest.pos_to_string(destination_wormholePos)) end + debugf("locate_or_build_portal() called at wormholePos%s with suggested orient %s, targeted to %s", suggested_wormholePos, suggested_orientation, destination_wormholePos) local result_anchorPos; local result_orientation; @@ -1280,13 +1283,13 @@ local function locate_or_build_portal(portal_definition, suggested_wormholePos, if result_target ~= nil and vector.equals(result_target, destination_wormholePos) then -- It already links back to the portal the player is teleporting from, so don't -- extinguish it or the player's portal will also extinguish. - if DEBUG then minetest.chat_send_all(" Build unnecessary: already a lit portal that links back here at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end + debugf(" Build unnecessary: already a lit portal that links back here at %s, orientation %s", found_anchorPos, result_orientation) else - if DEBUG then minetest.chat_send_all(" Build unnecessary: already a lit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation .. ", linking to " .. result_target_str .. ". Extinguishing...") end + debugf(" Build unnecessary: already a lit portal at %s, orientation %s, linking to %s. Extinguishing...", found_anchorPos, result_orientation, result_target_str) extinguish_portal(found_anchorPos, portal_definition.frame_node_name, false) end else - if DEBUG then minetest.chat_send_all(" Build unnecessary: already an unlit portal at " .. minetest.pos_to_string(found_anchorPos) .. ", orientation " .. result_orientation) end + debugf(" Build unnecessary: already an unlit portal at %s, orientation %s", found_anchorPos, result_orientation) end -- ignite the portal set_portal_metadata_and_ignite(portal_definition, result_anchorPos, result_orientation, destination_wormholePos) @@ -1303,11 +1306,12 @@ 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 + debugf("IGNITE the %s at %s", ignition_node_name, ignition_pos) -- find which sort of portals are made from the node that was clicked on local portal_definition_list = list_portal_definitions_for_frame_node(ignition_node_name) @@ -1318,7 +1322,7 @@ local function ignite_portal(ignition_pos, ignition_node_name) -- 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) if anchorPos == nil then - if DEBUG then minetest.chat_send_all("No " .. portal_definition.name .. " portal frame found at " .. minetest.pos_to_string(ignition_pos)) end + debugf("No %s portal frame found at ", portal_definition.name, ignition_pos) continue = true -- no portal is here, but perhaps there's more than one portal type we need to search for elseif is_ignited then -- Found a portal, check its metadata and timer is healthy. @@ -1330,10 +1334,10 @@ local function ignite_portal(ignition_pos, ignition_node_name) -- 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 + debugf("Broken portal detected, allowing reignition/repair") 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 + debugf("This portal links to %s. p1=%s p2=%s", meta:get_string("target"), meta:get_string("p1"), meta:get_string("p2")) -- Check the portal's timer is running, and fix if it's not. -- A portal's timer can stop running if the game is played without that portal type being @@ -1341,7 +1345,7 @@ local function ignite_portal(ignition_pos, ignition_node_name) -- (if this is a frequent problem, then change the value of "run_at_every_load" in the lbm) local timer = minetest.get_node_timer(get_timerPos_from_p1_and_p2(minetest.string_to_pos(p1), minetest.string_to_pos(p2))) if timer ~= nil and timer:get_timeout() == 0 then - if DEBUG then minetest.chat_send_all("Portal timer was not running: restarting the timer.") end + debugf("Portal timer was not running: restarting the timer.") timer:start(1) end end @@ -1350,23 +1354,25 @@ local function ignite_portal(ignition_pos, ignition_node_name) end 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 + debugf("Found portal frame. Looked at %s, found at %s orientation %s", ignition_pos, anchorPos, orientation) 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 destination_orientation == nil then + debugf("No destination_orientation given") + destination_orientation = orientation 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 if destination_anchorPos == nil then - if DEBUG then minetest.chat_send_all("No portal destination available here!") end + debugf("No portal destination available here!") return false else local destination_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(destination_anchorPos, destination_orientation) - if DEBUG then minetest.chat_send_all("Destination set to " .. minetest.pos_to_string(destination_anchorPos)) end + debugf("Destination set to %s", destination_anchorPos) -- ignition/BURN_BABY_BURN set_portal_metadata_and_ignite(portal_definition, anchorPos, orientation, destination_wormholePos) @@ -1405,7 +1411,7 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, local local_p1, local_p2 = portal_definition.shape:get_p1_and_p2_from_anchorPos(local_anchorPos, local_orientation) local p1_at_playerPos = minetest.string_to_pos(meta:get_string("p1")) if p1_at_playerPos == nil or not vector.equals(local_p1, p1_at_playerPos) then - if DEBUG then minetest.chat_send_all("the player already teleported from " .. minetest.pos_to_string(local_anchorPos) .. ", and is now standing in a different portal - " .. meta:get_string("p1")) end + debugf("the player already teleported from %s, and is now standing in a different portal - %s", local_anchorPos, meta:get_string("p1")) return -- the player already teleported, and is now standing in a different portal end @@ -1413,7 +1419,7 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, if dest_wormhole_node == nil then -- area not emerged yet, delay and retry - if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() could not find anything yet at " .. minetest.pos_to_string(destination_wormholePos)) end + debugf("ensure_remote_portal_then_teleport() could not find anything yet at %s", destination_wormholePos) minetest.after(1, ensure_remote_portal_then_teleport, playerName, portal_definition, local_anchorPos, local_orientation, destination_wormholePos) else local local_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(local_anchorPos, local_orientation) @@ -1430,9 +1436,9 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, local remoteMeta = minetest.get_meta(destination_wormholePos) local remoteTarget = minetest.string_to_pos(remoteMeta:get_string("target")) if remoteTarget == nil then - if DEBUG then minetest.chat_send_all("Failed to test whether target portal links back to this one") end + debugf("Failed to test whether target portal links back to this one") elseif not vector.equals(remoteTarget, local_wormholePos) then - if DEBUG then minetest.chat_send_all("Target portal is already linked, extinguishing then relighting to point back at this one") end + debugf("Target portal is already linked, extinguishing then relighting to point back at this one") extinguish_portal(remoteTarget, portal_definition.frame_node_name, false) set_portal_metadata_and_ignite( portal_definition, @@ -1442,7 +1448,7 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, ) end - if DEBUG then minetest.chat_send_all("Teleporting player from wormholePos" .. minetest.pos_to_string(local_wormholePos) .. " to wormholePos" .. minetest.pos_to_string(destination_wormholePos)) end + debugf("Teleporting player from wormholePos%s to wormholePos%s", local_wormholePos, destination_wormholePos) -- play the teleport sound if portal_definition.sounds.teleport ~= nil then @@ -1469,7 +1475,7 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, -- which will leave a confused player. -- I don't think this is worth preventing, but I document it incase someone describes entering a portal -- and then the portal turning off. - if DEBUG then minetest.chat_send_all("ensure_remote_portal_then_teleport() saw " .. dest_wormhole_node.name .. " at " .. minetest.pos_to_string(destination_wormholePos) .. " rather than a wormhole. Calling locate_or_build_portal()") end + debugf("ensure_remote_portal_then_teleport() saw %s at %s rather than a wormhole. Calling locate_or_build_portal()", dest_wormhole_node.name, destination_wormholePos) local new_dest_anchorPos, new_dest_orientation = locate_or_build_portal(portal_definition, destination_wormholePos, local_orientation, local_wormholePos) local new_dest_wormholePos = portal_definition.shape.get_wormholePos_from_anchorPos(new_dest_anchorPos, new_dest_orientation) @@ -1486,10 +1492,10 @@ local function ensure_remote_portal_then_teleport(playerName, portal_definition, -- local portal to also be extinguished. local message = "Local portal at " .. minetest.pos_to_string(local_anchorPos) .. " was extinguished while linking to existing portal at " .. minetest.pos_to_string(new_dest_anchorPos) minetest.log("error", message) - if DEBUG then minetest.chat_send_all("!ERROR! - " .. message) end + debugf("!ERROR! - " .. message) else destination_wormholePos = new_dest_wormholePos - if DEBUG then minetest.chat_send_all(" updating target to where remote portal was found - " .. minetest.pos_to_string(destination_wormholePos)) end + debugf(" updating target to where remote portal was found - %s", destination_wormholePos) set_portal_metadata( portal_definition, @@ -1824,22 +1830,22 @@ function register_frame_node(frame_node_name) extended_node_def.replaced_by_portalapi.mesecons = extended_node_def.mesecons 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) + debugf("portal frame material: mesecons action ON") + 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 + debugf("portal frame material: mesecons action OFF") extinguish_portal(pos, node.name, false) end }} extended_node_def.replaced_by_portalapi.on_destruct = extended_node_def.on_destruct extended_node_def.on_destruct = function(pos) - if DEBUG then minetest.chat_send_all("portal frame material: destruct") end + debugf("portal frame material: destruct") extinguish_portal(pos, frame_node_name, true) end extended_node_def.replaced_by_portalapi.on_blast = extended_node_def.on_blast extended_node_def.on_blast = function(pos, intensity) - if DEBUG then minetest.chat_send_all("portal frame material: blast") end + debugf("portal frame material: blast") extinguish_portal(pos, frame_node_name, extended_node_def.replaced_by_portalapi.on_blast == nil) if extended_node_def.replaced_by_portalapi.on_blast ~= nil then extended_node_def.replaced_by_portalapi.on_blast(pos, intensity) @@ -1935,9 +1941,9 @@ minetest.register_lbm({ local timer = minetest.get_node_timer(timerPos) if timer ~= nil then timer:start(1) - if DEBUG then minetest.chat_send_all("LBM started portal timer " .. minetest.pos_to_string(timerPos)) end - elseif DEBUG then - minetest.chat_send_all("get_node_timer" .. minetest.pos_to_string(timerPos) .. " returned null") + debugf("LBM started portal timer %s", timerPos) + else + debugf("get_node_timer%s returned null", timerPos) end end end @@ -2081,7 +2087,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 +2095,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 @@ -2159,7 +2165,7 @@ function nether.register_portal_ignition_item(item_name, ignition_failure_sound) local done = false if pt.under and nether.is_frame_node[node.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 @@ -2180,24 +2186,24 @@ 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 == nil then debugf("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 local name = minetest.get_name_from_content_id(id) local nodedef = minetest.registered_nodes[name] @@ -2205,7 +2211,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 + debugf("volume_is_natural_and_unprotected() found unnatural node %s", name) return false end end @@ -2215,13 +2221,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 + debugf("Volume is protected %s-%s", minp, maxp) + return false; + end + + debugf("Volume is natural and unprotected for player '%s', %s-%s", player_name, minp, maxp) 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 --- 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) +-- 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()") @@ -2251,18 +2317,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 + debugf("volume_is_natural_and_unprotected check failed, but a portal frame is here %s, so this is still a good target y level", anchorPos) return y end end @@ -2291,7 +2361,7 @@ function nether.find_nearest_working_portal(portal_name, anchorPos, distance_lim 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 + debugf("checking portal from mod_storage at %s orientation %s", portal_info.anchorPos, portal_info.orientation) -- the mod_storage list of portals is unreliable - e.g. it won't know if inactive portals have been -- destroyed, so check the portal is still there @@ -2300,7 +2370,7 @@ function nether.find_nearest_working_portal(portal_name, anchorPos, distance_lim if portalFound then return portal_info.anchorPos, portal_info.orientation else - if DEBUG then minetest.chat_send_all("Portal wasn't found, removing portal from mod_storage at " .. minetest.pos_to_string(portal_info.anchorPos) .. " orientation " .. portal_info.orientation) end + debugf("Portal wasn't found, removing portal from mod_storage at %s orientation %s", portal_info.anchorPos, portal_info.orientation) -- The portal at that location must have been destroyed remove_portal_location_info(portal_name, portal_info.anchorPos) end diff --git a/portal_api.txt b/portal_api.txt index b07e2cb..c5118f1 100644 --- a/portal_api.txt +++ b/portal_api.txt @@ -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 diff --git a/portal_examples.lua b/portal_examples.lua index 1a33943..ac71848 100644 --- a/portal_examples.lua +++ b/portal_examples.lua @@ -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