diff --git a/mods/boxes/init.lua b/mods/boxes/init.lua index b8f0077..962d076 100644 --- a/mods/boxes/init.lua +++ b/mods/boxes/init.lua @@ -1,232 +1,390 @@ boxes = {} local function bytes_to_string(bytes) - s = {} - for i = 1, #bytes do - s[i] = string.char(bytes[i]) - end - return table.concat(s) + s = {} + for i = 1, #bytes do + s[i] = string.char(bytes[i]) + end + return table.concat(s) end local function string_to_bytes(str) - s = {} - for i = 1, string.len(str) do - s[i] = string.byte(str, i) - end - return s + 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 + --[[ 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) + -- 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 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} + 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 + 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 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 + -- 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 + 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 + 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") + --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 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 version = read_u8() + assert (version == 1) - local sx = read_u16() - local sy = read_u16() - local sz = read_u16() + local sx = read_u16() + local sy = read_u16() + local sz = read_u16() - -- Read cid mapping - local cid_mapping = {} - cid_index = read_u16() - for i = 0, cid_index - 1 do - cid_mapping[i] = minetest.get_content_id(read_string()) - end + -- 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() + 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() + -- 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 + -- 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 +--[[ + 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 -- test code --[[ minetest.after(5, function() - local data = boxes.save({x = -1, y = -4, z = -1}, - {x = 1, y = -2, z = 1}) - print(string.len(data)) - boxes.load({x = -10, y = 0, z = 5}, data) + local data = boxes.save({x = -1, y = -4, z = -1}, + {x = 1, y = -2, z = 1}) + print(string.len(data)) + boxes.load({x = -10, y = 0, z = 5}, data) + + print(dump(boxes_tree)) + local minp = boxes.valloc(237) + print(dump(minp)) + print(dump(boxes_tree)) + boxes.vfree(minp) + print(dump(boxes_tree)) + end) ]]