From ffa939600d29bae7a5edbf6ab0ef075cda80424a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 20:24:55 +0200 Subject: [PATCH] feat(core): implement save-format v3 world and entity serialization --- core/src/SaveV3.cpp | 258 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 core/src/SaveV3.cpp diff --git a/core/src/SaveV3.cpp b/core/src/SaveV3.cpp new file mode 100644 index 0000000..aff5e5c --- /dev/null +++ b/core/src/SaveV3.cpp @@ -0,0 +1,258 @@ +#include "vf/SaveV3.h" + +#include + +#include "vf/Serialize.h" + +namespace vf { + +namespace { + +constexpr uint32_t magicV1 = 0x31465856; // "VXF1" +constexpr uint32_t magicV3 = 0x33465856; // "VXF3" +constexpr uint32_t formatVersion3 = 3; + +constexpr uint32_t tagMobs = 0x4d4f4253; +constexpr uint32_t tagFluid = 0x464c5544; +constexpr uint32_t tagLight = 0x4c494754; + +void putU32(std::vector& out, uint32_t value) { + out.push_back(static_cast(value & 0xff)); + out.push_back(static_cast((value >> 8) & 0xff)); + out.push_back(static_cast((value >> 16) & 0xff)); + out.push_back(static_cast((value >> 24) & 0xff)); +} + +void putI32(std::vector& out, int32_t value) { putU32(out, static_cast(value)); } + +void putU16(std::vector& out, uint16_t value) { + out.push_back(static_cast(value & 0xff)); + out.push_back(static_cast((value >> 8) & 0xff)); +} + +void putF32(std::vector& out, float value) { + uint32_t bits; + std::memcpy(&bits, &value, 4); + putU32(out, bits); +} + +void putBytes(std::vector& out, const std::vector& bytes) { + putU32(out, static_cast(bytes.size())); + out.insert(out.end(), bytes.begin(), bytes.end()); +} + +struct Reader { + const uint8_t* data; + std::size_t size; + std::size_t pos = 0; + + bool u32(uint32_t& out) { + if (pos + 4 > size) return false; + out = static_cast(data[pos]) | (static_cast(data[pos + 1]) << 8) | + (static_cast(data[pos + 2]) << 16) | + (static_cast(data[pos + 3]) << 24); + pos += 4; + return true; + } + bool i32(int32_t& out) { + uint32_t v; + if (!u32(v)) return false; + out = static_cast(v); + return true; + } + bool u16(uint16_t& out) { + if (pos + 2 > size) return false; + out = static_cast(data[pos]) | (static_cast(data[pos + 1]) << 8); + pos += 2; + return true; + } + bool u8(uint8_t& out) { + if (pos + 1 > size) return false; + out = data[pos++]; + return true; + } + bool f32(float& out) { + uint32_t bits; + if (!u32(bits)) return false; + std::memcpy(&out, &bits, 4); + return true; + } + bool bytes(std::vector& out) { + uint32_t len; + if (!u32(len)) return false; + if (pos + len > size) return false; + out.assign(data + pos, data + pos + len); + pos += len; + return true; + } +}; + +void writeBlocks(std::vector& out, const World& world) { + putU32(out, static_cast(world.chunkCount())); + for (const auto& entry : world.allChunks()) { + const Vec3i& coord = entry.first; + putI32(out, coord.x); + putI32(out, coord.y); + putI32(out, coord.z); + const auto& blocks = entry.second->raw(); + std::size_t i = 0; + while (i < blocks.size()) { + Block value = blocks[i]; + uint16_t run = 1; + while (i + run < blocks.size() && blocks[i + run] == value && run < 0xffff) ++run; + out.push_back(static_cast(value)); + putU16(out, run); + i += run; + } + } +} + +bool readBlocks(Reader& reader, World& world) { + uint32_t chunkTotal = 0; + if (!reader.u32(chunkTotal)) return false; + for (uint32_t c = 0; c < chunkTotal; ++c) { + int32_t cx, cy, cz; + if (!reader.i32(cx) || !reader.i32(cy) || !reader.i32(cz)) return false; + Chunk& chunk = world.ensureChunk({cx, cy, cz}); + auto& blocks = chunk.raw(); + std::size_t written = 0; + while (written < blocks.size()) { + uint8_t value; + uint16_t run; + if (!reader.u8(value) || !reader.u16(run)) return false; + if (run == 0 || written + run > blocks.size()) return false; + for (uint16_t r = 0; r < run; ++r) blocks[written++] = static_cast(value); + } + } + return true; +} + +void writeGrid(std::vector& out, const GridSnapshot& grid) { + putI32(out, grid.origin.x); + putI32(out, grid.origin.y); + putI32(out, grid.origin.z); + putI32(out, grid.dims.x); + putI32(out, grid.dims.y); + putI32(out, grid.dims.z); + putBytes(out, grid.channelA); + putBytes(out, grid.channelB); +} + +bool readGrid(Reader& reader, GridSnapshot& grid) { + if (!reader.i32(grid.origin.x) || !reader.i32(grid.origin.y) || !reader.i32(grid.origin.z)) + return false; + if (!reader.i32(grid.dims.x) || !reader.i32(grid.dims.y) || !reader.i32(grid.dims.z)) + return false; + return reader.bytes(grid.channelA) && reader.bytes(grid.channelB); +} + +void writeMob(std::vector& out, const Mob& mob) { + putF32(out, mob.position.x); + putF32(out, mob.position.y); + putF32(out, mob.position.z); + putF32(out, mob.velocity.x); + putF32(out, mob.velocity.y); + putF32(out, mob.velocity.z); + putF32(out, mob.width); + putF32(out, mob.height); + out.push_back(mob.onGround ? 1u : 0u); + out.push_back(static_cast(mob.state)); + putU32(out, mob.rng); + putF32(out, mob.wanderDir.x); + putF32(out, mob.wanderDir.y); + putF32(out, mob.wanderDir.z); +} + +bool readMob(Reader& reader, Mob& mob) { + uint8_t onGround = 0, state = 0; + if (!reader.f32(mob.position.x) || !reader.f32(mob.position.y) || !reader.f32(mob.position.z)) + return false; + if (!reader.f32(mob.velocity.x) || !reader.f32(mob.velocity.y) || !reader.f32(mob.velocity.z)) + return false; + if (!reader.f32(mob.width) || !reader.f32(mob.height)) return false; + if (!reader.u8(onGround) || !reader.u8(state)) return false; + if (!reader.u32(mob.rng)) return false; + if (!reader.f32(mob.wanderDir.x) || !reader.f32(mob.wanderDir.y) || !reader.f32(mob.wanderDir.z)) + return false; + mob.onGround = onGround != 0; + mob.state = static_cast(state); + return true; +} + +} // namespace + +std::vector serializeSaveV3(const SaveV3& save) { + std::vector out; + putU32(out, magicV3); + putU32(out, formatVersion3); + writeBlocks(out, save.world); + + std::vector>> sections; + + std::vector mobPayload; + putU32(mobPayload, static_cast(save.mobs.size())); + for (const Mob& mob : save.mobs) writeMob(mobPayload, mob); + sections.push_back({tagMobs, std::move(mobPayload)}); + + if (save.hasFluid) { + std::vector payload; + writeGrid(payload, save.fluid); + sections.push_back({tagFluid, std::move(payload)}); + } + if (save.hasLight) { + std::vector payload; + writeGrid(payload, save.light); + sections.push_back({tagLight, std::move(payload)}); + } + + putU32(out, static_cast(sections.size())); + for (const auto& section : sections) { + putU32(out, section.first); + putBytes(out, section.second); + } + return out; +} + +bool deserializeSaveV3(const uint8_t* data, std::size_t size, SaveV3& out) { + Reader peek{data, size}; + uint32_t magic = 0; + if (!peek.u32(magic)) return false; + + if (magic == magicV1) { + return deserializeWorld(data, size, out.world); // legacy v1/v2 migration + } + if (magic != magicV3) return false; + + Reader reader{data, size}; + uint32_t fileMagic = 0, version = 0; + reader.u32(fileMagic); + if (!reader.u32(version) || version != formatVersion3) return false; + if (!readBlocks(reader, out.world)) return false; + + uint32_t sectionCount = 0; + if (!reader.u32(sectionCount)) return false; + for (uint32_t s = 0; s < sectionCount; ++s) { + uint32_t tag = 0, length = 0; + if (!reader.u32(tag) || !reader.u32(length)) return false; + std::size_t sectionStart = reader.pos; + if (sectionStart + length > size) return false; + + if (tag == tagMobs) { + uint32_t count = 0; + if (!reader.u32(count)) return false; + out.mobs.resize(count); + for (uint32_t m = 0; m < count; ++m) + if (!readMob(reader, out.mobs[m])) return false; + } else if (tag == tagFluid) { + if (!readGrid(reader, out.fluid)) return false; + out.hasFluid = true; + } else if (tag == tagLight) { + if (!readGrid(reader, out.light)) return false; + out.hasLight = true; + } + reader.pos = sectionStart + length; // resync / skip unknown sections + } + return true; +} + +} // namespace vf