feat: extend WorldBackup

This commit is contained in:
dualfroz committed 2026-09-10 13:12:39 +02:00
1 parent aa4bd109fe
commit 5cf0d6cc06
1 file changed
+164
+164
View File
@@ -154,3 +154,167 @@ std::vector<uint32_t> BackupIndex::chainTo(uint32_t id, bool& complete,
uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) { uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) {
failure.clear(); 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<LiveChunk> 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<SaveVersion>(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<uint32_t> walk = chain.chainTo(id, complete, walkFailure);
if (!complete) {
result.failure = walkFailure;
return result;
}
result.chain = walk;
std::vector<LiveChunk> 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<Vec3i> coords;
std::vector<ByteBuffer> 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));
}