mirror of
https://github.com/minetest-mods/areas.git
synced 2024-12-22 04:42:23 +01:00
79e799cfa1
On newer Minetest servers, handles saving jobs in async environment. To prevent conflicts, the save file is locked whie saving, and if a code requests saving while the file is locked, data is saved again immediately after finishing the current save.
356 lines
9.3 KiB
Lua
356 lines
9.3 KiB
Lua
local S = minetest.get_translator("areas")
|
|
|
|
function areas:player_exists(name)
|
|
return minetest.get_auth_handler().get_auth(name) ~= nil
|
|
end
|
|
|
|
-- When saving is done in an async thread, the function will not be present in this global namespace.
|
|
if not areas._internal_do_save then
|
|
local saving_requested = false
|
|
local saving_locked = false
|
|
|
|
-- Required cuz we are referring to _G.areas._internal_do_save *inside*
|
|
-- async env (it does not exist in the main thread)
|
|
local function async_func(...)
|
|
return areas._internal_do_save(...)
|
|
end
|
|
|
|
local function done_callback()
|
|
saving_locked = false
|
|
if saving_requested == true then
|
|
saving_requested = false
|
|
return areas:save()
|
|
end
|
|
end
|
|
|
|
function areas:save()
|
|
if saving_locked == true then
|
|
saving_requested = true
|
|
else
|
|
saving_locked = true
|
|
return core.handle_async(async_func, done_callback, self.areas, self.config.filename)
|
|
end
|
|
end
|
|
else
|
|
-- Save the areas table to a file
|
|
function areas:save()
|
|
return areas._internal_do_save(self.areas, self.config.filename)
|
|
end
|
|
end
|
|
|
|
-- Load the areas table from the save file
|
|
function areas:load()
|
|
local file, err = io.open(self.config.filename, "r")
|
|
if err then
|
|
self.areas = self.areas or {}
|
|
return err
|
|
end
|
|
local data = file:read("*a")
|
|
if data:sub(1, 1) == "[" then
|
|
self.areas, err = minetest.parse_json(data)
|
|
else
|
|
self.areas, err = minetest.deserialize(data)
|
|
end
|
|
if type(self.areas) ~= "table" then
|
|
self.areas = {}
|
|
end
|
|
if err and #data > 10 then
|
|
minetest.log("error", "[areas] Failed to load area data: " ..
|
|
tostring(err))
|
|
end
|
|
file:close()
|
|
self:populateStore()
|
|
end
|
|
|
|
--- Checks an AreaStore ID.
|
|
-- Deletes the AreaStore (falling back to the iterative method)
|
|
-- and prints an error message if the ID is invalid.
|
|
-- @return Whether the ID was valid.
|
|
function areas:checkAreaStoreId(sid)
|
|
if not sid then
|
|
minetest.log("error", "AreaStore failed to find an ID for an "
|
|
.."area! Falling back to iterative area checking.")
|
|
self.store = nil
|
|
self.store_ids = nil
|
|
end
|
|
return sid and true or false
|
|
end
|
|
|
|
-- Populates the AreaStore after loading, if needed.
|
|
function areas:populateStore()
|
|
if not rawget(_G, "AreaStore") then
|
|
return
|
|
end
|
|
local store = AreaStore()
|
|
local store_ids = {}
|
|
for id, area in pairs(areas.areas) do
|
|
local sid = store:insert_area(area.pos1,
|
|
area.pos2, tostring(id))
|
|
if not self:checkAreaStoreId(sid) then
|
|
return
|
|
end
|
|
store_ids[id] = sid
|
|
end
|
|
self.store = store
|
|
self.store_ids = store_ids
|
|
end
|
|
|
|
-- Guarentees returning an unused index in areas.areas
|
|
local index_cache = 0
|
|
local function findFirstUnusedIndex()
|
|
local t = areas.areas
|
|
repeat index_cache = index_cache + 1
|
|
until t[index_cache] == nil
|
|
return index_cache
|
|
end
|
|
|
|
--- Add an area.
|
|
-- @return The new area's ID.
|
|
function areas:add(owner, name, pos1, pos2, parent)
|
|
local id = findFirstUnusedIndex()
|
|
self.areas[id] = {
|
|
name = name,
|
|
pos1 = pos1,
|
|
pos2 = pos2,
|
|
owner = owner,
|
|
parent = parent
|
|
}
|
|
|
|
for i=1, #areas.registered_on_adds do
|
|
areas.registered_on_adds[i](id, self.areas[id])
|
|
end
|
|
|
|
-- Add to AreaStore
|
|
if self.store then
|
|
local sid = self.store:insert_area(pos1, pos2, tostring(id))
|
|
if self:checkAreaStoreId(sid) then
|
|
self.store_ids[id] = sid
|
|
end
|
|
end
|
|
return id
|
|
end
|
|
|
|
--- Remove an area, and optionally its children recursively.
|
|
-- If an area is deleted non-recursively the children will
|
|
-- have the removed area's parent as their new parent.
|
|
function areas:remove(id, recurse)
|
|
if recurse then
|
|
-- Recursively find child entries and remove them
|
|
local cids = self:getChildren(id)
|
|
for _, cid in pairs(cids) do
|
|
self:remove(cid, true)
|
|
end
|
|
else
|
|
-- Update parents
|
|
local parent = self.areas[id].parent
|
|
local children = self:getChildren(id)
|
|
for _, cid in pairs(children) do
|
|
-- The subarea parent will be niled out if the
|
|
-- removed area does not have a parent
|
|
self.areas[cid].parent = parent
|
|
end
|
|
end
|
|
|
|
for i=1, #areas.registered_on_removes do
|
|
areas.registered_on_removes[i](id)
|
|
end
|
|
|
|
-- Remove main entry
|
|
self.areas[id] = nil
|
|
|
|
-- Remove from AreaStore
|
|
if self.store then
|
|
self.store:remove_area(self.store_ids[id])
|
|
self.store_ids[id] = nil
|
|
end
|
|
end
|
|
|
|
--- Move an area.
|
|
function areas:move(id, area, pos1, pos2)
|
|
area.pos1 = pos1
|
|
area.pos2 = pos2
|
|
|
|
for i=1, #areas.registered_on_moves do
|
|
areas.registered_on_moves[i](id, area, pos1, pos2)
|
|
end
|
|
|
|
if self.store then
|
|
self.store:remove_area(areas.store_ids[id])
|
|
local sid = self.store:insert_area(pos1, pos2, tostring(id))
|
|
if self:checkAreaStoreId(sid) then
|
|
self.store_ids[id] = sid
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Checks if an area between two points is entirely contained by another area.
|
|
-- Positions must be sorted.
|
|
function areas:isSubarea(pos1, pos2, id)
|
|
local area = self.areas[id]
|
|
if not area then
|
|
return false
|
|
end
|
|
local ap1, ap2 = area.pos1, area.pos2
|
|
local ap1x, ap1y, ap1z = ap1.x, ap1.y, ap1.z
|
|
local ap2x, ap2y, ap2z = ap2.x, ap2.y, ap2.z
|
|
local p1x, p1y, p1z = pos1.x, pos1.y, pos1.z
|
|
local p2x, p2y, p2z = pos2.x, pos2.y, pos2.z
|
|
if
|
|
(p1x >= ap1x and p1x <= ap2x) and
|
|
(p2x >= ap1x and p2x <= ap2x) and
|
|
(p1y >= ap1y and p1y <= ap2y) and
|
|
(p2y >= ap1y and p2y <= ap2y) and
|
|
(p1z >= ap1z and p1z <= ap2z) and
|
|
(p2z >= ap1z and p2z <= ap2z) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Returns a table (list) of children of an area given its identifier
|
|
function areas:getChildren(id)
|
|
local children = {}
|
|
for cid, area in pairs(self.areas) do
|
|
if area.parent and area.parent == id then
|
|
table.insert(children, cid)
|
|
end
|
|
end
|
|
return children
|
|
end
|
|
|
|
-- checks all possible restrictions registered with
|
|
-- areas:registerProtectionCondition
|
|
-- builtin callbacks below
|
|
function areas:canPlayerAddArea(pos1, pos2, name)
|
|
local allowed = true
|
|
local errMsg
|
|
for i=1, #areas.registered_protection_conditions do
|
|
local res, msg = areas.registered_protection_conditions[i](pos1, pos2, name)
|
|
if res == true then
|
|
-- always allow to protect, no matter of other conditions
|
|
return true
|
|
elseif res == false then
|
|
-- there might be another callback that returns true, so we can't break here
|
|
allowed = false
|
|
-- save the first error that occurred
|
|
errMsg = errMsg or msg
|
|
elseif res ~= nil then
|
|
local origin = areas.callback_origins[areas.registered_protection_conditions[i]]
|
|
error("\n[Mod] areas: Invalid api usage from mod '" ..
|
|
origin.mod .. "' in callback registerProtectionCondition() at " ..
|
|
origin.source .. ":" .. origin.line)
|
|
end
|
|
end
|
|
|
|
return allowed, errMsg
|
|
end
|
|
|
|
-- Checks if the user has sufficient privileges.
|
|
areas:registerProtectionCondition(function(pos1, pos2, name)
|
|
local privs = minetest.get_player_privs(name)
|
|
if privs.areas then
|
|
-- always allow administrators to create areas
|
|
return true
|
|
end
|
|
|
|
-- Check self protection privilege
|
|
if not areas.config.self_protection or
|
|
not privs[areas.config.self_protection_privilege] then
|
|
return false, S("Self protection is disabled or you do not have"
|
|
.." the necessary privilege.")
|
|
end
|
|
end)
|
|
|
|
-- check if the area is too big
|
|
areas:registerProtectionCondition(function(pos1, pos2, name)
|
|
local privs = minetest.get_player_privs(name)
|
|
local max_size = privs.areas_high_limit and
|
|
areas.config.self_protection_max_size_high or
|
|
areas.config.self_protection_max_size
|
|
if
|
|
(pos2.x - pos1.x + 1) > max_size.x or
|
|
(pos2.y - pos1.y + 1) > max_size.y or
|
|
(pos2.z - pos1.z + 1) > max_size.z then
|
|
return false, S("Area is too big.")
|
|
end
|
|
end)
|
|
|
|
-- Check number of areas the user has and make sure it not above the max
|
|
areas:registerProtectionCondition(function(pos1, pos2, name)
|
|
local privs = minetest.get_player_privs(name)
|
|
local count = 0
|
|
for _, area in pairs(areas.areas) do
|
|
if area.owner == name then
|
|
count = count + 1
|
|
end
|
|
end
|
|
local max_areas = privs.areas_high_limit and
|
|
areas.config.self_protection_max_areas_high or
|
|
areas.config.self_protection_max_areas
|
|
if count >= max_areas then
|
|
return false, S("You have reached the maximum amount of"
|
|
.." areas that you are allowed to protect.")
|
|
end
|
|
end)
|
|
|
|
-- checks if the area intersects other areas that the player do not own.
|
|
areas:registerProtectionCondition(function(pos1, pos2, name)
|
|
local can, id = areas:canInteractInArea(pos1, pos2, name)
|
|
if not can then
|
|
local area = areas.areas[id]
|
|
return false, S("The area intersects with @1 [@2] (@3).",
|
|
area.name, id, area.owner)
|
|
end
|
|
end)
|
|
|
|
-- Given an id returns a string in the format:
|
|
-- "name [id]: owner (x1, y1, z1) (x2, y2, z2) -> children"
|
|
function areas:toString(id)
|
|
local area = self.areas[id]
|
|
local message = ("%s [%d]: %s %s %s"):format(
|
|
area.name, id, area.owner,
|
|
minetest.pos_to_string(area.pos1),
|
|
minetest.pos_to_string(area.pos2))
|
|
|
|
local children = areas:getChildren(id)
|
|
if #children > 0 then
|
|
message = message.." -> "..table.concat(children, ", ")
|
|
end
|
|
return message
|
|
end
|
|
|
|
-- Re-order areas in table by their identifiers
|
|
function areas:sort()
|
|
local sa = {}
|
|
for k, area in pairs(self.areas) do
|
|
if not area.parent then
|
|
table.insert(sa, area)
|
|
local newid = #sa
|
|
for _, subarea in pairs(self.areas) do
|
|
if subarea.parent == k then
|
|
subarea.parent = newid
|
|
table.insert(sa, subarea)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self.areas = sa
|
|
end
|
|
|
|
-- Checks if a player owns an area or a parent of it
|
|
function areas:isAreaOwner(id, name)
|
|
local cur = self.areas[id]
|
|
if cur and minetest.check_player_privs(name, self.adminPrivs) then
|
|
return true
|
|
end
|
|
while cur do
|
|
if cur.owner == name then
|
|
return true
|
|
elseif cur.parent then
|
|
cur = self.areas[cur.parent]
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
return false
|
|
end
|