feat(vf): add SaveChunkMeta and SaveChunkRecord

This commit is contained in:
dualfroz committed 2026-09-04 15:39:18 +02:00
1 parent c75911fe22
commit d41bcfbb70
1 file changed
+158
+158
View File
@@ -0,0 +1,158 @@
#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);