feat: expand Schematic

This commit is contained in:
dualfroz committed 2026-09-09 15:53:42 +02:00
1 parent 0f1f623a69
commit 238bb77a01
1 file changed
+186
+186
View File
@@ -321,3 +321,189 @@ ByteBuffer Schematic::encode(SchematicVersion version) const {
if (writeVoidMask) presence.push_back(filled ? 1u : 0u); 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<uint16_t>(palette.size()));
for (Block block : palette) body.putU8(static_cast<uint8_t>(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<uint32_t>(raw.size()));
body.putU32(static_cast<uint32_t>(payload.size()));
body.putBytes(payload.data(), payload.size());
ByteWriter out;
out.putU32(schematicMagic);
out.putU32(static_cast<uint32_t>(version));
out.putU32(static_cast<uint32_t>(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<uint32_t>(SchematicVersion::V1) &&
version != static_cast<uint32_t>(SchematicVersion::V2))
return SchematicError::UnsupportedVersion;
if (!head.getU32(bodyLength)) return SchematicError::Truncated;
std::size_t bodyStart = head.position();
std::size_t bodySize = static_cast<std::size_t>(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<std::size_t>(dims.x) * static_cast<std::size_t>(dims.y) *
static_cast<std::size_t>(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<Block> 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<uint8_t>(Block::Count)) return SchematicError::BadPalette;
palette.push_back(static_cast<Block>(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<uint32_t>(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<uint8_t> 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<uint16_t> indices;
unpackIndices(raw.data(), cellCount, bits, indices);
std::vector<uint16_t> 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<uint32_t>(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