feat: add writeSaveContainer and readSaveContainer to SaveMigration
This commit is contained in:
1 parent
12f0549bbb
commit
966c022003
1 file changed
+160
@@ -468,3 +468,163 @@ ByteBuffer packSaveContainer(SaveVersion version, int regionX, int regionZ, uint
|
|||||||
return out;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user