Skip to content

Commit

Permalink
Store schematic block data more efficiently
Browse files Browse the repository at this point in the history
Store the block indices and palette instead of storing all the individual blocks. Basically copies what the schematics are already doing minus the specific encodings like varints and packed lists. It is much more memory efficient than the old method.
  • Loading branch information
SandroHc committed Dec 16, 2023
1 parent a170ea3 commit 7256208
Show file tree
Hide file tree
Showing 9 changed files with 21,878 additions and 59,731 deletions.
141 changes: 67 additions & 74 deletions src/main/java/net/sandrohc/schematic4j/parser/SchematicaParser.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package net.sandrohc.schematic4j.parser;

import java.util.*;
import java.util.Map.Entry;

import org.checkerframework.checker.nullness.qual.NonNull;
Expand All @@ -11,14 +10,21 @@
import net.sandrohc.schematic4j.exception.ParsingException;
import net.sandrohc.schematic4j.nbt.tag.CompoundTag;
import net.sandrohc.schematic4j.nbt.tag.ListTag;
import net.sandrohc.schematic4j.nbt.tag.ShortTag;
import net.sandrohc.schematic4j.nbt.tag.NumberTag;
import net.sandrohc.schematic4j.nbt.tag.Tag;
import net.sandrohc.schematic4j.schematic.Schematic;
import net.sandrohc.schematic4j.schematic.SchematicaSchematic.Builder;
import net.sandrohc.schematic4j.schematic.types.*;
import net.sandrohc.schematic4j.schematic.SchematicaSchematic;
import net.sandrohc.schematic4j.schematic.types.SchematicBlockEntity;
import net.sandrohc.schematic4j.schematic.types.SchematicEntity;
import net.sandrohc.schematic4j.schematic.types.SchematicItem;

import static java.util.stream.Collectors.toMap;
import static net.sandrohc.schematic4j.utils.TagUtils.*;
import static net.sandrohc.schematic4j.utils.TagUtils.getByte;
import static net.sandrohc.schematic4j.utils.TagUtils.getByteArrayOrThrow;
import static net.sandrohc.schematic4j.utils.TagUtils.getCompound;
import static net.sandrohc.schematic4j.utils.TagUtils.getCompoundList;
import static net.sandrohc.schematic4j.utils.TagUtils.getCompoundOrThrow;
import static net.sandrohc.schematic4j.utils.TagUtils.getShort;
import static net.sandrohc.schematic4j.utils.TagUtils.getString;

/**
* Parses Schematica files (<i>.schematic</i>).
Expand Down Expand Up @@ -48,109 +54,96 @@ public class SchematicaParser implements Parser {
public static final String NBT_TILE_ENTITIES = "TileEntities";
public static final String NBT_ENTITIES = "Entities";

public static final String DEFAULT_BLOCK_NAME = "minecraft:unknown";


@Override
public @NonNull Schematic parse(@Nullable CompoundTag nbt) throws ParsingException {
log.debug("Parsing Schematica schematic");

final Builder builder = new Builder();
final SchematicaSchematic schematic = new SchematicaSchematic();
if (nbt == null) {
return builder.build();
return schematic;
}

parseIcon(nbt, builder);
parseBlocks(nbt, builder);
parseBlockEntities(nbt, builder);
parseEntities(nbt, builder);
parseMaterials(nbt, builder);
parseIcon(nbt, schematic);
parseBlocks(nbt, schematic);
parseBlockEntities(nbt, schematic);
parseEntities(nbt, schematic);
parseMaterials(nbt, schematic);

return builder.build();
return schematic;
}

private void parseIcon(CompoundTag root, Builder builder) {
private void parseIcon(CompoundTag root, SchematicaSchematic schematic) {
log.trace("Parsing icon");

getCompound(root, NBT_ICON).ifPresent(iconTag -> builder.icon(new SchematicItem(
getString(iconTag, NBT_ICON_ID).orElse("minecraft:dirt"),
getByte(iconTag, NBT_ICON_COUNT).orElse((byte) 1),
getShort(iconTag, NBT_ICON_DAMAGE).orElse((short) 0)
)));
getCompound(root, NBT_ICON).ifPresent(iconTag -> {
schematic.icon = new SchematicItem(
getString(iconTag, NBT_ICON_ID).orElse("minecraft:dirt"),
getByte(iconTag, NBT_ICON_COUNT).orElse((byte) 1),
getShort(iconTag, NBT_ICON_DAMAGE).orElse((short) 0)
);
});
}

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

short width = getShortOrThrow(root, NBT_WIDTH);
short height = getShortOrThrow(root, NBT_HEIGHT);
short length = getShortOrThrow(root, NBT_LENGTH);

builder.width(width).height(height).length(length);
log.trace("Dimensions: width={}, height={}, length={}", width, height, length);
schematic.width = (int) getShort(root, NBT_WIDTH).orElse((short) 0);
schematic.height = (int) getShort(root, NBT_HEIGHT).orElse((short) 0);
schematic.length = (int) getShort(root, NBT_LENGTH).orElse((short) 0);

/* Mappings */
final CompoundTag mapping = getCompoundOrThrow(root, NBT_MAPPING_SCHEMATICA);
log.trace("Mapping size: {}", mapping.size());
Map<Integer, SchematicBlock> blocksById = new HashMap<>();
Map<Integer, String> blockNamesById = mapping.entrySet().stream()
.collect(toMap(
entry -> ((ShortTag) entry.getValue()).asInt(), // ID
Entry::getKey // Name
));
final CompoundTag paletteTag = getCompoundOrThrow(root, NBT_MAPPING_SCHEMATICA);
final int biggestId = paletteTag.values().stream().mapToInt(tag -> tag instanceof NumberTag ? ((NumberTag<?>) tag).asInt() : 0).max().orElse(0) + 1;
log.trace("Palette size: {}, biggest ID: {}", paletteTag.size(), biggestId);
final String[] palette = new String[biggestId];
for (Entry<String, Tag<?>> entry : paletteTag) {
final String blockName = entry.getKey();
final int index = ((NumberTag<?>) entry.getValue()).asInt();
palette[index] = blockName;
}

