Skip to content

Commit

Permalink
Support for the Sponge v3 spec
Browse files Browse the repository at this point in the history
  • Loading branch information
SandroHc committed Dec 13, 2023
1 parent 84f3496 commit 11a7a39
Show file tree
Hide file tree
Showing 13 changed files with 14,090 additions and 93 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Java parser for the .schem/.schematic/.litematic Minecraft formats. 🗺

## Supported formats

| Format | Extension | Links |
|-----------------------------------|------------|---------------------------------------------------|
| [Sponge Schematic Format][sponge] | .schem | Spec: [v1][sponge-spec-v1][v2][sponge-spec-v2] |
| [Schematica][schematica] | .schematic | [Spec][schematica-spec] |
| Format | Extension | Links |
|-----------------------------------|------------|--------------------------------------------------------------------------|
| [Sponge Schematic Format][sponge] | .schem | Spec: [v1][sponge-spec-v1][v2][sponge-spec-v2] [v3][sponge-spec-v3] |
| [Schematica][schematica] | .schematic | [Spec][schematica-spec] |

[sponge]: https://github.com/SpongePowered/Schematic-Specification
[sponge-spec-v1]: https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-1.md
[sponge-spec-v2]: https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md
[sponge-spec-v3]: https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md
[schematica]: https://curseforge.com/minecraft/mc-mods/schematica
[schematica-spec]: https://minecraft.fandom.com/wiki/Schematic_file_format

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ public ListTag<T> clone() {
return copy;
}

