From fc2662e51ec6035da44a0f82e42e81f9d0698b58 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 24 Feb 2022 10:33:25 +0100 Subject: [PATCH] Fix //snapshot in 1.18 and re-implement biome/entity restoration (#1620) * Re-add "//snap" and "//snapshot" * Place code in correct method * Use CompoundBinaryTags in AnvilChunk18 and implement biome/entity restoration * Address comments * Fix biome reading * Fix retrieval of entities from zipped snapshot world Co-authored-by: Alex --- .../v1_17_R1_2/PaperweightFaweAdapter.java | 4 +- .../fawe/v1_18_R1/PaperweightFaweAdapter.java | 4 +- .../core/util/NbtUtils.java | 48 ++++ .../factory/parser/DefaultItemParser.java | 5 +- .../platform/PlatformCommandManager.java | 4 +- .../worldedit/world/chunk/AnvilChunk15.java | 2 - .../worldedit/world/chunk/AnvilChunk17.java | 12 +- .../worldedit/world/chunk/AnvilChunk18.java | 245 ++++++++++++++---- .../world/chunk/PackedIntArrayReader.java | 36 ++- .../world/storage/ChunkStoreHelper.java | 47 ++-- .../storage/TrueZipMcRegionChunkStore.java | 17 +- .../storage/ZippedMcRegionChunkStore.java | 15 +- 12 files changed, 334 insertions(+), 105 deletions(-) diff --git a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightFaweAdapter.java index ecdd507f9..b69272704 100644 --- a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightFaweAdapter.java @@ -8,6 +8,7 @@ import com.fastasyncworldedit.core.entity.LazyBaseEntity; import com.fastasyncworldedit.core.extent.processor.lighting.RelighterFactory; import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.implementation.packet.ChunkPacket; +import com.fastasyncworldedit.core.util.NbtUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -367,8 +368,7 @@ public final class PaperweightFaweAdapter extends CachedBukkitAdapter implements readEntityIntoTag(mcEntity, minecraftTag); //add Id for AbstractChangeSet to work final CompoundBinaryTag tag = (CompoundBinaryTag) toNativeBinary(minecraftTag); - final Map tags = new HashMap<>(); - tag.keySet().forEach(key -> tags.put(key, tag.get(key))); + final Map tags = NbtUtils.getCompoundBinaryTagValues(tag); tags.put("Id", StringBinaryTag.of(id)); return CompoundBinaryTag.from(tags); }; diff --git a/worldedit-bukkit/adapters/adapter-1_18/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R1/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_18/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R1/PaperweightFaweAdapter.java index a5b24b387..b41718636 100644 --- a/worldedit-bukkit/adapters/adapter-1_18/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R1/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_18/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R1/PaperweightFaweAdapter.java @@ -8,6 +8,7 @@ import com.fastasyncworldedit.core.entity.LazyBaseEntity; import com.fastasyncworldedit.core.extent.processor.lighting.RelighterFactory; import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.implementation.packet.ChunkPacket; +import com.fastasyncworldedit.core.util.NbtUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -359,8 +360,7 @@ public final class PaperweightFaweAdapter extends CachedBukkitAdapter implements readEntityIntoTag(mcEntity, minecraftTag); //add Id for AbstractChangeSet to work final CompoundBinaryTag tag = (CompoundBinaryTag) toNativeBinary(minecraftTag); - final Map tags = new HashMap<>(); - tag.keySet().forEach(key -> tags.put(key, tag.get(key))); + final Map tags = NbtUtils.getCompoundBinaryTagValues(tag); tags.put("Id", StringBinaryTag.of(id)); return CompoundBinaryTag.from(tags); }; diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java index 9b39d9270..e6dbee8e5 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/NbtUtils.java @@ -2,9 +2,16 @@ package com.fastasyncworldedit.core.util; import com.sk89q.worldedit.util.nbt.BinaryTag; import com.sk89q.worldedit.util.nbt.BinaryTagType; +import com.sk89q.worldedit.util.nbt.BinaryTagTypes; +import com.sk89q.worldedit.util.nbt.ByteBinaryTag; import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; +import com.sk89q.worldedit.util.nbt.IntBinaryTag; +import com.sk89q.worldedit.util.nbt.ShortBinaryTag; import com.sk89q.worldedit.world.storage.InvalidFormatException; +import java.util.HashMap; +import java.util.Map; + public class NbtUtils { /** @@ -32,4 +39,45 @@ public class NbtUtils { return childTagCast; } + /** + * Get an integer from a tag. + * + * @param tag the tag to read from + * @param key the key to look for + * @return child tag + * @throws InvalidFormatException if the format of the items is invalid + * @since TODO + */ + public static int getInt(CompoundBinaryTag tag, String key) throws InvalidFormatException { + BinaryTag childTag = tag.get(key); + if (childTag == null) { + throw new InvalidFormatException("Missing a \"" + key + "\" tag"); + } + + BinaryTagType type = childTag.type(); + if (type == BinaryTagTypes.INT) { + return ((IntBinaryTag) childTag).intValue(); + } + if (type == BinaryTagTypes.BYTE) { + return ((ByteBinaryTag) childTag).intValue(); + } + if (type == BinaryTagTypes.SHORT) { + return ((ShortBinaryTag) childTag).intValue(); + } + throw new InvalidFormatException(key + " tag is not of int, short or byte tag type."); + } + + /** + * Get a mutable map of the values stored inside a {@link CompoundBinaryTag} + * + * @param tag {@link CompoundBinaryTag} to get values for + * @return Mutable map of values + * @since TODO + */ + public static Map getCompoundBinaryTagValues(CompoundBinaryTag tag) { + Map value = new HashMap<>(); + tag.forEach((e) -> value.put(e.getKey(), e.getValue())); + return value; + } + } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/DefaultItemParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/DefaultItemParser.java index d10d3c836..0f22d0b93 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/DefaultItemParser.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/DefaultItemParser.java @@ -20,6 +20,7 @@ package com.sk89q.worldedit.extension.factory.parser; import com.fastasyncworldedit.core.configuration.Caption; +import com.fastasyncworldedit.core.util.NbtUtils; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.blocks.BaseItem; import com.sk89q.worldedit.blocks.BaseItemStack; @@ -132,9 +133,7 @@ public class DefaultItemParser extends InputParser { if (itemNbtData == null) { itemNbtData = otherTag; } else { - for (String key : otherTag.keySet()) { - itemNbtData.put(key, otherTag.get(key)); - } + itemNbtData.put(NbtUtils.getCompoundBinaryTagValues(otherTag)); } } catch (IOException e) { throw new NoMatchException(TranslatableComponent.of( diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java index 49a420544..3922710fc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java @@ -467,7 +467,9 @@ public final class PlatformCommandManager { ); registerSubCommands( "snapshot", - ImmutableList.of("snap"), + //FAWE start - add "/" aliases as well + ImmutableList.of("snap", "/snapshot", "/snap"), + //FAWE end "Snapshot commands for restoring backups", SnapshotCommandsRegistration.builder(), new SnapshotCommands(worldEdit) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk15.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk15.java index 13ede2ed6..7565f66c1 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk15.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk15.java @@ -31,9 +31,7 @@ import com.sk89q.worldedit.world.biome.BiomeTypes; /** * The chunk format for Minecraft 1.15 and newer */ -//FAWE start - biome and entity restore public class AnvilChunk15 extends AnvilChunk13 { - //FAWE end /** * Construct the chunk with a compound tag. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk17.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk17.java index b930610d6..3eb4a47bc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk17.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk17.java @@ -95,7 +95,7 @@ public class AnvilChunk17 implements Chunk { blocks = new BlockState[16][]; // initialise with default length - ListBinaryTag sections = NbtUtils.getChildTag(rootTag, "Sections", BinaryTagTypes.LIST); + ListBinaryTag sections = rootTag.getList("Sections"); for (BinaryTag rawSectionTag : sections) { if (!(rawSectionTag instanceof CompoundBinaryTag)) { @@ -107,7 +107,7 @@ public class AnvilChunk17 implements Chunk { continue; // Empty section. } - int y = NbtUtils.getChildTag(sectionTag, "Y", BinaryTagTypes.BYTE).value(); + int y = NbtUtils.getInt(tag, "Y"); updateSectionIndexRange(y); // parse palette @@ -143,7 +143,7 @@ public class AnvilChunk17 implements Chunk { } // parse block states - long[] blockStatesSerialized = NbtUtils.getChildTag(sectionTag, "BlockStates", BinaryTagTypes.LONG_ARRAY).value(); + long[] blockStatesSerialized = sectionTag.getLongArray("BlockStates"); BlockState[] chunkSectionBlocks = new BlockState[4096]; blocks[y - minSectionPosition] = chunkSectionBlocks; @@ -197,7 +197,7 @@ public class AnvilChunk17 implements Chunk { if (rootTag.get("TileEntities") == null) { return; } - ListBinaryTag tags = NbtUtils.getChildTag(rootTag, "TileEntities", BinaryTagTypes.LIST); + ListBinaryTag tags = rootTag.getList("TileEntities"); for (BinaryTag tag : tags) { if (!(tag instanceof CompoundBinaryTag)) { @@ -274,7 +274,7 @@ public class AnvilChunk17 implements Chunk { if (rootTag.get("Biomes") == null) { return; } - int[] stored = NbtUtils.getChildTag(rootTag, "Biomes", BinaryTagTypes.INT_ARRAY).value(); + int[] stored = rootTag.getIntArray("Biomes"); for (int i = 0; i < 1024; i++) { biomes[i] = BiomeTypes.getLegacy(stored[i]); } @@ -297,7 +297,7 @@ public class AnvilChunk17 implements Chunk { if (entityTagSupplier == null || (entityTag = entityTagSupplier.get()) == null) { return; } - ListBinaryTag tags = NbtUtils.getChildTag(entityTag, "Entities", BinaryTagTypes.LIST); + ListBinaryTag tags = entityTag.getList("Entities"); for (BinaryTag tag : tags) { if (!(tag instanceof CompoundBinaryTag)) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk18.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk18.java index 6d8348d5e..018ca46ab 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk18.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/AnvilChunk18.java @@ -19,91 +19,134 @@ package com.sk89q.worldedit.world.chunk; +import com.fastasyncworldedit.core.util.NbtUtils; import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.IntTag; -import com.sk89q.jnbt.ListTag; -import com.sk89q.jnbt.LongArrayTag; -import com.sk89q.jnbt.NBTUtils; -import com.sk89q.jnbt.Tag; +import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.registry.state.Property; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.util.nbt.BinaryTag; +import com.sk89q.worldedit.util.nbt.BinaryTagTypes; +import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; +import com.sk89q.worldedit.util.nbt.ListBinaryTag; import com.sk89q.worldedit.world.DataException; +import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.entity.EntityTypes; import com.sk89q.worldedit.world.storage.InvalidFormatException; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; /** * The chunk format for Minecraft 1.18 and newer */ public class AnvilChunk18 implements Chunk { - private final CompoundTag rootTag; + //FAWE start - CBT + private final CompoundBinaryTag rootTag; + //FAWE end private final Int2ObjectOpenHashMap blocks; - private final int rootX; - private final int rootZ; + //FAWE start - entity and biome restore + private final int sectionCount; + private final Supplier entityTagSupplier; + private Int2ObjectOpenHashMap biomes = null; + private List entities; + private Map tileEntities; + //FAWE end - private Map> tileEntities; /** * Construct the chunk with a compound tag. * * @param tag the tag to read * @throws DataException on a data error + * @deprecated Use {@link AnvilChunk18#AnvilChunk18(CompoundBinaryTag, Supplier)} */ + @Deprecated public AnvilChunk18(CompoundTag tag) throws DataException { + //FAWE start - CBT + this(tag.asBinaryTag(), () -> null); + } + + /** + * Construct the chunk with a compound tag. + * + * @param tag the tag to read + * @throws DataException on a data error + * @deprecated Use {@link AnvilChunk18#AnvilChunk18(CompoundBinaryTag, Supplier)} + * @since TODO + */ + @Deprecated + public AnvilChunk18(CompoundTag tag, Supplier entitiesTag) throws DataException { + //FAWE start - CBT + this(tag.asBinaryTag(), () -> { + CompoundTag compoundTag = entitiesTag.get(); + return compoundTag == null ? null : compoundTag.asBinaryTag(); + }); + } + + /** + * Construct the chunk with a compound tag. + * + * @param tag the tag to read + * @throws DataException on a data error + * @since TODO + */ + public AnvilChunk18(CompoundBinaryTag tag, Supplier entityTag) throws DataException { + //FAWE end rootTag = tag; + entityTagSupplier = entityTag; - rootX = NBTUtils.getChildTag(rootTag.getValue(), "xPos", IntTag.class).getValue(); - rootZ = NBTUtils.getChildTag(rootTag.getValue(), "zPos", IntTag.class).getValue(); + //FAWE start - CBT + ListBinaryTag sections = rootTag.getList("sections"); + this.sectionCount = sections.size(); - List sections = NBTUtils.getChildTag(rootTag.getValue(), "sections", ListTag.class).getValue(); blocks = new Int2ObjectOpenHashMap<>(sections.size()); - for (Tag rawSectionTag : sections) { - if (!(rawSectionTag instanceof CompoundTag sectionTag)) { + for (BinaryTag rawSectionTag : sections) { + if (!(rawSectionTag instanceof CompoundBinaryTag sectionTag)) { continue; } - Object yValue = sectionTag.getValue().get("Y").getValue(); // sometimes a byte, sometimes an int - if (!(yValue instanceof Number)) { - throw new InvalidFormatException("Y is not numeric: " + yValue); - } - int y = ((Number) yValue).intValue(); + int y = NbtUtils.getInt(sectionTag, "Y"); // sometimes a byte, sometimes an int - Tag rawBlockStatesTag = sectionTag.getValue().get("block_states"); // null for sections outside of the world limits - if (rawBlockStatesTag instanceof CompoundTag blockStatesTag) { + BinaryTag rawBlockStatesTag = sectionTag.get("block_states"); // null for sections outside of the world limits + if (rawBlockStatesTag instanceof CompoundBinaryTag blockStatesTag) { // parse palette - List paletteEntries = blockStatesTag.getList("palette", CompoundTag.class); + ListBinaryTag paletteEntries = blockStatesTag.getList("palette"); int paletteSize = paletteEntries.size(); if (paletteSize == 0) { continue; } BlockState[] palette = new BlockState[paletteSize]; for (int paletteEntryId = 0; paletteEntryId < paletteSize; paletteEntryId++) { - CompoundTag paletteEntry = paletteEntries.get(paletteEntryId); + CompoundBinaryTag paletteEntry = (CompoundBinaryTag) paletteEntries.get(paletteEntryId); BlockType type = BlockTypes.get(paletteEntry.getString("Name")); if (type == null) { throw new InvalidFormatException("Invalid block type: " + paletteEntry.getString("Name")); } BlockState blockState = type.getDefaultState(); - if (paletteEntry.containsKey("Properties")) { - CompoundTag properties = NBTUtils.getChildTag(paletteEntry.getValue(), "Properties", CompoundTag.class); + BinaryTag propertiesTag = paletteEntry.get("Properties"); + if (propertiesTag instanceof CompoundBinaryTag properties) { for (Property property : blockState.getStates().keySet()) { - if (properties.containsKey(property.getName())) { - String value = properties.getString(property.getName()); + String value; + if (!(value = properties.getString(property.getName())).isEmpty()) { try { blockState = getBlockStateWith(blockState, property, value); } catch (IllegalArgumentException e) { - throw new InvalidFormatException("Invalid block state for " + blockState.getBlockType().getId() + ", " + property.getName() + ": " + value); + throw new InvalidFormatException("Invalid block state for " + blockState + .getBlockType() + .getId() + ", " + property.getName() + ": " + value); } } } @@ -117,7 +160,7 @@ public class AnvilChunk18 implements Chunk { } // parse block states - long[] blockStatesSerialized = NBTUtils.getChildTag(blockStatesTag.getValue(), "data", LongArrayTag.class).getValue(); + long[] blockStatesSerialized = blockStatesTag.getLongArray("data"); BlockState[] chunkSectionBlocks = new BlockState[16 * 16 * 16]; blocks.put(y, chunkSectionBlocks); @@ -125,6 +168,7 @@ public class AnvilChunk18 implements Chunk { readBlockStates(palette, blockStatesSerialized, chunkSectionBlocks); } } + //FAWE end } protected void readBlockStates(BlockState[] palette, long[] blockStatesSerialized, BlockState[] chunkSectionBlocks) throws InvalidFormatException { @@ -146,26 +190,24 @@ public class AnvilChunk18 implements Chunk { * Used to load the tile entities. */ private void populateTileEntities() throws DataException { + //FAWE start - CBT tileEntities = new HashMap<>(); - if (!rootTag.getValue().containsKey("block_entities")) { + if (!(rootTag.get("block_entities") instanceof ListBinaryTag tags)) { return; } - List tags = NBTUtils.getChildTag(rootTag.getValue(), - "block_entities", ListTag.class).getValue(); - - for (Tag tag : tags) { - if (!(tag instanceof CompoundTag t)) { + for (BinaryTag tag : tags) { + if (!(tag instanceof CompoundBinaryTag t)) { throw new InvalidFormatException("CompoundTag expected in block_entities"); } - Map values = new HashMap<>(t.getValue()); - int x = ((IntTag) values.get("x")).getValue(); - int y = ((IntTag) values.get("y")).getValue(); - int z = ((IntTag) values.get("z")).getValue(); + int x = t.getInt("x"); + int y = t.getInt("y"); + int z = t.getInt("z"); BlockVector3 vec = BlockVector3.at(x, y, z); - tileEntities.put(vec, values); + tileEntities.put(vec, t); } + //FAWE end } /** @@ -178,24 +220,21 @@ public class AnvilChunk18 implements Chunk { * @throws DataException thrown if there is a data error */ @Nullable - private CompoundTag getBlockTileEntity(BlockVector3 position) throws DataException { + private CompoundBinaryTag getBlockTileEntity(BlockVector3 position) throws DataException { + //FAWE start - CBT if (tileEntities == null) { populateTileEntities(); } - Map values = tileEntities.get(position); - if (values == null) { - return null; - } - - return new CompoundTag(values); + return tileEntities.get(position); + //FAWE end } @Override public BaseBlock getBlock(BlockVector3 position) throws DataException { - int x = position.getX() - rootX * 16; + int x = position.getX() & 15; int y = position.getY(); - int z = position.getZ() - rootZ * 16; + int z = position.getZ() & 15; int section = y >> 4; int yIndex = y & 0x0F; @@ -206,7 +245,7 @@ public class AnvilChunk18 implements Chunk { } BlockState state = sectionBlocks[sectionBlocks.length == 1 ? 0 : ((yIndex << 8) | (z << 4) | x)]; - CompoundTag tileEntity = getBlockTileEntity(position); + CompoundBinaryTag tileEntity = getBlockTileEntity(position); if (tileEntity != null) { return state.toBaseBlock(tileEntity); @@ -215,4 +254,110 @@ public class AnvilChunk18 implements Chunk { return state.toBaseBlock(); } + @Override + public BiomeType getBiome(final BlockVector3 position) throws DataException { + if (biomes == null) { + populateBiomes(); + } + int x = (position.getX() & 15) >> 2; + int y = (position.getY() & 15) >> 2; + int z = (position.getZ() & 15) >> 2; + int section = position.getY() >> 4; + BiomeType[] sectionBiomes = biomes.get(section); + if (sectionBiomes.length == 1) { + return sectionBiomes[0]; + } + return biomes.get(section)[y << 4 | z << 2 | x]; + } + + private void populateBiomes() throws DataException { + biomes = new Int2ObjectOpenHashMap<>(sectionCount); + ListBinaryTag sections = rootTag.getList("sections"); + for (BinaryTag rawSectionTag : sections) { + if (!(rawSectionTag instanceof CompoundBinaryTag sectionTag)) { + continue; + } + + int y = NbtUtils.getInt(sectionTag, "Y"); // sometimes a byte, sometimes an int + + BinaryTag rawBlockStatesTag = sectionTag.get("biomes"); // null for sections outside of the world limits + if (rawBlockStatesTag instanceof CompoundBinaryTag biomeTypesTag) { + + // parse palette + ListBinaryTag paletteEntries = biomeTypesTag.getList("palette"); + int paletteSize = paletteEntries.size(); + if (paletteSize == 0) { + continue; + } + BiomeType[] palette = new BiomeType[paletteSize]; + for (int paletteEntryId = 0; paletteEntryId < paletteSize; paletteEntryId++) { + String paletteEntry = paletteEntries.getString(paletteEntryId); + BiomeType type = BiomeType.REGISTRY.get(paletteEntry); + if (type == null) { + throw new InvalidFormatException("Invalid biome type: " + paletteEntry); + } + palette[paletteEntryId] = type; + } + if (paletteSize == 1) { + // the same block everywhere + biomes.put(y, palette); + continue; + } + + // parse block states + long[] biomesSerialized = biomeTypesTag.getLongArray("data"); + if (biomesSerialized.length == 0) { + throw new InvalidFormatException("Biome data not present."); + } + + BiomeType[] chunkSectionBiomes = new BiomeType[64]; + biomes.put(y, chunkSectionBiomes); + + readBiomes(palette, biomesSerialized, chunkSectionBiomes); + } + } + } + + protected void readBiomes(BiomeType[] palette, long[] biomesSerialized, BiomeType[] chunkSectionBiomes) throws + InvalidFormatException { + PackedIntArrayReader reader = new PackedIntArrayReader(biomesSerialized, 64); + for (int biomePos = 0; biomePos < chunkSectionBiomes.length; biomePos++) { + int index = reader.get(biomePos); + if (index >= palette.length) { + throw new InvalidFormatException("Invalid biome table entry: " + index); + } + chunkSectionBiomes[biomePos] = palette[index]; + } + } + + @Override + public List getEntities() throws DataException { + if (entities == null) { + populateEntities(); + } + return entities; + } + + /** + * Used to load the biomes. + */ + private void populateEntities() throws DataException { + entities = new ArrayList<>(); + CompoundBinaryTag entityTag; + if (entityTagSupplier == null || (entityTag = entityTagSupplier.get()) == null) { + return; + } + ListBinaryTag tags = NbtUtils.getChildTag(entityTag, "Entities", BinaryTagTypes.LIST); + + for (BinaryTag tag : tags) { + if (!(tag instanceof CompoundBinaryTag t)) { + throw new InvalidFormatException("CompoundTag expected in Entities"); + } + + entities.add(new BaseEntity(EntityTypes.get(t.getString("id")), LazyReference.computed(t))); + } + + } + + } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/PackedIntArrayReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/PackedIntArrayReader.java index 72de4d890..bb2c309a5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/PackedIntArrayReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/chunk/PackedIntArrayReader.java @@ -32,28 +32,56 @@ public class PackedIntArrayReader { } } - private static final int SIZE = 4096; - private final long[] data; private final int elementBits; private final long maxValue; private final int elementsPerLong; private final int factor; + //FAWE start - allow other sizes of data to be parsed + private final int storedSize; + //FAWE end + /** + * Create a new PackedIntArrayReader instance based on an array of longs containing 4096 integers. + * + * @param data long array containing data + */ public PackedIntArrayReader(long[] data) { this.data = data; + this.storedSize = 4096; this.elementBits = data.length * 64 / 4096; this.maxValue = (1L << elementBits) - 1L; this.elementsPerLong = 64 / elementBits; this.factor = FACTORS[elementsPerLong - 1]; - int j = (SIZE + this.elementsPerLong - 1) / this.elementsPerLong; + int j = (storedSize + this.elementsPerLong - 1) / this.elementsPerLong; if (j != data.length) { throw new IllegalStateException("Invalid packed-int array provided, should be of length " + j); } } + //FAWE start - allow other sizes of data to be parsed + /** + * Create a new PackedIntArrayReader instance based on an array of longs containing a certain number of integers. + * + * @param data long array containing data + * @param storedSize the amount of integers stored in the long array + */ + public PackedIntArrayReader(long[] data, int storedSize) { + this.data = data; + this.storedSize = storedSize; + this.elementBits = data.length * 64 / storedSize; + this.maxValue = (1L << elementBits) - 1L; + this.elementsPerLong = 64 / elementBits; + this.factor = FACTORS[elementsPerLong - 1]; + int j = (storedSize + this.elementsPerLong - 1) / this.elementsPerLong; + if (j != data.length) { + throw new IllegalStateException("Invalid packed-int array provided, should be of length " + j); + } + } + //FAWE end + public int get(int index) { - checkElementIndex(index, SIZE); + checkElementIndex(index, storedSize); int i = this.adjustIndex(index); long l = this.data[i]; int j = (index - i * this.elementsPerLong) * this.elementBits; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java index e6db601c3..9c264d7b9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ChunkStoreHelper.java @@ -73,28 +73,8 @@ public class ChunkStoreHelper { * @throws DataException if the rootTag is not valid chunk data */ public static Chunk getChunk(CompoundTag rootTag) throws DataException { - int dataVersion = rootTag.getInt("DataVersion"); - if (dataVersion == 0) { - dataVersion = -1; - } - - final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); - final int currentDataVersion = platform.getDataVersion(); - if ((dataVersion > 0 || hasLevelSections(rootTag)) && dataVersion < currentDataVersion) { // only fix up MCA format, DFU doesn't support MCR chunks - final DataFixer dataFixer = platform.getDataFixer(); - if (dataFixer != null) { - rootTag = (CompoundTag) AdventureNBTConverter.fromAdventure(dataFixer.fixUp(DataFixer.FixTypes.CHUNK, - rootTag.asBinaryTag(), dataVersion)); - dataVersion = currentDataVersion; - } - } - - if (dataVersion >= Constants.DATA_VERSION_MC_1_18) { - return new AnvilChunk18(rootTag); - } //FAWE start - biome and entity restore return getChunk(rootTag, () -> null); - //FAWE end } /** @@ -105,9 +85,32 @@ public class ChunkStoreHelper { * location * @return a Chunk implementation * @throws DataException if the rootTag is not valid chunk data + * @since TODO */ public static Chunk getChunk(CompoundTag rootTag, Supplier entitiesTag) throws DataException { //FAWE end + int dataVersion = rootTag.getInt("DataVersion"); + if (dataVersion == 0) { + dataVersion = -1; + } + + final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + final int currentDataVersion = platform.getDataVersion(); + if ((dataVersion > 0 || hasLevelSections(rootTag)) && dataVersion < currentDataVersion) { // only fix up MCA format, DFU doesn't support MCR chunks + final DataFixer dataFixer = platform.getDataFixer(); + if (dataFixer != null) { + //FAWE start - CBT + rootTag = (CompoundTag) AdventureNBTConverter.fromAdventure(dataFixer.fixUp(DataFixer.FixTypes.CHUNK, + rootTag.asBinaryTag(), dataVersion)); + //FAWE end + dataVersion = currentDataVersion; + } + } + + if (dataVersion >= Constants.DATA_VERSION_MC_1_18) { + return new AnvilChunk18(rootTag, entitiesTag); + } + Map children = rootTag.getValue(); CompoundTag tag = null; @@ -130,10 +133,6 @@ public class ChunkStoreHelper { throw new ChunkStoreException("Missing root 'Level' tag"); } - int dataVersion = rootTag.getInt("DataVersion"); - if (dataVersion == 0) { - dataVersion = -1; - } //FAWE start - biome and entity restore if (dataVersion >= Constants.DATA_VERSION_MC_1_17) { return new AnvilChunk17(tag, entitiesTag); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/TrueZipMcRegionChunkStore.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/TrueZipMcRegionChunkStore.java index 76f9a5ba3..b4e4a04d0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/TrueZipMcRegionChunkStore.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/TrueZipMcRegionChunkStore.java @@ -80,14 +80,13 @@ public class TrueZipMcRegionChunkStore extends McRegionChunkStore { protected InputStream getInputStream(String name, String worldName, @Nullable String folderOverride) throws IOException, DataException { // Detect subfolder for the world's files - if (folderOverride != null) { - if (!folderOverride.isEmpty()) { - name = folderOverride + "/" + name; - } - } else if (folder != null) { - //FAWE end + if (folder != null) { if (!folder.isEmpty()) { + //FAWE end name = folder + "/" + name; + if (folderOverride != null) { + name = name.replace("region", folderOverride); + } } } else { Pattern pattern = Pattern.compile(".*\\.mc[ra]$"); @@ -105,6 +104,12 @@ public class TrueZipMcRegionChunkStore extends McRegionChunkStore { endIndex = entryName.lastIndexOf('\\'); } folder = entryName.substring(0, endIndex); + //FAWE start - biome and entity restore + if (folderOverride != null && folder.endsWith(folderOverride)) { + name = folder + "/" + name; + break; + } + //FAWE end if (folder.endsWith("poi") || folder.endsWith("entities")) { continue; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ZippedMcRegionChunkStore.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ZippedMcRegionChunkStore.java index 067515e13..ac1626c27 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ZippedMcRegionChunkStore.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/ZippedMcRegionChunkStore.java @@ -78,14 +78,13 @@ public class ZippedMcRegionChunkStore extends McRegionChunkStore { protected InputStream getInputStream(String name, String worldName, @Nullable String folderOverride) throws IOException, DataException { // Detect subfolder for the world's files - if (folderOverride != null) { - if (!folderOverride.isEmpty()) { - name = folderOverride + "/" + name; - } - } else if (folder != null) { + if (folder != null) { if (!folder.isEmpty()) { //FAWE end name = folder + "/" + name; + if (folderOverride != null) { + name = name.replace("region", folderOverride); + } } } else { Pattern pattern = Pattern.compile(".*\\.mc[ra]$"); @@ -101,6 +100,12 @@ public class ZippedMcRegionChunkStore extends McRegionChunkStore { endIndex = entryName.lastIndexOf('\\'); } folder = entryName.substring(0, endIndex); + //FAWE start - biome and entity restore + if (folderOverride != null && folder.endsWith(folderOverride)) { + name = folder + "/" + name; + break; + } + //FAWE end if (folder.endsWith("poi") || folder.endsWith("entities")) { continue; }