2017-01-26 17:04:52 -08:00

761 lines
19 KiB
Lua

boxes = {}
local function bytes_to_string(bytes)
local s = {}
for i = 1, #bytes do
s[i] = string.char(bytes[i])
end
return table.concat(s)
end
local function string_to_bytes(str)
local s = {}
for i = 1, string.len(str) do
s[i] = string.byte(str, i)
end
return s
end
function boxes.save(minp, maxp)
--[[ TODO: save the box in incremental steps to avoid stalling the server. The
problem is that we have to iterate over all positions since there is no way
to get the metadata of all nodes in an area, although the engine can.
However, we still use a voxelmanip to load node bulk data.
]]
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local data = vm:get_data()
local param2 = vm:get_param2_data()
local flat_data = {}
local flat_index = 1
local function add_u8(x)
flat_data[flat_index] = x
flat_index = flat_index + 1
end
local function add_u16(x)
add_u8(math.floor(x / 256))
add_u8(x % 256)
end
local function add_position(p)
add_u16(p.x - minp.x)
add_u16(p.y - minp.y)
add_u16(p.z - minp.z)
end
local function add_string(s)
add_u16(string.len(s))
for i = 1, string.len(s) do
add_u8(string.byte(s, i))
end
end
-- Version
add_u8(1)
local sx = maxp.x - minp.x + 1
local sy = maxp.y - minp.y + 1
local sz = maxp.z - minp.z + 1
-- Size
add_u16(sx)
add_u16(sy)
add_u16(sz)
local cid_mapping = {}
local cid_index = 0
local cid_rmapping = {}
local schem_size = sx * sy * sz
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
local cid = data[index]
if cid_rmapping[cid] == nil then
cid_rmapping[cid] = cid_index
cid_mapping[cid_index] = minetest.get_name_from_content_id(cid)
cid_index = cid_index + 1
end
index = index + 1
end
end
end
-- Write cid mapping
add_u16(cid_index)
for i = 0, cid_index - 1 do
add_string(cid_mapping[i])
end
-- Write bulk node data
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
add_u16(cid_rmapping[data[index]])
add_u8(param2[index])
index = index + 1
end
end
end
local meta_to_save = {}
local meta_save_index = 1
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
for x = minp.x, maxp.x do
local meta = minetest.get_meta({x = x, y = y, z = z})
local meta_table = meta:to_table()
if next(meta_table.fields) ~= nil or next(meta_table.inventory) ~= nil then
local inv = {}
for inv_name, inv_list in pairs(meta_table.inventory) do
local inv_list_2 = {}
for i, stack in ipairs(inv_list) do
inv_list_2[i] = stack:to_string()
end
inv[inv_name] = inv_list_2
end
meta_to_save[meta_save_index] =
{ x = x - minp.x, y = y - minp.y, z = z - minp.z,
fields = meta_table.fields,
inventory = inv
}
meta_save_index = meta_save_index + 1
end
end
end
end
add_u16(meta_save_index - 1)
for i = 1, meta_save_index - 1 do
local m = meta_to_save[i]
add_u16(m.x)
add_u16(m.y)
add_u16(m.z)
add_string(minetest.serialize(m.fields))
add_string(minetest.serialize(m.inventory))
end
--print(dump(flat_data))
--print(dump(bytes_to_string(flat_data)))
local raw_data = bytes_to_string(flat_data)
return minetest.compress(raw_data, "deflate")
end
function boxes.load(minp, compressed)
local raw_data = minetest.decompress(compressed, "deflate")
local raw_data_index = 1
local function read_u8()
local c = string.byte(raw_data, raw_data_index)
raw_data_index = raw_data_index + 1
return c
end
local function read_u16()
local a = read_u8()
local b = read_u8()
return 256 * a + b
end
local function read_string()
local len = read_u16()
local r = string.sub(raw_data, raw_data_index, raw_data_index + len - 1)
raw_data_index = raw_data_index + len
return r
end
local version = read_u8()
assert (version == 1)
local sx = read_u16()
local sy = read_u16()
local sz = read_u16()
-- Read cid mapping
local cid_mapping = {}
local cid_index = read_u16()
for i = 0, cid_index - 1 do
cid_mapping[i] = minetest.get_content_id(read_string())
end
local maxp = {x = minp.x + sx - 1, y = minp.y + sy - 1, z = minp.z + sz - 1}
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
-- Read bulk node data
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid_mapping[read_u16()]
param2[index] = read_u8()
index = index + 1
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
-- Finally, read metadata
local nmeta = read_u16()
for i = 1, nmeta do
local x = read_u16()
local y = read_u16()
local z = read_u16()
local p = {x = minp.x + x, y = minp.y + y, z = minp.z + z}
local meta = minetest.get_meta(p)
local fields = minetest.deserialize(read_string())
local inv = minetest.deserialize(read_string())
for inv_name, inv_list in pairs(inv) do
for i, stack in ipairs(inv_list) do
inv_list[i] = ItemStack(inv_list[i])
end
end
meta:from_table({fields = fields, inventory = inv})
end
end
function boxes.extent(compressed)
local raw_data = minetest.decompress(compressed, "deflate")
local raw_data_index = 1
local function read_u8()
local c = string.byte(raw_data, raw_data_index)
raw_data_index = raw_data_index + 1
return c
end
local function read_u16()
local a = read_u8()
local b = read_u8()
return 256 * a + b
end
local version = read_u8()
assert (version == 1)
local sx = read_u16()
local sy = read_u16()
local sz = read_u16()
return {x = sx, y = sy, z = sz}
end
-- Set the region from minp to maxp to air
function boxes.cleanup(minp, maxp)
local vm = minetest.get_voxel_manip(minp, maxp)
local emin, emax = vm:get_emerged_area()
local va = VoxelArea:new{MinEdge=emin,MaxEdge=emax}
local vmdata = vm:get_data()
local param2 = vm:get_param2_data()
local cid = minetest.get_content_id("air")
for z = minp.z, maxp.z do
for y = minp.y, maxp.y do
local index = va:index(minp.x, y, z)
for x = minp.x, maxp.x do
vmdata[index] = cid
param2[index] = 0
index = index + 1
end
end
end
vm:set_data(vmdata)
vm:set_param2_data(param2)
vm:update_liquids()
vm:write_to_map()
vm:update_map()
end
--[[
Now for box allocation.
To keep things simple, we will always allocate boxes with same width and
depth, and with a sizee that is a multiple of boxes_resolution. Height is
assumed to be unlimited above box_alloc_miny.
In order to do that, we keep a quadtree of allocatable positions; an allocated
box will always be a leaf of that tree. We also keep for each subtree the max
size that can be allocated in it.
Thus, this is https://en.wikipedia.org/wiki/Buddy_memory_allocation in 2D.
]]
local boxes_resolution = 64
local box_alloc_miny = 50
local boxes_tree = {
minp = {x = -16384, z = -16384},
edge_size = 32768,
max_alloc_size = 32768,
}
local function split_leaf(node)
assert (node.edge_size >= 2 * boxes_resolution)
assert (node.children == nil)
local new_size = node.edge_size / 2
local x = node.minp.x
local z = node.minp.z
node.children = {
{
minp = {x = x, z = z},
edge_size = new_size,
max_alloc_size = new_size,
parent = node
},
{
minp = {x = x + new_size, z = z},
edge_size = new_size,
max_alloc_size = new_size,
parent = node,
},
{
minp = {x = x, z = z + new_size},
edge_size = new_size,
max_alloc_size = new_size,
parent = node,
},
{
minp = {x = x + new_size, z = z + new_size},
edge_size = new_size,
max_alloc_size = new_size,
parent = node,
},
}
end
-- Update `max_alloc_size` for node and all its parents.
-- Also, merge subtrees if possible.
local function update_alloc_sizes(node)
while node ~= nil do
local max_size = 0
local el = node.edge_size / 2
local can_merge = true
for _, child in ipairs(node.children) do
max_size = math.max(max_size, child.max_alloc_size)
if child.max_alloc_size < el then
can_merge = false
end
end
if can_merge then
node.children = nil
node.max_alloc_size = node.edge_size
else
node.max_alloc_size = max_size
end
node = node.parent
end
end
-- Function to know how close to zero a node is.
-- Used to keep allocations close to (0, box_alloc_miny, 0).
local function zero_close(node)
local x = node.minp.x
local z = node.minp.z
local size = node.edge_size
if x < 0 then x = x + size end
if z < 0 then z = z + size end
return math.abs(x) + math.abs(z)
end
-- Allocated a box of size `size` horizontally, and unbounded vertically
-- Returns box minimum position. The minimum position must be given to
-- boxes.vfree to free the corresponding area.
function boxes.valloc(size)
assert (size > 0)
if boxes_tree.max_alloc_size < size then
return nil
end
-- Find leaf with enough room, splitting it if big enough
local node = boxes_tree
while node.edge_size / 2 >= size and node.edge_size / 2 >= boxes_resolution do
if node.children == nil then
split_leaf(node)
end
local best = nil
local best_distance = 1e10 -- infinity
for _, child in ipairs(node.children) do
if child.max_alloc_size >= size and zero_close(child) < best_distance then
best_distance = zero_close(child)
best = child
end
end
assert (best ~= nil)
node = best
end
local result = {x = node.minp.x, y = box_alloc_miny, z = node.minp.z}
node.max_alloc_size = 0
update_alloc_sizes(node.parent)
return result
end
function boxes.vfree(minp)
assert (minp.y == box_alloc_miny)
assert (boxes_tree.minp.x <= minp.x)
assert (minp.x < boxes_tree.minp.x + boxes_tree.edge_size)
assert (boxes_tree.minp.z <= minp.z)
assert (minp.z < boxes_tree.minp.z + boxes_tree.edge_size)
-- Find leaf containing minp
local node = boxes_tree
while node.children ~= nil do
local cld = nil
local el = node.edge_size / 2
for _, child in ipairs(node.children) do
if child.minp.x <= minp.x and minp.x < child.minp.x + el
and child.minp.z <= minp.z and minp.z < child.minp.z + el then
cld = child
break
end
end
assert (cld ~= nil)
node = cld
end
assert (node.max_alloc_size == 0)
node.max_alloc_size = node.edge_size
update_alloc_sizes(node.parent)
end
function boxes.read_box(box_data)
local data_index = 1
local function read_u8()
local c = string.byte(box_data, data_index)
data_index = data_index + 1
return c
end
local function read_u16()
local a = read_u8()
local b = read_u8()
return 256 * a + b
end
local function read_pos()
local x = read_u16()
local y = read_u16()
local z = read_u16()
return {x = x, y = y, z = z}
end
local size = read_pos()
local entry = read_pos()
local exit = read_pos()
return {
size = size,
entry = entry,
exit = exit,
data = string.sub(box_data, data_index)
}
end
function boxes.write_box(box)
local data_index = 1
local data = {}
local function write_u8(x)
data[data_index] = x
data_index = data_index + 1
end
local function write_u16(x)
write_u8(math.floor(x / 256))
write_u8(x % 256)
end
local function write_pos(p)
write_u16(p.x)
write_u16(p.y)
write_u16(p.z)
end
write_pos(box.size)
write_pos(box.entry)
write_pos(box.exit)
return bytes_to_string(data) .. box.data
end
function vector.min(a, b)
return {
x = math.min(a.x, b.x),
y = math.min(a.y, b.y),
z = math.min(a.z, b.z),
}
end
function vector.max(a, b)
return {
x = math.max(a.x, b.x),
y = math.max(a.y, b.y),
z = math.max(a.z, b.z),
}
end
local players_in_boxes = {}
local function read_file(filename)
local file = io.open(filename, "r")
if file ~= nil then
local file_content = file:read("*all")
io.close(file)
return file_content
end
return ""
end
local function write_file(filename, data)
local file, err = io.open(filename, "w")
if file then
file:write(data)
io.close(file)
else
error(err)
end
end
local modpath = minetest.get_modpath(minetest.get_current_modname())
local worldpath = minetest.get_worldpath()
local entry_lobby_data = read_file(worldpath .. "/entry.box")
if entry_lobby_data == "" then
entry_lobby_data = read_file(modpath .. "/entry.box")
write_file(worldpath .. "/entry.box", entry_lobby_data)
end
local exit_lobby_data = read_file(worldpath .. "/exit.box")
if exit_lobby_data == "" then
exit_lobby_data = read_file(modpath .. "/exit.box")
write_file(worldpath .. "/exit.box", exit_lobby_data)
end
if db.box_get_data(0) == "" then
local box_data = read_file(modpath .. "/box.box")
db.box_set_data(0, box_data)
end
local function pop_node(pos)
local node = minetest.get_node(pos)
local meta = minetest.get_meta(pos)
local r = {pos = pos, node = node, meta = meta:to_table()}
minetest.remove_node(pos)
return r
end
local function unpop_node(data)
minetest.set_node(data.pos, data.node)
local meta = minetest.get_meta(data.pos)
meta:from_table(data.meta)
end
local function open_door(player, minp, maxp)
local nds = {}
local i = 1
for x = minp.x, maxp.x do
for y = minp.y, maxp.y do
for z = minp.z, maxp.z do
nds[i] = pop_node({x = x, y = y, z = z})
i = i + 1
end
end
end
local od = players_in_boxes[player:get_player_name()].open_doors
od[#od + 1] = {minx = maxp.x + 0.5, nodes = nds}
end
function boxes.open_exit(player)
local name = player:get_player_name()
local ex = players_in_boxes[name].exit_door
open_door(player, vector.add(ex, {x = -1, y = 0, z = 0}), vector.add(ex, {x = 0, y = 1, z = 0}))
end
-- Close doors behind players
minetest.register_globalstep(function(dtime)
for playername, info in pairs(players_in_boxes) do
local i = 1
local player = minetest.get_player_by_name(playername)
local pos = player:get_pos()
local doors = info.open_doors
local n = #doors
while doors[i] do
if pos.x >= doors[i].minx then
for _, data in ipairs(doors[i].nodes) do
unpop_node(data)
end
doors[i] = doors[n]
doors[n] = nil
n = n - 1
else
i = i + 1
end
end
end
end)
function boxes.open_box(player, box_id)
local name = player:get_player_name()
if players_in_boxes[name] ~= nil then
return
end
local box_data = db.box_get_data(box_id)
local box = boxes.read_box(box_data)
local lobby_spawn = boxes.read_box(entry_lobby_data)
local lobby_exit = boxes.read_box(exit_lobby_data)
local lobby_spawn_offset = vector.subtract(box.entry, lobby_spawn.exit)
local lobby_exit_offset = vector.subtract(box.exit, lobby_exit.entry)
local pmin = vector.min({x = 0, y = 0, z = 0}, vector.min(lobby_spawn_offset,
lobby_exit_offset))
local box_offset = {x = -pmin.x, y = -pmin.y, z = -pmin.z}
local spawn_offset = vector.add(box_offset, lobby_spawn_offset)
local exit_offset = vector.add(box_offset, lobby_exit_offset)
local pmax = vector.max(vector.add(box_offset, box.size),
vector.max(vector.add(spawn_offset, lobby_spawn.size),
vector.add(exit_offset, lobby_exit.size)))
local request_size = math.max(pmax.x, pmax.z)
local minp = boxes.valloc(request_size)
boxes.load(vector.add(minp, box_offset), box.data)
boxes.load(vector.add(minp, spawn_offset), lobby_spawn.data)
boxes.load(vector.add(minp, exit_offset), lobby_exit.data)
player:set_pos(vector.add(minp, vector.add(spawn_offset, lobby_spawn.entry)))
players_in_boxes[name] = {
minp = minp,
maxp = vector.add(minp, vector.subtract(pmax, 1)),
exit_door = vector.add(minp, vector.add(box_offset, box.exit)),
open_doors = {},
}
local entry_door = vector.add(minp, vector.add(box_offset, box.entry))
open_door(player, vector.add(entry_door, {x = -1, y = 0, z = 0}),
vector.add(entry_door, {x = 0, y = 1, z = 0}))
boxes.open_exit(player)
end
function boxes.close_box(player)
local name = player:get_player_name()
if players_in_boxes[name] == nil then
return
end
local bx = players_in_boxes[name]
boxes.cleanup(bx.minp, bx.maxp)
boxes.vfree(bx.minp)
players_in_boxes[name] = nil
end
minetest.register_on_leaveplayer(function(player)
boxes.close_box(player)
end)
minetest.register_chatcommand("enter", {
params = "<boxid>",
description = "Enter box with this id",
privs = {server = true},
func = function(name, param)
local player = minetest.get_player_by_name(name)
local id = tonumber(param)
if not id then
return
end
local data = db.box_get_data(id)
if not data then
return
end
boxes.open_box(player, id)
end,
})
minetest.register_chatcommand("leave", {
params = "",
description = "Leave the current box",
privs = {server = true},
func = function(name, param)
local player = minetest.get_player_by_name(name)
boxes.close_box(player)
end,
})
local lobby_updates = {}
minetest.register_chatcommand("update_lobby", {
params = "entry|exit",
description = "Set corresponding lobby. Use without parameter to start \
updating a lobby, then punch both corners of the lobby and the bottom node \
of the exit. In the case of the entry lobby, stand at its spawnpoint to run \
the command.",
privs = {server = true},
func = function(name, param)
if param ~= "entry" and param ~= "exit" and param ~= "" then
return
end
if param == "" then
lobby_updates[name] = {}
elseif not lobby_updates[name] then
minetest.chat_send_player(name, "Not all positions have been set.")
return
else
local pos1 = lobby_updates[name].pos1
local pos2 = lobby_updates[name].pos2
local pos3 = lobby_updates[name].pos3
if not pos1 or not pos2 or not pos3 then
minetest.chat_send_player(name, "Not all positions have been set.")
return
end
local minp = vector.min(pos1, pos2)
local maxp = vector.max(pos1, pos2)
local data = boxes.save(minp, maxp)
local p3 = vector.subtract(pos3, minp)
if param == "exit" then
if p3.x < 0 or p3.y < 0 or p3.z < 0 then
minetest.chat_send_player(name, "The entry is not inside the lobby.")
return
end
exit_lobby_data = boxes.write_box({
size = boxes.extent(data),
entry = p3,
exit = p3,
data = data,
})
write_file(worldpath .. "/exit.box", exit_lobby_data)
else
local player = minetest.get_player_by_name(name)
local entry = vector.subtract(vector.round(player:get_pos()), minp)
p3.x = p3.x + 1
if entry.x < 0 or entry.y < 0 or entry.z < 0 then
minetest.chat_send_player(name, "You're not standing inside the lobby.")
return
end
if p3.x < 0 or p3.y < 0 or p3.z < 0 then
minetest.chat_send_player(name, "The exit is not inside the lobby.")
return
end
entry_lobby_data = boxes.write_box({
size = boxes.extent(data),
entry = entry,
exit = p3,
data = data,
})
write_file(worldpath .. "/entry.box", entry_lobby_data)
end
minetest.chat_send_player(name, "Updated.")
lobby_updates[name] = nil
end
end,
})
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
if not puncher then return end
local name = puncher:get_player_name()
if not lobby_updates[name] then return end
if not lobby_updates[name].pos1 then
lobby_updates[name].pos1 = pos
minetest.chat_send_player(name, "Position 1 set to " .. dump(pos) .. ".")
elseif not lobby_updates[name].pos2 then
lobby_updates[name].pos2 = pos
minetest.chat_send_player(name, "Position 2 set to " .. dump(pos) .. ".")
elseif not lobby_updates[name].pos3 then
lobby_updates[name].pos3 = pos
minetest.chat_send_player(name, "Position 3 set to " .. dump(pos) .. ".")
end
end)