389 lines
14 KiB
Lua

local S = minetest.get_translator(minetest.get_current_modname())
-- pocket data tables have the following properties:
-- pending = true -- pocket is being initialized, don't teleport there just yet
-- destination = a vector relative to the pocket's minp that is where new arrivals teleport tonumber
-- name = a name for the pocket.
-- owner = if set, this pocket is "owned" by this particular player.
-- protected = if true, this pocket is protected and only the owner can modify its contents
-- minp = the lower corner of the pocket's region
local pockets_by_name = {}
local player_origin = {}
local pockets_deleted = {} -- record deleted pockets for possible later undeletion, indexed by hash
local personal_pockets = {} -- to be filled out if personal pockets are enabled
local protected_areas = AreaStore()
--The world is a cube of side length 61840. Coordinates go from -30912 to 30927 in any direction.
--The side length is a multiple of 80
--773 * 80 = 61840
-- so there are 773 * 773 = 597529 chunk in a horizontal layer. Should be plenty for distributing these things in.
local mapgen_chunksize = tonumber(minetest.get_mapgen_setting("chunksize"))
local mapblock_size = mapgen_chunksize * 16 -- should be 80 in almost all cases, but avoiding hardcoding it for extensibility
local block_grid_dimension = math.floor(61840 / mapblock_size) -- should be 773
local min_coordinate = -30912
local layer_elevation = tonumber(minetest.settings:get("pocket_dimensions_altitude")) or 30000
layer_elevation = math.floor(layer_elevation / mapblock_size) * mapblock_size - 32 -- round to mapblock boundary
pocket_dimensions.pocket_size = mapblock_size
--------------------------------------------------------------------------------------
-- Loading and saving data
local filename = minetest.get_worldpath() .. "/pocket_dimensions_data.lua"
local load_data = function()
local f, e = loadfile(filename)
if f then
local data = f()
pockets_by_name = data.pockets_by_name
player_origin = data.player_origin
pockets_deleted = data.pockets_deleted
if personal_pockets_enabled then
for name, pocket_data in pairs(pockets_by_name) do
if pocket_data.personal and pocket_data.owner then
if personal_pockets[pocket_data.owner] then
minetest.log("error", "[pocket_dimensions] "
.. pocket_data.owner .. " owns multiple personal pockets, "
.. personal_pockets[pocket_data.owner].name .. " and " .. pocket_data.name
.. ".")
end
personal_pockets[pocket_data.owner] = pocket_data
end
end
end
else
return
end
-- add saved protected areas
for name, pocket_data in pairs(pockets_by_name) do
if pocket_data.protected then
protected_areas:insert_area(pocket_data.minp, vector.add(pocket_data.minp, mapblock_size), pocket_data.owner)
end
end
end
local save_data = function()
local data = {}
data.pockets_by_name = pockets_by_name
data.player_origin = player_origin
data.pockets_deleted = pockets_deleted
local file, e = io.open(filename, "w");
if not file then
return error(e);
end
file:write(minetest.serialize(data))
file:close()
end
load_data()
--------------------------------------------------------------
-- protection
local old_is_protected = minetest.is_protected
function minetest.is_protected(pos, name)
if minetest.check_player_privs(name, "protection_bypass") then
return false
end
local protection = protected_areas:get_areas_for_pos(pos, false, true)
for _, area in pairs(protection) do
if area.data ~= name then
return true
end
end
return old_is_protected(pos, name)
end
pocket_dimensions.set_protection = function(pocket_data, protection)
pocket_data.protected = protection
-- clear any existing protection
protected = protected_areas:get_areas_for_pos(pocket_data.minp)
for id, _ in pairs(protected) do -- there should only be one result
protected_areas:remove_area(id)
end
-- add protection if warranted
if pocket_data.protected then
protected_areas:insert_area(pocket_data.minp, vector.add(pocket_data.minp, mapblock_size), pocket_data.owner or "")
end
minetest.log("action", "[pocket_dimensions] Protection ownership of pocket dimension " .. pocket_data.name .. " set to " .. tostring(pocket_data.protected))
save_data()
end
-----------------------------------------------------------------------------------------------------
-- check if players have got out of pocket dimensions by other means and clear their origin locations
local since_last_check = 0
minetest.register_globalstep(function(dtime)
since_last_check = since_last_check + dtime
if since_last_check > 10 then
for name, _ in pairs(player_origin) do
local pos = minetest.get_player_by_name(name):get_pos()
if pos.y < layer_elevation or pos.y > layer_elevation + mapblock_size then
player_origin[name] = nil -- somehow, player escaped the pocket dimension layer
since_last_check = 0 -- note that we changed data
end
end
if since_last_check == 0 then
save_data()
return
end
since_last_check = 0
end
end)
------------------------------------------------------------------------------------------------
pocket_dimensions.get_pocket = function(pocket_name)
return pockets_by_name[string.lower(pocket_name)]
end
pocket_dimensions.get_all_pockets = function()
local ret = {}
for name, def in pairs(pockets_by_name) do
table.insert(ret, def)
end
return ret
end
pocket_dimensions.get_deleted_pockets = function()
local ret = {}
for hash, def in pairs(pockets_deleted) do
table.insert(ret, def)
end
return ret
end
pocket_dimensions.pocket_containing_pos = function(pos)
for name, pocket_data in pairs(pockets_by_name) do
local pos_diff = vector.subtract(pos, pocket_data.minp)
if pos_diff.y >=0 and pos_diff.y <= mapblock_size and -- check y first to eliminate possibility player's not in a pocket dimension at all
pos_diff.x >=0 and pos_diff.x <= mapblock_size and
pos_diff.z >=0 and pos_diff.z <= mapblock_size then
return pocket_data
end
end
end
pocket_dimensions.rename_pocket = function(old_name, new_name)
local new_name_lower = string.lower(new_name)
local old_name_lower = string.lower(old_name)
if pockets_by_name[new_name_lower] or not pockets_by_name[old_name_lower] then
return false
end
pockets_by_name[new_name_lower] = pocket_data
pockets_by_name[old_name_lower] = nil
pocket_data.name = new_name
save_data()
return true
end
pocket_dimensions.set_destination = function(pocket_data, destination)
local dest = vector.round(destination)
assert(dest.x > pocket_data.minp.x and dest.y > pocket_data.minp.y and dest.z > pocket_data.minp.z
and dest.x < pocket_data.minp.x + mapblock_size
and dest.y < pocket_data.minp.y + mapblock_size
and dest.z < pocket_data.minp.z + mapblock_size,
"[pocket_dimensions] attempting to set destination point " ..
minetest.pos_to_string(dest) ..
" that wasn't within pocket dimension "..
pocket_data.name)
pocket_data.destination = dest
save_data()
end
---------------------------------------------------------------------------------
-- entering and exiting
------------------------------------------------------------------------------------------
-- Teleport effects
local particle_node_pos_spread = vector.new(0.5,0.5,0.5)
local particle_user_pos_spread = vector.new(0.5,1.5,0.5)
local particle_speed_spread = vector.new(0.1,0.1,0.1)
local particle_poof = function(pos)
minetest.add_particlespawner({
amount = 100,
time = 0.1,
minpos = vector.subtract(pos, particle_node_pos_spread),
maxpos = vector.add(pos, particle_user_pos_spread),
minvel = particle_speed_spread,
maxvel = particle_speed_spread,
minacc = {x=0, y=0, z=0},
maxacc = {x=0, y=0, z=0},
minexptime = 0.1,
maxexptime = 0.5,
minsize = 1,
maxsize = 1,
collisiondetection = false,
vertical = false,
texture = "pocket_dimensions_spark.png",
})
end
local teleport_player = function(player, dest)
local source_pos = player:get_pos()
particle_poof(source_pos)
minetest.sound_play({name="pocket_dimensions_teleport_from"}, {pos = source_pos}, true)
player:set_pos(dest)
particle_poof(dest)
minetest.sound_play({name="pocket_dimensions_teleport_to"}, {pos = dest}, true)
end
pocket_dimensions.teleport_player_to_pocket = function(player_name, pocket_name)
local pocket_data = pocket_dimensions.get_pocket(pocket_name)
if pocket_data == nil or pocket_data.pending then
return false
end
local player = minetest.get_player_by_name(player_name)
if not player_origin[player_name] then
player_origin[player_name] = player:get_pos()
save_data()
end
teleport_player(player, pocket_data.destination)
return true
end
-- returns a place to put players if they have no origin recorded
local get_fallback_origin = function()
local spawnpoint = minetest.setting_get_pos("static_spawnpoint")
if not spawnpoint then
local x = math.random()*1000 - 500
local z = math.random()*1000 - 500
local y =minetest.get_spawn_level(x,z)
spawnpoint = {x=x,y=y,z=z}
end
end
pocket_dimensions.return_player_to_origin = function(player_name)
local player = minetest.get_player_by_name(player_name)
local origin = player_origin[player_name]
if origin then
teleport_player(player, origin)
player_origin[player_name] = nil
save_data()
return
end
-- If the player's lost their origin data somehow, dump them somewhere using the spawn system to find an adequate place.
local spawnpoint = get_fallback_origin()
minetest.log("error", "[pocket_dimensions] Somehow "..name.." was at "..minetest.pos_to_string(clicker:get_pos())..
" inside a pocket dimension but they had no origin point recorded when they tried to leave. Sending them to "..
minetest.pos_to_string(spawnpoint).." as a fallback.")
teleport_player(clicker, spawnpoint)
end
-------------------------------------------------------------------------------------
-- pocket creation
local mapgens = {}
pocket_dimensions.register_pocket_type = function(type_name, mapgen_callback)
mapgens[type_name] = mapgen_callback
end
local emerge_callback = function(blockpos, action, calls_remaining, pocket_data)
local mapgen_callback = mapgens[pocket_data.type]
assert(mapgen_callback, "[pocket_dimensions] pocket type " .. pocket_data.type .. " had no registered mapgen callback")
local dest = mapgen_callback(pocket_data)
pocket_dimensions.set_destination(pocket_data, dest)
pocket_data.pending = nil
save_data()
minetest.log("action", "[pocket_dimensions] Finished initializing terrain map for pocket dimension " .. pocket_data.name)
end
pocket_dimensions.create_pocket = function(pocket_name, pocket_data_override)
pocket_data_override = pocket_data_override or {}
pocket_data_override.type = pocket_data_override.type or "grassy"
if pocket_name == nil or pocket_name == "" then
return false, S("Please provide a name for the pocket dimension")
end
local pocket_name_lower = string.lower(pocket_name)
if pockets_by_name[pocket_name_lower] then
return false, S("The name @1 is already in use.", pocket_name)
end
local count = 0
while count < 100 do
local x = math.random(0, block_grid_dimension) * mapblock_size + min_coordinate
local z = math.random(0, block_grid_dimension) * mapblock_size + min_coordinate
local pos = {x=x, y=layer_elevation, z=z}
if pocket_dimensions.pocket_containing_pos(pos) == nil then
local pocket_data = {pending=true, minp=pos, name=pocket_name}
pockets_by_name[pocket_name_lower] = pocket_data
for key, value in pairs(pocket_data_override) do
pocket_data[key] = value
end
minetest.emerge_area(pos, pos, emerge_callback, pocket_data)
save_data()
minetest.log("action", "[pocket_dimensions] Created a pocket dimension named " .. pocket_name .. " at " .. minetest.pos_to_string(pos))
return true, S("Pocket dimension @1 created", pocket_name)
end
end
return false, S("Failed to find a new location for this pocket dimension.")
end
pocket_dimensions.delete_pocket = function(pocket_data)
local pocket_name_lower = string.lower(pocket_data.name)
local pocket_hash = minetest.hash_node_position(pocket_data.minp)
pockets_deleted[pocket_hash] = pocket_data
pockets_by_name[pocket_name_lower] = nil
for name, personal_pocket_data in pairs(personal_pockets) do
if pocket_data == personal_pocket_data then
-- we're deleting a personal pocket, remove its record
personal_pockets[name] = nil
break
end
end
save_data()
minetest.log("action", "[pocket_dimensions] Deleted the pocket dimension " .. pocket_data.name .. " at " .. minetest.pos_to_string(pocket_data.minp))
return true, S("Deleted pocket dimension @1 at @2. Note that this doesn't affect the map, just moves this pocket dimension out of regular access and into the deleted list.", pocket_data.name, minetest.pos_to_string(pocket_data.minp))
end
pocket_dimensions.undelete_pocket = function(pocket_data)
local pocket_hash = minetest.hash_node_position(pocket_data.minp)
local pocket_name_lower = string.lower(pocket_data.name)
if pockets_by_name[pocket_name_lower] then
return false, S("Cannot undelete, a pocket dimension with the name @1 already exists", pocket_name)
end
pockets_deleted[pocket_hash] = nil
pockets_by_name[pocket_name_lower] = pocket_data
if pocket_data.personal and pocket_data.owner and not personal_pockets[pocket_data.owner] then
-- it was a personal pocket and the player hasn't created a new one, so restore that association
personal_pockets[pocket_data.owner] = pocket_data
end
save_data()
minetest.log("action", "[pocket_dimensions] Undeleted the pocket dimension " .. pocket_data.name .. " at " .. minetest.pos_to_string(pocket_data.minp))
return true, S("Undeleted pocket dimension @1 at @2. Note that this doesn't affect the map, just moves this pocket dimension out of regular access and into the deleted list.", pocket_data.name, minetest.pos_to_string(pocket_data.minp))
end
------------------------------------------------------------------
-- TODO: some cross-validation to ensure only owned pockets are personal and only one pocket is personal per player
pocket_dimensions.get_personal_pocket = function(player_name)
return personal_pockets[player_name]
end
pocket_dimensions.set_personal_pocket = function(pocket_data, player_name)
if pocket_data.personal and not player_name then
-- clear personal pocket
personal_pockets[player] = nil
pocket_data.personal = nil
else
pocket_data.personal = true
pocket_data.owner = player_name
personal_pockets[player_name] = pocket_data
end
save_data()
end
pocket_dimensions.set_owner = function(pocket_data, player_name)
pocket_data.owner = player_name
save_data()
end