diff --git a/worldeditadditions_core/init.lua b/worldeditadditions_core/init.lua index c28c79e..bad30be 100644 --- a/worldeditadditions_core/init.lua +++ b/worldeditadditions_core/init.lua @@ -9,6 +9,7 @@ local modpath = minetest.get_modpath("worldeditadditions_core") worldeditadditions_core = { + version = "1.15-dev", modpath = modpath, registered_commands = {}, -- Storage for per-player node limits before safe_region kicks in. diff --git a/worldeditadditions_core/utils/io/StagedVoxelRegion.lua b/worldeditadditions_core/utils/io/StagedVoxelRegion.lua index effb748..4455ef7 100644 --- a/worldeditadditions_core/utils/io/StagedVoxelRegion.lua +++ b/worldeditadditions_core/utils/io/StagedVoxelRegion.lua @@ -1,7 +1,7 @@ local weac = worldeditadditions_core local weaschem = weac.parse.file.weaschem - +local voxeltools = dofile(weac.modpath.."utils/io/voxeltools.lua") --- A region of the world that is to be or has been saved to/from disk. -- This class exists to make moving things to/from disk easier and less complicated. @@ -38,13 +38,15 @@ end -- To save data, you probably want to call the save() method. -- @param pos1 Vector3 The position in WORLD SPACE of pos1 of the defined region to stage for saving. -- @param pos2 Vector3 The position in WORLD SPACE of pos2 of the defined region to stage for saving. +-- @param offset Vector3 Apply this offset before placing in the world. e.g. if you have a schematic of a tree, and want it to place centred on the base thereof. -- @param voxelmanip VoxelManipulator The voxel manipulator to take data from and save to disk. -- @returns bool,StagedVoxelRegion A success boolean, followed by the new StagedVoxelRegion instance. -function StagedVoxelRegion.NewFromVoxelManip(pos1, pos2, voxelmanip) - +function StagedVoxelRegion.NewFromVoxelManip(pos1, pos2, offset, voxelmanip) + local data, param2 = voxeltools.voxelmanip2raw(voxelmanip, pos1, pos2) + return StagedVoxelRegion.NewFromRaw(pos1, pos2, offset, data, param2) end ---- Creates a new StagedVoxelRegion from the given VoxelManipulator data. +-- Creates a new StagedVoxelRegion from the given VoxelManipulator data. -- To save data, you probably want to call the save() method. -- @param pos1 Vector3 The position in WORLD SPACE of pos1 of the defined region to stage for saving. -- @param pos2 Vector3 The position in WORLD SPACE of pos2 of the defined region to stage for saving. @@ -52,23 +54,25 @@ end -- @param data number[] A table of numbers representing the node ids. -- @param param2 number[] A table of numbers representing the param2 data. Should exactly match the data number[] in size. -- @returns bool,StagedVoxelRegion A success boolean, followed by the new StagedVoxelRegion instance. -function StagedVoxelRegion.NewFromTable(pos1, pos2, area, data, param2) +-- function StagedVoxelRegion.NewFromTable(pos1, pos2, area, data, param2) -end +-- end --- Creates a new StagedVoxelRegion from raw data/param2 tables. -- @static --- @param pos1 Vector3 The position in WORLD SPACE of pos1 of the defined region to stage for saving. --- @param pos2 Vector3 The position in WORLD SPACE of pos2 of the defined region to stage for saving. --- @param data number[] A table of numbers representing the node ids. Must be ALREADY TRIMMED, NOT just taken straight from a VoxelManip! --- @param param2 number[] A table of numbers representing the param2 data. Should exactly match the data number[] in size. Must be ALREADY TRIMMED, NOT just taken straight from a VoxelManip! +-- @param pos1 Vector3 The position in WORLD SPACE of pos1 of the defined region to stage for saving. +-- @param pos2 Vector3 The position in WORLD SPACE of pos2 of the defined region to stage for saving. +-- @param offset Vector3 Apply this offset before placing in the world. e.g. if you have a schematic of a tree, and want it to place centred on the base thereof. +-- @param data number[] A table of numbers representing the node ids. Must be ALREADY TRIMMED, NOT just taken straight from a VoxelManip! +-- @param param2 number[] A table of numbers representing the param2 data. Should exactly match the data number[] in size. Must be ALREADY TRIMMED, NOT just taken straight from a VoxelManip! -- @returns bool,StagedVoxelRegion A success boolean, followed by the new StagedVoxelRegion instance. -function StagedVoxelRegion.NewFromRaw(pos1, pos2, data, param2) +function StagedVoxelRegion.NewFromRaw(pos1, pos2, offset, data, param2) return make_instance({ name = "untitled", description = "", pos1 = pos1:clone(), pos2 = pos2:clone(), + offset = offset, tables = { data = data, param2 = param2 @@ -136,8 +140,59 @@ end -- @param filepath string The filepath to save the StagedVoxelRegion to. -- @param format="auto" string The format to save in. Default: automatic, determine from file extension. See worldeditadditions_core.io.FileFormats for more information. -- @returns bool Whether the operation was successful or not. -function StagedVoxelRegion.save(filepath, format) +function StagedVoxelRegion.save(svr, filepath, format) + local handle = io.open(filepath, "w") + if handle == nil then return false, "Failed to open handle to filepath '"..filepath.."'" end + local parts = {} + + --- + -- Magic bytes + --- + table.insert(parts, "WEASCHEM 1\n") + + --- + -- Header + --- + local header = { + name = svr.name, + size = (svr.pos2 - svr.pos1):abs(), + offset = svr.offset, + + type = "full", -- TODO: Add delta support later + generator = "WorldEditAdditions/"..weac.version.." "..minetest.get_version().project.."/"..minetest.get_version().string, + } + if svr.description then header.description = svr.description end + table.insert(parts, minetest.write_json(header, false).."\n") + + --- + -- ID map + --- + local id_map, wid2sid = voxeltools.make_id_maps(svr.tables.data) + table.insert(parts, minetest.write_json(id_map, false).."\n") + + --- + -- Data tables + --- + local data, param2 = weac.table.map(svr.tables.data, function(val) + return wid2sid[data] + end), svr.tables.param2 + + table.insert(parts, table.concat(voxeltools.runlength_encode(data), ",")) + table.insert(parts, "\n") + table.insert(parts, table.concat(voxeltools.runlength_encode(param2), ",")) + table.insert(parts, "\n") + + + --- + -- Writing + --- + + -- TODO: Implement compression here - maybe via minetest.compress(data, method, ...) + local schematic = table.concat(parts, "") + + handle:write(schematic) + handle:close() end --- Loads a file of the an array. diff --git a/worldeditadditions_core/utils/io/voxeltools.lua b/worldeditadditions_core/utils/io/voxeltools.lua index d630e17..e8895fe 100644 --- a/worldeditadditions_core/utils/io/voxeltools.lua +++ b/worldeditadditions_core/utils/io/voxeltools.lua @@ -42,3 +42,80 @@ local function voxelmanip2raw(voxelmanip, pos1, pos2) return result_data, result_param2 end + +--- Makes a node id map for saving to disk based on a given RAW data array of node ids. +-- Also sequentially packs node ids to save space and potentially improve compression ratio, though this is unproven. +-- @param data table The RAW data table to read ids from to build the map. +-- @returns table,table A pair of maps to transform node ids from the world to the schematic file. +-- +-- 1. The sID: number → node_name: string map to be saved in the schematic. sID stands for *schematic* id, which is NOT the same as the node id in the world. +-- 2. The wID → sID node id map. wID = the node id in the current Minetest world, and sID = the *schematic* id as in #1. All world node ids MUST be pushed through this map before being saved to the schematic file. +local function make_id_maps(data) + local map = {} + local id2id = {} + local next_id = 0 + for _, wid in pairs(data) do + if id2id[wid] == nil then + id2id[wid] = next_id + next_id = next_id + 1 + local sid = id2id[wid] + map[sid] = minetest.get_name_from_content_id(wid) + end + end + return map, id2id +end + +--- Encodes a table of numbers (ZERO INDEXED) with run-length encoding. +-- The reason for the existence of this function is avoiding very long strings when serialising / deserialising. Long strings can be a problem in more ways than one. +-- @param tbl number[] The table of numbers to runlength encode. +local function runlength_encode(tbl) + local result = {} + local next = 0 + local prev, count = nil, 0 + for i, val in pairs(tbl) do + if prev ~= val then + local msg = tostring(prev) + if count > 1 then msg = tostring(count).."x"..msg end + result[next] = msg + next = next + 1 + count = 0 + end + prev = val + count = count + 1 + end + local msg_last = tostring(prev) + if count > 1 then msg_last = tostring(count) .. "x" .. msg_last end + result[next] = msg_last + + return result +end + + +local function runlength_decode(tbl) + local unpacked = {} + local next = 0 + + for i, val in pairs(tbl) do + local count, sid = string.match(val, "(%d+)x(%d+)") + if count == nil then + unpacked[next] = tonumber(val) + next = next + 1 + else + sid = tonumber(sid) + for _ = 1, count do + unpacked[next] = sid + next = next + 1 + end + end + end + + return unpacked +end + +return { + voxelmanip2raw, + make_id_maps, + + runlength_encode, + runlength_decode +} \ No newline at end of file diff --git a/worldeditadditions_core/utils/parse/file/init.lua b/worldeditadditions_core/utils/parse/file/init.lua index 31bc0d1..584a202 100644 --- a/worldeditadditions_core/utils/parse/file/init.lua +++ b/worldeditadditions_core/utils/parse/file/init.lua @@ -1,6 +1,7 @@ +local weac = worldeditadditions_core -local localpath = wea_c.modpath.."/utils/parse/file/" +local localpath = weac.modpath.."/utils/parse/file/" --- Parsers specifically for file formats. -- @namespace worldeditadditions_core.parse.file diff --git a/worldeditadditions_core/utils/parse/file/weaschem.lua b/worldeditadditions_core/utils/parse/file/weaschem.lua index 17ba21d..1e2f61b 100644 --- a/worldeditadditions_core/utils/parse/file/weaschem.lua +++ b/worldeditadditions_core/utils/parse/file/weaschem.lua @@ -3,7 +3,7 @@ local Vector3 local parse_json, split if worldeditadditions_core then Vector3 = weac.Vector3 - parse_json = weac.parse.json + parse_json = dofile(weac.modpath.."/utils/parse/json.lua") split = weac.split else Vector3 = require("worldeditadditions_core.utils.vector3")