X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=worldedit%2Fserialization.lua;h=e796fc1477653a8afa0d64047c0734747e6970a5;hb=b2e086f9ec07ea0cdf4b7af81758cf544240a936;hp=8e4923170219c5055b59d726be69a1c1dd3e046b;hpb=fdca506505841da24b2b51929da57ab75495276d;p=worldedit.git diff --git a/worldedit/serialization.lua b/worldedit/serialization.lua index 8e49231..e796fc1 100644 --- a/worldedit/serialization.lua +++ b/worldedit/serialization.lua @@ -1,39 +1,107 @@ -worldedit = worldedit or {} +--- Schematic serialization and deserialiation. +-- @module worldedit.serialization -dofile(minetest.get_modpath("worldedit") .. "/table_save.lua") --wip: remove dependency +worldedit.LATEST_SERIALIZATION_VERSION = 5 +local LATEST_SERIALIZATION_HEADER = worldedit.LATEST_SERIALIZATION_VERSION .. ":" ---modifies positions `pos1` and `pos2` so that each component of `pos1` is less than or equal to its corresponding conent of `pos2`, returning two new positions -worldedit.sort_pos = function(pos1, pos2) - pos1 = {x=pos1.x, y=pos1.y, z=pos1.z} - pos2 = {x=pos2.x, y=pos2.y, z=pos2.z} - if pos1.x > pos2.x then - pos2.x, pos1.x = pos1.x, pos2.x - end - if pos1.y > pos2.y then - pos2.y, pos1.y = pos1.y, pos2.y + +--[[ +Serialization version history: + 1: Original format. Serialized Lua table with a weird linked format... + 2: Position and node seperated into sub-tables in fields `1` and `2`. + 3: List of nodes, one per line, with fields seperated by spaces. + Format: + 4: Serialized Lua table containing a list of nodes with `x`, `y`, `z`, + `name`, `param1`, `param2`, and `meta` fields. + 5: Added header and made `param1`, `param2`, and `meta` fields optional. + Header format: ,,...: +--]] + + +--- Reads the header of serialized data. +-- @param value Serialized WorldEdit data. +-- @return The version as a positive natural number, or 0 for unknown versions. +-- @return Extra header fields as a list of strings, or nil if not supported. +-- @return Content (data after header). +function worldedit.read_header(value) + if value:find("^[0-9]+[%-:]") then + local header_end = value:find(":", 1, true) + local header = value:sub(1, header_end - 1):split(",") + local version = tonumber(header[1]) + table.remove(header, 1) + local content = value:sub(header_end + 1) + return version, header, content end - if pos1.z > pos2.z then - pos2.z, pos1.z = pos1.z, pos2.z + -- Old versions that didn't include a header with a version number + if value:find("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)") and not value:find("%{") then -- List format + return 3, nil, value + elseif value:find("^[^\"']+%{%d+%}") then + if value:find("%[\"meta\"%]") then -- Meta flat table format + return 2, nil, value + end + return 1, nil, value -- Flat table format + elseif value:find("%{") then -- Raw nested table format + return 4, nil, value end - return pos1, pos2 + return nil end ---converts the region defined by positions `pos1` and `pos2` into a single string, returning the serialized data and the number of nodes serialized -worldedit.serialize = function(pos1, pos2) - local pos1, pos2 = worldedit.sort_pos(pos1, pos2) + +--- Converts the region defined by positions `pos1` and `pos2` +-- into a single string. +-- @return The serialized data. +-- @return The number of nodes serialized. +function worldedit.serialize(pos1, pos2) + pos1, pos2 = worldedit.sort_pos(pos1, pos2) + + worldedit.keep_loaded(pos1, pos2) + + local get_node, get_meta, hash_node_position = + minetest.get_node, minetest.get_meta, minetest.hash_node_position + + -- Find the positions which have metadata + local has_meta = {} + local meta_positions = minetest.find_nodes_with_meta(pos1, pos2) + for i = 1, #meta_positions do + has_meta[hash_node_position(meta_positions[i])] = true + end + local pos = {x=pos1.x, y=0, z=0} local count = 0 local result = {} - local env = minetest.env while pos.x <= pos2.x do pos.y = pos1.y while pos.y <= pos2.y do pos.z = pos1.z while pos.z <= pos2.z do - local node = env:get_node(pos) + local node = get_node(pos) if node.name ~= "air" and node.name ~= "ignore" then count = count + 1 - result[count] = pos.x - pos1.x .. " " .. pos.y - pos1.y .. " " .. pos.z - pos1.z .. " " .. node.name .. " " .. node.param1 .. " " .. node.param2 + + local meta + if has_meta[hash_node_position(pos)] then + meta = get_meta(pos):to_table() + + -- Convert metadata item stacks to item strings + for _, invlist in pairs(meta.inventory) do + for index = 1, #invlist do + local itemstack = invlist[index] + if itemstack.to_string then + invlist[index] = itemstack:to_string() + end + end + end + end + + result[count] = { + x = pos.x - pos1.x, + y = pos.y - pos1.y, + z = pos.z - pos1.z, + name = node.name, + param1 = node.param1 ~= 0 and node.param1 or nil, + param2 = node.param2 ~= 0 and node.param2 or nil, + meta = meta, + } end pos.z = pos.z + 1 end @@ -41,206 +109,140 @@ worldedit.serialize = function(pos1, pos2) end pos.x = pos.x + 1 end - result = table.concat(result, "\n") --join all node entries into single string - return result, count + -- Serialize entries + result = minetest.serialize(result) + return LATEST_SERIALIZATION_HEADER .. result, count end ---determines the volume the nodes represented by string `value` would occupy if deserialized at `originpos`, returning the two corner positions and the number of nodes -worldedit.allocate = function(originpos, value) - local huge = math.huge - local pos1 = {x=huge, y=huge, z=huge} - local pos2 = {x=-huge, y=-huge, z=-huge} - local originx, originy, originz = originpos.x, originpos.y, originpos.z - local count = 0 - for x, y, z, name, param1, param2 in value:gmatch("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do --match node entries - x, y, z = originx + tonumber(x), originy + tonumber(y), originz + tonumber(z) - if x < pos1.x then - pos1.x = x - end - if y < pos1.y then - pos1.y = y - end - if z < pos1.z then - pos1.z = z - end - if x > pos2.x then - pos2.x = x + +--- Loads the schematic in `value` into a node list in the latest format. +-- Contains code based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile) +-- by ChillCode, available under the MIT license. +-- @return A node list in the latest format, or nil on failure. +local function load_schematic(value) + local version, header, content = worldedit.read_header(value) + local nodes = {} + if version == 1 or version == 2 then -- Original flat table format + local tables = minetest.deserialize(content) + if not tables then return nil end + + -- Transform the node table into an array of nodes + for i = 1, #tables do + for j, v in pairs(tables[i]) do + if type(v) == "table" then + tables[i][j] = tables[v[1]] + end + end end - if y > pos2.y then - pos2.y = y + nodes = tables[1] + + if version == 1 then --original flat table format + for i, entry in ipairs(nodes) do + local pos = entry[1] + entry.x, entry.y, entry.z = pos.x, pos.y, pos.z + entry[1] = nil + local node = entry[2] + entry.name, entry.param1, entry.param2 = node.name, node.param1, node.param2 + entry[2] = nil + end end - if z > pos2.z then - pos2.z = z + elseif version == 3 then -- List format + for x, y, z, name, param1, param2 in content:gmatch( + "([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+" .. + "([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do + param1, param2 = tonumber(param1), tonumber(param2) + table.insert(nodes, { + x = tonumber(x), + y = tonumber(y), + z = tonumber(z), + name = name, + param1 = param1 ~= 0 and param1 or nil, + param2 = param2 ~= 0 and param2 or nil, + }) + end + elseif version == 4 or version == 5 then -- Nested table format + if not jit then + -- This is broken for larger tables in the current version of LuaJIT + nodes = minetest.deserialize(content) + else + -- XXX: This is a filthy hack that works surprisingly well - in LuaJIT, `minetest.deserialize` will fail due to the register limit + nodes = {} + content = content:gsub("return%s*{", "", 1):gsub("}%s*$", "", 1) -- remove the starting and ending values to leave only the node data + local escaped = content:gsub("\\\\", "@@"):gsub("\\\"", "@@"):gsub("(\"[^\"]*\")", function(s) return string.rep("@", #s) end) + local startpos, startpos1, endpos = 1, 1 + while true do -- go through each individual node entry (except the last) + startpos, endpos = escaped:find("},%s*{", startpos) + if not startpos then + break + end + local current = content:sub(startpos1, startpos) + local entry = minetest.deserialize("return " .. current) + table.insert(nodes, entry) + startpos, startpos1 = endpos, endpos + end + local entry = minetest.deserialize("return " .. content:sub(startpos1)) -- process the last entry + table.insert(nodes, entry) end - count = count + 1 + else + return nil end - return pos1, pos2, count + return nodes end ---loads the nodes represented by string `value` at position `originpos`, returning the number of nodes deserialized -worldedit.deserialize = function(originpos, value) - local pos = {x=0, y=0, z=0} - local node = {name="", param1=0, param2=0} - local originx, originy, originz = originpos.x, originpos.y, originpos.z - local count = 0 - local env = minetest.env - for x, y, z, name, param1, param2 in value:gmatch("([+-]?%d+)%s+([+-]?%d+)%s+([+-]?%d+)%s+([^%s]+)%s+(%d+)%s+(%d+)[^\r\n]*[\r\n]*") do --match node entries - pos.x = originx + tonumber(x) - pos.y = originy + tonumber(y) - pos.z = originz + tonumber(z) - node.name = name - node.param1 = param1 - node.param2 = param2 - env:add_node(pos, node) - count = count + 1 - end - return count +--- Determines the volume the nodes represented by string `value` would occupy +-- if deserialized at `origin_pos`. +-- @return Low corner position. +-- @return High corner position. +-- @return The number of nodes. +function worldedit.allocate(origin_pos, value) + local nodes = load_schematic(value) + if not nodes then return nil end + return worldedit.allocate_with_nodes(origin_pos, nodes) end ---determines the volume the nodes represented by string `value` would occupy if deserialized at `originpos`, returning the two corner positions and the number of nodes ---based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile) by ChillCode, available under the MIT license (GPL compatible) -worldedit.allocate_old = function(originpos, value) - --obtain the node table - local count = 0 - local get_tables = loadstring(value) - if get_tables == nil then --error loading value - return count - end - local tables = get_tables() - - --transform the node table into an array of nodes - for i = 1, #tables do - for j, v in pairs(tables[i]) do - if type(v) == "table" then - tables[i][j] = tables[v[1]] - end - end - end +-- Internal +function worldedit.allocate_with_nodes(origin_pos, nodes) local huge = math.huge - local pos1 = {x=huge, y=huge, z=huge} - local pos2 = {x=-huge, y=-huge, z=-huge} - local originx, originy, originz = originpos.x, originpos.y, originpos.z - - --load the node array - for i, v in ipairs(tables[1]) do - local pos = v[1] - local x, y, z = originx - pos.x, originy - pos.y, originz - pos.z - if x < pos1.x then - pos1.x = x - end - if y < pos1.y then - pos1.y = y - end - if z < pos1.z then - pos1.z = z - end - if x > pos2.x then - pos2.x = x - end - if y > pos2.y then - pos2.y = y - end - if z > pos2.z then - pos2.z = z - end - count = count + 1 + local pos1x, pos1y, pos1z = huge, huge, huge + local pos2x, pos2y, pos2z = -huge, -huge, -huge + local origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z + for i, entry in ipairs(nodes) do + local x, y, z = origin_x + entry.x, origin_y + entry.y, origin_z + entry.z + if x < pos1x then pos1x = x end + if y < pos1y then pos1y = y end + if z < pos1z then pos1z = z end + if x > pos2x then pos2x = x end + if y > pos2y then pos2y = y end + if z > pos2z then pos2z = z end end - return pos1, pos2, count + local pos1 = {x=pos1x, y=pos1y, z=pos1z} + local pos2 = {x=pos2x, y=pos2y, z=pos2z} + return pos1, pos2, #nodes end ---loads the nodes represented by string `value` at position `originpos`, returning the number of nodes deserialized ---based on [table.save/table.load](http://lua-users.org/wiki/SaveTableToFile) by ChillCode, available under the MIT license (GPL compatible) -worldedit.deserialize_old = function(originpos, value) - --obtain the node table - local count = 0 - local get_tables = loadstring(value) - if get_tables == nil then --error loading value - return count - end - local tables = get_tables() - --transform the node table into an array of nodes - for i = 1, #tables do - for j, v in pairs(tables[i]) do - if type(v) == "table" then - tables[i][j] = tables[v[1]] - end - end - end +--- Loads the nodes represented by string `value` at position `origin_pos`. +-- @return The number of nodes deserialized. +function worldedit.deserialize(origin_pos, value) + local nodes = load_schematic(value) + if not nodes then return nil end + if #nodes == 0 then return #nodes end - --load the node array - local env = minetest.env - local originx, originy, originz = originpos.x, originpos.y, originpos.z - for i, v in ipairs(tables[1]) do - local pos = v[1] - pos.x, pos.y, pos.z = originx - pos.x, originy - pos.y, originz - pos.z - env:add_node(pos, v[2]) - count = count + 1 - end - return count -end + local pos1, pos2 = worldedit.allocate_with_nodes(origin_pos, nodes) + worldedit.keep_loaded(pos1, pos2) ---saves the nodes and meta defined by positions `pos1` and `pos2` into a file, returning the number of nodes saved -worldedit.metasave = function(pos1, pos2, file) --wip: simply work with strings instead of doing IO - local path = minetest.get_worldpath() .. "/schems" - local filename = path .. "/" .. file .. ".wem" - os.execute("mkdir \"" .. path .. "\"") --create directory if it does not already exist - local rows = {} - local pos1, pos2 = worldedit.sort_pos(pos1, pos2) - local pos = {x=pos1.x, y=0, z=0} + local origin_x, origin_y, origin_z = origin_pos.x, origin_pos.y, origin_pos.z local count = 0 - local result = {} - local env = minetest.env - while pos.x <= pos2.x do - pos.y = pos1.y - while pos.y <= pos2.y do - pos.z = pos1.z - while pos.z <= pos2.z do - local node = env:get_node(pos) - if node.name ~= "air" and node.name ~= "ignore" then - count = count + 1 - local row = { - x = pos.x-pos1.x, - y = pos.y-pos1.y, - z = pos.z-pos1.z, - name = node.name, - param1 = node.param1, - param2 = node.param2, - meta = env:get_meta(pos):to_table(), - } - table.insert(rows, row) - end - pos.z = pos.z + 1 - end - pos.y = pos.y + 1 + local add_node, get_meta = minetest.add_node, minetest.get_meta + for i, entry in ipairs(nodes) do + entry.x, entry.y, entry.z = origin_x + entry.x, origin_y + entry.y, origin_z + entry.z + -- Entry acts as both position and node + add_node(entry, entry) + if entry.meta then + get_meta(entry):from_table(entry.meta) end - pos.x = pos.x + 1 end - local err = table.save(rows,filename) - if err then return _,err end - return count + return #nodes end ---loads the nodes and meta from `file` to position `pos1`, returning the number of nodes loaded -worldedit.metaload = function(pos1, file) --wip: simply work with strings instead of doing IO - local filename = minetest.get_worldpath() .. "/schems/" .. file .. ".wem" - local rows, err = table.load(filename) - if err then return _,err end - local pos = {x=0, y=0, z=0} - local node = {name="", param1=0, param2=0} - local count = 0 - local env = minetest.env - for i,row in pairs(rows) do - pos.x = pos1.x + tonumber(row.x) - pos.y = pos1.y + tonumber(row.y) - pos.z = pos1.z + tonumber(row.z) - node.name = row.name - node.param1 = row.param1 - node.param2 = row.param2 - env:add_node(pos, node) - env:get_meta(pos):from_table(row.meta) - count = count + 1 - end - return count -end \ No newline at end of file