diff --git a/core/src/SaveMigration.cpp b/core/src/SaveMigration.cpp index 3c5988f..f022385 100644 --- a/core/src/SaveMigration.cpp +++ b/core/src/SaveMigration.cpp @@ -468,3 +468,163 @@ ByteBuffer packSaveContainer(SaveVersion version, int regionX, int regionZ, uint return out; } +ByteBuffer writeSaveContainer(const SaveContainer& container) { + std::vector 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 coords; + std::vector 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 entries; + if (!readSaveTable(data, size, header, entries)) return false; + + out.version = static_cast(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(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 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(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 entries; + if (!readSaveTable(data, size, header, entries)) return false; + + ByteBuffer table(data + saveHeaderBytes, data + saveHeaderBytes + saveTableBytes(header)); + SaveVersion version = static_cast(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(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(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(from); v < static_cast(to); ++v) { + MigrationStep step; + step.from = static_cast(v); + step.to = static_cast(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; +} +