From 5cf0d6cc06163ed9238edd805445984a08fbfb37 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 13:12:39 +0200 Subject: [PATCH] feat: extend WorldBackup --- core/src/WorldBackup.cpp | 164 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/core/src/WorldBackup.cpp b/core/src/WorldBackup.cpp index 1fab4c0..2fa5635 100644 --- a/core/src/WorldBackup.cpp +++ b/core/src/WorldBackup.cpp @@ -154,3 +154,167 @@ std::vector BackupIndex::chainTo(uint32_t id, bool& complete, uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) { failure.clear(); + IntegrityReport report = scanner.scan(save.data(), save.size()); + if (!report.headerReadable) { + failure = "save is not a readable container, nothing to back up"; + return 0; + } + if (report.countAtLeast(IssueSeverity::Recoverable) != 0) { + failure = "save reports " + std::to_string(report.countAtLeast(IssueSeverity::Recoverable)) + + " unresolved integrity findings; repair it before backing it up"; + return 0; + } + if (!isCanonicalSaveLayout(save.data(), save.size())) { + failure = + "save layout is fragmented; compact it first, a payload level backup cannot reproduce " + "hole placement byte for byte"; + return 0; + } + + SaveHeader header; + std::vector current; + if (!sliceContainer(save, header, current)) { + failure = "save table does not address its own payloads"; + return 0; + } + + BackupEntry entry; + entry.kind = kind; + entry.tick = tick; + entry.version = static_cast(header.version); + entry.regionX = header.regionX; + entry.regionZ = header.regionZ; + entry.sectorBytes = header.sectorBytes; + + const BackupEntry* parent = nullptr; + if (kind == BackupKind::Incremental) { + uint32_t parentId = chain.latestId(); + parent = chain.find(parentId); + if (parent == nullptr) { + failure = "no full backup to build on; capture a full backup first"; + return 0; + } + bool complete = false; + std::string walkFailure; + chain.chainTo(parentId, complete, walkFailure); + if (!complete) { + failure = "cannot extend a broken chain: " + walkFailure; + return 0; + } + if (parent->version != entry.version || parent->regionX != entry.regionX || + parent->regionZ != entry.regionZ) { + failure = "chain covers region (" + std::to_string(parent->regionX) + ", " + + std::to_string(parent->regionZ) + ") at " + saveVersionName(parent->version) + + ", the save is region (" + std::to_string(entry.regionX) + ", " + + std::to_string(entry.regionZ) + ") at " + saveVersionName(entry.version); + return 0; + } + if (tick < parent->tick) { + failure = "backup tick " + std::to_string(tick) + " runs behind parent " + + idText(parent->id) + " at tick " + std::to_string(parent->tick); + return 0; + } + entry.parentId = parentId; + } + + for (const LiveChunk& chunk : current) { + BackupChunk stamp; + stamp.coord = chunk.coord; + stamp.checksum = chunk.checksum; + entry.state.push_back(stamp); + + bool changed = true; + if (parent != nullptr) { + const BackupChunk* before = parent->findState(chunk.coord); + changed = before == nullptr || before->checksum != chunk.checksum; + } + if (!changed) continue; + BackupChunk written; + written.coord = chunk.coord; + written.checksum = chunk.checksum; + written.payload = chunk.payload; + entry.stored.push_back(std::move(written)); + } + + if (parent != nullptr) { + for (const BackupChunk& before : parent->state) { + bool stillHere = false; + for (const LiveChunk& chunk : current) + if (chunk.coord == before.coord) stillHere = true; + if (!stillHere) entry.removed.push_back(before.coord); + } + } + + entry.id = nextId++; + chain.entries.push_back(std::move(entry)); + return chain.entries.back().id; +} + +uint32_t BackupStore::captureMigrated(BackupKind kind, const ByteBuffer& save, uint64_t tick) { + failure.clear(); + SaveMigrator migrator; + MigrationResult migrated = migrator.migrateToCurrent(save); + if (!migrated.ok) { + failure = "migration to " + std::string(saveVersionName(currentSaveVersion)) + + " failed: " + migrated.failure; + return 0; + } + return capture(kind, migrated.output, tick); +} + +RestoreResult BackupStore::restore(uint32_t id) const { + RestoreResult result; + result.fromId = id; + + bool complete = false; + std::string walkFailure; + std::vector walk = chain.chainTo(id, complete, walkFailure); + if (!complete) { + result.failure = walkFailure; + return result; + } + result.chain = walk; + + std::vector live; + const BackupEntry* target = chain.find(id); + for (std::size_t step = 0; step < walk.size(); ++step) { + const BackupEntry* entry = chain.find(walk[step]); + for (const Vec3i& coord : entry->removed) { + eraseLive(live, coord); + ++result.chunksRemoved; + } + for (const BackupChunk& chunk : entry->stored) { + insertLive(live, chunk.coord, chunk.checksum, chunk.payload); + if (entry->kind == BackupKind::Full) ++result.chunksFromFull; + else ++result.chunksFromIncrements; + } + if (entry->kind == BackupKind::Incremental) ++result.incrementsApplied; + } + + if (live.size() != target->state.size()) { + result.failure = "restored " + std::to_string(live.size()) + " chunks but backup " + + idText(id) + " recorded " + std::to_string(target->state.size()); + return result; + } + for (const BackupChunk& stamp : target->state) { + std::size_t at = findLive(live, stamp.coord); + if (at == live.size() || live[at].checksum != stamp.checksum) { + result.failure = "restored chunk (" + std::to_string(stamp.coord.x) + ", " + + std::to_string(stamp.coord.y) + ", " + std::to_string(stamp.coord.z) + + ") does not match the checksum backup " + idText(id) + " recorded"; + return result; + } + } + + std::sort(live.begin(), live.end(), [](const LiveChunk& a, const LiveChunk& b) { + return saveCoordLess(a.coord, b.coord); + }); + std::vector coords; + std::vector payloads; + coords.reserve(live.size()); + payloads.reserve(live.size()); + for (LiveChunk& chunk : live) { + coords.push_back(chunk.coord); + payloads.push_back(std::move(chunk.payload)); + } +