Add: LitematicConverter for converting .litematic to .schematic
This commit is contained in:
1 parent
aadec09c45
commit
b6b42f4ed0
1 file changed
+193
@@ -0,0 +1,193 @@
|
|||||||
|
package io.ib67.sfcraft.util;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.minecraft.nbt.*;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.PipedInputStream;
|
||||||
|
import java.io.PipedOutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cleaned up version of https://github.com/GoldenDelicios/Lite2Edit
|
||||||
|
* Uses {@link NbtCompound} from Minecraft.
|
||||||
|
*/
|
||||||
|
public class LitematicConverter implements AutoCloseable {
|
||||||
|
public static final String NBT_LITEMATICA_ROOT = "";
|
||||||
|
private final InputStream input;
|
||||||
|
private final NbtSizeTracker sizeTracker;
|
||||||
|
private NbtCompound root;
|
||||||
|
|
||||||
|
public LitematicConverter(InputStream input, NbtSizeTracker sizeTracker) {
|
||||||
|
this.input = input;
|
||||||
|
this.sizeTracker = sizeTracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public void read(
|
||||||
|
BiConsumer<Integer, NbtCompound> schematicOutput
|
||||||
|
) {
|
||||||
|
if (root != null) throw new IllegalStateException("Litematica file is already read");
|
||||||
|
root = NbtIo.readCompressed(input, sizeTracker).getCompound(NBT_LITEMATICA_ROOT);
|
||||||
|
var dataVersion = root.getInt("MinecraftDataVersion");
|
||||||
|
var regionsNbt = root.getList("Regions", NbtElement.COMPOUND_TYPE);
|
||||||
|
var i = 0;
|
||||||
|
for (NbtElement nbtElement : regionsNbt) {
|
||||||
|
var compound = (NbtCompound) nbtElement;
|
||||||
|
var j = i++;
|
||||||
|
var schematic = convertRegionToSchematic(dataVersion, compound);
|
||||||
|
schematicOutput.accept(j, schematic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param region region
|
||||||
|
* @return a path to temp file.
|
||||||
|
*/
|
||||||
|
private NbtCompound convertRegionToSchematic(int dataVersion, NbtCompound region) {
|
||||||
|
var worldEditTag = new NbtCompound();
|
||||||
|
|
||||||
|
// prepare metadata
|
||||||
|
var size = readSizeTuple(region);
|
||||||
|
worldEditTag.put("Metadata", convertToWeMeta(size, region));
|
||||||
|
worldEditTag.putInt("DataVersion", dataVersion);
|
||||||
|
|
||||||
|
// dimensional data.
|
||||||
|
worldEditTag.putShort("Height", (short) Math.abs(size.y));
|
||||||
|
worldEditTag.putShort("Length", (short) Math.abs(size.z));
|
||||||
|
worldEditTag.putShort("Width", (short) Math.abs(size.x));
|
||||||
|
|
||||||
|
// block & tile entities
|
||||||
|
var paletteNbt = region.getList("BlockStatePalette", NbtElement.COMPOUND_TYPE);
|
||||||
|
var wePalette = convertToWEPalette(paletteNbt);
|
||||||
|
worldEditTag.putInt("PaletteMax", wePalette.getKeys().size());
|
||||||
|
worldEditTag.put("Palette", wePalette);
|
||||||
|
var tileEntities = region.getList("TileEntities", NbtElement.COMPOUND_TYPE);
|
||||||
|
worldEditTag.put("BlockEntities", convertToWETileEntities(tileEntities));
|
||||||
|
worldEditTag.putInt("Version", 2);
|
||||||
|
worldEditTag.putIntArray("Offset", new int[3]);
|
||||||
|
worldEditTag.putByteArray("BlockData", convertToWEBlocks(size.x * size.y * size.z, region));
|
||||||
|
var schematicsRoot = new NbtCompound();
|
||||||
|
schematicsRoot.put("Schematic", worldEditTag);
|
||||||
|
return schematicsRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SizeTuple readSizeTuple(NbtCompound region) {
|
||||||
|
var size = (NbtCompound) region.get("Size");
|
||||||
|
var sizeX = size.getInt("x");
|
||||||
|
var sizeY = size.getInt("y");
|
||||||
|
var sizeZ = size.getInt("z");
|
||||||
|
return new SizeTuple(sizeX, sizeY, sizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NbtList convertToWETileEntities(NbtList tileEntities) {
|
||||||
|
var weTEs = new NbtList();
|
||||||
|
for (NbtElement _tileEntity : tileEntities) {
|
||||||
|
var tE = (NbtCompound) _tileEntity;
|
||||||
|
tE.putIntArray("Pos", new int[]{
|
||||||
|
tE.getInt("x"),
|
||||||
|
tE.getInt("y"),
|
||||||
|
tE.getInt("z")
|
||||||
|
});
|
||||||
|
tE.putString("Id", tE.getString("id"));
|
||||||
|
// other properties
|
||||||
|
tE.remove("x");
|
||||||
|
tE.remove("y");
|
||||||
|
tE.remove("z");
|
||||||
|
tE.remove("id");
|
||||||
|
weTEs.add(tE);
|
||||||
|
}
|
||||||
|
return weTEs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NbtCompound convertToWEPalette(NbtList paletteNbt) {
|
||||||
|
var wePalette = new NbtCompound();
|
||||||
|
for (int i = 0; i < paletteNbt.size(); i++) {
|
||||||
|
var entry = (NbtCompound) paletteNbt.get(i);
|
||||||
|
var name = new StringBuilder(entry.getString("Name"));
|
||||||
|
var properties = entry.getCompound("Properties");
|
||||||
|
if (!properties.isEmpty()) {
|
||||||
|
name.append("[");
|
||||||
|
var props = new ArrayList<String>();
|
||||||
|
for (String key : properties.getKeys()) {
|
||||||
|
props.add(key + "=" + properties.getString(key));
|
||||||
|
}
|
||||||
|
name.append(props.stream().collect(Collectors.joining(",")));
|
||||||
|
name.append("]");
|
||||||
|
}
|
||||||
|
wePalette.putInt(name.toString(), i);
|
||||||
|
}
|
||||||
|
return wePalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NbtElement convertToWeMeta(SizeTuple size, NbtCompound region) {
|
||||||
|
var pos = (NbtCompound) region.get("Position");
|
||||||
|
var nbt = new NbtCompound();
|
||||||
|
nbt.putInt("WEOffsetX", pos.getInt("x") + size.x < 0 ? size.x + 1 : 0);
|
||||||
|
nbt.putInt("WEOffsetY", pos.getInt("y") + size.y < 0 ? size.y + 1 : 0);
|
||||||
|
nbt.putInt("WEOffsetZ", pos.getInt("z") + size.z < 0 ? size.z + 1 : 0);
|
||||||
|
return nbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] convertToWEBlocks(int totalBlocks, NbtCompound region) {
|
||||||
|
var buf = Unpooled.buffer();
|
||||||
|
var blockStates = region.getLongArray("BlockStates");
|
||||||
|
|
||||||
|
int bitsPerBlock = region.getList("BlockStatePalette", NbtElement.COMPOUND_TYPE).size();
|
||||||
|
bitsPerBlock = Math.max(2, Integer.SIZE - Integer.numberOfTrailingZeros(bitsPerBlock - 1));
|
||||||
|
|
||||||
|
int bitCounts = 0, blockIterated = 0;
|
||||||
|
long bitMask, bits = 0;
|
||||||
|
for (long blockState : blockStates) {
|
||||||
|
int remainingBits = bitCounts + 64;
|
||||||
|
if (bitCounts != 0) {
|
||||||
|
bitMask = (1L << (bitsPerBlock - bitCounts)) - 1;
|
||||||
|
long newBits = (blockState & bitMask) << bitCounts;
|
||||||
|
bits = bits | newBits;
|
||||||
|
blockState = blockState >>> (bitsPerBlock - bitCounts);
|
||||||
|
remainingBits -= bitsPerBlock;
|
||||||
|
writeBlocks(buf, (short) bits);
|
||||||
|
blockIterated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bitMask = (1L << bitsPerBlock) - 1;
|
||||||
|
while(remainingBits >= bitsPerBlock){
|
||||||
|
bits = blockState & bitMask;
|
||||||
|
blockState = blockState >>> bitsPerBlock;
|
||||||
|
remainingBits -= bitsPerBlock;
|
||||||
|
if(blockIterated >= totalBlocks) break;
|
||||||
|
writeBlocks(buf, (short) bits);
|
||||||
|
blockIterated++;
|
||||||
|
}
|
||||||
|
bits = blockState;
|
||||||
|
bitCounts = remainingBits;
|
||||||
|
}
|
||||||
|
return buf.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int writeBlocks(ByteBuf buf, short block) {
|
||||||
|
int b = block >>> 7;
|
||||||
|
if (b == 0) {
|
||||||
|
buf.writeByte(block);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
buf.writeByte(block | 128);
|
||||||
|
buf.writeByte(block);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
input.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
record SizeTuple(
|
||||||
|
int x, int y, int z
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user