diff --git a/core/src/WorldBackup.cpp b/core/src/WorldBackup.cpp new file mode 100644 index 0000000..1fab4c0 --- /dev/null +++ b/core/src/WorldBackup.cpp @@ -0,0 +1,156 @@ +#include "vf/WorldBackup.h" + +#include + +namespace vf { + +namespace { + +struct LiveChunk { + Vec3i coord; + uint32_t checksum = 0; + ByteBuffer payload; +}; + +std::size_t findLive(const std::vector& 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& 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& live, const Vec3i& coord) { + std::size_t at = findLive(live, coord); + if (at != live.size()) live.erase(live.begin() + static_cast(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& 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 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(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(at), + save.begin() + static_cast(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 BackupIndex::chainTo(uint32_t id, bool& complete, + std::string& failure) const { + std::vector 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(); +