Pregenerate spawn points

Overhaul of mod to pre-generate static spawnpoints at intervals and save between server reboots.

* The spawn points are generated in the background on a timer, up to a maximum count
* Players requesting a new spawn point will receive it immediately, if there is one available
master
Tai Kedzierski 2018-12-29 16:18:36 +00:00
parent f4f6d6ce43
commit 4b1c6bac85
8 changed files with 247 additions and 148 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.swp

View File

@ -1,33 +1,49 @@
# r-Spawn for Minetest
A spawn command for Minetest without needing a fixed point -- `singleplayer` rejoice!
A spawn engine for Minetest without needing a fixed point.
Players are each given their own randomized spawn point near the spawn origin on first joining. If no `static_spawnpoint` is defined in `minetest.conf`, the origin is 0,0,0. If static spawn point is defined, that point is used as origin instead.
Players are each given their own randomized spawn point near the spawn origin on first joining.
If `static_spawnpoint` is defined in `minetest.conf`, that point is used as spawn point instead (compatibility)
## Features
* A normal game in singleplayer mode will still alow the player access to a spawn location
* Player is assigned randomized spawnpoint on joining
* Player will respawn at their spawnpoint if they die.
* Players will respawn at their bed if this option is active (default `bedspawn = true`)
* Their `/spawn` location will still be the randomized location.
* If `beds` spawning is active, then beds can be used to reset the players' spawn point.
* Players will not spawn in spaces that are protected by any other player than the Server Admin.
* Additional commands
* Commands
* Players can return to their spawn point with the `/spawn` command if they have `spawn` privilege.
* Players can request a new spawn point by typing `/newspawn` if they have the `newspawn` privilege.
* Players can set their spawn point by typing `/setspawn` if they have the `setspawn` privelege.
* Secondary mode: `spawn_anywhere`
* Players can assign a new random spawn for another player using `/playerspawn` if they have the `spawnadmin` privilege.
### Spawn Anywhere
In the case of a server, players can be given spawns very far from eachother, and maybe not meet anybody for a long time ...!
If `spawn_anywhere` is set in minetest.conf, any *new* player will be given a spawn point anywhere in the world. In the case of a server, players can be given spawns very far from eachother, and maybe not meet anybody for a long time ...!
KNOWN ISSUE - Any player not yet registered with a spawn point will be given a spawn point anywhere in the world. If applying retroactively to a server, this will cause existing players to be re-spawned once.
## Considerations for a server
## Settings
If running on a server consider the following
Note that the spawn generation is performed in the background on a timer, allowing storing a collection of random spawn points to be generated ahead of time.
* make sure the space around the origin is clear of ownership, or is owned by the server admin
* make sure there is sufficient space (try for 32 nodes radius around and above origin) and walkable nodes in the area
*Generic settings used*
Failure to take these into consideration will often mean that the calculation of a new spawn point will take longer and be more processor-intense.
* `name` - on servers, sets the name of the admin, players can spawn in areas protected by the admin.
* `water_level` - Spawns are always set above water level, default `1`
* `static_spawnpoint` - main plce the player will start at, default `{0,0,0}`
* `enable_bed_respawn` - from `beds` mod - if active, then respawning will happen at beds, instead of randomized spawnpoint
*rSpawn-specific settings*
* Settings related to spawn generation
* `rspawn.max_pregen` - maximum number of spawn points to pre-generate, default `5`
* `rspawn.search_radius` - lateral radius around random point, within which a spawn poitn will be sought, default `32`
* `rspawn.gen_frequency` - how frequently (in seconds) to generate a new spawn point, default `30`
* `rspawn.spawn_anywhere` - whether to spawn anywhere in the world at sea level
* default `true`
* if `false`, will randomize around the static spawn point
* `rspawn.kick_on_fail` - whether to kick the player if a randomized spawn cannot be set, default `false`
* `rspawn.debug` - whether to print debugging messages, default `false`
## License

196
init.lua
View File

