mirror of
https://git.minetest.land/VoxeLibre/VoxeLibre.git
synced 2024-11-29 22:11:10 +01:00
463 lines
14 KiB
Lua
463 lines
14 KiB
Lua
mcl_spawn = {}
|
|
|
|
local S = minetest.get_translator("mcl_spawn")
|
|
local mg_name = minetest.get_mapgen_setting("mg_name")
|
|
local storage = minetest.get_mod_storage()
|
|
|
|
-- Parameters
|
|
-------------
|
|
|
|
local respawn_search_interval = 30 -- seconds
|
|
local respawn_search_initial_delay = 30 -- seconds
|
|
local trees_distance_max = 30 -- nodes
|
|
local attempts_to_find_pos = 50
|
|
local attempts_to_find_trees = 50
|
|
local node_groups_white_list = {"group:soil"}
|
|
local biomes_white_list = {
|
|
"ColdTaiga",
|
|
"Taiga",
|
|
"MegaTaiga",
|
|
"MegaSpruceTaiga",
|
|
"Plains",
|
|
"SunflowerPlains",
|
|
"Forest",
|
|
"FlowerForest",
|
|
"BirchForest",
|
|
"BirchForestM",
|
|
"Jungle",
|
|
"JungleM",
|
|
"JungleEdge",
|
|
"JungleEdgeM",
|
|
"Savanna",
|
|
"SavannaM",
|
|
}
|
|
|
|
-- Resolution of search grid in nodes.
|
|
local res = 64
|
|
local half_res = 32 -- for emerge areas around the position
|
|
local alt_min = -10
|
|
local alt_max = 200
|
|
-- Number of points checked in the square search grid (edge * edge).
|
|
local checks = 128 * 128
|
|
-- Starting point for biome checks. This also sets the y co-ordinate for all
|
|
-- points checked, so the suitable biomes must be active at this y.
|
|
local start_pos = minetest.setting_get_pos("static_spawnpoint") or {x = 0, y = 8, z = 0}
|
|
-- Table of suitable biomes
|
|
local biome_ids = {}
|
|
|
|
-- Bed spawning offsets
|
|
local node_search_list =
|
|
{
|
|
--[[1]] {x = 0, y = 0, z = -1}, --
|
|
--[[2]] {x = -1, y = 0, z = 0}, --
|
|
--[[3]] {x = -1, y = 0, z = 1}, --
|
|
--[[4]] {x = 0, y = 0, z = 2}, -- z^ 8 4 9
|
|
--[[5]] {x = 1, y = 0, z = 1}, -- | 3 5
|
|
--[[6]] {x = 1, y = 0, z = 0}, -- | 2 * 6
|
|
--[[7]] {x = -1, y = 0, z = -1}, -- | 7 1 A
|
|
--[[8]] {x = -1, y = 0, z = 2}, -- +----->
|
|
--[[9]] {x = 1, y = 0, z = 2}, -- x
|
|
--[[A]] {x = 1, y = 0, z = -1}, --
|
|
--[[B]] {x = 0, y = 1, z = 0}, --
|
|
--[[C]] {x = 0, y = 1, z = 1}, --
|
|
}
|
|
|
|
-- End of parameters
|
|
--------------------
|
|
|
|
|
|
-- Initial variables
|
|
|
|
local success = storage:get_int("mcl_spawn_success")==1
|
|
local searched = (storage:get_int("mcl_spawn_searched")==1) or mg_name == "v6" or mg_name == "singlenode" or minetest.settings:get("static_spawnpoint")
|
|
local wsp = minetest.string_to_pos(storage:get_string("mcl_spawn_world_spawn_point")) or {} -- world spawn position
|
|
local check = storage:get_int("mcl_spawn_check") or 0
|
|
local cp = minetest.string_to_pos(storage:get_string("mcl_spawn_cp")) or {x=start_pos.x, y=start_pos.y, z=start_pos.z}
|
|
local edge_len = storage:get_int("mcl_spawn_edge_len") or 1
|
|
local edge_dist = storage:get_int("mcl_spawn_edge_dist") or 0
|
|
local dir_step = storage:get_int("mcl_spawn_dir_step") or 0
|
|
local dir_ind = storage:get_int("mcl_spawn_dir_ind") or 1
|
|
local emerge_pos1, emerge_pos2
|
|
|
|
-- Get world 'mapgen_limit' and 'chunksize' to calculate 'spawn_limit'.
|
|
-- This accounts for how mapchunks are not generated if they or their shell exceed
|
|
-- 'mapgen_limit'.
|
|
|
|
local mapgen_limit = tonumber(minetest.get_mapgen_setting("mapgen_limit"))
|
|
local chunksize = tonumber(minetest.get_mapgen_setting("chunksize"))
|
|
local spawn_limit = math.max(mapgen_limit - (chunksize + 1) * 16, 0)
|
|
|
|
|
|
--Functions
|
|
-----------
|
|
|
|
local function get_far_node(pos)
|
|
local node = minetest.get_node(pos)
|
|
if node.name ~= "ignore" then
|
|
return node
|
|
end
|
|
minetest.get_voxel_manip():read_from_map(pos, pos)
|
|
return minetest.get_node(pos)
|
|
end
|
|
|
|
local function get_trees(pos, pos2)
|
|
if emerge_pos1 and emerge_pos2 and pos then
|
|
if not pos2 then
|
|
local b1 = {x = math.max(pos.x-trees_distance_max, emerge_pos1.x), y = math.max(pos.y-trees_distance_max, emerge_pos1.y), z = math.max(pos.z-trees_distance_max, emerge_pos1.z)}
|
|
local b2 = {x = math.min(pos.x+trees_distance_max, emerge_pos2.x), y = math.min(pos.y+trees_distance_max, emerge_pos2.y), z = math.min(pos.z+trees_distance_max, emerge_pos2.z)}
|
|
return minetest.find_nodes_in_area(b1, b2, {"group:tree"}, false)
|
|
else
|
|
local b1 = {x = math.max(pos.x , emerge_pos1.x), y = math.max(pos.y , emerge_pos1.y), z = math.max(pos.z, emerge_pos1.z)}
|
|
local b2 = {x = math.min(pos2.x, emerge_pos2.x), y = math.min(pos2.y, emerge_pos2.y), z = math.min(pos2.z, emerge_pos2.z)}
|
|
return minetest.find_nodes_in_area(b1, b2, {"group:tree"}, false)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function good_for_respawn(pos, player)
|
|
local pos0 = {x = pos.x, y = pos.y - 1, z = pos.z}
|
|
local pos1 = {x = pos.x, y = pos.y, z = pos.z}
|
|
local pos2 = {x = pos.x, y = pos.y + 1, z = pos.z}
|
|
local node0 = get_far_node(pos0)
|
|
local node1 = get_far_node(pos1)
|
|
local node2 = get_far_node(pos2)
|
|
|
|
local nn0, nn1, nn2 = node0.name, node1.name, node2.name
|
|
if minetest.get_item_group(nn0, "destroys_items") ~=0
|
|
or minetest.get_item_group(nn1, "destroys_items") ~=0
|
|
or minetest.get_item_group(nn2, "destroys_items") ~=0
|
|
or minetest.get_item_group(nn0, "portal") ~=0
|
|
or minetest.get_item_group(nn1, "portal") ~=0
|
|
or minetest.get_item_group(nn2, "portal") ~=0
|
|
or minetest.is_protected(pos0, player or "")
|
|
or minetest.is_protected(pos1, player or "")
|
|
or minetest.is_protected(pos2, player or "")
|
|
or (not player and minetest.get_node_light(pos1, 0.5) < 8)
|
|
or (not player and minetest.get_node_light(pos2, 0.5) < 8)
|
|
or nn0 == "ignore"
|
|
or nn1 == "ignore"
|
|
or nn2 == "ignore"
|
|
then
|
|
return false
|
|
end
|
|
|
|
local def0 = minetest.registered_nodes[nn0]
|
|
local def1 = minetest.registered_nodes[nn1]
|
|
local def2 = minetest.registered_nodes[nn2]
|
|
return def0.walkable and (not def1.walkable) and (not def2.walkable) and
|
|
(def1.damage_per_second == nil or def2.damage_per_second <= 0) and
|
|
(def1.damage_per_second == nil or def2.damage_per_second <= 0)
|
|
end
|
|
|
|
local function can_find_tree(pos1, trees)
|
|
if not emerge_pos1 or not emerge_pos2 then return false end
|
|
local trees = trees or get_trees(pos1)
|
|
if not trees then return false end
|
|
|
|
if (attempts_to_find_trees * 3 < #trees) then
|
|
-- random search
|
|
for i = 1, attempts_to_find_trees do
|
|
local pos2 = trees[math.random(1,#trees)]
|
|
if not minetest.is_protected(pos2, "") then
|
|
if pos2.x < pos1.x then
|
|
pos2.x = pos2.x + 1
|
|
elseif pos2.x > pos1.x then
|
|
pos2.x = pos2.x - 1
|
|
end
|
|
if pos2.z < pos1.z then
|
|
pos2.z = pos2.z + 1
|
|
elseif pos2.z > pos1.z then
|
|
pos2.z = pos2.z - 1
|
|
end
|
|
local way = minetest.find_path(pos1, pos2, res, 1, 3, "A*_noprefetch")
|
|
if way then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
for i, pos2 in ipairs(trees) do
|
|
-- full search
|
|
if not minetest.is_protected(pos2, "") then
|
|
if pos2.x < pos1.x then
|
|
pos2.x = pos2.x + 1
|
|
elseif pos2.x > pos1.x then
|
|
pos2.x = pos2.x - 1
|
|
end
|
|
if pos2.z < pos1.z then
|
|
pos2.z = pos2.z + 1
|
|
elseif pos2.z > pos1.z then
|
|
pos2.z = pos2.z - 1
|
|
end
|
|
local way = minetest.find_path(pos1, pos2, res, 1, 3, "A*_noprefetch")
|
|
if way then
|
|
return true
|
|
end
|
|
end
|
|
if i > attempts_to_find_trees then return false end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function next_pos()
|
|
if edge_dist >= edge_len then
|
|
edge_dist = 1
|
|
dir_ind = (dir_ind % 4) + 1
|
|
dir_step = dir_step + 1
|
|
edge_len = math.floor(dir_step / 2) + 1
|
|
else
|
|
edge_dist = edge_dist + 1
|
|
end
|
|
if dir_ind==1 then
|
|
cp.z = cp.z + res
|
|
elseif dir_ind==2 then
|
|
cp.x = cp.x - res
|
|
elseif dir_ind==3 then
|
|
cp.z = cp.z - res
|
|
else
|
|
cp.x = cp.x + res
|
|
end
|
|
end
|
|
|
|
-- Spawn position search
|
|
|
|
local function next_biome()
|
|
while check <= checks do
|
|
local biome_data = minetest.get_biome_data(cp)
|
|
-- Sometimes biome_data is nil
|
|
local biome = biome_data and biome_data.biome
|
|
if biome then
|
|
minetest.log("verbose", "[mcl_spawn] Search white-listed biome at "..minetest.pos_to_string(cp)..": "..minetest.get_biome_name(biome))
|
|
for _, biome_id in ipairs(biome_ids) do
|
|
if biome == biome_id then
|
|
cp.y = minetest.get_spawn_level(cp.x, cp.z) or start_pos.y
|
|
if cp.y then
|
|
wsp = {x = cp.x, y = cp.y, z = cp.z}
|
|
return true
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
next_pos()
|
|
|
|
-- Check for position being outside world edge
|
|
if math.abs(cp.x) > spawn_limit or math.abs(cp.z) > spawn_limit then
|
|
check = checks + 1
|
|
return false
|
|
end
|
|
|
|
check = check + 1
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function ecb_search_continue(blockpos, action, calls_remaining, param)
|
|
if calls_remaining <= 0 then
|
|
emerge_pos1 = {x = wsp.x-half_res, y = alt_min, z = wsp.z-half_res}
|
|
emerge_pos2 = {x = wsp.x+half_res, y = alt_max, z = wsp.z+half_res}
|
|
local nodes = minetest.find_nodes_in_area_under_air(emerge_pos1, emerge_pos2, node_groups_white_list)
|
|
minetest.log("verbose", "[mcl_spawn] Data emerge callback: "..minetest.pos_to_string(wsp).." - "..tostring(nodes and #nodes) .. " node(s) found under air")
|
|
if nodes then
|
|
local trees = get_trees(emerge_pos1, emerge_pos2)
|
|
if trees then
|
|
if attempts_to_find_pos * 3 < #nodes then
|
|
-- random
|
|
for i=1, attempts_to_find_pos do
|
|
wsp = nodes[math.random(1,#nodes)]
|
|
if wsp then
|
|
wsp.y = wsp.y + 1
|
|
if good_for_respawn(wsp) and can_find_tree(wsp, trees) then
|
|
minetest.log("action", "[mcl_spawn] Dynamic world spawn determined to be "..minetest.pos_to_string(wsp))
|
|
searched = true
|
|
success = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- in a sequence
|
|
for i=1, math.min(#nodes, attempts_to_find_pos) do
|
|
wsp = nodes[i]
|
|
if wsp then
|
|
wsp.y = wsp.y + 1
|
|
if good_for_respawn(wsp) and can_find_tree(wsp, trees) then
|
|
minetest.log("action", "[mcl_spawn] Dynamic world spawn determined to be "..minetest.pos_to_string(wsp))
|
|
searched = true
|
|
success = true
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
next_pos()
|
|
mcl_spawn.search()
|
|
end
|
|
end
|
|
|
|
function mcl_spawn.search()
|
|
if not next_biome() or check > checks then
|
|
return false
|
|
end
|
|
check = check + 1
|
|
if not wsp.y then
|
|
wsp.y = 8
|
|
end
|
|
local pos1 = {x = wsp.x-half_res, y = alt_min, z = wsp.z-half_res}
|
|
local pos2 = {x = wsp.x+half_res, y = alt_max, z = wsp.z+half_res}
|
|
minetest.emerge_area(pos1, pos2, ecb_search_continue)
|
|
end
|
|
|
|
|
|
mcl_spawn.get_world_spawn_pos = function()
|
|
if success then
|
|
return wsp
|
|
end
|
|
minetest.log("action", "[mcl_spawn] Failed to determine dynamic world spawn!")
|
|
return start_pos
|
|
end
|
|
|
|
-- Returns a spawn position of player.
|
|
-- If player is nil or not a player, a world spawn point is returned.
|
|
-- The second return value is true if returned spawn point is player-chosen,
|
|
-- false otherwise.
|
|
mcl_spawn.get_bed_spawn_pos = function(player)
|
|
local spawn, custom_spawn = nil, false
|
|
if player ~= nil and player:is_player() then
|
|
local attr = player:get_meta():get_string("mcl_beds:spawn")
|
|
if attr ~= nil and attr ~= "" then
|
|
spawn = minetest.string_to_pos(attr)
|
|
custom_spawn = true
|
|
end
|
|
end
|
|
if not spawn or spawn == "" then
|
|
spawn = mcl_spawn.get_world_spawn_pos()
|
|
custom_spawn = false
|
|
end
|
|
return spawn, custom_spawn
|
|
end
|
|
|
|
-- Sets the player's spawn position to pos.
|
|
-- Set pos to nil to clear the spawn position.
|
|
-- If message is set, informs the player with a chat message when the spawn position
|
|
-- changed.
|
|
mcl_spawn.set_spawn_pos = function(player, pos, message)
|
|
local spawn_changed = false
|
|
local meta = player:get_meta()
|
|
if pos == nil then
|
|
if meta:get_string("mcl_beds:spawn") ~= "" then
|
|
spawn_changed = true
|
|
if message then
|
|
minetest.chat_send_player(player:get_player_name(), S("Respawn position cleared!"))
|
|
end
|
|
end
|
|
meta:set_string("mcl_beds:spawn", "")
|
|
else
|
|
local oldpos = minetest.string_to_pos(meta:get_string("mcl_beds:spawn"))
|
|
meta:set_string("mcl_beds:spawn", minetest.pos_to_string(pos))
|
|
if oldpos then
|
|
-- We don't bother sending a message if the new spawn pos is basically the same
|
|
spawn_changed = vector.distance(pos, oldpos) > 0.1
|
|
else
|
|
-- If it wasn't set and now it will be set, it means it is changed
|
|
spawn_changed = true
|
|
end
|
|
if spawn_changed and message then
|
|
minetest.chat_send_player(player:get_player_name(), S("New respawn position set!"))
|
|
end
|
|
end
|
|
return spawn_changed
|
|
end
|
|
|
|
mcl_spawn.get_player_spawn_pos = function(player)
|
|
local pos, custom_spawn = mcl_spawn.get_bed_spawn_pos(player)
|
|
if pos and custom_spawn then
|
|
-- Check if bed is still there
|
|
local node_bed = get_far_node(pos)
|
|
local bgroup = minetest.get_item_group(node_bed.name, "bed")
|
|
if bgroup ~= 1 and bgroup ~= 2 then
|
|
-- Bed is destroyed:
|
|
if player ~= nil and player:is_player() then
|
|
player:get_meta():set_string("mcl_beds:spawn", "")
|
|
end
|
|
minetest.chat_send_player(player:get_player_name(), S("Your spawn bed was missing or blocked."))
|
|
return mcl_spawn.get_world_spawn_pos(), false
|
|
end
|
|
|
|
-- Find spawning position on/near the bed free of solid or damaging blocks iterating a square spiral 15x15:
|
|
|
|
local dir = minetest.facedir_to_dir(minetest.get_node(pos).param2)
|
|
local offset
|
|
for _, o in ipairs(node_search_list) do
|
|
if dir.z == -1 then
|
|
offset = {x = o.x, y = o.y, z = o.z}
|
|
elseif dir.z == 1 then
|
|
offset = {x = -o.x, y = o.y, z = -o.z}
|
|
elseif dir.x == -1 then
|
|
offset = {x = o.z, y = o.y, z = -o.x}
|
|
else -- dir.x == 1
|
|
offset = {x = -o.z, y = o.y, z = o.x}
|
|
end
|
|
local player_spawn_pos = vector.add(pos, offset)
|
|
if good_for_respawn(player_spawn_pos, player:get_player_name()) then
|
|
return player_spawn_pos, true
|
|
end
|
|
end
|
|
-- We here if we didn't find suitable place for respawn
|
|
end
|
|
return mcl_spawn.get_world_spawn_pos(), false
|
|
end
|
|
|
|
mcl_spawn.spawn = function(player)
|
|
local pos, in_bed = mcl_spawn.get_player_spawn_pos(player)
|
|
player:set_pos(pos)
|
|
return in_bed or success
|
|
end
|
|
|
|
-- Respawn player at specified respawn position
|
|
minetest.register_on_respawnplayer(mcl_spawn.spawn)
|
|
|
|
function mcl_spawn.shadow_worker()
|
|
if #biome_ids > 1 then
|
|
for _, biome_name in pairs(biomes_white_list) do
|
|
table.insert(biome_ids, minetest.get_biome_id(biome_name))
|
|
end
|
|
end
|
|
if not searched then
|
|
searched = true
|
|
mcl_spawn.search()
|
|
minetest.log("action", "[mcl_spawn] Started world spawn point search")
|
|
end
|
|
if success and ((not good_for_respawn(wsp)) or (not can_find_tree(wsp))) then
|
|
success = false
|
|
minetest.log("action", "[mcl_spawn] World spawn position isn't safe anymore: "..minetest.pos_to_string(wsp))
|
|
mcl_spawn.search()
|
|
end
|
|
|
|
minetest.after(respawn_search_interval, mcl_spawn.shadow_worker)
|
|
end
|
|
|
|
minetest.after(respawn_search_initial_delay, function()
|
|
mcl_spawn.shadow_worker()
|
|
|
|
minetest.register_on_shutdown(function()
|
|
storage:set_int("mcl_spawn_success", success and 1 or 0)
|
|
if wsp and wsp.x then
|
|
storage:set_string("mcl_spawn_world_spawn_point", minetest.pos_to_string(wsp))
|
|
end
|
|
storage:set_int("mcl_spawn_searched", searched and 1 or 0)
|
|
storage:set_int("mcl_spawn_check", check)
|
|
storage:set_string("mcl_spawn_cp", minetest.pos_to_string(cp))
|
|
storage:set_int("mcl_spawn_edge_len", edge_len)
|
|
storage:set_int("mcl_spawn_edge_dist", edge_dist)
|
|
storage:set_int("mcl_spawn_dir_step", dir_step)
|
|
storage:set_int("mcl_spawn_dir_ind", dir_ind)
|
|
end)
|
|
end)
|