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