708 lines
27 KiB
C++
708 lines
27 KiB
C++
#include "vf/SaveMigration.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
|
|
#include "vf/Anvil.h"
|
|
#include "vf/Block.h"
|
|
|
|
namespace vf {
|
|
|
|
namespace {
|
|
|
|
std::size_t divideUp(std::size_t value, std::size_t unit) {
|
|
return unit == 0 ? 0 : (value + unit - 1) / unit;
|
|
}
|
|
|
|
void putU32Be(std::vector<uint8_t>& out, uint32_t value) {
|
|
out.push_back(static_cast<uint8_t>((value >> 24) & 0xFFu));
|
|
out.push_back(static_cast<uint8_t>((value >> 16) & 0xFFu));
|
|
out.push_back(static_cast<uint8_t>((value >> 8) & 0xFFu));
|
|
out.push_back(static_cast<uint8_t>(value & 0xFFu));
|
|
}
|
|
|
|
uint32_t readU32Be(const uint8_t* at) {
|
|
return (static_cast<uint32_t>(at[0]) << 24) | (static_cast<uint32_t>(at[1]) << 16) |
|
|
(static_cast<uint32_t>(at[2]) << 8) | static_cast<uint32_t>(at[3]);
|
|
}
|
|
|
|
struct PayloadCursor {
|
|
const uint8_t* data;
|
|
std::size_t size;
|
|
std::size_t at = 0;
|
|
bool bad = false;
|
|
|
|
uint32_t u32() {
|
|
if (bad || at + 4 > size) {
|
|
bad = true;
|
|
return 0;
|
|
}
|
|
uint32_t value = readU32Be(data + at);
|
|
at += 4;
|
|
return value;
|
|
}
|
|
uint64_t u64() {
|
|
uint32_t high = u32();
|
|
uint32_t low = u32();
|
|
return (static_cast<uint64_t>(high) << 32) | static_cast<uint64_t>(low);
|
|
}
|
|
uint8_t u8() {
|
|
if (bad || at + 1 > size) {
|
|
bad = true;
|
|
return 0;
|
|
}
|
|
return data[at++];
|
|
}
|
|
bool take(std::size_t count, std::vector<uint8_t>& out) {
|
|
if (bad || at + count > size) {
|
|
bad = true;
|
|
return false;
|
|
}
|
|
out.assign(data + at, data + at + count);
|
|
at += count;
|
|
return true;
|
|
}
|
|
bool skip(std::size_t count) {
|
|
if (bad || at + count > size) {
|
|
bad = true;
|
|
return false;
|
|
}
|
|
at += count;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
void writeU64Be(std::vector<uint8_t>& out, uint64_t value) {
|
|
putU32Be(out, static_cast<uint32_t>(value >> 32));
|
|
putU32Be(out, static_cast<uint32_t>(value & 0xFFFFFFFFull));
|
|
}
|
|
|
|
void buildPalette(const Chunk& chunk, std::vector<Block>& palette, std::vector<uint32_t>& indices) {
|
|
std::array<int, static_cast<std::size_t>(Block::Count)> slotOf;
|
|
slotOf.fill(-1);
|
|
palette.clear();
|
|
indices.clear();
|
|
indices.reserve(chunkVolume);
|
|
for (Block block : chunk.raw()) {
|
|
int& slot = slotOf[static_cast<std::size_t>(block)];
|
|
if (slot < 0) {
|
|
slot = static_cast<int>(palette.size());
|
|
palette.push_back(block);
|
|
}
|
|
indices.push_back(static_cast<uint32_t>(slot));
|
|
}
|
|
}
|
|
|
|
std::string versionPair(SaveVersion from, SaveVersion to) {
|
|
return std::string(saveVersionName(from)) + " -> " + saveVersionName(to);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const SaveChunkRecord* SaveContainer::find(const Vec3i& coord) const {
|
|
for (const SaveChunkRecord& record : chunks)
|
|
if (record.coord == coord) return &record;
|
|
return nullptr;
|
|
}
|
|
|
|
bool saveReadU32At(const uint8_t* data, std::size_t size, std::size_t at, uint32_t& out) {
|
|
if (at + 4 > size) return false;
|
|
out = readU32Be(data + at);
|
|
return true;
|
|
}
|
|
|
|
bool saveWriteU32At(uint8_t* data, std::size_t size, std::size_t at, uint32_t value) {
|
|
if (at + 4 > size) return false;
|
|
data[at + 0] = static_cast<uint8_t>((value >> 24) & 0xFFu);
|
|
data[at + 1] = static_cast<uint8_t>((value >> 16) & 0xFFu);
|
|
data[at + 2] = static_cast<uint8_t>((value >> 8) & 0xFFu);
|
|
data[at + 3] = static_cast<uint8_t>(value & 0xFFu);
|
|
return true;
|
|
}
|
|
|
|
bool isKnownSaveVersion(uint32_t version) { return version >= 1u && version <= 4u; }
|
|
|
|
bool versionHasPalette(SaveVersion version) { return version >= SaveVersion::V2; }
|
|
bool versionHasLight(SaveVersion version) { return version >= SaveVersion::V3; }
|
|
bool versionHasChecksum(SaveVersion version) { return version >= SaveVersion::V3; }
|
|
bool versionHasMetadata(SaveVersion version) { return version >= SaveVersion::V4; }
|
|
|
|
const char* saveVersionName(SaveVersion version) {
|
|
switch (version) {
|
|
case SaveVersion::V1: return "v1";
|
|
case SaveVersion::V2: return "v2";
|
|
case SaveVersion::V3: return "v3";
|
|
case SaveVersion::V4: return "v4";
|
|
}
|
|
return "v?";
|
|
}
|
|
|
|
bool readSaveHeader(const uint8_t* data, std::size_t size, SaveHeader& out) {
|
|
if (size < saveHeaderBytes) return false;
|
|
out.magic = readU32Be(data + 0);
|
|
out.version = readU32Be(data + 4);
|
|
out.regionX = static_cast<int32_t>(readU32Be(data + 8));
|
|
out.regionZ = static_cast<int32_t>(readU32Be(data + 12));
|
|
out.chunkCount = readU32Be(data + 16);
|
|
out.sectorBytes = readU32Be(data + 20);
|
|
out.tableChecksum = readU32Be(data + 24);
|
|
return true;
|
|
}
|
|
|
|
std::size_t saveTableBytes(const SaveHeader& header) {
|
|
return static_cast<std::size_t>(header.chunkCount) * saveEntryBytes;
|
|
}
|
|
|
|
std::size_t saveHeaderSectors(const SaveHeader& header) {
|
|
if (header.sectorBytes == 0) return 0;
|
|
return divideUp(saveHeaderBytes + saveTableBytes(header), header.sectorBytes);
|
|
}
|
|
|
|
std::size_t saveDataStart(const SaveHeader& header) {
|
|
return saveHeaderSectors(header) * static_cast<std::size_t>(header.sectorBytes);
|
|
}
|
|
|
|
bool readSaveTable(const uint8_t* data, std::size_t size, const SaveHeader& header,
|
|
std::vector<SaveTableEntry>& out) {
|
|
out.clear();
|
|
for (uint32_t i = 0; i < header.chunkCount; ++i) {
|
|
std::size_t at = saveHeaderBytes + static_cast<std::size_t>(i) * saveEntryBytes;
|
|
if (at + saveEntryBytes > size) return false;
|
|
SaveTableEntry entry;
|
|
entry.coord.x = static_cast<int32_t>(readU32Be(data + at + 0));
|
|
entry.coord.y = static_cast<int32_t>(readU32Be(data + at + 4));
|
|
entry.coord.z = static_cast<int32_t>(readU32Be(data + at + 8));
|
|
entry.sectorOffset = readU32Be(data + at + 12);
|
|
entry.sectorCount = readU32Be(data + at + 16);
|
|
entry.payloadBytes = readU32Be(data + at + 20);
|
|
out.push_back(entry);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<uint8_t> defaultChunkLight(const Chunk& chunk, uint8_t skyLevel) {
|
|
uint8_t sky = skyLevel > 15 ? 15 : skyLevel;
|
|
std::vector<uint8_t> plane(chunkVolume, 0);
|
|
const std::array<Block, chunkVolume>& blocks = chunk.raw();
|
|
for (std::size_t i = 0; i < blocks.size(); ++i) {
|
|
uint8_t skyHere = occludesLight(blocks[i]) ? 0u : sky;
|
|
uint8_t emitted = static_cast<uint8_t>(blockLightEmission(blocks[i]));
|
|
plane[i] = static_cast<uint8_t>((skyHere << 4) | (emitted & 0x0Fu));
|
|
}
|
|
return plane;
|
|
}
|
|
|
|
bool chunkIsAllAir(const Chunk& chunk) {
|
|
for (Block block : chunk.raw())
|
|
if (block != Block::Air) return false;
|
|
return true;
|
|
}
|
|
|
|
ByteBuffer encodeChunkPayload(SaveVersion version, const SaveChunkRecord& record,
|
|
bool compressBody) {
|
|
ByteBuffer out;
|
|
if (version == SaveVersion::V1) {
|
|
putU32Be(out, static_cast<uint32_t>(chunkVolume));
|
|
for (Block block : record.blocks.raw()) out.push_back(static_cast<uint8_t>(block));
|
|
return out;
|
|
}
|
|
|
|
std::vector<Block> palette;
|
|
std::vector<uint32_t> indices;
|
|
buildPalette(record.blocks, palette, indices);
|
|
|
|
if (version == SaveVersion::V2 || version == SaveVersion::V3) {
|
|
putU32Be(out, static_cast<uint32_t>(palette.size()));
|
|
for (Block block : palette) out.push_back(static_cast<uint8_t>(block));
|
|
putU32Be(out, static_cast<uint32_t>(indices.size()));
|
|
for (uint32_t index : indices) out.push_back(static_cast<uint8_t>(index));
|
|
if (version == SaveVersion::V3) {
|
|
putU32Be(out, static_cast<uint32_t>(record.light.size()));
|
|
out.insert(out.end(), record.light.begin(), record.light.end());
|
|
putU32Be(out, fnv1a32(out.data(), out.size()));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
int bitWidth = BitPacker::bitWidthForPalette(palette.size());
|
|
std::vector<uint64_t> words = BitPacker::pack(indices, bitWidth);
|
|
|
|
ByteBuffer body;
|
|
putU32Be(body, static_cast<uint32_t>(palette.size()));
|
|
for (Block block : palette) body.push_back(static_cast<uint8_t>(block));
|
|
putU32Be(body, static_cast<uint32_t>(bitWidth));
|
|
putU32Be(body, static_cast<uint32_t>(words.size()));
|
|
for (uint64_t word : words) writeU64Be(body, word);
|
|
putU32Be(body, static_cast<uint32_t>(record.light.size()));
|
|
body.insert(body.end(), record.light.begin(), record.light.end());
|
|
writeU64Be(body, record.meta.inhabitedTicks);
|
|
writeU64Be(body, record.meta.lastSavedTick);
|
|
body.push_back(record.meta.generationStage);
|
|
|
|
uint32_t bodyChecksum = fnv1a32(body.data(), body.size());
|
|
ByteBuffer stored = compressBody ? compress(body) : body;
|
|
|
|
uint32_t flags = 0;
|
|
if (record.empty || chunkIsAllAir(record.blocks)) flags |= saveRecordEmpty;
|
|
if (compressBody) flags |= saveRecordCompressed;
|
|
|
|
putU32Be(out, flags);
|
|
putU32Be(out, static_cast<uint32_t>(stored.size()));
|
|
out.insert(out.end(), stored.begin(), stored.end());
|
|
putU32Be(out, bodyChecksum);
|
|
return out;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool decodeBodyV4(const uint8_t* body, std::size_t bodySize, SaveChunkRecord& out,
|
|
SavePayloadInfo* info) {
|
|
PayloadCursor cursor{body, bodySize};
|
|
uint32_t paletteCount = cursor.u32();
|
|
if (cursor.bad || paletteCount == 0 || paletteCount > 4096u) return false;
|
|
std::vector<uint8_t> paletteBytes;
|
|
if (!cursor.take(paletteCount, paletteBytes)) return false;
|
|
|
|
uint32_t bitWidth = cursor.u32();
|
|
uint32_t wordCount = cursor.u32();
|
|
if (cursor.bad || bitWidth == 0 || bitWidth > 32u) return false;
|
|
int perWord = 64 / static_cast<int>(bitWidth);
|
|
std::size_t neededWords = divideUp(chunkVolume, static_cast<std::size_t>(perWord));
|
|
if (wordCount < neededWords) return false;
|
|
if (wordCount > neededWords + 64u) return false;
|
|
|
|
std::vector<uint64_t> words;
|
|
words.reserve(wordCount);
|
|
for (uint32_t i = 0; i < wordCount; ++i) words.push_back(cursor.u64());
|
|
if (cursor.bad) return false;
|
|
|
|
uint32_t lightBytes = cursor.u32();
|
|
if (cursor.bad || lightBytes > static_cast<uint32_t>(chunkVolume)) return false;
|
|
if (!cursor.take(lightBytes, out.light)) return false;
|
|
|
|
out.meta.inhabitedTicks = cursor.u64();
|
|
out.meta.lastSavedTick = cursor.u64();
|
|
out.meta.generationStage = cursor.u8();
|
|
if (cursor.bad) return false;
|
|
|
|
std::vector<uint32_t> indices =
|
|
BitPacker::unpack(words, static_cast<int>(bitWidth), chunkVolume);
|
|
uint32_t maxIndex = 0;
|
|
std::array<Block, chunkVolume>& blocks = out.blocks.raw();
|
|
for (std::size_t i = 0; i < blocks.size(); ++i) {
|
|
uint32_t index = indices[i];
|
|
if (index > maxIndex) maxIndex = index;
|
|
blocks[i] = index < paletteCount ? static_cast<Block>(paletteBytes[index]) : Block::Air;
|
|
}
|
|
if (info) {
|
|
info->paletteCount = paletteCount;
|
|
info->maxIndex = maxIndex;
|
|
info->bitWidth = bitWidth;
|
|
info->lightBytes = lightBytes;
|
|
info->rawBodyBytes = bodySize;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool decodeChunkPayload(SaveVersion version, const uint8_t* data, std::size_t size,
|
|
SaveChunkRecord& out, SavePayloadInfo* info) {
|
|
if (info) *info = SavePayloadInfo{};
|
|
out.light.clear();
|
|
out.empty = false;
|
|
out.meta = SaveChunkMeta{};
|
|
|
|
if (version == SaveVersion::V1) {
|
|
PayloadCursor cursor{data, size};
|
|
uint32_t blockBytes = cursor.u32();
|
|
if (cursor.bad || blockBytes != static_cast<uint32_t>(chunkVolume)) return false;
|
|
std::vector<uint8_t> raw;
|
|
if (!cursor.take(blockBytes, raw)) return false;
|
|
std::array<Block, chunkVolume>& blocks = out.blocks.raw();
|
|
for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = static_cast<Block>(raw[i]);
|
|
out.empty = chunkIsAllAir(out.blocks);
|
|
if (info) info->rawBodyBytes = size;
|
|
return true;
|
|
}
|
|
|
|
if (version == SaveVersion::V2 || version == SaveVersion::V3) {
|
|
PayloadCursor cursor{data, size};
|
|
uint32_t paletteCount = cursor.u32();
|
|
if (cursor.bad || paletteCount == 0 || paletteCount > 4096u) return false;
|
|
std::vector<uint8_t> paletteBytes;
|
|
if (!cursor.take(paletteCount, paletteBytes)) return false;
|
|
uint32_t indexBytes = cursor.u32();
|
|
if (cursor.bad || indexBytes != static_cast<uint32_t>(chunkVolume)) return false;
|
|
std::vector<uint8_t> indices;
|
|
if (!cursor.take(indexBytes, indices)) return false;
|
|
|
|
uint32_t maxIndex = 0;
|
|
std::array<Block, chunkVolume>& blocks = out.blocks.raw();
|
|
for (std::size_t i = 0; i < blocks.size(); ++i) {
|
|
uint32_t index = indices[i];
|
|
if (index > maxIndex) maxIndex = index;
|
|
blocks[i] = index < paletteCount ? static_cast<Block>(paletteBytes[index]) : Block::Air;
|
|
}
|
|
uint32_t lightBytes = 0;
|
|
if (version == SaveVersion::V3) {
|
|
lightBytes = cursor.u32();
|
|
if (cursor.bad || lightBytes > static_cast<uint32_t>(chunkVolume)) return false;
|
|
if (!cursor.take(lightBytes, out.light)) return false;
|
|
if (!cursor.skip(4)) return false;
|
|
}
|
|
out.empty = chunkIsAllAir(out.blocks);
|
|
if (info) {
|
|
info->paletteCount = paletteCount;
|
|
info->maxIndex = maxIndex;
|
|
info->bitWidth = 8;
|
|
info->lightBytes = lightBytes;
|
|
info->rawBodyBytes = size;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
PayloadCursor cursor{data, size};
|
|
uint32_t flags = cursor.u32();
|
|
uint32_t storedBytes = cursor.u32();
|
|
if (cursor.bad || storedBytes > size) return false;
|
|
std::vector<uint8_t> stored;
|
|
if (!cursor.take(storedBytes, stored)) return false;
|
|
if (!cursor.skip(4)) return false;
|
|
|
|
ByteBuffer body;
|
|
if ((flags & saveRecordCompressed) != 0) {
|
|
if (!decompress(stored.data(), stored.size(), body)) return false;
|
|
} else {
|
|
body = std::move(stored);
|
|
}
|
|
if (!decodeBodyV4(body.data(), body.size(), out, info)) return false;
|
|
out.empty = (flags & saveRecordEmpty) != 0;
|
|
if (info) {
|
|
info->compressed = (flags & saveRecordCompressed) != 0;
|
|
info->empty = out.empty;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool verifyChunkPayloadChecksum(SaveVersion version, const uint8_t* data, std::size_t size,
|
|
uint32_t& stored, uint32_t& computed) {
|
|
if (!versionHasChecksum(version)) return false;
|
|
if (version == SaveVersion::V3) {
|
|
if (size < 4) return false;
|
|
stored = readU32Be(data + size - 4);
|
|
computed = fnv1a32(data, size - 4);
|
|
return true;
|
|
}
|
|
if (size < 12) return false;
|
|
uint32_t flags = readU32Be(data + 0);
|
|
uint32_t storedBytes = readU32Be(data + 4);
|
|
if (static_cast<std::size_t>(storedBytes) + 12 > size) return false;
|
|
stored = readU32Be(data + 8 + storedBytes);
|
|
if ((flags & saveRecordCompressed) != 0) {
|
|
ByteBuffer body;
|
|
if (!decompress(data + 8, storedBytes, body)) return false;
|
|
computed = fnv1a32(body.data(), body.size());
|
|
} else {
|
|
computed = fnv1a32(data + 8, storedBytes);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool saveCoordLess(const Vec3i& a, const Vec3i& b) {
|
|
if (a.x != b.x) return a.x < b.x;
|
|
if (a.y != b.y) return a.y < b.y;
|
|
return a.z < b.z;
|
|
}
|
|
|
|
ByteBuffer packSaveContainer(SaveVersion version, int regionX, int regionZ, uint32_t sectorBytes,
|
|
const std::vector<Vec3i>& coords,
|
|
const std::vector<ByteBuffer>& payloads,
|
|
std::vector<uint32_t>* sectorOffsets) {
|
|
uint32_t sectors = sectorBytes == 0 ? saveSectorBytes : sectorBytes;
|
|
|
|
SaveHeader header;
|
|
header.magic = saveMagic;
|
|
header.version = static_cast<uint32_t>(version);
|
|
header.regionX = regionX;
|
|
header.regionZ = regionZ;
|
|
header.chunkCount = static_cast<uint32_t>(coords.size());
|
|
header.sectorBytes = sectors;
|
|
|
|
std::size_t sectorCursor = saveHeaderSectors(header);
|
|
if (sectorOffsets) sectorOffsets->clear();
|
|
|
|
ByteBuffer table;
|
|
table.reserve(coords.size() * saveEntryBytes);
|
|
std::vector<std::size_t> starts;
|
|
starts.reserve(coords.size());
|
|
for (std::size_t i = 0; i < coords.size(); ++i) {
|
|
uint32_t sectorCount = static_cast<uint32_t>(divideUp(payloads[i].size(), sectors));
|
|
putU32Be(table, static_cast<uint32_t>(coords[i].x));
|
|
putU32Be(table, static_cast<uint32_t>(coords[i].y));
|
|
putU32Be(table, static_cast<uint32_t>(coords[i].z));
|
|
putU32Be(table, static_cast<uint32_t>(sectorCursor));
|
|
putU32Be(table, sectorCount);
|
|
putU32Be(table, static_cast<uint32_t>(payloads[i].size()));
|
|
if (sectorOffsets) sectorOffsets->push_back(static_cast<uint32_t>(sectorCursor));
|
|
starts.push_back(sectorCursor * static_cast<std::size_t>(sectors));
|
|
sectorCursor += sectorCount;
|
|
}
|
|
|
|
header.tableChecksum =
|
|
versionHasChecksum(version) ? fnv1a32(table.data(), table.size()) : 0u;
|
|
|
|
ByteBuffer out(sectorCursor * static_cast<std::size_t>(sectors), 0);
|
|
saveWriteU32At(out.data(), out.size(), 0, header.magic);
|
|
saveWriteU32At(out.data(), out.size(), 4, header.version);
|
|
saveWriteU32At(out.data(), out.size(), 8, static_cast<uint32_t>(header.regionX));
|
|
saveWriteU32At(out.data(), out.size(), 12, static_cast<uint32_t>(header.regionZ));
|
|
saveWriteU32At(out.data(), out.size(), 16, header.chunkCount);
|
|
saveWriteU32At(out.data(), out.size(), 20, header.sectorBytes);
|
|
saveWriteU32At(out.data(), out.size(), 24, header.tableChecksum);
|
|
std::copy(table.begin(), table.end(), out.begin() + static_cast<std::ptrdiff_t>(saveHeaderBytes));
|
|
|
|
for (std::size_t i = 0; i < payloads.size(); ++i)
|
|
std::copy(payloads[i].begin(), payloads[i].end(),
|
|
out.begin() + static_cast<std::ptrdiff_t>(starts[i]));
|
|
return out;
|
|
}
|
|
|
|
ByteBuffer writeSaveContainer(const SaveContainer& container) {
|
|
std::vector<const SaveChunkRecord*> ordered;
|
|
ordered.reserve(container.chunks.size());
|
|
for (const SaveChunkRecord& record : container.chunks) ordered.push_back(&record);
|
|
std::sort(ordered.begin(), ordered.end(),
|
|
[](const SaveChunkRecord* a, const SaveChunkRecord* b) {
|
|
return saveCoordLess(a->coord, b->coord);
|
|
});
|
|
|
|
bool compressPayloads = container.compressPayloads && container.version == SaveVersion::V4;
|
|
std::vector<Vec3i> coords;
|
|
std::vector<ByteBuffer> payloads;
|
|
coords.reserve(ordered.size());
|
|
payloads.reserve(ordered.size());
|
|
for (const SaveChunkRecord* record : ordered) {
|
|
coords.push_back(record->coord);
|
|
payloads.push_back(encodeChunkPayload(container.version, *record, compressPayloads));
|
|
}
|
|
return packSaveContainer(container.version, container.regionX, container.regionZ,
|
|
container.sectorBytes, coords, payloads, nullptr);
|
|
}
|
|
|
|
bool readSaveContainer(const uint8_t* data, std::size_t size, SaveContainer& out) {
|
|
SaveHeader header;
|
|
if (!readSaveHeader(data, size, header)) return false;
|
|
if (header.magic != saveMagic) return false;
|
|
if (!isKnownSaveVersion(header.version)) return false;
|
|
if (header.sectorBytes == 0) return false;
|
|
|
|
std::vector<SaveTableEntry> entries;
|
|
if (!readSaveTable(data, size, header, entries)) return false;
|
|
|
|
out.version = static_cast<SaveVersion>(header.version);
|
|
out.regionX = header.regionX;
|
|
out.regionZ = header.regionZ;
|
|
out.sectorBytes = header.sectorBytes;
|
|
out.compressPayloads = false;
|
|
out.chunks.clear();
|
|
out.chunks.reserve(entries.size());
|
|
|
|
for (const SaveTableEntry& entry : entries) {
|
|
std::size_t at = static_cast<std::size_t>(entry.sectorOffset) * header.sectorBytes;
|
|
if (at + entry.payloadBytes > size) return false;
|
|
SaveChunkRecord record;
|
|
SavePayloadInfo info;
|
|
record.coord = entry.coord;
|
|
if (!decodeChunkPayload(out.version, data + at, entry.payloadBytes, record, &info))
|
|
return false;
|
|
if (info.compressed) out.compressPayloads = true;
|
|
out.chunks.push_back(std::move(record));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool readChunkPayloadBytes(const uint8_t* data, std::size_t size, const Vec3i& coord,
|
|
ByteBuffer& out) {
|
|
SaveHeader header;
|
|
if (!readSaveHeader(data, size, header)) return false;
|
|
if (header.magic != saveMagic || header.sectorBytes == 0) return false;
|
|
std::vector<SaveTableEntry> entries;
|
|
if (!readSaveTable(data, size, header, entries)) return false;
|
|
for (const SaveTableEntry& entry : entries) {
|
|
if (!(entry.coord == coord)) continue;
|
|
std::size_t at = static_cast<std::size_t>(entry.sectorOffset) * header.sectorBytes;
|
|
if (at + entry.payloadBytes > size) return false;
|
|
out.assign(data + at, data + at + entry.payloadBytes);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isCanonicalSaveLayout(const uint8_t* data, std::size_t size) {
|
|
SaveHeader header;
|
|
if (!readSaveHeader(data, size, header)) return false;
|
|
if (header.magic != saveMagic || !isKnownSaveVersion(header.version)) return false;
|
|
if (header.sectorBytes == 0 || size % header.sectorBytes != 0) return false;
|
|
|
|
std::vector<SaveTableEntry> entries;
|
|
if (!readSaveTable(data, size, header, entries)) return false;
|
|
|
|
ByteBuffer table(data + saveHeaderBytes, data + saveHeaderBytes + saveTableBytes(header));
|
|
SaveVersion version = static_cast<SaveVersion>(header.version);
|
|
uint32_t expectedTableChecksum =
|
|
versionHasChecksum(version) ? fnv1a32(table.data(), table.size()) : 0u;
|
|
if (header.tableChecksum != expectedTableChecksum) return false;
|
|
|
|
std::size_t cursor = saveHeaderSectors(header);
|
|
for (std::size_t i = 0; i < entries.size(); ++i) {
|
|
if (i > 0 && !saveCoordLess(entries[i - 1].coord, entries[i].coord)) return false;
|
|
if (entries[i].sectorOffset != cursor) return false;
|
|
if (entries[i].sectorCount != divideUp(entries[i].payloadBytes, header.sectorBytes))
|
|
return false;
|
|
cursor += entries[i].sectorCount;
|
|
}
|
|
return cursor * static_cast<std::size_t>(header.sectorBytes) == size;
|
|
}
|
|
|
|
SaveContainer saveContainerFromWorld(const World& world, RegionCoord region, SaveVersion version) {
|
|
SaveContainer container;
|
|
container.version = version;
|
|
container.regionX = region.x;
|
|
container.regionZ = region.z;
|
|
for (const auto& entry : world.allChunks()) {
|
|
const Vec3i& coord = entry.first;
|
|
if (!(regionForChunk(coord) == region)) continue;
|
|
if (coord.y < saveChunkYMin || coord.y > saveChunkYMax) continue;
|
|
if (entry.second->empty()) continue;
|
|
SaveChunkRecord record;
|
|
record.coord = coord;
|
|
record.blocks = *entry.second;
|
|
if (versionHasLight(version)) record.light = defaultChunkLight(record.blocks, saveDefaultSkyLight);
|
|
container.chunks.push_back(std::move(record));
|
|
}
|
|
return container;
|
|
}
|
|
|
|
void applySaveContainerToWorld(const SaveContainer& container, World& world) {
|
|
for (const SaveChunkRecord& record : container.chunks) {
|
|
if (record.empty) continue;
|
|
world.ensureChunk(record.coord) = record.blocks;
|
|
}
|
|
}
|
|
|
|
bool SaveMigrator::peekVersion(const uint8_t* data, std::size_t size, SaveVersion& out) {
|
|
SaveHeader header;
|
|
if (!readSaveHeader(data, size, header)) return false;
|
|
if (header.magic != saveMagic) return false;
|
|
if (!isKnownSaveVersion(header.version)) return false;
|
|
out = static_cast<SaveVersion>(header.version);
|
|
return true;
|
|
}
|
|
|
|
MigrationPlan SaveMigrator::planFor(SaveVersion from, SaveVersion to) {
|
|
MigrationPlan plan;
|
|
plan.from = from;
|
|
plan.to = to;
|
|
if (to < from) {
|
|
plan.possible = false;
|
|
plan.refusal = "downgrade " + versionPair(from, to) +
|
|
" is refused: the newer format carries chunk metadata, a light plane and "
|
|
"checksums that the older one cannot express, so the step would drop data "
|
|
"silently";
|
|
return plan;
|
|
}
|
|
plan.possible = true;
|
|
for (uint32_t v = static_cast<uint32_t>(from); v < static_cast<uint32_t>(to); ++v) {
|
|
MigrationStep step;
|
|
step.from = static_cast<SaveVersion>(v);
|
|
step.to = static_cast<SaveVersion>(v + 1);
|
|
if (step.from == SaveVersion::V1)
|
|
step.summary = "raw block ids replaced by a per-chunk palette and an index array";
|
|
else if (step.from == SaveVersion::V2)
|
|
step.summary = "light plane and a payload checksum added";
|
|
else
|
|
step.summary = "chunk metadata added and palette indices bit-packed";
|
|
plan.steps.push_back(step);
|
|
}
|
|
return plan;
|
|
}
|
|
|
|
bool SaveMigrator::applyStep(const ByteBuffer& in, const MigrationStep& step,
|
|
MigrationResult& result, ByteBuffer& out) const {
|
|
SaveContainer container;
|
|
if (!readSaveContainer(in.data(), in.size(), container)) {
|
|
result.failure = "container is not readable at " + std::string(saveVersionName(step.from));
|
|
return false;
|
|
}
|
|
if (container.version != step.from) {
|
|
result.failure = "expected " + std::string(saveVersionName(step.from)) + " but found " +
|
|
saveVersionName(container.version);
|
|
return false;
|
|
}
|
|
|
|
for (SaveChunkRecord& record : container.chunks) {
|
|
if (step.to == SaveVersion::V2) {
|
|
std::vector<Block> palette;
|
|
std::vector<uint32_t> indices;
|
|
buildPalette(record.blocks, palette, indices);
|
|
result.paletteEntriesCreated += palette.size();
|
|
} else if (step.to == SaveVersion::V3) {
|
|
record.light = defaultChunkLight(record.blocks, skyLightForNewPlanes);
|
|
result.lightCellsInitialized += record.light.size();
|
|
} else {
|
|
record.meta = metadataDefaults;
|
|
++result.metadataRecordsCreated;
|
|
}
|
|
}
|
|
|
|
container.version = step.to;
|
|
container.compressPayloads = compressMigratedPayloads && step.to == currentSaveVersion;
|
|
out = writeSaveContainer(container);
|
|
return true;
|
|
}
|
|
|
|
MigrationResult SaveMigrator::migrate(const uint8_t* data, std::size_t size,
|
|
SaveVersion target) const {
|
|
MigrationResult result;
|
|
result.to = target;
|
|
|
|
SaveVersion from = currentSaveVersion;
|
|
if (!peekVersion(data, size, from)) {
|
|
result.failure = "save header is unreadable: bad magic, short buffer or unknown version";
|
|
return result;
|
|
}
|
|
result.from = from;
|
|
|
|
MigrationPlan plan = planFor(from, target);
|
|
if (!plan.possible) {
|
|
result.failure = plan.refusal;
|
|
return result;
|
|
}
|
|
|
|
SaveContainer source;
|
|
if (!readSaveContainer(data, size, source)) {
|
|
result.failure = "save body is unreadable at " + std::string(saveVersionName(from));
|
|
return result;
|
|
}
|
|
result.chunksMigrated = source.chunks.size();
|
|
|
|
ByteBuffer current(data, data + size);
|
|
for (const MigrationStep& step : plan.steps) {
|
|
ByteBuffer next;
|
|
if (!applyStep(current, step, result, next)) return result;
|
|
current = std::move(next);
|
|
result.applied.push_back(step);
|
|
}
|
|
|
|
result.output = std::move(current);
|
|
result.ok = true;
|
|
return result;
|
|
}
|
|
|
|
MigrationResult SaveMigrator::migrateToCurrent(const ByteBuffer& buffer) const {
|
|
return migrate(buffer.data(), buffer.size(), currentSaveVersion);
|
|
}
|
|
|
|
} // namespace vf
|