Files
sfcraft/src/main/java/io/ib67/sfcraft/util/LitematicConverter.java
T
2025-01-06 21:38:19 +08:00

187 lines
6.7 KiB
Java

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 {
private final InputStream input;
private final NbtSizeTracker sizeTracker;
public LitematicConverter(InputStream input, NbtSizeTracker sizeTracker) {
this.input = input;
this.sizeTracker = sizeTracker;
}
@SneakyThrows
public void read(
BiConsumer<String, NbtCompound> schematicOutput
) {
var root = NbtIo.readCompressed(input, sizeTracker);
var dataVersion = root.getInt("MinecraftDataVersion");
var regionsNbt = root.getCompound("Regions");
var i = 0;
for (String regionName : regionsNbt.getKeys()) {
var compound = regionsNbt.getCompound(regionName);
var j = i++;
var schematic = convertRegionToSchematic(dataVersion, compound);
schematicOutput.accept(regionName, schematic);
}
}
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(Math.abs(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.numberOfLeadingZeros(bitsPerBlock - 1));
int bitCounts = 0, blockIterated = 0;
long bitMask, bits = 0;
for (long blockState : blockStates) {
int remainingBits = bitCounts + 64;
if (bitCounts != 0) {
bitMask = (1 << (bitsPerBlock - bitCounts)) - 1;
long newBits = (blockState & bitMask) << bitCounts;
bits = bits | newBits;
blockState = blockState >>> (bitsPerBlock - bitCounts);
remainingBits -= bitsPerBlock;
writeBlocks(buf, (short) bits);
blockIterated++;
}
bitMask = (1 << 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
) {
}
}