// Load the (optional) palette
final byte[] blocks = getByteArrayOrThrow(root, NBT_BLOCKS);
final byte[] blockMetadata = getByteArrayOrThrow(root, NBT_DATA);
final byte[] blocksRaw = getByteArrayOrThrow(root, NBT_BLOCKS);
final byte[] blockDataRaw = getByteArrayOrThrow(root, NBT_DATA);

boolean extra = false;
byte[] extraBlocks = null;
if (root.containsKey(NBT_ADD_BLOCKS)) {
extra = true;
byte[] extraBlocksNibble = getByteArrayOrThrow(root, NBT_ADD_BLOCKS);
extraBlocks = new byte[extraBlocksNibble.length * 2];
for (int i = 0; i < extraBlocksNibble.length; i++) {
extraBlocks[i * 2] = (byte) ((extraBlocksNibble[i] >> 4) & 0xF);
extraBlocks[i * 2 + 1] = (byte) (extraBlocksNibble[i] & 0xF);
}
extra = true;
} else if (root.containsKey(NBT_ADD_BLOCKS_SCHEMATICA)) {
extraBlocks = getByteArrayOrThrow(root, NBT_ADD_BLOCKS_SCHEMATICA);
extra = true;
extraBlocks = getByteArrayOrThrow(root, NBT_ADD_BLOCKS_SCHEMATICA);
}

int totalVolume = blocksRaw.length;
int expectedTotalVolume = schematic.width * schematic.height * schematic.length;
if (totalVolume != expectedTotalVolume) {
log.warn("Number of blocks does not match expected. Expected {} blocks, but got {}", expectedTotalVolume, totalVolume);
}

// Load the block data
SchematicBlock[][][] blockData = new SchematicBlock[width][height][length];

int expectedBlocks = width * height * length;
if (blocks.length != expectedBlocks)
log.warn("Number of blocks does not match expected. Expected {} blocks, but got {}", expectedBlocks, blocks.length);

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

final int blockId = (blocks[index] & 0xFF) | (extra ? ((extraBlocks[index] & 0xFF) << 8) : 0);
final int metadata = blockMetadata[index] & 0xFF;

final SchematicBlock block = blocksById.computeIfAbsent(blockId + metadata * 10067/* prime number */, key -> {
String blockName = blockNamesById.getOrDefault(blockId, DEFAULT_BLOCK_NAME);
if (metadata != 0) blockName += "[metadata=" + metadata + ']';
int[] blocks = new int[totalVolume];
int[] blockMetadata = new int[totalVolume];

return new SchematicBlock(blockName);
});
for (int index = 0; index < totalVolume; index++) {
final int blockId = (blocksRaw[index] & 0xFF) | (extra ? ((extraBlocks[index] & 0xFF) << 8) : 0);
final int metadata = blockDataRaw[index] & 0xFF;

blockData[x][y][z] = block;
}
}
blocks[index] = blockId;
blockMetadata[index] = metadata;
}

builder.blocks(blockData);
log.debug("Loaded {} blocks", width * height * length);
schematic.blockIds = blocks;
schematic.blockMetadata = blockMetadata;
schematic.blockPalette = palette;
log.debug("Loaded {} blocks", blocks.length);
}

private void parseBlockEntities(CompoundTag root, Builder builder) {
private void parseBlockEntities(CompoundTag root, SchematicaSchematic schematic) {
final ListTag<CompoundTag> blockEntitiesTag = getCompoundList(root, NBT_TILE_ENTITIES).orElse(null);
if (blockEntitiesTag == null) {
log.trace("No block entities found");
Expand All @@ -166,11 +159,11 @@ private void parseBlockEntities(CompoundTag root, Builder builder) {
blockEntities[i++] = blockEntity;
}

schematic.blockEntities = blockEntities;
log.debug("Loaded {} block entities", blockEntities.length);
builder.blockEntities(blockEntities);
}

private void parseEntities(CompoundTag root, Builder builder) {
private void parseEntities(CompoundTag root, SchematicaSchematic schematic) {
final ListTag<CompoundTag> entitiesTag = getCompoundList(root, NBT_ENTITIES).orElse(null);
if (entitiesTag == null) {
log.trace("No entities found");
Expand All @@ -186,13 +179,13 @@ private void parseEntities(CompoundTag root, Builder builder) {
entities[i++] = entity;
}

schematic.entities = entities;
log.debug("Loaded {} entities", entities.length);
builder.entities(entities);
}

private void parseMaterials(CompoundTag root, Builder builder) {
private void parseMaterials(CompoundTag root, SchematicaSchematic schematic) {
log.trace("Parsing materials");
getString(root, NBT_MATERIALS).ifPresent(builder::materials);
getString(root, NBT_MATERIALS).ifPresent(materials -> schematic.materials = materials);
}

@Override
Expand Down
Loading

0 comments on commit 7256208

Please sign in to comment.