#include "vf/SaveMigration.h" #include #include #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& out, uint32_t value) { out.push_back(static_cast((value >> 24) & 0xFFu)); out.push_back(static_cast((value >> 16) & 0xFFu)); out.push_back(static_cast((value >> 8) & 0xFFu)); out.push_back(static_cast(value & 0xFFu)); } uint32_t readU32Be(const uint8_t* at) { return (static_cast(at[0]) << 24) | (static_cast(at[1]) << 16) | (static_cast(at[2]) << 8) | static_cast(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(high) << 32) | static_cast(low); } uint8_t u8() { if (bad || at + 1 > size) { bad = true; return 0; } return data[at++]; } bool take(std::size_t count, std::vector& 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& out, uint64_t value) { putU32Be(out, static_cast(value >> 32)); putU32Be(out, static_cast(value & 0xFFFFFFFFull)); } void buildPalette(const Chunk& chunk, std::vector& palette, std::vector& indices) { std::array(Block::Count)> slotOf; slotOf.fill(-1); palette.clear(); indices.clear(); indices.reserve(chunkVolume); for (Block block : chunk.raw()) { int& slot = slotOf[static_cast(block)]; if (slot < 0) { slot = static_cast(palette.size()); palette.push_back(block); } indices.push_back(static_cast(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((value >> 24) & 0xFFu); data[at + 1] = static_cast((value >> 16) & 0xFFu); data[at + 2] = static_cast((value >> 8) & 0xFFu); data[at + 3] = static_cast(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(readU32Be(data + 8)); out.regionZ = static_cast(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(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(header.sectorBytes); } bool readSaveTable(const uint8_t* data, std::size_t size, const SaveHeader& header, std::vector& out) { out.clear(); for (uint32_t i = 0; i < header.chunkCount; ++i) { std::size_t at = saveHeaderBytes + static_cast(i) * saveEntryBytes; if (at + saveEntryBytes > size) return false; SaveTableEntry entry; entry.coord.x = static_cast(readU32Be(data + at + 0)); entry.coord.y = static_cast(readU32Be(data + at + 4)); entry.coord.z = static_cast(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 defaultChunkLight(const Chunk& chunk, uint8_t skyLevel) { uint8_t sky = skyLevel > 15 ? 15 : skyLevel; std::vector plane(chunkVolume, 0); const std::array& 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(blockLightEmission(blocks[i])); plane[i] = static_cast((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(chunkVolume)); for (Block block : record.blocks.raw()) out.push_back(static_cast(block)); return out; } std::vector palette; std::vector indices; buildPalette(record.blocks, palette, indices); if (version == SaveVersion::V2 || version == SaveVersion::V3) { putU32Be(out, static_cast(palette.size())); for (Block block : palette) out.push_back(static_cast(block)); putU32Be(out, static_cast(indices.size())); for (uint32_t index : indices) out.push_back(static_cast(index)); if (version == SaveVersion::V3) { putU32Be(out, static_cast(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 words = BitPacker::pack(indices, bitWidth); ByteBuffer body; putU32Be(body, static_cast(palette.size())); for (Block block : palette) body.push_back(static_cast(block)); putU32Be(body, static_cast(bitWidth)); putU32Be(body, static_cast(words.size())); for (uint64_t word : words) writeU64Be(body, word); putU32Be(body, static_cast(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(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 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(bitWidth); std::size_t neededWords = divideUp(chunkVolume, static_cast(perWord)); if (wordCount < neededWords) return false; if (wordCount > neededWords + 64u) return false; std::vector 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(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 indices = BitPacker::unpack(words, static_cast(bitWidth), chunkVolume); uint32_t maxIndex = 0; std::array& 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(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(chunkVolume)) return false; std::vector raw; if (!cursor.take(blockBytes, raw)) return false; std::array& blocks = out.blocks.raw(); for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = static_cast(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 paletteBytes; if (!cursor.take(paletteCount, paletteBytes)) return false; uint32_t indexBytes = cursor.u32(); if (cursor.bad || indexBytes != static_cast(chunkVolume)) return false; std::vector indices; if (!cursor.take(indexBytes, indices)) return false; uint32_t maxIndex = 0; std::array& 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(paletteBytes[index]) : Block::Air; } uint32_t lightBytes = 0; if (version == SaveVersion::V3) { lightBytes = cursor.u32(); if (cursor.bad || lightBytes > static_cast(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 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(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& coords, const std::vector& payloads, std::vector* sectorOffsets) { uint32_t sectors = sectorBytes == 0 ? saveSectorBytes : sectorBytes; SaveHeader header; header.magic = saveMagic; header.version = static_cast(version); header.regionX = regionX; header.regionZ = regionZ; header.chunkCount = static_cast(coords.size()); header.sectorBytes = sectors; std::size_t sectorCursor = saveHeaderSectors(header); if (sectorOffsets) sectorOffsets->clear(); ByteBuffer table; table.reserve(coords.size() * saveEntryBytes); std::vector starts; starts.reserve(coords.size()); for (std::size_t i = 0; i < coords.size(); ++i) { uint32_t sectorCount = static_cast(divideUp(payloads[i].size(), sectors)); putU32Be(table, static_cast(coords[i].x)); putU32Be(table, static_cast(coords[i].y)); putU32Be(table, static_cast(coords[i].z)); putU32Be(table, static_cast(sectorCursor)); putU32Be(table, sectorCount); putU32Be(table, static_cast(payloads[i].size())); if (sectorOffsets) sectorOffsets->push_back(static_cast(sectorCursor)); starts.push_back(sectorCursor * static_cast(sectors)); sectorCursor += sectorCount; } header.tableChecksum = versionHasChecksum(version) ? fnv1a32(table.data(), table.size()) : 0u; ByteBuffer out(sectorCursor * static_cast(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(header.regionX)); saveWriteU32At(out.data(), out.size(), 12, static_cast(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(saveHeaderBytes)); for (std::size_t i = 0; i < payloads.size(); ++i) std::copy(payloads[i].begin(), payloads[i].end(), out.begin() + static_cast(starts[i])); 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; } 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 palette; std::vector 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