diff --git a/core/src/Schematic.cpp b/core/src/Schematic.cpp index 1ed9bcd..9df78ae 100644 --- a/core/src/Schematic.cpp +++ b/core/src/Schematic.cpp @@ -321,3 +321,189 @@ ByteBuffer Schematic::encode(SchematicVersion version) const { if (writeVoidMask) presence.push_back(filled ? 1u : 0u); } + ByteBuffer raw; + raw.reserve(packedByteCount(cellCount, bits) + (writeVoidMask ? packedByteCount(cellCount, 1) : 0u)); + packIndices(indices, bits, raw); + if (writeVoidMask) packIndices(presence, 1u, raw); + ByteBuffer payload = compress(raw.data(), raw.size()); + + ByteWriter body; + putI32(body, dims.x); + putI32(body, dims.y); + putI32(body, dims.z); + putI32(body, cells.anchor().x); + putI32(body, cells.anchor().y); + putI32(body, cells.anchor().z); + body.putU16(static_cast(palette.size())); + for (Block block : palette) body.putU8(static_cast(block)); + body.putU8(bits); + if (version == SchematicVersion::V2) { + body.putString(meta.name); + body.putString(meta.author); + body.putU64(meta.tick); + body.putU8(writeVoidMask ? voidMaskFlag : 0u); + } + body.putU32(static_cast(raw.size())); + body.putU32(static_cast(payload.size())); + body.putBytes(payload.data(), payload.size()); + + ByteWriter out; + out.putU32(schematicMagic); + out.putU32(static_cast(version)); + out.putU32(static_cast(body.size())); + out.putBytes(body.bytes().data(), body.size()); + out.putU32(fnv1a32(body.bytes().data(), body.size())); + return out.bytes(); +} + +SchematicError Schematic::decode(const uint8_t* data, std::size_t size, Schematic& out) { + ByteReader head(data, size); + uint32_t magic = 0, version = 0, bodyLength = 0; + if (!head.getU32(magic)) return SchematicError::Truncated; + if (magic != schematicMagic) return SchematicError::BadMagic; + if (!head.getU32(version)) return SchematicError::Truncated; + if (version != static_cast(SchematicVersion::V1) && + version != static_cast(SchematicVersion::V2)) + return SchematicError::UnsupportedVersion; + if (!head.getU32(bodyLength)) return SchematicError::Truncated; + + std::size_t bodyStart = head.position(); + std::size_t bodySize = static_cast(bodyLength); + if (size < bodyStart + bodySize + 4u) return SchematicError::Truncated; + + ByteReader tail(data + bodyStart + bodySize, 4u); + uint32_t storedChecksum = 0; + tail.getU32(storedChecksum); + if (fnv1a32(data + bodyStart, bodySize) != storedChecksum) return SchematicError::BadChecksum; + + ByteReader body(data + bodyStart, bodySize); + Vec3i dims{0, 0, 0}; + Vec3i anchor{0, 0, 0}; + if (!getI32(body, dims.x) || !getI32(body, dims.y) || !getI32(body, dims.z)) + return SchematicError::Truncated; + if (!getI32(body, anchor.x) || !getI32(body, anchor.y) || !getI32(body, anchor.z)) + return SchematicError::Truncated; + bool wholeVolumeEmpty = dims.x == 0 && dims.y == 0 && dims.z == 0; + if (!wholeVolumeEmpty && (dims.x <= 0 || dims.y <= 0 || dims.z <= 0)) + return SchematicError::DimensionMismatch; + + std::size_t cellCount = static_cast(dims.x) * static_cast(dims.y) * + static_cast(dims.z); + if (cellCount > maxSchematicCells) return SchematicError::DimensionMismatch; + + uint16_t paletteSize = 0; + if (!body.getU16(paletteSize)) return SchematicError::Truncated; + if (paletteSize == 0u) return SchematicError::BadPalette; + std::vector palette; + palette.reserve(paletteSize); + for (uint16_t i = 0; i < paletteSize; ++i) { + uint8_t id = 0; + if (!body.getU8(id)) return SchematicError::Truncated; + if (id >= static_cast(Block::Count)) return SchematicError::BadPalette; + palette.push_back(static_cast(id)); + } + + uint8_t bits = 0; + if (!body.getU8(bits)) return SchematicError::Truncated; + if (bits != bitsForPalette(paletteSize)) return SchematicError::BadPalette; + + std::string name; + std::string author; + uint64_t tick = 0; + uint8_t flags = 0; + if (version == static_cast(SchematicVersion::V2)) { + if (!body.getString(name) || !body.getString(author)) return SchematicError::Truncated; + if (!body.getU64(tick)) return SchematicError::Truncated; + if (!body.getU8(flags)) return SchematicError::Truncated; + } + bool hasVoidMask = (flags & voidMaskFlag) != 0u; + + uint32_t rawLength = 0, payloadLength = 0; + if (!body.getU32(rawLength) || !body.getU32(payloadLength)) return SchematicError::Truncated; + if (payloadLength > body.remaining()) return SchematicError::Truncated; + + std::size_t expectedRaw = packedByteCount(cellCount, bits); + if (hasVoidMask) expectedRaw += packedByteCount(cellCount, 1u); + if (rawLength != expectedRaw) return SchematicError::DimensionMismatch; + + std::vector payload(payloadLength, 0u); + if (payloadLength != 0u && !body.getBytes(payload.data(), payloadLength)) + return SchematicError::Truncated; + + ByteBuffer raw; + if (!decompress(payload.data(), payload.size(), raw)) return SchematicError::BadChecksum; + if (raw.size() != expectedRaw) return SchematicError::DimensionMismatch; + + std::vector indices; + unpackIndices(raw.data(), cellCount, bits, indices); + std::vector presence; + if (hasVoidMask) + unpackIndices(raw.data() + packedByteCount(cellCount, bits), cellCount, 1u, presence); + + ClipboardBuffer buffer(dims, anchor); + std::size_t cursor = 0; + for (int y = 0; y < dims.y; ++y) + for (int z = 0; z < dims.z; ++z) + for (int x = 0; x < dims.x; ++x) { + uint16_t slot = indices[cursor]; + if (slot >= paletteSize) return SchematicError::BadPalette; + bool filled = hasVoidMask ? presence[cursor] != 0u : true; + buffer.put(x, y, z, palette[slot], filled); + ++cursor; + } + + out = Schematic{}; + out.cells = buffer; + out.meta.name = name; + out.meta.author = author; + out.meta.tick = tick; + out.meta.version = SchematicVersion::V2; + out.migrated = version == static_cast(SchematicVersion::V1); + out.refreshHeaderFromBuffer(); + return SchematicError::None; +} + +std::size_t Schematic::pasteInto(World& world, const Vec3i& target, + ClipboardBuffer::PasteMode mode) const { + const Vec3i& dims = cells.dims(); + const Vec3i& anchor = cells.anchor(); + std::size_t written = 0; + for (int y = 0; y < dims.y; ++y) + for (int z = 0; z < dims.z; ++z) + for (int x = 0; x < dims.x; ++x) { + if (!cells.filledAt(x, y, z)) continue; + Vec3i at{target.x + x - anchor.x, target.y + y - anchor.y, + target.z + z - anchor.z}; + Block existing = world.blockAt(at.x, at.y, at.z); + if (mode == ClipboardBuffer::PasteMode::IntoAirOnly && !isAir(existing)) continue; + if (mode == ClipboardBuffer::PasteMode::ReplaceExisting && isAir(existing)) continue; + Block value = cells.at(x, y, z); + if (existing == value) continue; + world.setBlock(at.x, at.y, at.z, value); + ++written; + } + return written; +} + +std::size_t Schematic::pasteInto(World& world, EditHistory& history, const Vec3i& target, + ClipboardBuffer::PasteMode mode, uint64_t tick) const { + const Vec3i& dims = cells.dims(); + const Vec3i& anchor = cells.anchor(); + history.beginBatch(meta.name.empty() ? std::string("paste") : "paste " + meta.name, tick); + std::size_t written = 0; + for (int y = 0; y < dims.y; ++y) + for (int z = 0; z < dims.z; ++z) + for (int x = 0; x < dims.x; ++x) { + if (!cells.filledAt(x, y, z)) continue; + Vec3i at{target.x + x - anchor.x, target.y + y - anchor.y, + target.z + z - anchor.z}; + Block existing = world.blockAt(at.x, at.y, at.z); + if (mode == ClipboardBuffer::PasteMode::IntoAirOnly && !isAir(existing)) continue; + if (mode == ClipboardBuffer::PasteMode::ReplaceExisting && isAir(existing)) continue; + if (history.recordSet(world, at, cells.at(x, y, z))) ++written; + } + history.endBatch(); + return written; +} + +} // namespace vf