260 lines
9.9 KiB
C++
260 lines
9.9 KiB
C++
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Chunk.h"
|
|
#include "Codec.h"
|
|
#include "Math.h"
|
|
#include "Region.h"
|
|
#include "World.h"
|
|
|
|
namespace vf {
|
|
|
|
// The region save container "VXSV" and the migration chain that lifts an old
|
|
// container to the current version.
|
|
//
|
|
// One container holds the chunks of a single region tile (regionSize columns
|
|
// per side, see Region.h) and is laid out like a real region file: a fixed
|
|
// header, an entry table addressing sector runs, then the chunk payloads. The
|
|
// sector indirection is what makes a chunk rewritable without touching its
|
|
// neighbours - and what makes the file fragment, which RegionCompact repairs.
|
|
//
|
|
// Header, 28 bytes, big-endian throughout (same order as Codec's ByteWriter):
|
|
// 0 u32 magic saveMagic
|
|
// 4 u32 version 1..4
|
|
// 8 i32 regionX
|
|
// 12 i32 regionZ
|
|
// 16 u32 chunkCount entries in the table
|
|
// 20 u32 sectorBytes bytes per sector
|
|
// 24 u32 tableChecksum fnv1a32 of the table, v3+ only, 0 before that
|
|
//
|
|
// Entry, 24 bytes, one per chunk:
|
|
// 0 i32 chunkX
|
|
// 4 i32 chunkY
|
|
// 8 i32 chunkZ
|
|
// 12 u32 sectorOffset from the start of the buffer
|
|
// 16 u32 sectorCount
|
|
// 20 u32 payloadBytes bytes actually used inside the run
|
|
//
|
|
// Payload record, by version:
|
|
// v1 u32 blockBytes, u8 blocks[chunkVolume]
|
|
// v2 u32 paletteCount, u8 palette[], u32 indexBytes, u8 indices[chunkVolume]
|
|
// v3 v2 body, then u32 lightBytes, u8 light[chunkVolume], u32 checksum
|
|
// v4 u32 flags, u32 storedBodyBytes, body, u32 checksum
|
|
// body = u32 paletteCount, u8 palette[], u32 bitWidth, u32 wordCount,
|
|
// u64 words[], u32 lightBytes, u8 light[], u64 inhabitedTicks,
|
|
// u64 lastSavedTick, u8 generationStage
|
|
//
|
|
// The v4 body is bit-packed through Anvil's BitPacker and may be framed by the
|
|
// Codec compressor; the checksum always covers the uncompressed body, so it
|
|
// stays comparable across repacking.
|
|
|
|
enum class SaveVersion : uint32_t {
|
|
V1 = 1,
|
|
V2 = 2,
|
|
V3 = 3,
|
|
V4 = 4,
|
|
};
|
|
|
|
constexpr SaveVersion currentSaveVersion = SaveVersion::V4;
|
|
constexpr SaveVersion oldestSaveVersion = SaveVersion::V1;
|
|
|
|
constexpr uint32_t saveMagic = 0x56585356u; // "VXSV"
|
|
constexpr std::size_t saveHeaderBytes = 28;
|
|
constexpr std::size_t saveEntryBytes = 24;
|
|
constexpr uint32_t saveSectorBytes = 256;
|
|
|
|
// The vertical span a chunk coordinate may occupy, in chunk units. Anything
|
|
// outside is a corrupt coordinate, not a tall world.
|
|
constexpr int saveChunkYMin = -4;
|
|
constexpr int saveChunkYMax = 19;
|
|
|
|
constexpr uint32_t saveRecordEmpty = 1u; // v4 flag: tombstone, holds no blocks
|
|
constexpr uint32_t saveRecordCompressed = 2u; // v4 flag: body framed by compress()
|
|
|
|
constexpr uint8_t saveStageComplete = 3;
|
|
constexpr uint8_t saveDefaultSkyLight = 15;
|
|
|
|
// Per-chunk bookkeeping introduced by v4. Chunks lifted from v1..v3 get these
|
|
// defaults: no recorded playtime, saved at tick 0, fully generated.
|
|
struct SaveChunkMeta {
|
|
uint64_t inhabitedTicks = 0;
|
|
uint64_t lastSavedTick = 0;
|
|
uint8_t generationStage = saveStageComplete;
|
|
};
|
|
|
|
// One chunk as the container sees it: where it lives, its blocks, its light
|
|
// plane and its metadata. `light` is empty for v1/v2 containers and holds
|
|
// chunkVolume bytes of (sky << 4) | block from v3 on.
|
|
struct SaveChunkRecord {
|
|
Vec3i coord;
|
|
bool empty = false;
|
|
Chunk blocks;
|
|
std::vector<uint8_t> light;
|
|
SaveChunkMeta meta;
|
|
};
|
|
|
|
struct SaveContainer {
|
|
SaveVersion version = currentSaveVersion;
|
|
int regionX = 0;
|
|
int regionZ = 0;
|
|
uint32_t sectorBytes = saveSectorBytes;
|
|
bool compressPayloads = false;
|
|
std::vector<SaveChunkRecord> chunks;
|
|
|
|
const SaveChunkRecord* find(const Vec3i& coord) const;
|
|
};
|
|
|
|
// What a payload record turned out to contain, filled while decoding so the
|
|
// scanner never has to parse the same bytes twice.
|
|
struct SavePayloadInfo {
|
|
uint32_t paletteCount = 0;
|
|
uint32_t maxIndex = 0;
|
|
uint32_t bitWidth = 0;
|
|
uint32_t lightBytes = 0;
|
|
bool compressed = false;
|
|
bool empty = false;
|
|
std::size_t rawBodyBytes = 0;
|
|
};
|
|
|
|
struct SaveHeader {
|
|
uint32_t magic = 0;
|
|
uint32_t version = 0;
|
|
int32_t regionX = 0;
|
|
int32_t regionZ = 0;
|
|
uint32_t chunkCount = 0;
|
|
uint32_t sectorBytes = 0;
|
|
uint32_t tableChecksum = 0;
|
|
};
|
|
|
|
struct SaveTableEntry {
|
|
Vec3i coord;
|
|
uint32_t sectorOffset = 0;
|
|
uint32_t sectorCount = 0;
|
|
uint32_t payloadBytes = 0;
|
|
};
|
|
|
|
// Big-endian field access at an absolute byte offset, bounds checked. The
|
|
// scanner and the compactor both need random access into a buffer they may not
|
|
// be able to parse from the front.
|
|
bool saveReadU32At(const uint8_t* data, std::size_t size, std::size_t at, uint32_t& out);
|
|
bool saveWriteU32At(uint8_t* data, std::size_t size, std::size_t at, uint32_t value);
|
|
|
|
bool isKnownSaveVersion(uint32_t version);
|
|
bool versionHasPalette(SaveVersion version);
|
|
bool versionHasLight(SaveVersion version);
|
|
bool versionHasChecksum(SaveVersion version);
|
|
bool versionHasMetadata(SaveVersion version);
|
|
const char* saveVersionName(SaveVersion version);
|
|
|
|
// Reads the header without trusting any of it beyond the buffer length.
|
|
bool readSaveHeader(const uint8_t* data, std::size_t size, SaveHeader& out);
|
|
std::size_t saveTableBytes(const SaveHeader& header);
|
|
std::size_t saveHeaderSectors(const SaveHeader& header);
|
|
std::size_t saveDataStart(const SaveHeader& header);
|
|
|
|
// Reads as many entries as the buffer actually holds; `out` may end up shorter
|
|
// than header.chunkCount, which is itself a finding for the scanner.
|
|
bool readSaveTable(const uint8_t* data, std::size_t size, const SaveHeader& header,
|
|
std::vector<SaveTableEntry>& out);
|
|
|
|
// The light plane a chunk gets when a version that has none is lifted: sky is
|
|
// blocked by anything opaque, block light is the emission of the block itself.
|
|
std::vector<uint8_t> defaultChunkLight(const Chunk& chunk, uint8_t skyLevel);
|
|
bool chunkIsAllAir(const Chunk& chunk);
|
|
|
|
ByteBuffer encodeChunkPayload(SaveVersion version, const SaveChunkRecord& record,
|
|
bool compressBody);
|
|
|
|
// Structural decode. A wrong checksum does NOT fail the decode - the scanner
|
|
// reports it separately, and repair needs the blocks out of a record whose
|
|
// checksum went stale.
|
|
bool decodeChunkPayload(SaveVersion version, const uint8_t* data, std::size_t size,
|
|
SaveChunkRecord& out, SavePayloadInfo* info = nullptr);
|
|
|
|
// Pulls the stored checksum and the one the bytes actually hash to. False for
|
|
// v1/v2, which carry none.
|
|
bool verifyChunkPayloadChecksum(SaveVersion version, const uint8_t* data, std::size_t size,
|
|
uint32_t& stored, uint32_t& computed);
|
|
|
|
// Lays out a container from payloads that are already encoded: header, entry
|
|
// table, then every payload back to back on sector boundaries. `coords` must
|
|
// already be in ascending coordinate order - this is the one place that decides
|
|
// what a canonical buffer looks like, and the writer, the compactor and the
|
|
// backup restore all go through it so their output is comparable byte for byte.
|
|
ByteBuffer packSaveContainer(SaveVersion version, int regionX, int regionZ, uint32_t sectorBytes,
|
|
const std::vector<Vec3i>& coords,
|
|
const std::vector<ByteBuffer>& payloads,
|
|
std::vector<uint32_t>* sectorOffsets = nullptr);
|
|
|
|
bool saveCoordLess(const Vec3i& a, const Vec3i& b);
|
|
|
|
// Writes a canonical container: entries sorted by coordinate, payloads packed
|
|
// back to back on sector boundaries, no holes, buffer ending on a sector edge.
|
|
// Deterministic - the same container always produces the same bytes.
|
|
ByteBuffer writeSaveContainer(const SaveContainer& container);
|
|
bool readSaveContainer(const uint8_t* data, std::size_t size, SaveContainer& out);
|
|
|
|
// Copies one chunk's payload record out verbatim, for byte-level comparison.
|
|
bool readChunkPayloadBytes(const uint8_t* data, std::size_t size, const Vec3i& coord,
|
|
ByteBuffer& out);
|
|
|
|
// True when the layout matches what writeSaveContainer would produce. Backups
|
|
// require it, because they store payloads rather than layout.
|
|
bool isCanonicalSaveLayout(const uint8_t* data, std::size_t size);
|
|
|
|
SaveContainer saveContainerFromWorld(const World& world, RegionCoord region, SaveVersion version);
|
|
void applySaveContainerToWorld(const SaveContainer& container, World& world);
|
|
|
|
struct MigrationStep {
|
|
SaveVersion from = SaveVersion::V1;
|
|
SaveVersion to = SaveVersion::V2;
|
|
std::string summary;
|
|
};
|
|
|
|
struct MigrationPlan {
|
|
SaveVersion from = currentSaveVersion;
|
|
SaveVersion to = currentSaveVersion;
|
|
std::vector<MigrationStep> steps;
|
|
bool possible = false;
|
|
std::string refusal;
|
|
};
|
|
|
|
struct MigrationResult {
|
|
bool ok = false;
|
|
SaveVersion from = currentSaveVersion;
|
|
SaveVersion to = currentSaveVersion;
|
|
std::vector<MigrationStep> applied;
|
|
std::size_t chunksMigrated = 0;
|
|
std::size_t paletteEntriesCreated = 0;
|
|
std::size_t lightCellsInitialized = 0;
|
|
std::size_t metadataRecordsCreated = 0;
|
|
std::string failure;
|
|
ByteBuffer output;
|
|
};
|
|
|
|
// Lifts a container one version at a time. Every step is a real transcode: the
|
|
// intermediate buffer is a valid container of the intermediate version, so a
|
|
// v1 save passes through readable v2 and v3 states on its way to v4.
|
|
class SaveMigrator {
|
|
public:
|
|
uint8_t skyLightForNewPlanes = saveDefaultSkyLight;
|
|
SaveChunkMeta metadataDefaults;
|
|
bool compressMigratedPayloads = false;
|
|
|
|
static bool peekVersion(const uint8_t* data, std::size_t size, SaveVersion& out);
|
|
static MigrationPlan planFor(SaveVersion from, SaveVersion to);
|
|
|
|
MigrationResult migrate(const uint8_t* data, std::size_t size, SaveVersion target) const;
|
|
MigrationResult migrateToCurrent(const ByteBuffer& buffer) const;
|
|
|
|
private:
|
|
bool applyStep(const ByteBuffer& in, const MigrationStep& step, MigrationResult& result,
|
|
ByteBuffer& out) const;
|
|
};
|
|
|
|
} // namespace vf
|