@ -3,78 +3,74 @@ rspawn.playerspawns = {}
local mpath = minetest.get_modpath("rspawn")
local function notnil_or(d, v)
if v == nil then
return d
else
return v
end
end
-- Water level, plus one to ensure we are above the sea.
local water_level = tonumber(minetest.settings:get("water_level", "1") )+1
local radial_step = 16
local static_spawnpoint = minetest.setting_get_pos("static_spawnpoint") or {x=0, y=50, z=0}
-- Setting with no namespace for interoperability
local static_spawnpoint = minetest.setting_get_pos("static_spawnpoint") or {x=0, y=0, z=0}
-- Setting from beds mod
rspawn.bedspawn = minetest.setting_getbool("enable_bed_respawn", true) -- from beds mod
-- Detect server mode, of sorts
rspawn.adminname = minetest.settings:get("name", "singleplayer")
rspawn.spawnanywhere = minetest.settings:get_bool("spawn_anywhere", true)
rspawn.bedspawn = minetest.setting_getbool("enable_bed_respawn", true)
-- rSpawn specific settings
rspawn.debug_on = minetest.settings:get_bool("rspawn.debug")
rspawn.spawnanywhere = notnil_or(true, minetest.settings:get_bool("rspawn.spawn_anywhere") )
rspawn.kick_on_fail = notnil_or(false, minetest.settings:get_bool("rspawn.kick_on_fail"))
rspawn.max_pregen_spawns = tonumber(minetest.settings:get("rspawn.max_pregen") or 5)
rspawn.search_radius = tonumber(minetest.settings:get("rspawn.search_radius") or 32)
rspawn.gen_frequency = tonumber(minetest.settings:get("rspawn.gen_frequency") or 30)
dofile(mpath.."/src/data.lua")
dofile(mpath.."/src/commands.lua")
dofile(mpath.."/src/forceload.lua")
dofile(mpath.."/src/debugging.lua")
local dbg = dofile(mpath.."/src/debugging.lua")
rspawn:spawnload()
local function forceload_operate(pos1, pos2, handler)
local i,j,k
for i=pos1.x,pos2.x,16 do
for j=pos1.y,pos2.y,16 do
for k=pos1.z,pos2.z,16 do
handler({x=i,y=j,z=k})
end
end
end
end
local function forceload_blocks_in(pos1, pos2)
forceload_operate(pos1, pos2, minetest.forceload_block)
end
local function forceload_free_blocks_in(pos1, pos2)
forceload_operate(pos1, pos2, minetest.forceload_free_block)
end
local function daylight_above(min_daylight, pos)
local level = minetest.get_node_light(pos, 0.5)
return min_daylight <= level
end
function rspawn:newspawn(pos, radius)
-- Given a seed position and a radius, find an exact spawn location
-- that is walkable and with 2 air nodes above it
if not radius then
radius = radial_step
end
if radius > 4*radial_step then
dbg("__ No valid spawnable location around "..minetest.pos_to_string(pos))
return
end
dbg("Trying somewhere around "..minetest.pos_to_string(pos))
local breadth = radius/2
function rspawn:get_positions_for(pos, radius)
local breadth = radius
local altitude = radius*2
local pos1 = {x=pos.x-breadth, y=pos.y, z=pos.z-breadth}
local pos2 = {x=pos.x+breadth, y=pos.y+altitude, z=pos.z+breadth}
dbg("Searching "..minetest.pos_to_string(pos1).." to "..minetest.pos_to_string(pos2))
return pos1,pos2
end
minetest.emerge_area(pos1, pos2)
forceload_blocks_in(pos1, pos2)
function rspawn:newspawn(pos, radius)
-- Given a seed position and a radius, find an exact spawn location
-- that is walkable and with 2 air nodes above it
rspawn:debug("Trying somewhere around "..minetest.pos_to_string(pos))
local pos1,pos2 = rspawn:get_positions_for(pos, radius)
rspawn:debug("Searching "..minetest.pos_to_string(pos1).." to "..minetest.pos_to_string(pos2))
local airnodes = minetest.find_nodes_in_area(pos1, pos2, {"air"})
local validnodes = {}
dbg("Found "..tostring(#airnodes).." air nodes within "..tostring(radius))
rspawn:debug("Found "..tostring(#airnodes).." air nodes within "..tostring(radius))
for _,anode in pairs(airnodes) do
local under = minetest.get_node( {x=anode.x, y=anode.y-1, z=anode.z} ).name
local over = minetest.get_node( {x=anode.x, y=anode.y+1, z=anode.z} ).name
@ -90,17 +86,11 @@ function rspawn:newspawn(pos, radius)
end
if #validnodes > 0 then
minetest.log("info", "New spawn point found with radius "..tostring(radius))
forceload_free_blocks_in(pos1, pos2)
rspawn:debug("Valid spawn points found with radius "..tostring(radius))
return validnodes[math.random(1,#validnodes)]
else
rspawn:debug("No valid air nodes")
end
local pos = rspawn:newspawn(pos, radius+radial_step)
if not pos then
-- Nothing found, do cleanup with this largest forceloaded area
forceload_free_blocks_in(pos1, pos2)
end
return pos
end
function rspawn:genpos()
@ -110,7 +100,8 @@ function rspawn:genpos()
if rspawn.spawnanywhere then
pos = {
x = math.random(-30000,30000),
y = math.random(water_level, water_level+10),
--y = math.random(water_level, water_level+20),
y = water_level, -- always at waterlevel
z = math.random(-30000,30000),
}
end
@ -118,75 +109,62 @@ function rspawn:genpos()
return pos
end
function rspawn:set_new_playerspawn(player, args)
local newpos
if args == "here" then
newpos = player:get_pos()
elseif args then
newpos = minetest.string_to_pos(args)
end
if not newpos then
newpos = rspawn:genpos()
end
local spawnpos = rspawn:newspawn(newpos)
local name = player:get_player_name()
if spawnpos then
rspawn.playerspawns[name] = spawnpos
rspawn:spawnsave()
return spawnpos
end
end
local function confirm_new_spawn(name, newpos)
minetest.chat_send_player(name, "New spawn set at "..minetest.pos_to_string(newpos))
local spos = minetest.pos_to_string(newpos)
rspawn.debug("Saving spawn for "..name, spos)
rspawn.playerspawns[name] = newpos
rspawn:spawnsave()
minetest.chat_send_player(name, "New spawn set at "..spos)
minetest.get_player_by_name(name):setpos(rspawn.playerspawns[name])
end
function rspawn:double_set_new_playerspawn(player, attempts)
local cpos = minetest.pos_to_string(rspawn:genpos())
local name = player:get_player_name()
attempts = attempts or 1
function rspawn:set_newplayer_spawn(player)
local playername = player:get_player_name()
minetest.chat_send_player(name, tostring(attempts)..": Searching for a suitable spawn around "..cpos)
if not rspawn.playerspawns[playername] then
local newpos = rspawn:get_next_spawn()
dbg("Primary check on "..cpos)
local newpos = rspawn:set_new_playerspawn(player, cpos)
if newpos then
confirm_new_spawn(playername, newpos)
if not newpos then
-- Repeat only after some time: give the server time to get through previous emerge calls
minetest.after(4,function()
-- Second attempt at the same location - emerge calls should have yielded
-- map data to work with
dbg("Secondary check on "..cpos)
newpos = rspawn:set_new_playerspawn(player, cpos)
else
if rspawn.adminname ~= "singleplayer" or playername ~= rspawn.adminname then
minetest.chat_send_player(playername, "Please wait until a spawn point is available ...")
minetest.after(15, function()
rspawn:set_newplayer_spawn(player)
end)
elseif rspawn.kick_on_fail then
minetest.kick_player(playername, "No personalized spawn points available - please try again later.")
if not newpos then
if attempts > 0 then
-- Repeat the process at a new location
rspawn:double_set_new_playerspawn(player, attempts - 1)
else
minetest.chat_send_player(name, "! Could not identify suitable spawn location (try again?)")
end
else
confirm_new_spawn(name, newpos)
minetest.chat_send_player(playername, "Could not get custom spawn! Retrying in "..rspawn.gen_frequency.." seconds")
minetest.after(gen_frequency, function()
rspawn:set_newplayer_spawn(player)
end)
end
end)
end
end
end
function rspawn:renew_player_spawn(playername)
local player = minetest.get_player_by_name(playername)
local newpos = rspawn:get_next_spawn()
if newpos then
confirm_new_spawn(playername, newpos)
else
confirm_new_spawn(name, newpos)
minetest.chat_send_player(playername, "Could not get custom spawn!")
end
end
minetest.register_on_joinplayer(function(player)
-- Use the recursive mode - it is not acceptable for a player
-- not to receive a randomized spawn
minetest.after(1,function()
if not rspawn.playerspawns[player:get_player_name()] then
rspawn:double_set_new_playerspawn(player, 10)
end
end)
rspawn:set_newplayer_spawn(player)
end)
minetest.register_on_respawnplayer(function(player)
@ -203,3 +181,5 @@ minetest.register_on_respawnplayer(function(player)
player:setpos(rspawn.playerspawns[name])
return true
end)
dofile(mpath.."/src/pregeneration.lua")

View File

@ -35,18 +35,27 @@ minetest.register_chatcommand("setspawn", {
end
})
local function request_new_spawn(username, targetname)
local timername = username
if targetname ~= username then
timername = username.." "..targetname
end
if not newspawn_cooldown[timername] then
rspawn:renew_player_spawn(targetname)
newspawn_cooldown[timername] = 300
else
minetest.chat_send_player(username, tostring(math.ceil(newspawn_cooldown[timername])).."sec until you can randomize a new spawn for "..targetname)
end
end
minetest.register_chatcommand("newspawn", {
description = "Randomly select a new spawn position.",
params = "",
privs = "newspawn",
func = function(name, args)
if not newspawn_cooldown[name] then
rspawn:double_set_new_playerspawn(minetest.get_player_by_name(name), 2)
newspawn_cooldown[name] = 300
else
minetest.chat_send_player(name, tostring(math.ceil(newspawn_cooldown[name])).."sec until you can randomize a new spawn.")
end
end
request_new_spawn(name, name)
end
})
minetest.register_chatcommand("playerspawn", {
@ -54,13 +63,7 @@ minetest.register_chatcommand("playerspawn", {
params = "playername",
privs = "spawnadmin",
func = function(adminname, playername)
local jointname = adminname.."--"..playername
if not newspawn_cooldown[jointname] then
rspawn:double_set_new_playerspawn(minetest.get_player_by_name(playername), 2)
newspawn_cooldown[jointname] = 60
else
minetest.chat_send_player(adminname, tostring(math.ceil(newspawn_cooldown[jointname])).."sec until you can randomize a new spawn for "..playername)
end
request_new_spawn(adminname, playername)
end
})

View File

@ -12,15 +12,24 @@ function rspawn:spawnsave()
end
file:write(serdata)
file:close()
pregens = rspawn.playerspawns["pre gen"] or {}
minetest.debug("Wrote rspawn data with "..tostring(#pregens).." pregen nodes")
end
function rspawn:spawnload()
local file, err = io.open(spawnsfile, "r")
if err then
minetest.log("error", "[spawn] Data read failed")
return
end
rspawn.playerspawns = minetest.deserialize(file:read("*a"))
file:close()
if not err then
rspawn.playerspawns = minetest.deserialize(file:read("*a"))
file:close()
else
minetest.log("error", "[spawn] Data read failed - initializing")
rspawn.playerspawns = {}
end
pregens = rspawn.playerspawns["pre gen"] or {}
rspawn.playerspawns["pre gen"] = pregens
minetest.debug("Loaded rspawn data with "..tostring(#pregens).." pregen nodes")
end

View File

@ -1,7 +1,5 @@
local debug_on = minetest.settings:get_bool("rspawn.debug")
local function debug(message, data)
if not debug_on then
function rspawn:debug(message, data)
if not rspawn.debug_on then
return
end
@ -14,5 +12,3 @@ local function debug(message, data)
minetest.debug(debug_string)
end
return debug

23
src/forceload.lua Normal file
View File

@ -0,0 +1,23 @@
local function forceload_operate(pos1, pos2, handler)
local i,j,k
for i=pos1.x,pos2.x,16 do
for j=pos1.y,pos2.y,16 do
for k=pos1.z,pos2.z,16 do
handler({x=i,y=j,z=k})
end
end
end
end
function rspawn:forceload_blocks_in(pos1, pos2)
rspawn:debug("Forceloading blocks -----------¬", {pos1=minetest.pos_to_string(pos1),pos2=minetest.pos_to_string(pos2)})
minetest.emerge_area(pos1, pos2)
forceload_operate(pos1, pos2, minetest.forceload_block)
end
function rspawn:forceload_free_blocks_in(pos1, pos2)
rspawn:debug("Freeing forceloaded blocks ____/", {pos1=minetest.pos_to_string(pos1),pos2=minetest.pos_to_string(pos2)})
forceload_operate(pos1, pos2, minetest.forceload_free_block)
end

71
src/pregeneration.lua Normal file
View File

@ -0,0 +1,71 @@
local steptime = 0
-- Ensure pregen data is stored and saved properly
local function len_pgen()
return #rspawn.playerspawns["pre gen"]
end
local function set_pgen(idx, v)
rspawn.playerspawns["pre gen"][idx] = v
rspawn:spawnsave()
end
local function get_pgen(idx)
return rspawn.playerspawns["pre gen"][idx]
end
-- Spawn generation
local function push_new_spawn()
if len_pgen() >= rspawn.max_pregen_spawns then
rspawn:debug("Max pregenerated spawns ("..rspawn.max_pregen_spawns..") reached : "..len_pgen())
return
end
local random_pos = rspawn:genpos()
local pos1,pos2 = rspawn:get_positions_for(random_pos, rspawn.search_radius)
rspawn:forceload_blocks_in(pos1, pos2)
minetest.after(10, function()
-- Let the forceload do its thing, then act
local newpos = rspawn:newspawn(random_pos, rspawn.search_radius)
if newpos then
rspawn:debug("Generated "..minetest.pos_to_string(newpos))
set_pgen(len_pgen()+1, newpos )
else
rspawn:debug("Failed to generate new spawn point to push")
end
rspawn:forceload_free_blocks_in(pos1, pos2)
end)
end
minetest.register_globalstep(function(dtime)
steptime = steptime + dtime
if steptime > rspawn.gen_frequency then
steptime = 0
else
return
end
push_new_spawn()
end)
-- Access pregenrated spawns
function rspawn:get_next_spawn()
local nspawn
if len_pgen() > 0 then
nspawn = get_pgen(len_pgen() )
rspawn:debug("Returning pregenerated spawn",nspawn)
set_pgen(len_pgen(), nil)
else
push_new_spawn()
end
return nspawn
end