StagedVoxelRegion: initial save() implementation.

....but it's untested, as usual.
This commit is contained in:
Starbeamrainbowlabs 2023-09-24 02:07:08 +01:00
parent f02b1d0b33
commit 9dd92dbe70
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
5 changed files with 148 additions and 14 deletions

View file

@ -9,6 +9,7 @@
local modpath = minetest.get_modpath("worldeditadditions_core") local modpath = minetest.get_modpath("worldeditadditions_core")
worldeditadditions_core = { worldeditadditions_core = {
version = "1.15-dev",
modpath = modpath, modpath = modpath,
registered_commands = {}, registered_commands = {},
-- Storage for per-player node limits before safe_region kicks in. -- Storage for per-player node limits before safe_region kicks in.

View file

@ -1,7 +1,7 @@
local weac = worldeditadditions_core local weac = worldeditadditions_core
local weaschem = weac.parse.file.weaschem 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. --- 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. -- 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. -- 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 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 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. -- @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. -- @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 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. -- 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 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 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 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. -- @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. -- @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. --- Creates a new StagedVoxelRegion from raw data/param2 tables.
-- @static -- @static
-- @param pos1 Vector3 The position in WORLD SPACE of pos1 of the defined region to stage for saving. -- @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 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 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 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. -- @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({ return make_instance({
name = "untitled", name = "untitled",
description = "", description = "",
pos1 = pos1:clone(), pos1 = pos1:clone(),
pos2 = pos2:clone(), pos2 = pos2:clone(),
offset = offset,
tables = { tables = {
data = data, data = data,
param2 = param2 param2 = param2
@ -136,8 +140,59 @@ end
-- @param filepath string The filepath to save the StagedVoxelRegion to. -- @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. -- @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. -- @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 end
--- Loads a file of the an array. --- Loads a file of the an array.

View file

@ -42,3 +42,80 @@ local function voxelmanip2raw(voxelmanip, pos1, pos2)
return result_data, result_param2 return result_data, result_param2
end 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
}

View file

@ -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. --- Parsers specifically for file formats.
-- @namespace worldeditadditions_core.parse.file -- @namespace worldeditadditions_core.parse.file

View file

@ -3,7 +3,7 @@ local Vector3
local parse_json, split local parse_json, split
if worldeditadditions_core then if worldeditadditions_core then
Vector3 = weac.Vector3 Vector3 = weac.Vector3
parse_json = weac.parse.json parse_json = dofile(weac.modpath.."/utils/parse/json.lua")
split = weac.split split = weac.split
else else
Vector3 = require("worldeditadditions_core.utils.vector3") Vector3 = require("worldeditadditions_core.utils.vector3")