Files
voxelforge/core/src/WorldBackup.cpp
T
2026-09-10 13:50:18 +02:00

406 lines
14 KiB
C++

#include "vf/WorldBackup.h"
#include <algorithm>
namespace vf {
namespace {
struct LiveChunk {
Vec3i coord;
uint32_t checksum = 0;
ByteBuffer payload;
};
std::size_t findLive(const std::vector<LiveChunk>& live, const Vec3i& coord) {
for (std::size_t i = 0; i < live.size(); ++i)
if (live[i].coord == coord) return i;
return live.size();
}
void insertLive(std::vector<LiveChunk>& live, const Vec3i& coord, uint32_t checksum,
const ByteBuffer& payload) {
std::size_t at = findLive(live, coord);
if (at != live.size()) {
live[at].checksum = checksum;
live[at].payload = payload;
return;
}
LiveChunk fresh;
fresh.coord = coord;
fresh.checksum = checksum;
fresh.payload = payload;
live.push_back(std::move(fresh));
}
void eraseLive(std::vector<LiveChunk>& live, const Vec3i& coord) {
std::size_t at = findLive(live, coord);
if (at != live.size()) live.erase(live.begin() + static_cast<std::ptrdiff_t>(at));
}
// Pulls every chunk of a container out as (coordinate, checksum, payload
// bytes), in canonical order.
bool sliceContainer(const ByteBuffer& save, SaveHeader& header, std::vector<LiveChunk>& out) {
if (!readSaveHeader(save.data(), save.size(), header)) return false;
if (header.magic != saveMagic || !isKnownSaveVersion(header.version)) return false;
if (header.sectorBytes == 0) return false;
std::vector<SaveTableEntry> entries;
if (!readSaveTable(save.data(), save.size(), header, entries)) return false;
out.clear();
out.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 > save.size()) return false;
LiveChunk chunk;
chunk.coord = entry.coord;
chunk.payload.assign(save.begin() + static_cast<std::ptrdiff_t>(at),
save.begin() + static_cast<std::ptrdiff_t>(at + entry.payloadBytes));
chunk.checksum = fnv1a32(chunk.payload.data(), chunk.payload.size());
out.push_back(std::move(chunk));
}
return true;
}
std::string idText(uint32_t id) { return "#" + std::to_string(id); }
} // namespace
const char* backupKindName(BackupKind kind) {
switch (kind) {
case BackupKind::Full: return "full";
case BackupKind::Incremental: return "incremental";
}
return "unknown";
}
std::size_t BackupEntry::storedBytes() const {
std::size_t total = 0;
for (const BackupChunk& chunk : stored) total += chunk.payload.size();
return total;
}
const BackupChunk* BackupEntry::findStored(const Vec3i& coord) const {
for (const BackupChunk& chunk : stored)
if (chunk.coord == coord) return &chunk;
return nullptr;
}
const BackupChunk* BackupEntry::findState(const Vec3i& coord) const {
for (const BackupChunk& chunk : state)
if (chunk.coord == coord) return &chunk;
return nullptr;
}
const BackupEntry* BackupIndex::find(uint32_t id) const {
for (const BackupEntry& entry : entries)
if (entry.id == id) return &entry;
return nullptr;
}
BackupEntry* BackupIndex::find(uint32_t id) {
for (BackupEntry& entry : entries)
if (entry.id == id) return &entry;
return nullptr;
}
uint32_t BackupIndex::latestId() const {
uint32_t best = 0;
for (const BackupEntry& entry : entries)
if (entry.id > best) best = entry.id;
return best;
}
std::size_t BackupIndex::storedBytes() const {
std::size_t total = 0;
for (const BackupEntry& entry : entries) total += entry.storedBytes();
return total;
}
std::vector<uint32_t> BackupIndex::chainTo(uint32_t id, bool& complete,
std::string& failure) const {
std::vector<uint32_t> walk;
complete = false;
failure.clear();
const BackupEntry* entry = find(id);
if (entry == nullptr) {
failure = "backup " + idText(id) + " is not in the index";
return walk;
}
std::size_t guard = entries.size() + 1;
while (guard-- > 0) {
walk.push_back(entry->id);
if (entry->kind == BackupKind::Full) {
std::reverse(walk.begin(), walk.end());
complete = true;
return walk;
}
const BackupEntry* parent = find(entry->parentId);
if (parent == nullptr) {
failure = "chain broken: incremental " + idText(entry->id) + " expects parent " +
idText(entry->parentId) + ", which is not in the index";
walk.clear();
return walk;
}
entry = parent;
}
failure = "chain broken: the parent links from " + idText(id) + " never reach a full backup";
walk.clear();
return walk;
}
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<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));
}
result.save = packSaveContainer(target->version, target->regionX, target->regionZ,
target->sectorBytes, coords, payloads, nullptr);
result.chunksRestored = coords.size();
result.ok = true;
return result;
}
RestoreResult BackupStore::restoreLatest() const {
uint32_t latest = chain.latestId();
if (latest == 0) {
RestoreResult result;
result.failure = "the backup index is empty";
return result;
}
return restore(latest);
}
bool BackupStore::drop(uint32_t id) {
for (std::size_t i = 0; i < chain.entries.size(); ++i) {
if (chain.entries[i].id != id) continue;
chain.entries.erase(chain.entries.begin() + static_cast<std::ptrdiff_t>(i));
return true;
}
return false;
}
std::vector<uint32_t> BackupStore::prune(uint64_t nowTick) {
failure.clear();
std::vector<uint32_t> keep;
auto markKept = [&](uint32_t id) {
for (uint32_t held : keep)
if (held == id) return;
keep.push_back(id);
};
std::vector<uint32_t> byAge;
for (const BackupEntry& entry : chain.entries) byAge.push_back(entry.id);
std::sort(byAge.begin(), byAge.end(), [](uint32_t a, uint32_t b) { return a > b; });
for (std::size_t i = 0; i < byAge.size() && i < retention.keepLatest; ++i) markKept(byAge[i]);
auto keepNewestPerBucket = [&](uint64_t bucketTicks, std::size_t bucketCount) {
if (bucketTicks == 0 || bucketCount == 0) return;
uint64_t currentBucket = nowTick / bucketTicks;
for (std::size_t offset = 0; offset < bucketCount; ++offset) {
if (currentBucket < offset) break;
uint64_t wanted = currentBucket - offset;
uint32_t best = 0;
uint64_t bestTick = 0;
for (const BackupEntry& entry : chain.entries) {
if (entry.tick / bucketTicks != wanted) continue;
if (best == 0 || entry.tick > bestTick ||
(entry.tick == bestTick && entry.id > best)) {
best = entry.id;
bestTick = entry.tick;
}
}
if (best != 0) markKept(best);
}
};
keepNewestPerBucket(retention.dayTicks, retention.dailyBuckets);
keepNewestPerBucket(retention.weekTicks, retention.weeklyBuckets);
std::size_t scanned = 0;
while (scanned < keep.size()) {
const BackupEntry* entry = chain.find(keep[scanned]);
++scanned;
if (entry == nullptr || entry->kind == BackupKind::Full) continue;
markKept(entry->parentId);
}
std::vector<uint32_t> removed;
std::vector<BackupEntry> survivors;
for (BackupEntry& entry : chain.entries) {
bool kept = false;
for (uint32_t held : keep)
if (held == entry.id) kept = true;
if (kept) survivors.push_back(std::move(entry));
else removed.push_back(entry.id);
}
chain.entries = std::move(survivors);
std::sort(removed.begin(), removed.end());
return removed;
}
} // namespace vf