//TODO: make private
@SuppressWarnings("unchecked")
public void addUnchecked(Tag<?> tag) {
if (getTypeClass() != EndTag.class && typeClass != tag.getClass()) {
Expand Down
167 changes: 106 additions & 61 deletions src/main/java/net/sandrohc/schematic4j/parser/SpongeSchematicParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@
* The SCHEM format replaced the .SCHEMATIC format in versions 1.13+ of Minecraft Java Edition.
* <p>
* Specification:<br>
* - <a href="https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md">https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md</a>
* - <a href="https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md">https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-2.md</a>
*/
// TODO: create different parsers for v1 and v2, and extract common code to an abstract intermediate class
// TODO: implement v3
public class SpongeSchematicParser implements Parser {

public static final String NBT_ROOT = "Schematic";
Expand Down Expand Up @@ -88,6 +86,10 @@ public class SpongeSchematicParser implements Parser {
public static final String NBT_ENTITIES = "Entities";
public static final String NBT_ENTITIES_ID = "Id";
public static final String NBT_ENTITIES_POS = "Pos";
public static final String NBT_ENTITIES_EXTRA = "Extra";
public static final String NBT_V3_BLOCKS = "Blocks";
public static final String NBT_V3_BIOMES = "Biomes";
public static final String NBT_V3_DATA = "Data";

private static final Logger log = LoggerFactory.getLogger(SpongeSchematicParser.class);

Expand All @@ -111,10 +113,10 @@ public class SpongeSchematicParser implements Parser {
parseDataVersion(rootTag, builder, version);
parseMetadata(rootTag, builder);
parseOffset(rootTag, builder);
parseBlocks(rootTag, builder);
parseBlocks(rootTag, builder, version);
parseBlockEntities(rootTag, builder, version);
parseEntities(rootTag, builder);
parseBiomes(rootTag, builder);
parseEntities(rootTag, builder, version);
parseBiomes(rootTag, builder, version);

return builder.build();
}
Expand Down Expand Up @@ -146,24 +148,24 @@ private void parseMetadata(CompoundTag root, Builder builder) {
final Tag<?> tag = entry.getValue();

switch (key) {
case NBT_METADATA_NAME:
name = ((StringTag) tag).getValue();
break;
case NBT_METADATA_AUTHOR:
author = ((StringTag) tag).getValue();
break;
case NBT_METADATA_DATE:
long dateEpochMillis = ((LongTag) tag).asLong(); // milliseconds since the Unix epoch
date = epochToDate(dateEpochMillis);
break;
case NBT_METADATA_REQUIRED_MODS:
final ListTag<StringTag> stringTags = ((ListTag<?>) tag).asStringTagList();
requiredMods = StreamSupport.stream(stringTags.spliterator(), false)
.map(StringTag::getValue)
.toArray(String[]::new);
break;
default:
extra.put(key, unwrap(tag));
case NBT_METADATA_NAME:
name = ((StringTag) tag).getValue();
break;
case NBT_METADATA_AUTHOR:
author = ((StringTag) tag).getValue();
break;
case NBT_METADATA_DATE:
long dateEpochMillis = ((LongTag) tag).asLong(); // milliseconds since the Unix epoch
date = epochToDate(dateEpochMillis);
break;
case NBT_METADATA_REQUIRED_MODS:
final ListTag<StringTag> stringTags = ((ListTag<?>) tag).asStringTagList();
requiredMods = StreamSupport.stream(stringTags.spliterator(), false)
.map(StringTag::getValue)
.toArray(String[]::new);
break;
default:
extra.put(key, unwrap(tag));
}
}
} else {
Expand All @@ -182,7 +184,7 @@ private void parseOffset(CompoundTag root, Builder builder) {
});
}

private void parseBlocks(CompoundTag root, SchematicSponge.Builder builder) throws ParsingException {
private void parseBlocks(CompoundTag root, SchematicSponge.Builder builder, int version) throws ParsingException {
log.trace("Parsing blocks");

if (!containsAllTags(root, NBT_BLOCK_DATA, NBT_PALETTE)) {
Expand All @@ -191,29 +193,32 @@ private void parseBlocks(CompoundTag root, SchematicSponge.Builder builder) thro
return;
}

final short width = getShortOrThrow(root, NBT_WIDTH);
final short width = getShortOrThrow(root, NBT_WIDTH);
final short height = getShortOrThrow(root, NBT_HEIGHT);
final short length = getShortOrThrow(root, NBT_LENGTH);
final CompoundTag blocksTag = getBlocksTag(root, version);

builder.width(width).height(height).length(length);
log.trace("Dimensions: width={}, height={}, length={}", width, height, length);


// Load the (optional) palette
final CompoundTag palette = getCompoundOrThrow(root, NBT_PALETTE);
final CompoundTag palette = getCompoundOrThrow(blocksTag, NBT_PALETTE);
log.trace("Palette size: {}", palette.size());
Map<Integer, SchematicBlock> blockById = palette.entrySet().stream()
.collect(toMap(
entry -> ((IntTag) entry.getValue()).asInt(), // ID
entry -> new SchematicBlock(entry.getKey()) // Blockstate
));

final int paletteMax = root.getInt(NBT_PALETTE_MAX);
if (palette.size() != paletteMax)
final int paletteMax = blocksTag.getInt(NBT_PALETTE_MAX);
if (paletteMax > 0 && palette.size() != paletteMax)
log.warn("Palette actual size does not match expected size. Expected {} but got {}", paletteMax, palette.size());


// Load the block data
byte[] blockDataRaw = getByteArrayOrThrow(root, NBT_BLOCK_DATA);
final String blockDataKey = version >= 3 ? NBT_V3_DATA : NBT_BLOCK_DATA;
byte[] blockDataRaw = getByteArrayOrThrow(blocksTag, blockDataKey);
SchematicBlock[][][] blockData = new SchematicBlock[width][height][length];

// --- Uses code from https://github.com/SpongePowered/Sponge/blob/aa2c8c53b4f9f40297e6a4ee281bee4f4ce7707b/src/main/java/org/spongepowered/common/data/persistence/SchematicTranslator.java#L147-L175
Expand All @@ -234,7 +239,7 @@ private void parseBlocks(CompoundTag root, SchematicSponge.Builder builder) thro
i++;
}

// index = (y * length + z) * width + x
// index = x + (z * width) + (y * width * length)
int y = index / (width * length);
int z = (index % (width * length)) / width;
int x = (index % (width * length)) % width;
Expand All @@ -249,11 +254,21 @@ private void parseBlocks(CompoundTag root, SchematicSponge.Builder builder) thro
log.debug("Loaded {} blocks", width * height * length);
}

private static CompoundTag getBlocksTag(CompoundTag root, int version) {
if (version >= 3) {
return root.getCompoundTag(NBT_V3_BLOCKS);
} else {
return root;
}
}

private void parseBlockEntities(CompoundTag root, Builder builder, int version) throws ParsingException {
log.trace("Parsing block entities");

final CompoundTag blocksTag = getBlocksTag(root, version);
final Optional<ListTag<CompoundTag>> blockEntitiesListTag = getCompoundList(blocksTag, version == 1 ? NBT_TILE_ENTITIES : NBT_BLOCK_ENTITIES);

final Collection<SchematicBlockEntity> blockEntities;
final Optional<ListTag<CompoundTag>> blockEntitiesListTag = getCompoundList(root, version == 1 ? NBT_TILE_ENTITIES : NBT_BLOCK_ENTITIES);

if (blockEntitiesListTag.isPresent()) {
final ListTag<CompoundTag> blockEntitiesTag = blockEntitiesListTag.get();
Expand All @@ -262,11 +277,11 @@ private void parseBlockEntities(CompoundTag root, Builder builder, int version)

for (CompoundTag blockEntity : blockEntitiesTag) {
final String id = getStringOrThrow(blockEntity, NBT_BLOCK_ENTITIES_ID);
final int[] pos = getIntArray(blockEntity, NBT_BLOCK_ENTITIES_POS).orElseGet(() -> new int[] { 0, 0, 0 });
final int[] pos = getIntArray(blockEntity, NBT_BLOCK_ENTITIES_POS).orElseGet(() -> new int[]{0, 0, 0});

final Map<String, Object> extra = blockEntity.entrySet().stream()
.filter(tag -> !tag.getKey().equals(NBT_ENTITIES_ID) &&
!tag.getKey().equals(NBT_ENTITIES_POS))
!tag.getKey().equals(NBT_ENTITIES_POS))
.collect(toMap(Entry::getKey, e -> unwrap(e.getValue()), (a, b) -> b, TreeMap::new));

blockEntities.add(new SchematicBlockEntity(id, SchematicPosInt.from(pos), extra));
Expand All @@ -281,7 +296,7 @@ private void parseBlockEntities(CompoundTag root, Builder builder, int version)
builder.blockEntities(blockEntities);
}

private void parseEntities(CompoundTag root, SchematicSponge.Builder builder) throws ParsingException {
private void parseEntities(CompoundTag root, Builder builder, int version) throws ParsingException {
log.trace("Parsing entities");

final Collection<SchematicEntity> entities;
Expand All @@ -295,18 +310,18 @@ private void parseEntities(CompoundTag root, SchematicSponge.Builder builder) th
for (CompoundTag entity : entitiesTag) {
final String id = getStringOrThrow(entity, NBT_ENTITIES_ID);

final double[] pos = { 0, 0, 0 };
final double[] pos = {0, 0, 0};
getDoubleList(entity, NBT_ENTITIES_POS).ifPresent(posTag -> {
pos[0] = posTag.get(0).asDouble();
pos[1] = posTag.get(1).asDouble();
pos[2] = posTag.get(2).asDouble();
});

final Map<String, Object> extra = entity.entrySet().stream()
.filter(tag -> !tag.getKey().equals(NBT_ENTITIES_ID) &&
!tag.getKey().equals(NBT_ENTITIES_POS))
.collect(toMap(Entry::getKey, e -> unwrap(e.getValue()), (a, b) -> b, TreeMap::new));

final Map<String, Object> extra = new TreeMap<>();
final String extraTagName = version >= 3 ? NBT_V3_DATA : NBT_ENTITIES_EXTRA;
getCompound(entity, extraTagName).ifPresent(extraTag -> {
entity.entrySet().forEach(e -> extra.put(e.getKey(), unwrap(e.getValue())));
});
entities.add(new SchematicEntity(id, SchematicPosDouble.from(pos), extra));
}

Expand All @@ -319,7 +334,7 @@ private void parseEntities(CompoundTag root, SchematicSponge.Builder builder) th
builder.entities(entities);
}

private void parseBiomes(CompoundTag root, SchematicSponge.Builder builder) throws ParsingException {
private void parseBiomes(CompoundTag root, SchematicSponge.Builder builder, int version) throws ParsingException {
log.trace("Parsing biomes");

if (!containsAllTags(root, NBT_BIOME_DATA, NBT_BIOME_PALETTE)) {
Expand All @@ -328,46 +343,76 @@ private void parseBiomes(CompoundTag root, SchematicSponge.Builder builder) thro
return;
}

short width = getShortOrThrow(root, NBT_WIDTH);
short length = getShortOrThrow(root, NBT_LENGTH);
final short width = getShortOrThrow(root, NBT_WIDTH);
final short height = version >= 3 ? getShortOrThrow(root, NBT_HEIGHT) : 1;
final short length = getShortOrThrow(root, NBT_LENGTH);
final CompoundTag biomesTag = getBiomesTag(root, version);

// Load the (optional) palette
final CompoundTag palette = getCompoundOrThrow(root, NBT_BIOME_PALETTE);
final String biomePaletteKey = version >= 3 ? NBT_PALETTE : NBT_BIOME_PALETTE;
final CompoundTag palette = getCompoundOrThrow(biomesTag, biomePaletteKey);
log.trace("Biome palette size: {}", palette.size());
Map<Integer, SchematicBiome> biomeById = palette.entrySet().stream()
.collect(toMap(
entry -> ((IntTag) entry.getValue()).asInt(), // ID
entry -> new SchematicBiome(entry.getKey()) // Blockstate
));

final int paletteMax = root.getInt(NBT_BIOME_PALETTE_MAX);
if (palette.size() != paletteMax)
final int paletteMax = biomesTag.getInt(NBT_BIOME_PALETTE_MAX);
if (paletteMax > 0 && palette.size() != paletteMax)
log.warn("Biome palette actual size does not match expected size. Expected {} but got {}", paletteMax, palette.size());


// Load the block data
byte[] biomeDataRaw = getByteArrayOrThrow(root, NBT_BLOCK_DATA);
SchematicBiome[][][] biomeData = new SchematicBiome[width][1][length];

int expectedBlocks = width * length;
if (biomeDataRaw.length != expectedBlocks)
log.warn("Number of blocks does not match expected. Expected {} blocks, but got {}", expectedBlocks, biomeDataRaw.length);
// Load the biome data
final String biomeDataKey = version >= 3 ? NBT_V3_DATA : NBT_BIOME_DATA;
byte[] biomeDataRaw = getByteArrayOrThrow(biomesTag, biomeDataKey);
SchematicBiome[][][] biomeData = new SchematicBiome[width][height][length];

for (int x = 0; x < width; x++) {
for (int z = 0; z < length; z++) {
final int index = x + z*width; // flatten (x,z) into a single dimension

final int blockId = biomeDataRaw[index] & 0xFF;
final SchematicBiome block = biomeById.get(blockId);
int index = 0;
int i = 0;
while (i < biomeDataRaw.length) {
int value = 0;
int varintLength = 0;
while (true) {
value |= (biomeDataRaw[i] & 127) << (varintLength++ * 7);
if (varintLength > 5) {
throw new ParsingException("VarInt too big (probably corrupted data)");
}
if ((biomeDataRaw[i] & 128) != 128) {
i++;
break;
}
i++;
}

biomeData[x][0][z] = block;
if (version >= 3) {
// index = x + (z * width) + (y * width * length)
int y = index / (width * length);
int z = (index % (width * length)) / width;
int x = (index % (width * length)) % width;
biomeData[x][y][z] = biomeById.get(value);
} else {
// index = x + (z * width)
int x = index % width;
int z = index / width;
biomeData[x][0][z] = biomeById.get(value);
}

index++;
}

builder.biomes(biomeData);
log.debug("Loaded {} biomes", width * length);
}

private static CompoundTag getBiomesTag(CompoundTag root, int version) {
if (version >= 3) {
return root.getCompoundTag(NBT_V3_BIOMES);
} else {
return root;
}
}

@Override
public String toString() {
return "SpongeSchematicParser";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ public interface Schematic {
@NonNull SchematicFormat format();

/**
* The width of the schematic.
* The width of the schematic, the X axis.
*
* @return the schematic width
*/
int width();

/**
* The height of the schematic.
* The height of the schematic, the Y axis.
*
* @return the schematic height
*/
int height();

/**
* The length of the schematic.
* The length of the schematic, the Z axis.
*
* @return the schematic length
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ void guessFormat(SchematicFormat expected, String file) {

private static Stream<Arguments> guessFormatData() {
return Stream.of(
Arguments.of(SchematicFormat.SPONGE_V2, "/schematics/sponge/issue-1.schem"),
Arguments.of(SchematicFormat.SPONGE_V2, "/schematics/sponge/v2/issue-1.schem"),
Arguments.of(SchematicFormat.SPONGE_V2, "/schematics/sponge/v2/green-cottage.schem"),
Arguments.of(SchematicFormat.SPONGE_V2, "/schematics/sponge/v2/interieur-exterieur-chunk-project.schem"),
Arguments.of(SchematicFormat.SCHEMATICA, "/schematics/schematica/9383.schematic"),
Arguments.of(SchematicFormat.SCHEMATICA, "/schematics/schematica/12727.schematic")
);
Expand Down
Loading

0 comments on commit 11a7a39

Please sign in to comment.