From 4b1c6bac85d0203acab0cadbd2caaf96b222c9ea Mon Sep 17 00:00:00 2001 From: Tai Kedzierski Date: Sat, 29 Dec 2018 16:18:36 +0000 Subject: [PATCH] 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 --- .gitignore | 1 + README.md | 44 +++++++--- init.lua | 196 +++++++++++++++++++----------------------- src/commands.lua | 31 ++++--- src/data.lua | 21 +++-- src/debugging.lua | 8 +- src/forceload.lua | 23 +++++ src/pregeneration.lua | 71 +++++++++++++++ 8 files changed, 247 insertions(+), 148 deletions(-) create mode 100644 .gitignore create mode 100644 src/forceload.lua create mode 100644 src/pregeneration.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/README.md b/README.md index a64109e..4664daa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/init.lua b/init.lua index 14dac6d..a507ae1 100644 --- a/init.lua +++ b/init.lua @@ -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") diff --git a/src/commands.lua b/src/commands.lua index 3dfa67b..88ca2f4 100644 --- a/src/commands.lua +++ b/src/commands.lua @@ -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 }) diff --git a/src/data.lua b/src/data.lua index 40efd71..48e8262 100644 --- a/src/data.lua +++ b/src/data.lua @@ -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 diff --git a/src/debugging.lua b/src/debugging.lua index 8eeeaad..4da10d3 100644 --- a/src/debugging.lua +++ b/src/debugging.lua @@ -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 diff --git a/src/forceload.lua b/src/forceload.lua new file mode 100644 index 0000000..46bfe8a --- /dev/null +++ b/src/forceload.lua @@ -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 + diff --git a/src/pregeneration.lua b/src/pregeneration.lua new file mode 100644 index 0000000..5c25651 --- /dev/null +++ b/src/pregeneration.lua @@ -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