From 8c948e57052f870a1a1ca1f03683b79d534825a3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 6 Nov 2021 21:25:35 -0400 Subject: [PATCH 001/288] Refactor in preparation for adding support for POI (v1.14+) and ENTITIES (v1.17+) mca files --- .../net/querz/mca/BlockStateIterator.java | 36 +++ src/main/java/net/querz/mca/Chunk.java | 237 +++----------- src/main/java/net/querz/mca/DataVersion.java | 118 +++++++ src/main/java/net/querz/mca/MCAFile.java | 220 ++----------- src/main/java/net/querz/mca/MCAFileBase.java | 301 ++++++++++++++++++ src/main/java/net/querz/mca/MCAUtil.java | 36 +-- src/main/java/net/querz/mca/Section.java | 217 ++++++++----- src/main/java/net/querz/mca/SectionBase.java | 59 ++++ .../net/querz/mca/SectionedChunkBase.java | 192 +++++++++++ .../java/net/querz/nbt/tag/CompoundTag.java | 4 + .../java/net/querz/mca/DataVersionTest.java | 41 +++ 11 files changed, 985 insertions(+), 476 deletions(-) create mode 100644 src/main/java/net/querz/mca/BlockStateIterator.java create mode 100644 src/main/java/net/querz/mca/DataVersion.java create mode 100644 src/main/java/net/querz/mca/MCAFileBase.java create mode 100644 src/main/java/net/querz/mca/SectionBase.java create mode 100644 src/main/java/net/querz/mca/SectionedChunkBase.java create mode 100644 src/test/java/net/querz/mca/DataVersionTest.java diff --git a/src/main/java/net/querz/mca/BlockStateIterator.java b/src/main/java/net/querz/mca/BlockStateIterator.java new file mode 100644 index 00000000..668360b3 --- /dev/null +++ b/src/main/java/net/querz/mca/BlockStateIterator.java @@ -0,0 +1,36 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +import java.util.Iterator; + +/** + * Enhanced iterable/iterator for iterating over {@link Section} block data. + * class name {@link Section#blocksStates()}. + */ +public interface BlockStateIterator extends Iterable, Iterator { + /** + * Sets the block state for the current block. + * Be careful to remember that the block state tag returned by this iterator is a reference + * that will affect all blocks using that tag. If your intention is to modify "just this one block" + * then copy the tag before modification - then call this function. + * @param state State to set. Must not be null. + */ + void setBlockStateAtCurrent(CompoundTag state); + + /** + * Performs palette and block state cleanup if, and only if, changes were made via this iterator. + */ + void cleanupPaletteAndBlockStatesIfDirty(); + + /** current block index (in range 0-4095) */ + int currentIndex(); + /** current block x within section (in range 0-15) */ + int currentX(); + /** current block z within section (in range 0-15) */ + int currentZ(); + /** current block y within section (in range 0-15) */ + int currentY(); + /** current block world level y */ + int currentWorldY(); +} diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 7c3c620f..9bfd5a5a 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -15,40 +15,42 @@ import java.util.Iterator; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static net.querz.mca.LoadFlags.*; -public class Chunk implements Iterable
{ +/** + * Represents a REGION data mca chunk. + */ +public class Chunk extends SectionedChunkBase
{ - public static final int DEFAULT_DATA_VERSION = 2567; - - private boolean partial; - private boolean raw; - - private int lastMCAUpdate; - - private CompoundTag data; - - private int dataVersion; - private long lastUpdate; - private long inhabitedTime; - private int[] biomes; - private CompoundTag heightMaps; - private CompoundTag carvingMasks; - private Map sections = new TreeMap<>(); - private ListTag entities; - private ListTag tileEntities; - private ListTag tileTicks; - private ListTag liquidTicks; - private ListTag> lights; - private ListTag> liquidsToBeTicked; - private ListTag> toBeTicked; - private ListTag> postProcessing; - private String status; - private CompoundTag structures; + /** + * The default chunk data version used when no custom version is supplied. + *

Deprecated: use {@code DataVersion.latest().id()} instead. + */ + @Deprecated + public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); + + protected int dataVersion; + protected long lastUpdate; + protected long inhabitedTime; + protected int[] biomes; + protected CompoundTag heightMaps; + protected CompoundTag carvingMasks; + protected ListTag entities; // never populated for versions >= 1.17 + protected ListTag tileEntities; + protected ListTag tileTicks; + protected ListTag liquidTicks; + protected ListTag> lights; + protected ListTag> liquidsToBeTicked; + protected ListTag> toBeTicked; + protected ListTag> postProcessing; + protected String status; + protected CompoundTag structures; Chunk(int lastMCAUpdate) { - this.lastMCAUpdate = lastMCAUpdate; + super(lastMCAUpdate); } /** @@ -56,20 +58,11 @@ public class Chunk implements Iterable

{ * @param data The raw base data to be used. */ public Chunk(CompoundTag data) { - this.data = data; - initReferences(ALL_DATA); + super(data); } - private void initReferences(long loadFlags) { - if (data == null) { - throw new NullPointerException("data cannot be null"); - } - - if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { - raw = true; - return; - } - + @Override + protected void initReferences(long loadFlags) { CompoundTag level; if ((level = data.getCompoundTag("Level")) == null) { throw new IllegalArgumentException("data does not contain \"Level\" tag"); @@ -121,67 +114,6 @@ private void initReferences(long loadFlags) { sections.put(sectionIndex, newSection); } } - - // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. - if (loadFlags != ALL_DATA) { - data = null; - partial = true; - } - } - - /** - * Serializes this chunk to a RandomAccessFile. - * @param raf The RandomAccessFile to be written to. - * @param xPos The x-coordinate of the chunk. - * @param zPos The z-coodrinate of the chunk. - * @return The amount of bytes written to the RandomAccessFile. - * @throws UnsupportedOperationException When something went wrong during writing. - * @throws IOException When something went wrong during writing. - */ - public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { - if (partial) { - throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); - try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { - new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); - } - byte[] rawData = baos.toByteArray(); - raf.writeInt(rawData.length + 1); // including the byte to store the compression type - raf.writeByte(CompressionType.ZLIB.getID()); - raf.write(rawData); - return rawData.length + 5; - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * @param raf The RandomAccessFile to read the chunk data from. - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf) throws IOException { - deserialize(raf, ALL_DATA); - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * @param raf The RandomAccessFile to read the chunk data from. - * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { - byte compressionTypeByte = raf.readByte(); - CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); - if (compressionType == null) { - throw new IOException("invalid compression type " + compressionTypeByte); - } - BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); - NamedTag tag = new NBTDeserializer(false).fromStream(dis); - if (tag != null && tag.getTag() instanceof CompoundTag) { - data = (CompoundTag) tag.getTag(); - initReferences(loadFlags); - } else { - throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); - } } /** @@ -324,7 +256,6 @@ public int getDataVersion() { * @param dataVersion The DataVersion to be set. */ public void setDataVersion(int dataVersion) { - checkRaw(); this.dataVersion = dataVersion; for (Section section : sections.values()) { if (section != null) { @@ -333,22 +264,6 @@ public void setDataVersion(int dataVersion) { } } - /** - * @return The timestamp when this region file was last updated in seconds since 1970-01-01. - */ - public int getLastMCAUpdate() { - return lastMCAUpdate; - } - - /** - * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. - * @param lastMCAUpdate The time in seconds since 1970-01-01. - */ - public void setLastMCAUpdate(int lastMCAUpdate) { - checkRaw(); - this.lastMCAUpdate = lastMCAUpdate; - } - /** * @return The generation station of this chunk. */ @@ -365,25 +280,6 @@ public void setStatus(String status) { this.status = status; } - /** - * Fetches the section at the given y-coordinate. - * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. - * @return The Section. - */ - public Section getSection(int sectionY) { - return sections.get(sectionY); - } - - /** - * Sets a section at a givesn y-coordinate - * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. - * @param section The section to be set. - */ - public void setSection(int sectionY, Section section) { - checkRaw(); - sections.put(sectionY, section); - } - /** * @return The timestamp when this chunk was last updated as a UNIX timestamp. */ @@ -628,14 +524,8 @@ public void cleanupPalettesAndBlockStates() { } } - private void checkRaw() { - if (raw) { - throw new UnsupportedOperationException("cannot update field when working with raw data"); - } - } - public static Chunk newChunk() { - return newChunk(DEFAULT_DATA_VERSION); + return Chunk.newChunk(DataVersion.latest().id()); } public static Chunk newChunk(int dataVersion) { @@ -647,14 +537,7 @@ public static Chunk newChunk(int dataVersion) { return c; } - /** - * Provides a reference to the full chunk data. - * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. - */ - public CompoundTag getHandle() { - return data; - } - + @Override public CompoundTag updateHandle(int xPos, int zPos) { if (raw) { return data; @@ -675,40 +558,19 @@ public CompoundTag updateHandle(int xPos, int zPos) { level.putIntArray("Biomes", biomes); } } - if (heightMaps != null) { - level.put("Heightmaps", heightMaps); - } - if (carvingMasks != null) { - level.put("CarvingMasks", carvingMasks); - } - if (entities != null) { - level.put("Entities", entities); - } - if (tileEntities != null) { - level.put("TileEntities", tileEntities); - } - if (tileTicks != null) { - level.put("TileTicks", tileTicks); - } - if (liquidTicks != null) { - level.put("LiquidTicks", liquidTicks); - } - if (lights != null) { - level.put("Lights", lights); - } - if (liquidsToBeTicked != null) { - level.put("LiquidsToBeTicked", liquidsToBeTicked); - } - if (toBeTicked != null) { - level.put("ToBeTicked", toBeTicked); - } - if (postProcessing != null) { - level.put("PostProcessing", postProcessing); - } + level.putIfNotNull("Heightmaps", heightMaps); + level.putIfNotNull("CarvingMasks", carvingMasks); + level.putIfNotNull("Entities", entities); + level.putIfNotNull("TileEntities", tileEntities); + level.putIfNotNull("TileTicks", tileTicks); + level.putIfNotNull("LiquidTicks", liquidTicks); + level.putIfNotNull("Lights", lights); + level.putIfNotNull("LiquidsToBeTicked", liquidsToBeTicked); + level.putIfNotNull("ToBeTicked", toBeTicked); + level.putIfNotNull("PostProcessing", postProcessing); level.putString("Status", status); - if (structures != null) { - level.put("Structures", structures); - } + level.putIfNotNull("Structures", structures); + ListTag sections = new ListTag<>(CompoundTag.class); for (Section section : this.sections.values()) { if (section != null) { @@ -718,9 +580,4 @@ public CompoundTag updateHandle(int xPos, int zPos) { level.put("Sections", sections); return data; } - - @Override - public Iterator
iterator() { - return sections.values().iterator(); - } } diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java new file mode 100644 index 00000000..085733f1 --- /dev/null +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -0,0 +1,118 @@ +package net.querz.mca; + +import java.util.Arrays; + +// source: version.json file, found in the root directory of the client and server jars +// table of versions can also be found on https://minecraft.fandom.com/wiki/Data_version#List_of_data_versions +public enum DataVersion { + // Must be kept in ASC order + UNKNOWN(0, 0, 0), + JAVA_1_9_0(169, 9, 0), + JAVA_1_9_1(175, 9, 1), + JAVA_1_9_2(176, 9, 2), + JAVA_1_9_3(183, 9, 3), + JAVA_1_9_4(184, 9, 4), + + JAVA_1_10_0(510, 10, 0), + JAVA_1_10_1(511, 10, 1), + JAVA_1_10_2(512, 10, 2), + + JAVA_1_11_0(819, 11, 0), + JAVA_1_11_1(921, 11, 1), + JAVA_1_11_2(922, 11, 2), + + JAVA_1_12_0(1139, 12, 0), + JAVA_1_12_1(1241, 12, 1), + JAVA_1_12_2(1343, 12, 2), + + JAVA_1_13_0(1519, 13, 0), + JAVA_1_13_1(1628, 13, 1), + JAVA_1_13_2(1631, 13, 2), + + JAVA_1_14_0(1952, 14, 0), + JAVA_1_14_1(1957, 14, 1), + JAVA_1_14_2(1963, 14, 2), + JAVA_1_14_3(1968, 14, 3), + JAVA_1_14_4(1976, 14, 4), + + JAVA_1_15_0(2225, 15, 0), + JAVA_1_15_1(2227, 15, 1), + JAVA_1_15_2(2230, 15, 2), + + JAVA_1_16_0(2566, 16, 0), + JAVA_1_16_1(2567, 16, 1), + JAVA_1_16_2(2578, 16, 2), + JAVA_1_16_3(2580, 16, 3), + JAVA_1_16_4(2584, 16, 4), + JAVA_1_16_5(2586, 16, 5), + + JAVA_1_17_0(2724, 17, 0), + JAVA_1_17_1(2730, 17, 1); + + private static final int[] ids = Arrays.stream(values()).mapToInt(DataVersion::id).toArray(); + private final int id; + private final int minor; + private final int patch; + private final String str; + + DataVersion(int id, int minor, int patch) { + this.id = id; + this.minor = minor; + this.patch = patch; + if (minor > 0) { + this.str = String.format("%d (1.%d.%d)", id, minor, patch); + } else { + this.str = name(); + } + } + + public int id() { + return id; + } + + public int major() { + return 1; + } + + public int minor() { + return minor; + } + + public int patch() { + return patch; + } + + /** + * TRUE as of 1.14 + * Indicates if point of interest .mca files exist. E.g. 'poi/r.0.0.mca' + */ + public boolean hasPoiMca() { + return minor >= 14; + } + + /** + * TRUE as of 1.17 + * Entities were pulled out of region .mca files into their own .mca files. E.g. 'entities/r.0.0.mca' + */ + public boolean hasEntitiesMca() { + return minor >= 17; + } + + public static DataVersion bestFor(int dataVersion) { + int found = Arrays.binarySearch(ids, dataVersion); + if (found < 0) { + found = (found + 2) * -1; + if (found < 0) return UNKNOWN; + } + return values()[found]; + } + + public static DataVersion latest() { + return values()[values().length - 1]; + } + + @Override + public String toString() { + return str; + } +} diff --git a/src/main/java/net/querz/mca/MCAFile.java b/src/main/java/net/querz/mca/MCAFile.java index 0a6e7122..a67fb960 100644 --- a/src/main/java/net/querz/mca/MCAFile.java +++ b/src/main/java/net/querz/mca/MCAFile.java @@ -8,211 +8,50 @@ import java.util.Arrays; import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; -public class MCAFile implements Iterable { - +/** + * Represents a REGION data mca file. + */ +public class MCAFile extends MCAFileBase implements Iterable { /** * The default chunk data version used when no custom version is supplied. - * */ - public static final int DEFAULT_DATA_VERSION = 1628; - - private int regionX, regionZ; - private Chunk[] chunks; + *

Deprecated: use {@code DataVersion.latest().id()} instead. + */ + @Deprecated + public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); /** - * MCAFile represents a world save file used by Minecraft to store world - * data on the hard drive. - * This constructor needs the x- and z-coordinates of the stored region, - * which can usually be taken from the file name {@code r.x.z.mca} - * @param regionX The x-coordinate of this region. - * @param regionZ The z-coordinate of this region. - * */ + * {@inheritDoc} + */ public MCAFile(int regionX, int regionZ) { - this.regionX = regionX; - this.regionZ = regionZ; - } - - /** - * Reads an .mca file from a {@code RandomAccessFile} into this object. - * This method does not perform any cleanups on the data. - * @param raf The {@code RandomAccessFile} to read from. - * @throws IOException If something went wrong during deserialization. - * */ - public void deserialize(RandomAccessFile raf) throws IOException { - deserialize(raf, LoadFlags.ALL_DATA); + super(regionX, regionZ); } /** - * Reads an .mca file from a {@code RandomAccessFile} into this object. - * This method does not perform any cleanups on the data. - * @param raf The {@code RandomAccessFile} to read from. - * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded - * @throws IOException If something went wrong during deserialization. - * */ - public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { - chunks = new Chunk[1024]; - for (int i = 0; i < 1024; i++) { - raf.seek(i * 4); - int offset = raf.read() << 16; - offset |= (raf.read() & 0xFF) << 8; - offset |= raf.read() & 0xFF; - if (raf.readByte() == 0) { - continue; - } - raf.seek(4096 + i * 4); - int timestamp = raf.readInt(); - Chunk chunk = new Chunk(timestamp); - raf.seek(4096 * offset + 4); //+4: skip data size - chunk.deserialize(raf, loadFlags); - chunks[i] = chunk; - } - } - - /** - * Calls {@link MCAFile#serialize(RandomAccessFile, boolean)} without updating any timestamps. - * @see MCAFile#serialize(RandomAccessFile, boolean) - * @param raf The {@code RandomAccessFile} to write to. - * @return The amount of chunks written to the file. - * @throws IOException If something went wrong during serialization. - * */ - public int serialize(RandomAccessFile raf) throws IOException { - return serialize(raf, false); - } - - /** - * Serializes this object to an .mca file. - * This method does not perform any cleanups on the data. - * @param raf The {@code RandomAccessFile} to write to. - * @param changeLastUpdate Whether it should update all timestamps that show - * when this file was last updated. - * @return The amount of chunks written to the file. - * @throws IOException If something went wrong during serialization. - * */ - public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOException { - int globalOffset = 2; - int lastWritten = 0; - int timestamp = (int) (System.currentTimeMillis() / 1000L); - int chunksWritten = 0; - int chunkXOffset = MCAUtil.regionToChunk(regionX); - int chunkZOffset = MCAUtil.regionToChunk(regionZ); - - if (chunks == null) { - return 0; - } - - for (int cx = 0; cx < 32; cx++) { - for (int cz = 0; cz < 32; cz++) { - int index = getChunkIndex(cx, cz); - Chunk chunk = chunks[index]; - if (chunk == null) { - continue; - } - raf.seek(4096 * globalOffset); - lastWritten = chunk.serialize(raf, chunkXOffset + cx, chunkZOffset + cz); - - if (lastWritten == 0) { - continue; - } - - chunksWritten++; - - int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); - - raf.seek(index * 4); - raf.writeByte(globalOffset >>> 16); - raf.writeByte(globalOffset >> 8 & 0xFF); - raf.writeByte(globalOffset & 0xFF); - raf.writeByte(sectors); - - // write timestamp - raf.seek(index * 4 + 4096); - raf.writeInt(changeLastUpdate ? timestamp : chunk.getLastMCAUpdate()); - - globalOffset += sectors; - } - } - - // padding - if (lastWritten % 4096 != 0) { - raf.seek(globalOffset * 4096 - 1); - raf.write(0); - } - return chunksWritten; - } - - /** - * Set a specific Chunk at a specific index. The index must be in range of 0 - 1023. - * @param index The index of the Chunk. - * @param chunk The Chunk to be set. - * @throws IndexOutOfBoundsException If index is not in the range. + * {@inheritDoc} */ - public void setChunk(int index, Chunk chunk) { - checkIndex(index); - if (chunks == null) { - chunks = new Chunk[1024]; - } - chunks[index] = chunk; + @Override + public Class chunkClass() { + return Chunk.class; } /** - * Set a specific Chunk at a specific chunk location. - * The x- and z-value can be absolute chunk coordinates or they can be relative to the region origin. - * @param chunkX The x-coordinate of the Chunk. - * @param chunkZ The z-coordinate of the Chunk. - * @param chunk The chunk to be set. + * {@inheritDoc} */ - public void setChunk(int chunkX, int chunkZ, Chunk chunk) { - setChunk(getChunkIndex(chunkX, chunkZ), chunk); - } - - /** - * Returns the chunk data of a chunk at a specific index in this file. - * @param index The index of the chunk in this file. - * @return The chunk data. - * */ - public Chunk getChunk(int index) { - checkIndex(index); - if (chunks == null) { - return null; - } - return chunks[index]; - } - - /** - * Returns the chunk data of a chunk in this file. - * @param chunkX The x-coordinate of the chunk. - * @param chunkZ The z-coordinate of the chunk. - * @return The chunk data. - * */ - public Chunk getChunk(int chunkX, int chunkZ) { - return getChunk(getChunkIndex(chunkX, chunkZ)); + @Override + protected Chunk newChunk() { + return Chunk.newChunk(); } /** - * Calculates the index of a chunk from its x- and z-coordinates in this region. - * This works with absolute and relative coordinates. - * @param chunkX The x-coordinate of the chunk. - * @param chunkZ The z-coordinate of the chunk. - * @return The index of this chunk. - * */ - public static int getChunkIndex(int chunkX, int chunkZ) { - return (chunkX & 0x1F) + (chunkZ & 0x1F) * 32; - } - - private int checkIndex(int index) { - if (index < 0 || index > 1023) { - throw new IndexOutOfBoundsException(); - } - return index; - } - - private Chunk createChunkIfMissing(int blockX, int blockZ) { - int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); - Chunk chunk = getChunk(chunkX, chunkZ); - if (chunk == null) { - chunk = Chunk.newChunk(); - setChunk(getChunkIndex(chunkX, chunkZ), chunk); - } + * {@inheritDoc} + */ + @Override + protected Chunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { + Chunk chunk = new Chunk(timestamp); + chunk.deserialize(raf, loadFlags); return chunk; } @@ -297,9 +136,4 @@ public void cleanupPalettesAndBlockStates() { } } } - - @Override - public Iterator iterator() { - return Arrays.stream(chunks).iterator(); - } } diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java new file mode 100644 index 00000000..1172c97d --- /dev/null +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -0,0 +1,301 @@ +package net.querz.mca; + + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * An abstract representation of an mca file. + */ +public abstract class MCAFileBase implements Iterable { + + protected int regionX, regionZ; + protected T[] chunks; + + /** + * MCAFile represents a world save file used by Minecraft to store world + * data on the hard drive. + * This constructor needs the x- and z-coordinates of the stored region, + * which can usually be taken from the file name {@code r.x.z.mca} + * @param regionX The x-coordinate of this region. + * @param regionZ The z-coordinate of this region. + */ + public MCAFileBase(int regionX, int regionZ) { + this.regionX = regionX; + this.regionZ = regionZ; + } + + /** + * @return type of chunk this MCA File holds + */ + public abstract Class chunkClass(); + + /** + * Chunk creator. + */ + protected abstract T newChunk(); + + /** + * Called to deserialize a Chunk. Caller will have set the position of {@code raf} to start reading. + * @param raf The {@code RandomAccessFile} to read from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @param timestamp The timestamp when this chunk was last updated as a UNIX timestamp. + * @return Deserialized chunk. + * @throws IOException if something went wrong during deserialization. + */ + protected abstract T deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException; + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to read from. + * @throws IOException If something went wrong during deserialization. + */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, LoadFlags.ALL_DATA); + } + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to read from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException If something went wrong during deserialization. + */ + @SuppressWarnings("unchecked") + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + chunks = (T[]) Array.newInstance(chunkClass(), 1024); + for (int i = 0; i < 1024; i++) { + raf.seek(i * 4); + int offset = raf.read() << 16; + offset |= (raf.read() & 0xFF) << 8; + offset |= raf.read() & 0xFF; + if (raf.readByte() == 0) { + continue; + } + raf.seek(4096 + i * 4); + int timestamp = raf.readInt(); + raf.seek(4096 * offset + 4); //+4: skip data size + chunks[i] = deserializeChunk(raf, loadFlags, timestamp); + } + } + + /** + * Calls {@link MCAFileBase#serialize(RandomAccessFile, boolean)} without updating any timestamps. + * @see MCAFileBase#serialize(RandomAccessFile, boolean) + * @param raf The {@code RandomAccessFile} to write to. + * @return The amount of chunks written to the file. + * @throws IOException If something went wrong during serialization. + */ + public int serialize(RandomAccessFile raf) throws IOException { + return serialize(raf, false); + } + + /** + * Serializes this object to an .mca file. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to write to. + * @param changeLastUpdate Whether it should update all timestamps that show + * when this file was last updated. + * @return The amount of chunks written to the file. + * @throws IOException If something went wrong during serialization. + */ + public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOException { + int globalOffset = 2; + int lastWritten = 0; + int timestamp = (int) (System.currentTimeMillis() / 1000L); + int chunksWritten = 0; + int chunkXOffset = MCAUtil.regionToChunk(regionX); + int chunkZOffset = MCAUtil.regionToChunk(regionZ); + + if (chunks == null) { + return 0; + } + + for (int cz = 0; cz < 32; cz++) { + for (int cx = 0; cx < 32; cx++) { + int index = getChunkIndex(cx, cz); + Chunk chunk = chunks[index]; + if (chunk == null) { + continue; + } + raf.seek(4096 * globalOffset); + lastWritten = chunk.serialize(raf, chunkXOffset + cx, chunkZOffset + cz); + + if (lastWritten == 0) { + continue; + } + + chunksWritten++; + + int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); + + raf.seek(index * 4); + raf.writeByte(globalOffset >>> 16); + raf.writeByte(globalOffset >> 8 & 0xFF); + raf.writeByte(globalOffset & 0xFF); + raf.writeByte(sectors); + + // write timestamp + raf.seek(index * 4 + 4096); + raf.writeInt(changeLastUpdate ? timestamp : chunk.getLastMCAUpdate()); + + globalOffset += sectors; + } + } + + // padding + if (lastWritten % 4096 != 0) { + raf.seek(globalOffset * 4096 - 1); + raf.write(0); + } + return chunksWritten; + } + + /** + * Set a specific Chunk at a specific index. The index must be in range of 0 - 1023. + * @param index The index of the Chunk. + * @param chunk The Chunk to be set. + * @throws IndexOutOfBoundsException If index is not in the range. + */ + @SuppressWarnings("unchecked") + public void setChunk(int index, T chunk) { + checkIndex(index); + if (chunks == null) { + chunks = (T[]) Array.newInstance(chunkClass(), 1024); + } + chunks[index] = chunk; + } + + /** + * Set a specific Chunk at a specific chunk location. + * The x- and z-value can be absolute chunk coordinates or they can be relative to the region origin. + * @param chunkX The x-coordinate of the Chunk. + * @param chunkZ The z-coordinate of the Chunk. + * @param chunk The chunk to be set. + * + */ + public void setChunk(int chunkX, int chunkZ, T chunk) { + setChunk(getChunkIndex(chunkX, chunkZ), chunk); + } + + /** + * Returns the chunk data of a chunk at a specific index in this file. + * @param index The index of the chunk in this file. + * @return The chunk data. + */ + public T getChunk(int index) { + checkIndex(index); + if (chunks == null) { + return null; + } + return chunks[index]; + } + + /** + * Returns the chunk data of a chunk in this file. + * @param chunkX The x-coordinate of the chunk. + * @param chunkZ The z-coordinate of the chunk. + * @return The chunk data. + */ + public T getChunk(int chunkX, int chunkZ) { + return getChunk(getChunkIndex(chunkX, chunkZ)); + } + + /** + * Calculates the index of a chunk from its x- and z-coordinates in this region. + * This works with absolute and relative coordinates. + * @param chunkX The x-coordinate of the chunk. + * @param chunkZ The z-coordinate of the chunk. + * @return The index of this chunk. + */ + public static int getChunkIndex(int chunkX, int chunkZ) { + return ((chunkZ & 0x1F) << 5) | (chunkX & 0x1F); + } + + protected void checkIndex(int index) { + if (index < 0 || index > 1023) { + throw new IndexOutOfBoundsException(); + } + } + + protected T createChunkIfMissing(int blockX, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + T chunk = getChunk(chunkX, chunkZ); + if (chunk == null) { + chunk = newChunk(); + setChunk(getChunkIndex(chunkX, chunkZ), chunk); + } + return chunk; + } + + @Override + public ChunkIterator iterator() { + return new ChunkIteratorImpl<>(this); + } + + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + public interface ChunkIterator extends Iterator { + void set(I chunk); + /** current block index (in range 0-4095) */ + int currentIndex(); + /** current chunk x within region (in range 0-31) */ + int currentX(); + /** current chunk z within region (in range 0-31) */ + int currentZ(); + } + + protected static class ChunkIteratorImpl implements ChunkIterator { + private final MCAFileBase owner; + private int currentIndex; + + public ChunkIteratorImpl(MCAFileBase owner) { + this.owner = owner; + currentIndex = -1; + } + + @Override + public boolean hasNext() { + return currentIndex < 1023; + } + + @Override + public I next() { + if (!hasNext()) throw new NoSuchElementException(); + return owner.getChunk(++currentIndex); + } + + @Override + public void remove() { + owner.setChunk(currentIndex, null); + } + + @Override + public void set(I chunk) { + owner.setChunk(currentIndex, chunk); + } + + @Override + public int currentIndex() { + return currentIndex; + } + + @Override + public int currentX() { + return currentIndex & 0x1F; + } + + @Override + public int currentZ() { + return (currentIndex >> 5) & 0x1F; + } + } +} diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index f5ddecc5..547af2c7 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -11,7 +11,7 @@ /** * Provides main and utility functions to read and write .mca files and * to convert block, chunk and region coordinates. - * */ + */ public final class MCAUtil { private MCAUtil() {} @@ -21,7 +21,7 @@ private MCAUtil() {} * @param file The file to read the data from. * @return An in-memory representation of the MCA file with decompressed chunk data. * @throws IOException if something during deserialization goes wrong. - * */ + */ public static MCAFile read(String file) throws IOException { return read(new File(file), LoadFlags.ALL_DATA); } @@ -31,7 +31,7 @@ public static MCAFile read(String file) throws IOException { * @param file The file to read the data from. * @return An in-memory representation of the MCA file with decompressed chunk data. * @throws IOException if something during deserialization goes wrong. - * */ + */ public static MCAFile read(File file) throws IOException { return read(file, LoadFlags.ALL_DATA); } @@ -42,7 +42,7 @@ public static MCAFile read(File file) throws IOException { * @return An in-memory representation of the MCA file with decompressed chunk data. * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded * @throws IOException if something during deserialization goes wrong. - * */ + */ public static MCAFile read(String file, long loadFlags) throws IOException { return read(new File(file), loadFlags); } @@ -53,7 +53,7 @@ public static MCAFile read(String file, long loadFlags) throws IOException { * @return An in-memory representation of the MCA file with decompressed chunk data * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded * @throws IOException if something during deserialization goes wrong. - * */ + */ public static MCAFile read(File file, long loadFlags) throws IOException { MCAFile mcaFile = newMCAFile(file); try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { @@ -69,7 +69,7 @@ public static MCAFile read(File file, long loadFlags) throws IOException { * @param mcaFile The data of the MCA file to write. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. - * */ + */ public static int write(MCAFile mcaFile, String file) throws IOException { return write(mcaFile, new File(file), false); } @@ -81,7 +81,7 @@ public static int write(MCAFile mcaFile, String file) throws IOException { * @param mcaFile The data of the MCA file to write. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. - * */ + */ public static int write(MCAFile mcaFile, File file) throws IOException { return write(mcaFile, file, false); } @@ -93,7 +93,7 @@ public static int write(MCAFile mcaFile, File file) throws IOException { * @param changeLastUpdate Whether to adjust the timestamps of when the file was saved. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. - * */ + */ public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) throws IOException { return write(mcaFile, new File(file), changeLastUpdate); } @@ -108,7 +108,7 @@ public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) * @param changeLastUpdate Whether to adjust the timestamps of when the file was saved. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. - * */ + */ public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) throws IOException { File to = file; if (file.exists()) { @@ -131,7 +131,7 @@ public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) th * @param chunkX The x-value of the location of the chunk. * @param chunkZ The z-value of the location of the chunk. * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" - * */ + */ public static String createNameFromChunkLocation(int chunkX, int chunkZ) { return createNameFromRegionLocation( chunkToRegion(chunkX), chunkToRegion(chunkZ)); } @@ -142,7 +142,7 @@ public static String createNameFromChunkLocation(int chunkX, int chunkZ) { * @param blockX The x-value of the location of the block. * @param blockZ The z-value of the location of the block. * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" - * */ + */ public static String createNameFromBlockLocation(int blockX, int blockZ) { return createNameFromRegionLocation(blockToRegion(blockX), blockToRegion(blockZ)); } @@ -152,7 +152,7 @@ public static String createNameFromBlockLocation(int blockX, int blockZ) { * @param regionX The x-value of the location of the region. * @param regionZ The z-value of the location of the region. * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" - * */ + */ public static String createNameFromRegionLocation(int regionX, int regionZ) { return "r." + regionX + "." + regionZ + ".mca"; } @@ -161,7 +161,7 @@ public static String createNameFromRegionLocation(int regionX, int regionZ) { * Turns a block coordinate value into a chunk coordinate value. * @param block The block coordinate value. * @return The chunk coordinate value. - * */ + */ public static int blockToChunk(int block) { return block >> 4; } @@ -170,7 +170,7 @@ public static int blockToChunk(int block) { * Turns a block coordinate value into a region coordinate value. * @param block The block coordinate value. * @return The region coordinate value. - * */ + */ public static int blockToRegion(int block) { return block >> 9; } @@ -179,7 +179,7 @@ public static int blockToRegion(int block) { * Turns a chunk coordinate value into a region coordinate value. * @param chunk The chunk coordinate value. * @return The region coordinate value. - * */ + */ public static int chunkToRegion(int chunk) { return chunk >> 5; } @@ -188,7 +188,7 @@ public static int chunkToRegion(int chunk) { * Turns a region coordinate value into a chunk coordinate value. * @param region The region coordinate value. * @return The chunk coordinate value. - * */ + */ public static int regionToChunk(int region) { return region << 5; } @@ -197,7 +197,7 @@ public static int regionToChunk(int region) { * Turns a region coordinate value into a block coordinate value. * @param region The region coordinate value. * @return The block coordinate value. - * */ + */ public static int regionToBlock(int region) { return region << 9; } @@ -206,7 +206,7 @@ public static int regionToBlock(int region) { * Turns a chunk coordinate value into a block coordinate value. * @param chunk The chunk coordinate value. * @return The block coordinate value. - * */ + */ public static int chunkToBlock(int chunk) { return chunk << 4; } diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index 3b72b969..b9a3c841 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -5,29 +5,41 @@ import net.querz.nbt.tag.CompoundTag; import net.querz.nbt.tag.ListTag; import net.querz.nbt.tag.LongArrayTag; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class Section implements Comparable

{ - - private CompoundTag data; - private Map> valueIndexedPalette = new HashMap<>(); - private ListTag palette; - private byte[] blockLight; - private long[] blockStates; - private byte[] skyLight; - private int height; - int dataVersion; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Represents a REGION data chunk section. + */ +public class Section extends SectionBase
{ + + protected int dataVersion; + protected Map> valueIndexedPalette = new HashMap<>(); + protected ListTag palette; + protected byte[] blockLight; + protected long[] blockStates; + protected byte[] skyLight; + + public static byte[] createBlockLightBuffer() { + return new byte[2048]; + } + + public static long[] createBlockStates() { + return new long[256]; + } + + public static byte[] createSkyLightBuffer() { + return new byte[2048]; + } public Section(CompoundTag sectionRoot, int dataVersion) { this(sectionRoot, dataVersion, ALL_DATA); } public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { - data = sectionRoot; + super(sectionRoot); this.dataVersion = dataVersion; height = sectionRoot.getNumber("Y").byteValue(); @@ -41,23 +53,38 @@ public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { putValueIndexedPalette(data, i); } - ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); - LongArrayTag blockStates = sectionRoot.getLongArrayTag("BlockStates"); - ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); - if ((loadFlags & BLOCK_LIGHTS) != 0) { - this.blockLight = blockLight != null ? blockLight.getValue() : null; + ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); + if (blockLight != null) this.blockLight = blockLight.getValue(); } if ((loadFlags & BLOCK_STATES) != 0) { - this.blockStates = blockStates != null ? blockStates.getValue() : null; + LongArrayTag blockStates = sectionRoot.getLongArrayTag("BlockStates"); + if (blockStates != null) this.blockStates = blockStates.getValue(); } if ((loadFlags & SKY_LIGHT) != 0) { - this.skyLight = skyLight != null ? skyLight.getValue() : null; + ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); + if (skyLight != null) this.skyLight = skyLight.getValue(); } } Section() {} + private void assureBlockStates() { + if (blockStates == null) blockStates = createBlockStates(); + } + + private void assureBlockLight() { + if (blockLight == null) blockLight = createBlockLightBuffer(); + } + + private void assureSkyLight() { + if (skyLight == null) skyLight = createSkyLightBuffer(); + } + + private void assurePalette() { + if (palette == null) palette = new ListTag<>(CompoundTag.class); + } + void putValueIndexedPalette(CompoundTag data, int index) { PaletteIndex leaf = new PaletteIndex(data, index); String name = data.getString("Name"); @@ -89,14 +116,6 @@ PaletteIndex getValueIndexedPalette(CompoundTag data) { return null; } - @Override - public int compareTo(Section o) { - if (o == null) { - return -1; - } - return Integer.compare(height, o.height); - } - private static class PaletteIndex { CompoundTag data; @@ -108,25 +127,6 @@ private static class PaletteIndex { } } - /** - * Checks whether the data of this Section is empty. - * @return true if empty - */ - public boolean isEmpty() { - return data == null; - } - - /** - * @return the Y value of this section. - * */ - public int getHeight() { - return height; - } - - public void setHeight(int height) { - this.height = height; - } - /** * Fetches a block state based on a block location from this section. * The coordinates represent the location of the block inside of this Section. @@ -150,17 +150,23 @@ private CompoundTag getBlockStateAt(int index) { * @param blockY The y-coordinate of the block in this Section * @param blockZ The z-coordinate of the block in this Section * @param state The block state to be set - * @param cleanup When true, it will cleanup the palette of this section. + * @param cleanup When true, it will force a cleanup the palette of this section. * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. * Recalculating the Palette should only be executed once right before saving the Section to file. + * @return True if {@link Section#cleanupPaletteAndBlockStates()} was run as a result of this call. + * Note that it is possible that {@link Section#cleanupPaletteAndBlockStates()} needed to be called even if + * the {@code cleanup} argument was {@code false}. In summary if the last call made to this function returns + * {@code true} you can skip the call to {@link Section#cleanupPaletteAndBlockStates()}. */ - public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + public boolean setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + assurePalette(); int paletteSizeBefore = palette.size(); int paletteIndex = addToPalette(state); //power of 2 --> bits must increase, but only if the palette size changed //otherwise we would attempt to update all blockstates and the entire palette //every time an existing blockstate was added while having 2^x blockstates in the palette if (paletteSizeBefore != palette.size() && (paletteIndex & (paletteIndex - 1)) == 0) { + assureBlockStates(); adjustBlockStateBits(null, blockStates); cleanup = true; } @@ -169,15 +175,18 @@ public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag stat if (cleanup) { cleanupPaletteAndBlockStates(); + return true; } + return false; } /** * Returns the index of the block data in the palette. * @param blockStateIndex The index of the block in this section, ranging from 0-4095. * @return The index of the block data in the palette. - * */ + */ public int getPaletteIndex(int blockStateIndex) { + assureBlockStates(); int bits = blockStates.length >> 6; if (dataVersion < 2527) { @@ -204,8 +213,9 @@ public int getPaletteIndex(int blockStateIndex) { * @param blockIndex The index of the block in this section, ranging from 0-4095. * @param paletteIndex The block state to be set (index of block data in the palette). * @param blockStates The block states to be updated. - * */ + */ public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) { + Objects.requireNonNull(blockStates, "blockStates must not be null"); int bits = blockStates.length >> 6; if (dataVersion < 2527) { @@ -265,7 +275,7 @@ static long bitRange(long value, int from, int to) { * Recalculating the Palette should only be executed once right before saving the Section to file. */ public void cleanupPaletteAndBlockStates() { - if (blockStates != null) { + if (blockStates != null && palette != null) { Map oldToNewMapping = cleanupPalette(); adjustBlockStateBits(oldToNewMapping, blockStates); } @@ -391,12 +401,13 @@ public void setSkyLight(byte[] skyLight) { */ public static Section newSection() { Section s = new Section(); - s.blockStates = new long[256]; + s.blockLight = createBlockLightBuffer(); + s.blockStates = createBlockStates(); + s.skyLight = createSkyLightBuffer(); s.palette = new ListTag<>(CompoundTag.class); CompoundTag air = new CompoundTag(); air.putString("Name", "minecraft:air"); s.palette.add(air); - s.data = new CompoundTag(); return s; } @@ -407,6 +418,7 @@ public static Section newSection() { * @param y The Y-value of this Section * @return A reference to the raw CompoundTag this Section is based on */ + @Override public CompoundTag updateHandle(int y) { data.putByte("Y", (byte) y); if (palette != null) { @@ -424,50 +436,105 @@ public CompoundTag updateHandle(int y) { return data; } - public CompoundTag updateHandle() { - return updateHandle(height); - } - /** * Creates an iterable that iterates over all blocks in this section, in order of their indices. - * An index can be calculated using the following formula: + * XYZ can be calculated with the following formulas: *
 	 * {@code
-	 * index = (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF);
+	 * x = index & 0xF;
+	 * z = (index >> 4) & 0xF;
+	 * y = index >> 8;
 	 * }
 	 * 
* The CompoundTags are references to this Section's Palette and should only be modified if the intention is to * modify ALL blocks of the same type in this Section at the same time. - * */ - public Iterable blocksStates() { - return new BlockIterator(this); + */ + public BlockStateIterator blocksStates() { + return new BlockStateIteratorImpl(this); } - private static class BlockIterator implements Iterable, Iterator { + protected static class BlockStateIteratorImpl implements BlockStateIterator { - private Section section; + private final Section section; + private final int sectionWorldY; private int currentIndex; + private CompoundTag currentTag; + private boolean dirty; - public BlockIterator(Section section) { + public BlockStateIteratorImpl(Section section) { this.section = section; - currentIndex = 0; + this.sectionWorldY = section.getHeight() * 16; + currentIndex = -1; } @Override public boolean hasNext() { - return currentIndex < 4096; + return currentIndex < 4095; } @Override public CompoundTag next() { - CompoundTag blockState = section.getBlockStateAt(currentIndex); - currentIndex++; - return blockState; + return currentTag = section.getBlockStateAt(++currentIndex); } @Override public Iterator iterator() { return this; } + + @Override + public void setBlockStateAtCurrent(CompoundTag state) { + Objects.requireNonNull(state); + if (currentTag != state) { + dirty = !section.setBlockStateAt(currentX(), currentY(), currentZ(), state, false); + } + } + + @Override + public void cleanupPaletteAndBlockStatesIfDirty() { + if (dirty) section.cleanupPaletteAndBlockStates(); + } + + @Override + public int currentIndex() { + return currentIndex; + } + + @Override + public int currentX() { + return currentIndex & 0xF; + } + + @Override + public int currentZ() { + return (currentIndex >> 4) & 0xF; + } + + @Override + public int currentY() { + return currentIndex >> 8; + } + + @Override + public int currentWorldY() { + return sectionWorldY + (currentIndex >> 8); + } + } + + /** + * Streams all blocks in this section, in order of their indices. + * XYZ can be calculated with the following formulas: + *
+	 * {@code
+	 * x = index & 0xF;
+	 * z = (index >> 4) & 0xF;
+	 * y = index >> 8;
+	 * }
+	 * 
+ * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to + * modify ALL blocks of the same type in this Section at the same time. + */ + public Stream streamBlocksStates() { + return StreamSupport.stream(blocksStates().spliterator(), false); } } diff --git a/src/main/java/net/querz/mca/SectionBase.java b/src/main/java/net/querz/mca/SectionBase.java new file mode 100644 index 00000000..363a45a2 --- /dev/null +++ b/src/main/java/net/querz/mca/SectionBase.java @@ -0,0 +1,59 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import java.util.*; + +public abstract class SectionBase> implements Comparable { + + protected final CompoundTag data; + protected int height; + + public SectionBase(CompoundTag sectionRoot) { + Objects.requireNonNull(sectionRoot, "sectionRoot must not be null"); + this.data = sectionRoot; + } + + public SectionBase() { + data = new CompoundTag(); + } + + @Override + public int compareTo(T o) { + if (o == null) { + return -1; + } + return Integer.compare(height, o.height); + } + + /** + * Checks whether the data of this Section is empty. + * @return true if empty + */ + public boolean isEmpty() { + return data.isEmpty(); + } + + /** + * @return the Y value of this section. Multiply by 16 to get world Y value. + */ + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + /** + * Updates the raw CompoundTag that this Section is based on. + * This must be called before saving a Section to disk if the Section was manually created + * to set the Y of this Section. + * @param y The Y-value of this Section + * @return A reference to the raw CompoundTag this Section is based on + */ + public abstract CompoundTag updateHandle(int y); + + public CompoundTag updateHandle() { + return updateHandle(height); + } +} diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java new file mode 100644 index 00000000..0b1e8479 --- /dev/null +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -0,0 +1,192 @@ +package net.querz.mca; + +import net.querz.nbt.io.NBTDeserializer; +import net.querz.nbt.io.NBTSerializer; +import net.querz.nbt.io.NamedTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; + +import java.io.*; +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static net.querz.mca.LoadFlags.*; + +public abstract class SectionedChunkBase implements Iterable { + + protected boolean partial; + protected boolean raw; + protected int lastMCAUpdate; + protected CompoundTag data; + protected final TreeMap sections = new TreeMap<>(); + + SectionedChunkBase(int lastMCAUpdate) { + this.lastMCAUpdate = lastMCAUpdate; + } + + /** + * Create a new chunk based on raw base data from a region file. + * @param data The raw base data to be used. + */ + public SectionedChunkBase(CompoundTag data) { + this.data = data; + initReferences(ALL_DATA); + } + + private void initReferences0(long loadFlags) { + Objects.requireNonNull(data, "data cannot be null"); + + if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { + raw = true; + return; + } + + initReferences(loadFlags); + + // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. + if (loadFlags != ALL_DATA) { + data = null; + partial = true; + } + } + + /** + * Child classes should not call this method directly. + * Raw and partial data handling is taken care of, this method will not be called if {@code loadFlags} is + * {@link LoadFlags#RAW}. + */ + protected abstract void initReferences(long loadFlags); + + /** + * Serializes this chunk to a RandomAccessFile. + * @param raf The RandomAccessFile to be written to. + * @param xPos The x-coordinate of the chunk. + * @param zPos The z-coodrinate of the chunk. + * @return The amount of bytes written to the RandomAccessFile. + * @throws UnsupportedOperationException When something went wrong during writing. + * @throws IOException When something went wrong during writing. + */ + public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { + if (partial) { + throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { + new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); + } + byte[] rawData = baos.toByteArray(); + raf.writeInt(rawData.length + 1); // including the byte to store the compression type + raf.writeByte(CompressionType.ZLIB.getID()); + raf.write(rawData); + return rawData.length + 5; + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, ALL_DATA); + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + byte compressionTypeByte = raf.readByte(); + CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); + if (compressionType == null) { + throw new IOException("invalid compression type " + compressionTypeByte); + } + BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); + NamedTag tag = new NBTDeserializer(false).fromStream(dis); + if (tag != null && tag.getTag() instanceof CompoundTag) { + data = (CompoundTag) tag.getTag(); + initReferences0(loadFlags); + } else { + throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); + } + } + + /** + * @return The timestamp when this region file was last updated in seconds since 1970-01-01. + */ + public int getLastMCAUpdate() { + return lastMCAUpdate; + } + + /** + * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. + * @param lastMCAUpdate The time in seconds since 1970-01-01. + */ + public void setLastMCAUpdate(int lastMCAUpdate) { + checkRaw(); + this.lastMCAUpdate = lastMCAUpdate; + } + + /** + * Fetches the section at the given y-coordinate. + * @param sectionY The y-coordinate of the section in this chunk. One section y is equal to 16 world y's + * @return The Section. + */ + public T getSection(int sectionY) { + return sections.get(sectionY); + } + + /** + * Sets a section at a given section y-coordinate + * @param sectionY The y-coordinate of the section in this chunk. One section y is equal to 16 world y's + * @param section The section to be set. + */ + public void setSection(int sectionY, T section) { + checkRaw(); + sections.put(sectionY, section); + } + + /** + * Gets the minimum section y-coordinate. + * @return The y of the lowest section in the chunk. + */ + public int getMinSectionY() { + return sections.firstKey(); + } + + /** + * Gets the minimum section y-coordinate. + * @return The y of the highest populated section in the chunk. + */ + public int getMaxSectionY() { + return sections.lastKey(); + } + + + protected void checkRaw() { + if (raw) { + throw new UnsupportedOperationException("cannot update field when working with raw data"); + } + } + + /** + * Provides a reference to the full chunk data. + * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. + */ + public CompoundTag getHandle() { + return data; + } + + public abstract CompoundTag updateHandle(int xPos, int zPos); + + @Override + public Iterator iterator() { + return sections.values().iterator(); + } + + public Stream stream() { + return sections.values().stream(); + } +} diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index f34db876..651e5800 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -36,6 +36,10 @@ public int size() { return getValue().size(); } + public boolean isEmpty() { + return getValue().isEmpty(); + } + public Tag remove(String key) { return getValue().remove(key); } diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java new file mode 100644 index 00000000..6703395d --- /dev/null +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -0,0 +1,41 @@ +package net.querz.mca; + + +import junit.framework.TestCase; + +import java.util.Arrays; + +public class DataVersionTest extends TestCase { + + public void testBestForNegativeValue() { + assertEquals(DataVersion.UNKNOWN, DataVersion.bestFor(-42)); + } + + public void testBestForExactFirst() { + assertEquals(DataVersion.UNKNOWN, DataVersion.bestFor(0)); + } + + public void testBestForExactArbitrary() { + assertEquals(DataVersion.JAVA_1_15_0, DataVersion.bestFor(2225)); + } + + public void testBestForBetween() { + assertEquals(DataVersion.JAVA_1_10_2, DataVersion.bestFor(DataVersion.JAVA_1_11_0.id() - 1)); + assertEquals(DataVersion.JAVA_1_11_0, DataVersion.bestFor(DataVersion.JAVA_1_11_0.id() + 1)); + } + + public void testBestForExactLast() { + final DataVersion last = DataVersion.values()[DataVersion.values().length - 1]; + assertEquals(last, DataVersion.bestFor(last.id())); + } + + public void testBestForAfterLast() { + final DataVersion last = DataVersion.values()[DataVersion.values().length - 1]; + assertEquals(last, DataVersion.bestFor(last.id() + 123)); + } + + public void testToString() { + assertEquals("2724 (1.17.0)", DataVersion.JAVA_1_17_0.toString()); + assertEquals("UNKNOWN", DataVersion.UNKNOWN.toString()); + } +} From 78340a74362e89d174ef0ac7e3dd74c4564ef940 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 6 Nov 2021 21:27:18 -0400 Subject: [PATCH 002/288] Resolve lint warnings --- src/main/java/net/querz/mca/MCAFileBase.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 1172c97d..5284b128 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -80,7 +80,7 @@ public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException } raf.seek(4096 + i * 4); int timestamp = raf.readInt(); - raf.seek(4096 * offset + 4); //+4: skip data size + raf.seek(4096L * offset + 4); //+4: skip data size chunks[i] = deserializeChunk(raf, loadFlags, timestamp); } } @@ -124,7 +124,7 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx if (chunk == null) { continue; } - raf.seek(4096 * globalOffset); + raf.seek(4096L * globalOffset); lastWritten = chunk.serialize(raf, chunkXOffset + cx, chunkZOffset + cz); if (lastWritten == 0) { @@ -135,14 +135,14 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); - raf.seek(index * 4); + raf.seek(index * 4L); raf.writeByte(globalOffset >>> 16); raf.writeByte(globalOffset >> 8 & 0xFF); raf.writeByte(globalOffset & 0xFF); raf.writeByte(sectors); // write timestamp - raf.seek(index * 4 + 4096); + raf.seek(index * 4L + 4096); raf.writeInt(changeLastUpdate ? timestamp : chunk.getLastMCAUpdate()); globalOffset += sectors; @@ -151,7 +151,7 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx // padding if (lastWritten % 4096 != 0) { - raf.seek(globalOffset * 4096 - 1); + raf.seek(globalOffset * 4096L - 1); raf.write(0); } return chunksWritten; From 9daacd21e400bd95dc1ce360d577a22148e9784f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 08:19:25 -0500 Subject: [PATCH 003/288] Extracted ChunkIterator interface. Exposed region xz getters setters on MCAFileBase Fixed template param on SectionedChunkBase --- .../net/querz/mca/BlockStateIterator.java | 4 +- src/main/java/net/querz/mca/Chunk.java | 5 +- .../java/net/querz/mca/ChunkIterator.java | 33 ++++++++++ src/main/java/net/querz/mca/MCAFileBase.java | 62 +++++++++++++++---- src/main/java/net/querz/mca/MCAUtil.java | 2 +- src/main/java/net/querz/mca/Section.java | 5 +- .../net/querz/mca/SectionedChunkBase.java | 6 +- 7 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 src/main/java/net/querz/mca/ChunkIterator.java diff --git a/src/main/java/net/querz/mca/BlockStateIterator.java b/src/main/java/net/querz/mca/BlockStateIterator.java index 668360b3..d287199f 100644 --- a/src/main/java/net/querz/mca/BlockStateIterator.java +++ b/src/main/java/net/querz/mca/BlockStateIterator.java @@ -6,7 +6,7 @@ /** * Enhanced iterable/iterator for iterating over {@link Section} block data. - * class name {@link Section#blocksStates()}. + * See {@link Section#blocksStates()} */ public interface BlockStateIterator extends Iterable, Iterator { /** @@ -32,5 +32,5 @@ public interface BlockStateIterator extends Iterable, Iterator { @@ -191,7 +192,7 @@ public void setBiomeAt(int blockX, int blockZ, int biomeID) { */ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { checkRaw(); - if (dataVersion < 2202) { + if (dataVersion < 2202) { // pre-1.15 if (biomes == null || biomes.length != 256) { biomes = new int[256]; Arrays.fill(biomes, -1); diff --git a/src/main/java/net/querz/mca/ChunkIterator.java b/src/main/java/net/querz/mca/ChunkIterator.java new file mode 100644 index 00000000..0099c7bc --- /dev/null +++ b/src/main/java/net/querz/mca/ChunkIterator.java @@ -0,0 +1,33 @@ +package net.querz.mca; + +import java.util.Iterator; + +/** + * Enhanced iterator for iterating over {@link Chunk} data. + * All 1024 chunks will be returned by successive calls to {@link #next()}, even + * those which are {@code null}. + * See {@link MCAFileBase#iterator()} + */ +public interface ChunkIterator extends Iterator { + /** + * Replaces the current chunk with the one given by calling {@link MCAFileBase#setChunk(int, Chunk)} + * with the {@link #currentIndex()}. Take care as the given chunk is NOT copied by this call. + * @param chunk Chunk to set, may be null. + */ + void set(I chunk); + + /** + * @return Current chunk index (in range 0-1023) + */ + int currentIndex(); + + /** + * @return Current chunk x within region (in range 0-31) + */ + int currentX(); + + /** + * @return Current chunk z within region (in range 0-31) + */ + int currentZ(); +} diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 5284b128..ac24ca81 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -22,14 +22,61 @@ public abstract class MCAFileBase implements Iterable { * data on the hard drive. * This constructor needs the x- and z-coordinates of the stored region, * which can usually be taken from the file name {@code r.x.z.mca} - * @param regionX The x-coordinate of this region. - * @param regionZ The z-coordinate of this region. + * @param regionX The x-coordinate of this mca file in region coordinates. + * @param regionZ The z-coordinate of this mca file in region coordinates. */ public MCAFileBase(int regionX, int regionZ) { this.regionX = regionX; this.regionZ = regionZ; } + /** + * @return The x-value currently set for this mca file in region coordinates. + */ + public int getRegionX() { + return regionX; + } + + /** + * Sets a new x-value for this mca file in region coordinates. + */ + public void setRegionX(int regionX) { + this.regionX = regionX; + } + + /** + * @return The z-value currently set for this mca file in region coordinates. + */ + public int getRegionZ() { + return regionZ; + } + + /** + * Sets a new z-value for this mca file in region coordinates. + */ + public void setRegionZ(int regionZ) { + this.regionZ = regionZ; + } + + /** + * Sets both the x and z values for this mca file in region coordinates. + * @param regionX New x-value for this mca file in region coordinates. + * @param regionZ New z-value for this mca file in region coordinates. + */ + public void setRegionXZ(int regionX, int regionZ) { + this.regionX = regionX; + this.regionZ = regionZ; + } + + /** + * Returns result of calling {@link MCAUtil#createNameFromRegionLocation(int, int)} + * with current region coordinate values. + * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" + */ + public String createRegionName() { + return MCAUtil.createNameFromRegionLocation(regionX, regionZ); + } + /** * @return type of chunk this MCA File holds */ @@ -159,6 +206,7 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx /** * Set a specific Chunk at a specific index. The index must be in range of 0 - 1023. + * Take care as the given chunk is NOT copied by this call. * @param index The index of the Chunk. * @param chunk The Chunk to be set. * @throws IndexOutOfBoundsException If index is not in the range. @@ -243,16 +291,6 @@ public Stream stream() { return StreamSupport.stream(spliterator(), false); } - public interface ChunkIterator extends Iterator { - void set(I chunk); - /** current block index (in range 0-4095) */ - int currentIndex(); - /** current chunk x within region (in range 0-31) */ - int currentX(); - /** current chunk z within region (in range 0-31) */ - int currentZ(); - } - protected static class ChunkIteratorImpl implements ChunkIterator { private final MCAFileBase owner; private int currentIndex; diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 547af2c7..f995bf6a 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -148,7 +148,7 @@ public static String createNameFromBlockLocation(int blockX, int blockZ) { } /** - * Creates a filename string from provided chunk coordinates. + * Creates a filename string from provided region coordinates. * @param regionX The x-value of the location of the region. * @param regionZ The z-value of the location of the region. * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index b9a3c841..c4f8c779 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -11,7 +11,8 @@ import java.util.stream.StreamSupport; /** - * Represents a REGION data chunk section. + * Represents a REGION data chunk section. Sections can be thought of as "sub-chunks" + * which are 16x16x16 block cubes stacked atop each other to create a "chunk". */ public class Section extends SectionBase
{ @@ -516,7 +517,7 @@ public int currentY() { } @Override - public int currentWorldY() { + public int currentBlockY() { return sectionWorldY + (currentIndex >> 8); } } diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index 0b1e8479..0ca0a574 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -13,7 +13,11 @@ import static net.querz.mca.LoadFlags.*; -public abstract class SectionedChunkBase implements Iterable { +/** + * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. + * @param Concrete type of section. + */ +public abstract class SectionedChunkBase> implements Iterable { protected boolean partial; protected boolean raw; From b10a3a2291088c44b994f57a92d7c10a5f37eff0 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 10:15:39 -0500 Subject: [PATCH 004/288] Add non-full release version support to DataVersion enum --- src/main/java/net/querz/mca/DataVersion.java | 66 ++++++++++++++++++- .../java/net/querz/mca/DataVersionTest.java | 4 +- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 085733f1..65e5db75 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -47,20 +47,52 @@ public enum DataVersion { JAVA_1_16_5(2586, 16, 5), JAVA_1_17_0(2724, 17, 0), - JAVA_1_17_1(2730, 17, 1); + JAVA_1_17_1(2730, 17, 1), + + JAVA_1_18_XS1(2825, 18, 0, false, "XS1"), + JAVA_1_18_21W44A(2845, 18, 0, false, "21w44a"); private static final int[] ids = Arrays.stream(values()).mapToInt(DataVersion::id).toArray(); private final int id; private final int minor; private final int patch; + private final boolean isFullRelease; + private final String buildDescription; private final String str; DataVersion(int id, int minor, int patch) { + this(id, minor, patch, true, null); + } + + /** + * @param id data version + * @param minor minor version + * @param patch patch number + * @param isFullRelease indicates if this data version is from a full release or not + * @param buildDescription Suggested convention:
    + *
  • NULL for full release
  • + *
  • CT# for combat tests (e.g. CT6, CT6b)
  • + *
  • XS# for experimental snapshots(e.g. XS1, XS2)
  • + *
  • YYwWWz for weekly builds (e.g. 21w37a, 21w37b)
  • + *
  • PR# for pre-releases (e.g. PR1, PR2)
  • + *
  • RC# for release candidates (e.g. RC1, RC2)
+ */ + DataVersion(int id, int minor, int patch, boolean isFullRelease, String buildDescription) { + if (!isFullRelease && (buildDescription == null || buildDescription.isEmpty())) + throw new IllegalArgumentException("buildDescription required for non-full releases"); + if (isFullRelease && (buildDescription != null && !buildDescription.isEmpty())) + throw new IllegalArgumentException("buildDescription not allowed for full releases"); this.id = id; this.minor = minor; this.patch = patch; + this.isFullRelease = isFullRelease; + this.buildDescription = isFullRelease ? "FINAL" : buildDescription; if (minor > 0) { - this.str = String.format("%d (1.%d.%d)", id, minor, patch); + StringBuilder sb = new StringBuilder(); + sb.append(id).append(" (1.").append(minor); + if (patch > 0) sb.append('.').append(patch); + if (!isFullRelease) sb.append(' ').append(buildDescription); + this.str = sb.append(')').toString(); } else { this.str = name(); } @@ -82,6 +114,29 @@ public int patch() { return patch; } + /** + * True for full release. + * False for all other builds (e.g. experimental, pre-releases, and release-candidates). + */ + public boolean isFullRelease() { + return isFullRelease; + } + + /** + * Description of the minecraft build which this {@link DataVersion} refers to. + * You'll find {@link #toString()} to be more useful in general. + *

Convention used:

    + *
  • "FULL" for full release
  • + *
  • YYwWWz for weekly builds (e.g. 21w37a, 21w37b)
  • + *
  • CT# for combat tests (e.g. CT6, CT6b)
  • + *
  • XS# for experimental snapshots(e.g. XS1, XS2)
  • + *
  • PR# for pre-releases (e.g. PR1, PR2)
  • + *
  • RC# for release candidates (e.g. RC1, RC2)
+ */ + public String getBuildDescription() { + return buildDescription; + } + /** * TRUE as of 1.14 * Indicates if point of interest .mca files exist. E.g. 'poi/r.0.0.mca' @@ -98,6 +153,13 @@ public boolean hasEntitiesMca() { return minor >= 17; } + /** + * TRUE before 1.18, FALSE after + */ + public boolean regionChunksHaveLevelTag() { + return minor < 18; + } + public static DataVersion bestFor(int dataVersion) { int found = Arrays.binarySearch(ids, dataVersion); if (found < 0) { diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java index 6703395d..f601c981 100644 --- a/src/test/java/net/querz/mca/DataVersionTest.java +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -35,7 +35,9 @@ public void testBestForAfterLast() { } public void testToString() { - assertEquals("2724 (1.17.0)", DataVersion.JAVA_1_17_0.toString()); + assertEquals("2724 (1.17)", DataVersion.JAVA_1_17_0.toString()); + assertEquals("2730 (1.17.1)", DataVersion.JAVA_1_17_1.toString()); assertEquals("UNKNOWN", DataVersion.UNKNOWN.toString()); + assertEquals("2845 (1.18 21w44a)", DataVersion.JAVA_1_18_21W44A.toString()); } } From abb63e822fabfba0943465b97c098ab819ec5a8f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 10:20:46 -0500 Subject: [PATCH 005/288] add test for Chunk get Min/Max SectionY --- src/test/java/net/querz/mca/MCAFileTest.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index dc9c130d..e1cdc558 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -4,9 +4,8 @@ import net.querz.nbt.tag.EndTag; import net.querz.nbt.tag.ListTag; import static net.querz.mca.LoadFlags.*; -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; + +import java.io.*; import java.util.Arrays; public class MCAFileTest extends MCATestCase { @@ -234,6 +233,17 @@ public void testCleanupPaletteAndBlockStates() { assertEquals(256, s.updateHandle(0).getLongArray("BlockStates").length); } + public void testMaxAndMinSectionY() { + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); + Chunk c = f.getChunk(0, 0); + assertEquals(0, c.getMinSectionY()); + assertEquals(5, c.getMaxSectionY()); + c.setSection(-64 / 16, Section.newSection()); + c.setSection((320 - 16) / 16, Section.newSection()); + assertEquals(-4, c.getMinSectionY()); + assertEquals(19, c.getMaxSectionY()); + } + public void testSetBlockDataAt() { MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); Section section = f.getChunk(0, 0).getSection(0); From 5dd3cf9ef9649496180d6a60651adc6cbf37fac4 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 11:55:34 -0500 Subject: [PATCH 006/288] Enhancing test base temp file system. Enhancing test base assertThrowsNoException method to always report useful stacktrace (without using printStacktrace) --- src/test/java/net/querz/NBTTestCase.java | 49 +++++++++----------- src/test/java/net/querz/mca/MCAUtilTest.java | 5 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 7d412a59..67088ddb 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -1,5 +1,6 @@ package net.querz; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; import net.querz.nbt.io.NBTDeserializer; import net.querz.nbt.io.NBTSerializer; @@ -19,6 +20,8 @@ import java.math.BigInteger; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.UUID; @@ -151,20 +154,19 @@ protected void assertThrowsException(ExceptionSupplier< } } - protected T assertThrowsNoException(ExceptionSupplier r) { - return assertThrowsNoException(r, false); + private static class UnexpectedExceptionThrownException extends AssertionFailedError { + public UnexpectedExceptionThrownException(Exception ex) { + super("Threw exception " + ex.getClass().getName() + " with message \"" + ex.getMessage() + "\""); + this.setStackTrace(ex.getStackTrace()); + } } - protected T assertThrowsNoException(ExceptionSupplier r, boolean printStackTrace) { + protected T assertThrowsNoException(ExceptionSupplier r) { try { return r.run(); } catch (Exception ex) { - if (printStackTrace) { - ex.printStackTrace(); - } - TestCase.fail("Threw exception " + ex.getClass().getName() + " with message \"" + ex.getMessage() + "\""); + throw new UnexpectedExceptionThrownException(ex); } - return null; } protected void assertThrowsRuntimeException(Runnable r, Class e) { @@ -213,25 +215,20 @@ protected T assertThrowsNoRuntimeException(Supplier r) { } protected File getNewTmpFile(String name) { - String workingDir = System.getProperty("user.dir"); - File tmpDir = new File(workingDir, "tmp"); - if (!tmpDir.exists()) { - tmpDir.mkdirs(); - } - File tmpFile = new File(tmpDir, name); - if (tmpFile.exists()) { - tmpFile = new File(tmpDir, UUID.randomUUID() + name); - } - return tmpFile; - } - - protected File getTmpFile(String name) { - String workingDir = System.getProperty("user.dir"); - File tmpDir = new File(workingDir, "tmp"); - if (!tmpDir.exists()) { - tmpDir.mkdirs(); + final String workingDir = System.getProperty("user.dir"); + Path tmpPath = Paths.get( + workingDir, + "tmp", + this.getClass().getSimpleName(), + getName(), + UUID.randomUUID().toString(), + name); + File dir = tmpPath.getParent().toFile(); + if (!dir.exists()) { + dir.mkdirs(); } - return new File(tmpDir, name); + System.out.println("TEMP FILE: " + tmpPath); + return tmpPath.toFile(); } protected File copyResourceToTmp(String resource) { diff --git a/src/test/java/net/querz/mca/MCAUtilTest.java b/src/test/java/net/querz/mca/MCAUtilTest.java index a63dde4e..73447a4d 100644 --- a/src/test/java/net/querz/mca/MCAUtilTest.java +++ b/src/test/java/net/querz/mca/MCAUtilTest.java @@ -85,7 +85,8 @@ public void testMakeMyCoverageGreatAgain() { // test overwriting file MCAFile m = new MCAFile(0, 0); m.setChunk(0, Chunk.newChunk()); - assertThrowsNoException(() -> MCAUtil.write(m, getTmpFile("r.0.0.mca"), false), true); - assertThrowsNoException(() -> MCAUtil.write(m, getTmpFile("r.0.0.mca"), false), true); + File target = getNewTmpFile("r.0.0.mca"); + assertThrowsNoException(() -> MCAUtil.write(m, target, false)); + assertThrowsNoException(() -> MCAUtil.write(m, target, false)); } } From 6038be66a2c965abc6ca8b258b0732182abfa189 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 17:11:07 -0500 Subject: [PATCH 007/288] Adding pre 1.18 chunks to test resources; specifically from version 1.18 21w44a (DV 2845) Adding a .md file to test resources to describe the properties of the test data for future reference. --- src/test/resources/1_18/region/r.-1.-1.mca | Bin 0 -> 16384 bytes src/test/resources/1_18/region/r.15.-9.mca | Bin 0 -> 20480 bytes src/test/resources/ABOUT_TEST_DATA.md | 15 +++++++++++++++ 3 files changed, 15 insertions(+) create mode 100644 src/test/resources/1_18/region/r.-1.-1.mca create mode 100644 src/test/resources/1_18/region/r.15.-9.mca create mode 100644 src/test/resources/ABOUT_TEST_DATA.md diff --git a/src/test/resources/1_18/region/r.-1.-1.mca b/src/test/resources/1_18/region/r.-1.-1.mca new file mode 100644 index 0000000000000000000000000000000000000000..293c54425102b8de0b433dca0ed0e22a92517389 GIT binary patch literal 16384 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 Be7OJs literal 0 HcmV?d00001 diff --git a/src/test/resources/1_18/region/r.15.-9.mca b/src/test/resources/1_18/region/r.15.-9.mca new file mode 100644 index 0000000000000000000000000000000000000000..bf1c5b0f67f52c058c349815c8a903682b4d736b GIT binary patch literal 20480 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 Date: Sun, 7 Nov 2021 17:24:19 -0500 Subject: [PATCH 008/288] Leverage new DataVersions enum throughout existing code base. Added informational getters to MCAFileBase to expose min and max data values as read from disk. Exposed ability for lib users to control the default data version of chunks auto-created by various MCAFile calls. Enhanced data version consistency throughout mca files, chunks, and sections. It should require much less care by this libraries users to maintain consistent data version values. Also added "createChunk" "createSection" helpers on MCAFile and Chunk respectively which will create objects with consistent data versions. Added explicit upgrade handling for Section blocks state when upgrading data versions from 1.14 and below to 1.15 and above. Improved consistency of Section "Y" values and to reduce risk of accidents for library users. It is still possible for a Section to have a "height" (aka Y) different from where the Chunk thinks it belongs, the Chunk has the final say now for sure and the occurrence of this inconsistency should be reduced. --- src/main/java/net/querz/mca/Chunk.java | 108 +++++------ src/main/java/net/querz/mca/ChunkBase.java | 172 ++++++++++++++++++ src/main/java/net/querz/mca/DataVersion.java | 53 ++++-- src/main/java/net/querz/mca/MCAFile.java | 18 +- src/main/java/net/querz/mca/MCAFileBase.java | 88 ++++++++- src/main/java/net/querz/mca/Section.java | 62 +++++-- src/main/java/net/querz/mca/SectionBase.java | 14 +- .../net/querz/mca/SectionedChunkBase.java | 133 ++------------ .../net/querz/mca/VersionedDataContainer.java | 42 +++++ src/test/java/net/querz/NBTTestCase.java | 1 - .../java/net/querz/mca/DataVersionTest.java | 5 +- src/test/java/net/querz/mca/MCAFileTest.java | 27 ++- 12 files changed, 497 insertions(+), 226 deletions(-) create mode 100644 src/main/java/net/querz/mca/ChunkBase.java create mode 100644 src/main/java/net/querz/mca/VersionedDataContainer.java diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index d2263f7b..09b669d0 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -2,22 +2,10 @@ import net.querz.nbt.tag.CompoundTag; import net.querz.nbt.tag.ListTag; -import net.querz.nbt.io.NamedTag; -import net.querz.nbt.io.NBTDeserializer; -import net.querz.nbt.io.NBTSerializer; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.RandomAccessFile; + import java.util.Arrays; -import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import static net.querz.mca.DataVersion.JAVA_1_15_19W36A; import static net.querz.mca.LoadFlags.*; /** @@ -28,12 +16,11 @@ public class Chunk extends SectionedChunkBase
{ /** * The default chunk data version used when no custom version is supplied. - *

Deprecated: use {@code DataVersion.latest().id()} instead. + * @deprecated Use {@code DataVersion.latest().id()} instead. */ @Deprecated public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); - protected int dataVersion; protected long lastUpdate; protected long inhabitedTime; protected int[] biomes; @@ -63,16 +50,16 @@ public Chunk(CompoundTag data) { } @Override - protected void initReferences(long loadFlags) { - CompoundTag level; - if ((level = data.getCompoundTag("Level")) == null) { + protected void initReferences(final long loadFlags) { + CompoundTag level = getDataVersionEnum().regionChunksHaveLevelTag() ? data.getCompoundTag("Level") : data; + if (level == null) { throw new IllegalArgumentException("data does not contain \"Level\" tag"); } - dataVersion = data.getInt("DataVersion"); inhabitedTime = level.getLong("InhabitedTime"); lastUpdate = level.getLong("LastUpdate"); if ((loadFlags & BIOMES) != 0) { biomes = level.getIntArray("Biomes"); + if (biomes.length == 0) biomes = null; } if ((loadFlags & HEIGHTMAPS) != 0) { heightMaps = level.getCompoundTag("Heightmaps"); @@ -118,17 +105,19 @@ protected void initReferences(long loadFlags) { } /** - * @deprecated Use {@link #getBiomeAt(int, int, int)} instead + * May only be used for data versions LT 2203 which includes all of 1.14 + * and up until 19w36a (a 1.15 weekly snapshot). + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead for 1.15 and beyond */ @Deprecated public int getBiomeAt(int blockX, int blockZ) { - if (dataVersion < 2202) { + if (dataVersion < JAVA_1_15_19W36A.id()) { if (biomes == null || biomes.length != 256) { return -1; } return biomes[getBlockIndex(blockX, blockZ)]; } else { - throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2202 or higher, use Chunk#getBiomeAt(int,int,int) instead"); + throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2203 or higher, use Chunk#getBiomeAt(int,int,int) instead"); } } @@ -141,12 +130,7 @@ public int getBiomeAt(int blockX, int blockZ) { * @return The biome id or -1 if the biomes are not correctly initialized. */ public int getBiomeAt(int blockX, int blockY, int blockZ) { - if (dataVersion < 2202) { - if (biomes == null || biomes.length != 256) { - return -1; - } - return biomes[getBlockIndex(blockX, blockZ)]; - } else { + if (dataVersion >= JAVA_1_15_19W36A.id()) { if (biomes == null || biomes.length != 1024) { return -1; } @@ -155,13 +139,15 @@ public int getBiomeAt(int blockX, int blockY, int blockZ) { int biomeZ = (blockZ & 0xF) >> 2; return biomes[getBiomeIndex(biomeX, biomeY, biomeZ)]; + } else { + return getBiomeAt(blockX, blockZ); } } @Deprecated public void setBiomeAt(int blockX, int blockZ, int biomeID) { checkRaw(); - if (dataVersion < 2202) { + if (dataVersion < JAVA_1_15_19W36A.id()) { if (biomes == null || biomes.length != 256) { biomes = new int[256]; Arrays.fill(biomes, -1); @@ -192,13 +178,7 @@ public void setBiomeAt(int blockX, int blockZ, int biomeID) { */ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { checkRaw(); - if (dataVersion < 2202) { // pre-1.15 - if (biomes == null || biomes.length != 256) { - biomes = new int[256]; - Arrays.fill(biomes, -1); - } - biomes[getBlockIndex(blockX, blockZ)] = biomeID; - } else { + if (dataVersion >= JAVA_1_15_19W36A.id()) { if (biomes == null || biomes.length != 1024) { biomes = new int[1024]; Arrays.fill(biomes, -1); @@ -208,6 +188,12 @@ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { int biomeZ = (blockZ & 0xF) >> 2; biomes[getBiomeIndex(biomeX, blockY, biomeZ)] = biomeID; + } else { + if (biomes == null || biomes.length != 256) { + biomes = new int[256]; + Arrays.fill(biomes, -1); + } + biomes[getBlockIndex(blockX, blockZ)] = biomeID; } } @@ -239,28 +225,28 @@ public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag stat int sectionIndex = MCAUtil.blockToChunk(blockY); Section section = sections.get(sectionIndex); if (section == null) { - sections.put(sectionIndex, section = Section.newSection()); + sections.put(sectionIndex, section = createSection()); + section.setDataVersion(dataVersion); } section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); } /** - * @return The DataVersion of this chunk. + * Creates a new section appropriately initialized for use inside this chunk. */ - public int getDataVersion() { - return dataVersion; + public Section createSection() { + return new Section(dataVersion); } /** - * Sets the DataVersion of this chunk. This does not check if the data of this chunk conforms - * to that DataVersion, that is the responsibility of the developer. - * @param dataVersion The DataVersion to be set. + * {@inheritDoc} */ + @Override public void setDataVersion(int dataVersion) { - this.dataVersion = dataVersion; + super.setDataVersion(dataVersion); for (Section section : sections.values()) { if (section != null) { - section.dataVersion = dataVersion; + section.setDataVersion(dataVersion); } } } @@ -329,8 +315,9 @@ public int[] getBiomes() { public void setBiomes(int[] biomes) { checkRaw(); if (biomes != null) { - if (dataVersion < 2202 && biomes.length != 256 || dataVersion >= 2202 && biomes.length != 1024) { - throw new IllegalArgumentException("biomes array must have a length of " + (dataVersion < 2202 ? "256" : "1024")); + final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; + if (biomes.length != requiredSize) { + throw new IllegalArgumentException("biomes array must have a length of " + requiredSize); } } this.biomes = biomes; @@ -525,6 +512,11 @@ public void cleanupPalettesAndBlockStates() { } } + /** + * @deprecated Dangerous - assumes latest full release data version defined by {@link DataVersion} + * prefer using {@link MCAFileBase#createChunk()} or {@link MCAFileBase#createChunkIfMissing(int, int)}. + */ + @Deprecated public static Chunk newChunk() { return Chunk.newChunk(DataVersion.latest().id()); } @@ -543,21 +535,19 @@ public CompoundTag updateHandle(int xPos, int zPos) { if (raw) { return data; } - - data.putInt("DataVersion", dataVersion); - CompoundTag level = data.getCompoundTag("Level"); + super.updateHandle(xPos, zPos); + CompoundTag level = getDataVersionEnum().regionChunksHaveLevelTag() ? data.getCompoundTag("Level") : data; level.putInt("xPos", xPos); level.putInt("zPos", zPos); level.putLong("LastUpdate", lastUpdate); level.putLong("InhabitedTime", inhabitedTime); - if (dataVersion < 2202) { - if (biomes != null && biomes.length == 256) { - level.putIntArray("Biomes", biomes); - } - } else { - if (biomes != null && biomes.length == 1024) { - level.putIntArray("Biomes", biomes); - } + if (biomes != null) { + final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; + if (biomes.length != requiredSize) + throw new IllegalStateException( + String.format("Biomes array must be %d bytes for version %d, array size is %d", + requiredSize, dataVersion, biomes.length)); + level.putIntArray("Biomes", biomes); } level.putIfNotNull("Heightmaps", heightMaps); level.putIfNotNull("CarvingMasks", carvingMasks); diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java new file mode 100644 index 00000000..9fc347af --- /dev/null +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -0,0 +1,172 @@ +package net.querz.mca; + +import net.querz.nbt.io.NBTDeserializer; +import net.querz.nbt.io.NBTSerializer; +import net.querz.nbt.io.NamedTag; +import net.querz.nbt.tag.CompoundTag; + +import java.io.*; +import java.util.Objects; + +import static net.querz.mca.LoadFlags.ALL_DATA; +import static net.querz.mca.LoadFlags.RAW; + +/** + * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. + */ +public abstract class ChunkBase implements VersionedDataContainer { + protected int dataVersion; + protected boolean partial; + protected boolean raw; + protected int lastMCAUpdate; + protected CompoundTag data; + + ChunkBase(int lastMCAUpdate) { + this.lastMCAUpdate = lastMCAUpdate; + } + + /** + * Create a new chunk based on raw base data from a region file. + * @param data The raw base data to be used. + */ + public ChunkBase(CompoundTag data) { + this.data = data; + initReferences(ALL_DATA); + } + + private void initReferences0(long loadFlags) { + Objects.requireNonNull(data, "data cannot be null"); + dataVersion = data.getInt("DataVersion"); + + if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { + raw = true; + return; + } + + if (dataVersion == 0) { + throw new IllegalArgumentException("data does not contain \"DataVersion\" tag"); + } + + initReferences(loadFlags); + + // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. + if (loadFlags != ALL_DATA) { + data = null; + partial = true; + } + } + + /** + * Child classes should not call this method directly. + * Raw and partial data handling is taken care of, this method will not be called if {@code loadFlags} is + * {@link LoadFlags#RAW}. + */ + protected abstract void initReferences(final long loadFlags); + + /** + * {@inheritDoc} + */ + public int getDataVersion() { + return dataVersion; + } + + /** + * {@inheritDoc} + */ + public void setDataVersion(int dataVersion) { + this.dataVersion = Math.max(0, dataVersion); + } + + + /** + * Serializes this chunk to a RandomAccessFile. + * @param raf The RandomAccessFile to be written to. + * @param xPos The x-coordinate of the chunk. + * @param zPos The z-coodrinate of the chunk. + * @return The amount of bytes written to the RandomAccessFile. + * @throws UnsupportedOperationException When something went wrong during writing. + * @throws IOException When something went wrong during writing. + */ + public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { + if (partial) { + throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { + new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); + } + byte[] rawData = baos.toByteArray(); + raf.writeInt(rawData.length + 1); // including the byte to store the compression type + raf.writeByte(CompressionType.ZLIB.getID()); + raf.write(rawData); + return rawData.length + 5; + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, ALL_DATA); + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + byte compressionTypeByte = raf.readByte(); + CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); + if (compressionType == null) { + throw new IOException("invalid compression type " + compressionTypeByte); + } + BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); + NamedTag tag = new NBTDeserializer(false).fromStream(dis); + if (tag != null && tag.getTag() instanceof CompoundTag) { + data = (CompoundTag) tag.getTag(); + initReferences0(loadFlags); + } else { + throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); + } + } + + /** + * @return The timestamp when this region file was last updated in seconds since 1970-01-01. + */ + public int getLastMCAUpdate() { + return lastMCAUpdate; + } + + /** + * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. + * @param lastMCAUpdate The time in seconds since 1970-01-01. + */ + public void setLastMCAUpdate(int lastMCAUpdate) { + checkRaw(); + this.lastMCAUpdate = lastMCAUpdate; + } + + protected void checkRaw() { + if (raw) { + throw new UnsupportedOperationException("cannot update field when working with raw data"); + } + } + + /** + * Provides a reference to the full chunk data. + * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. + */ + public CompoundTag getHandle() { + return data; + } + + public CompoundTag updateHandle(int xPos, int zPos) { + if (!raw) { + data.putInt("DataVersion", dataVersion); + } + return data; + } +} diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 65e5db75..132ffb45 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -1,11 +1,12 @@ package net.querz.mca; import java.util.Arrays; +import java.util.Comparator; // source: version.json file, found in the root directory of the client and server jars // table of versions can also be found on https://minecraft.fandom.com/wiki/Data_version#List_of_data_versions public enum DataVersion { - // Must be kept in ASC order + // Kept in ASC order UNKNOWN(0, 0, 0), JAVA_1_9_0(169, 9, 0), JAVA_1_9_1(175, 9, 1), @@ -29,16 +30,22 @@ public enum DataVersion { JAVA_1_13_1(1628, 13, 1), JAVA_1_13_2(1631, 13, 2), + // poi/r.X.Z.mca files introduced JAVA_1_14_0(1952, 14, 0), JAVA_1_14_1(1957, 14, 1), JAVA_1_14_2(1963, 14, 2), JAVA_1_14_3(1968, 14, 3), JAVA_1_14_4(1976, 14, 4), + // 3D Biomes added. Biomes array in the Level tag for each chunk changed + // to contain 1024 integers instead of 256 see {@link Chunk} + JAVA_1_15_19W36A(2203, 10, -1, "19w36a"), JAVA_1_15_0(2225, 15, 0), JAVA_1_15_1(2227, 15, 1), JAVA_1_15_2(2230, 15, 2), + // block pallet packing changed in this version - see {@link Section} + JAVA_1_16_20W17A(2529, 16, -1, "20w17a"), JAVA_1_16_0(2566, 16, 0), JAVA_1_16_1(2567, 16, 1), JAVA_1_16_2(2578, 16, 2), @@ -46,13 +53,17 @@ public enum DataVersion { JAVA_1_16_4(2584, 16, 4), JAVA_1_16_5(2586, 16, 5), + // entities/r.X.Z.mca files introduced + // entities no longer inside region/r.X.Z.mca - except in un-migrated chunks of course JAVA_1_17_0(2724, 17, 0), JAVA_1_17_1(2730, 17, 1), - JAVA_1_18_XS1(2825, 18, 0, false, "XS1"), - JAVA_1_18_21W44A(2845, 18, 0, false, "21w44a"); + // fist experimental 1.18 build + // region mca chunk NBT no longer stored inside "Level" tag at some point before/by 21w44a (unsure when exactly) + JAVA_1_18_XS1(2825, 18, -1, "XS1"); - private static final int[] ids = Arrays.stream(values()).mapToInt(DataVersion::id).toArray(); + private static final int[] ids; + private static final DataVersion latestFullReleaseVersion; private final int id; private final int minor; private final int patch; @@ -60,15 +71,22 @@ public enum DataVersion { private final String buildDescription; private final String str; + static { + ids = Arrays.stream(values()).sorted().mapToInt(DataVersion::id).toArray(); + latestFullReleaseVersion = Arrays.stream(values()) + .sorted(Comparator.reverseOrder()) + .filter(DataVersion::isFullRelease) + .findFirst().get(); + } + DataVersion(int id, int minor, int patch) { - this(id, minor, patch, true, null); + this(id, minor, patch, null); } /** * @param id data version * @param minor minor version - * @param patch patch number - * @param isFullRelease indicates if this data version is from a full release or not + * @param patch patch number, LT0 to indicate this data version is not a full release version * @param buildDescription Suggested convention:

    *
  • NULL for full release
  • *
  • CT# for combat tests (e.g. CT6, CT6b)
  • @@ -77,15 +95,15 @@ public enum DataVersion { *
  • PR# for pre-releases (e.g. PR1, PR2)
  • *
  • RC# for release candidates (e.g. RC1, RC2)
*/ - DataVersion(int id, int minor, int patch, boolean isFullRelease, String buildDescription) { + DataVersion(int id, int minor, int patch, String buildDescription) { + this.isFullRelease = patch >= 0; if (!isFullRelease && (buildDescription == null || buildDescription.isEmpty())) throw new IllegalArgumentException("buildDescription required for non-full releases"); if (isFullRelease && (buildDescription != null && !buildDescription.isEmpty())) throw new IllegalArgumentException("buildDescription not allowed for full releases"); this.id = id; this.minor = minor; - this.patch = patch; - this.isFullRelease = isFullRelease; + this.patch = isFullRelease ? patch : -1; this.buildDescription = isFullRelease ? "FINAL" : buildDescription; if (minor > 0) { StringBuilder sb = new StringBuilder(); @@ -102,14 +120,24 @@ public int id() { return id; } + /** + * Version format: major.minor.patch + */ public int major() { return 1; } + /** + * Version format: major.minor.patch + */ public int minor() { return minor; } + /** + * Version format: major.minor.patch + *

This value will be < 0 if this is not a full release version.

+ */ public int patch() { return patch; } @@ -169,8 +197,11 @@ public static DataVersion bestFor(int dataVersion) { return values()[found]; } + /** + * @return The latest full release version defined. + */ public static DataVersion latest() { - return values()[values().length - 1]; + return latestFullReleaseVersion; } @Override diff --git a/src/main/java/net/querz/mca/MCAFile.java b/src/main/java/net/querz/mca/MCAFile.java index a67fb960..354f2c5d 100644 --- a/src/main/java/net/querz/mca/MCAFile.java +++ b/src/main/java/net/querz/mca/MCAFile.java @@ -29,6 +29,20 @@ public MCAFile(int regionX, int regionZ) { super(regionX, regionZ); } + /** + * {@inheritDoc} + */ + public MCAFile(int regionX, int regionZ, int defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + /** + * {@inheritDoc} + */ + public MCAFile(int regionX, int regionZ, DataVersion defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + /** * {@inheritDoc} */ @@ -41,8 +55,8 @@ public Class chunkClass() { * {@inheritDoc} */ @Override - protected Chunk newChunk() { - return Chunk.newChunk(); + public Chunk createChunk() { + return Chunk.newChunk(defaultDataVersion); } /** diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index ac24ca81..20355d07 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.io.RandomAccessFile; import java.lang.reflect.Array; -import java.util.Iterator; import java.util.NoSuchElementException; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -16,12 +15,18 @@ public abstract class MCAFileBase implements Iterable { protected int regionX, regionZ; protected T[] chunks; + protected int minDataVersion; + protected int maxDataVersion; + protected int defaultDataVersion = DataVersion.latest().id(); // data version to use when creating new chunks /** * MCAFile represents a world save file used by Minecraft to store world * data on the hard drive. * This constructor needs the x- and z-coordinates of the stored region, * which can usually be taken from the file name {@code r.x.z.mca} + * + *

Use this constructor when you plan to {@code deserialize(..)} an MCA file. + * If you are creating an MCA file from scratch prefer {@link #MCAFileBase(int, int, int)}. * @param regionX The x-coordinate of this mca file in region coordinates. * @param regionZ The z-coordinate of this mca file in region coordinates. */ @@ -30,6 +35,64 @@ public MCAFileBase(int regionX, int regionZ) { this.regionZ = regionZ; } + /** + * Use this constructor to specify a default data version when creating MCA files without loading + * from disk. + * + * @param regionX The x-coordinate of this mca file in region coordinates. + * @param regionZ The z-coordinate of this mca file in region coordinates. + * @param defaultDataVersion Data version which will be used when creating new chunks. + */ + public MCAFileBase(int regionX, int regionZ, int defaultDataVersion) { + this.regionX = regionX; + this.regionZ = regionZ; + this.defaultDataVersion = defaultDataVersion; + this.minDataVersion = defaultDataVersion; + this.maxDataVersion = defaultDataVersion; + } + + /** + * Use this constructor to specify a default data version when creating MCA files without loading + * from disk. + * + * @param regionX The x-coordinate of this mca file in region coordinates. + * @param regionZ The z-coordinate of this mca file in region coordinates. + * @param defaultDataVersion Data version which will be used when creating new chunks. + */ + public MCAFileBase(int regionX, int regionZ, DataVersion defaultDataVersion) { + this(regionX, regionZ, defaultDataVersion.id()); + } + + /** + * Get minimum data version of found in loaded chunk data + */ + public int getMinChunkDataVersion() { + return minDataVersion; + } + + /** + * Get maximum data version of found in loaded chunk data + */ + public int getMaxChunkDataVersion() { + return maxDataVersion; + } + + /** + * Get chunk version which will be used when automatically creating new chunks + * and for chunks created by {@link #createChunk()}. + */ + public int getDefaultChunkDataVersion() { + return defaultDataVersion; + } + + /** + * Set chunk version which will be used when automatically creating new chunks + * and for chunks created by {@link #createChunk()}. + */ + public void setDefaultChunkDataVersion(int defaultDataVersion) { + this.defaultDataVersion = defaultDataVersion; + } + /** * @return The x-value currently set for this mca file in region coordinates. */ @@ -83,9 +146,10 @@ public String createRegionName() { public abstract Class chunkClass(); /** - * Chunk creator. + * Creates a new chunk properly initialized to be compatible with this MCA file. At a minimum the new + * chunk will have an appropriate data version set. */ - protected abstract T newChunk(); + public abstract T createChunk(); /** * Called to deserialize a Chunk. Caller will have set the position of {@code raf} to start reading. @@ -117,6 +181,8 @@ public void deserialize(RandomAccessFile raf) throws IOException { @SuppressWarnings("unchecked") public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { chunks = (T[]) Array.newInstance(chunkClass(), 1024); + minDataVersion = Integer.MAX_VALUE; + maxDataVersion = Integer.MIN_VALUE; for (int i = 0; i < 1024; i++) { raf.seek(i * 4); int offset = raf.read() << 16; @@ -128,8 +194,20 @@ public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException raf.seek(4096 + i * 4); int timestamp = raf.readInt(); raf.seek(4096L * offset + 4); //+4: skip data size - chunks[i] = deserializeChunk(raf, loadFlags, timestamp); + T chunk = deserializeChunk(raf, loadFlags, timestamp); + chunks[i] = chunk; + if (chunk != null && chunk.hasDataVersion()) { + if (chunk.getDataVersion() < minDataVersion) { + minDataVersion = chunk.getDataVersion(); + } + if (chunk.getDataVersion() > maxDataVersion) { + maxDataVersion = chunk.getDataVersion(); + } + } } + maxDataVersion = Math.max(maxDataVersion, 0); + minDataVersion = Math.min(minDataVersion, maxDataVersion); + defaultDataVersion = maxDataVersion; } /** @@ -276,7 +354,7 @@ protected T createChunkIfMissing(int blockX, int blockZ) { int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); T chunk = getChunk(chunkX, chunkZ); if (chunk == null) { - chunk = newChunk(); + chunk = createChunk(); setChunk(getChunkIndex(chunkX, chunkZ), chunk); } return chunk; diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index c4f8c779..44501fa3 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -1,5 +1,6 @@ package net.querz.mca; +import static net.querz.mca.DataVersion.JAVA_1_16_20W17A; import static net.querz.mca.LoadFlags.*; import net.querz.nbt.tag.ByteArrayTag; import net.querz.nbt.tag.CompoundTag; @@ -16,7 +17,7 @@ */ public class Section extends SectionBase

{ - protected int dataVersion; + private int dataVersion; // for internal use only - must be kept in sync with chunk data version protected Map> valueIndexedPalette = new HashMap<>(); protected ListTag palette; protected byte[] blockLight; @@ -41,6 +42,9 @@ public Section(CompoundTag sectionRoot, int dataVersion) { public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { super(sectionRoot); + if (dataVersion <= 0) { + throw new IllegalArgumentException("Invalid data version - must be GT 0"); + } this.dataVersion = dataVersion; height = sectionRoot.getNumber("Y").byteValue(); @@ -68,7 +72,16 @@ public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { } } - Section() {} + Section(int dataVersion) { + this.dataVersion = dataVersion; + blockLight = createBlockLightBuffer(); + blockStates = createBlockStates(); + skyLight = createSkyLightBuffer(); + palette = new ListTag<>(CompoundTag.class); + CompoundTag air = new CompoundTag(); + air.putString("Name", "minecraft:air"); + palette.add(air); + } private void assureBlockStates() { if (blockStates == null) blockStates = createBlockStates(); @@ -190,7 +203,7 @@ public int getPaletteIndex(int blockStateIndex) { assureBlockStates(); int bits = blockStates.length >> 6; - if (dataVersion < 2527) { + if (dataVersion > 0 && dataVersion < JAVA_1_16_20W17A.id()) { double blockStatesIndex = blockStateIndex / (4096D / blockStates.length); int longIndex = (int) blockStatesIndex; int startBit = (int) ((blockStatesIndex - Math.floor(blockStatesIndex)) * 64D); @@ -219,7 +232,7 @@ public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates Objects.requireNonNull(blockStates, "blockStates must not be null"); int bits = blockStates.length >> 6; - if (dataVersion < 2527) { + if (dataVersion < JAVA_1_16_20W17A.id()) { double blockStatesIndex = blockIndex / (4096D / blockStates.length); int longIndex = (int) blockStatesIndex; int startBit = (int) ((blockStatesIndex - Math.floor(longIndex)) * 64D); @@ -237,6 +250,31 @@ public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates } } + private void upgradeFromBefore20W17A(final int targetVersion) { + int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); + newBits = Math.max(newBits, 4); + long[] newBlockStates; + + int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; + + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, getPaletteIndex(i), newBlockStates); + } + this.blockStates = newBlockStates; + this.dataVersion = targetVersion; + } + + protected void setDataVersion(int newDataVersion) { + if (newDataVersion <= 0) { + throw new IllegalArgumentException("Invalid data version - must be GT 0"); + } + if (dataVersion < JAVA_1_16_20W17A.id() && newDataVersion >= JAVA_1_16_20W17A.id()) { + upgradeFromBefore20W17A(newDataVersion); + } + dataVersion = newDataVersion; + } + /** * Fetches the palette of this Section. * @return The palette of this Section. @@ -318,7 +356,7 @@ void adjustBlockStateBits(Map oldToNewMapping, long[] blockSta long[] newBlockStates; - if (dataVersion < 2527) { + if (dataVersion < JAVA_1_16_20W17A.id()) { newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; } else { int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); @@ -399,17 +437,11 @@ public void setSkyLight(byte[] skyLight) { /** * Creates an empty Section with base values. * @return An empty Section + * @deprecated Dangerous - prefer using {@link Chunk#createSection()} instead. */ + @Deprecated public static Section newSection() { - Section s = new Section(); - s.blockLight = createBlockLightBuffer(); - s.blockStates = createBlockStates(); - s.skyLight = createSkyLightBuffer(); - s.palette = new ListTag<>(CompoundTag.class); - CompoundTag air = new CompoundTag(); - air.putString("Name", "minecraft:air"); - s.palette.add(air); - return s; + return new Section(DataVersion.latest().id()); } /** @@ -421,6 +453,8 @@ public static Section newSection() { */ @Override public CompoundTag updateHandle(int y) { + checkY(y); + // TODO(1.18): this data type changes in 1.18 data.putByte("Y", (byte) y); if (palette != null) { data.put("Palette", palette); diff --git a/src/main/java/net/querz/mca/SectionBase.java b/src/main/java/net/querz/mca/SectionBase.java index 363a45a2..b2575c7a 100644 --- a/src/main/java/net/querz/mca/SectionBase.java +++ b/src/main/java/net/querz/mca/SectionBase.java @@ -4,9 +4,10 @@ import java.util.*; public abstract class SectionBase> implements Comparable { - + protected static final int NO_HEIGHT_SENTINEL = Integer.MIN_VALUE; protected final CompoundTag data; - protected int height; + protected int height = NO_HEIGHT_SENTINEL; + protected SectionContainer container; public SectionBase(CompoundTag sectionRoot) { Objects.requireNonNull(sectionRoot, "sectionRoot must not be null"); @@ -44,11 +45,18 @@ public void setHeight(int height) { this.height = height; } + protected void checkY(int y) { + if (y == NO_HEIGHT_SENTINEL) { + throw new IndexOutOfBoundsException("section height not set"); + } + } + /** * Updates the raw CompoundTag that this Section is based on. * This must be called before saving a Section to disk if the Section was manually created * to set the Y of this Section. - * @param y The Y-value of this Section + * @param y The Y-value of this Section to include in the returned tag. + * DOES NOT update this sections height value permanently. * @return A reference to the raw CompoundTag this Section is based on */ public abstract CompoundTag updateHandle(int y); diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index 0ca0a574..66042034 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -17,16 +17,12 @@ * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. * @param Concrete type of section. */ -public abstract class SectionedChunkBase> implements Iterable { +public abstract class SectionedChunkBase> extends ChunkBase implements Iterable { - protected boolean partial; - protected boolean raw; - protected int lastMCAUpdate; - protected CompoundTag data; protected final TreeMap sections = new TreeMap<>(); SectionedChunkBase(int lastMCAUpdate) { - this.lastMCAUpdate = lastMCAUpdate; + super(lastMCAUpdate); } /** @@ -34,103 +30,7 @@ public abstract class SectionedChunkBase> implements It * @param data The raw base data to be used. */ public SectionedChunkBase(CompoundTag data) { - this.data = data; - initReferences(ALL_DATA); - } - - private void initReferences0(long loadFlags) { - Objects.requireNonNull(data, "data cannot be null"); - - if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { - raw = true; - return; - } - - initReferences(loadFlags); - - // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. - if (loadFlags != ALL_DATA) { - data = null; - partial = true; - } - } - - /** - * Child classes should not call this method directly. - * Raw and partial data handling is taken care of, this method will not be called if {@code loadFlags} is - * {@link LoadFlags#RAW}. - */ - protected abstract void initReferences(long loadFlags); - - /** - * Serializes this chunk to a RandomAccessFile. - * @param raf The RandomAccessFile to be written to. - * @param xPos The x-coordinate of the chunk. - * @param zPos The z-coodrinate of the chunk. - * @return The amount of bytes written to the RandomAccessFile. - * @throws UnsupportedOperationException When something went wrong during writing. - * @throws IOException When something went wrong during writing. - */ - public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { - if (partial) { - throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); - try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { - new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); - } - byte[] rawData = baos.toByteArray(); - raf.writeInt(rawData.length + 1); // including the byte to store the compression type - raf.writeByte(CompressionType.ZLIB.getID()); - raf.write(rawData); - return rawData.length + 5; - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * @param raf The RandomAccessFile to read the chunk data from. - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf) throws IOException { - deserialize(raf, ALL_DATA); - } - - /** - * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. - * @param raf The RandomAccessFile to read the chunk data from. - * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded - * @throws IOException When something went wrong during reading. - */ - public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { - byte compressionTypeByte = raf.readByte(); - CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); - if (compressionType == null) { - throw new IOException("invalid compression type " + compressionTypeByte); - } - BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); - NamedTag tag = new NBTDeserializer(false).fromStream(dis); - if (tag != null && tag.getTag() instanceof CompoundTag) { - data = (CompoundTag) tag.getTag(); - initReferences0(loadFlags); - } else { - throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); - } - } - - /** - * @return The timestamp when this region file was last updated in seconds since 1970-01-01. - */ - public int getLastMCAUpdate() { - return lastMCAUpdate; - } - - /** - * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. - * @param lastMCAUpdate The time in seconds since 1970-01-01. - */ - public void setLastMCAUpdate(int lastMCAUpdate) { - checkRaw(); - this.lastMCAUpdate = lastMCAUpdate; + super(data); } /** @@ -139,7 +39,11 @@ public void setLastMCAUpdate(int lastMCAUpdate) { * @return The Section. */ public T getSection(int sectionY) { - return sections.get(sectionY); + T section = sections.get(sectionY); + if (section != null) { + section.setHeight(sectionY); + } + return section; } /** @@ -149,6 +53,9 @@ public T getSection(int sectionY) { */ public void setSection(int sectionY, T section) { checkRaw(); + if (section != null) { + section.setHeight(sectionY); + } sections.put(sectionY, section); } @@ -168,23 +75,6 @@ public int getMaxSectionY() { return sections.lastKey(); } - - protected void checkRaw() { - if (raw) { - throw new UnsupportedOperationException("cannot update field when working with raw data"); - } - } - - /** - * Provides a reference to the full chunk data. - * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. - */ - public CompoundTag getHandle() { - return data; - } - - public abstract CompoundTag updateHandle(int xPos, int zPos); - @Override public Iterator iterator() { return sections.values().iterator(); @@ -193,4 +83,5 @@ public Iterator iterator() { public Stream stream() { return sections.values().stream(); } + } diff --git a/src/main/java/net/querz/mca/VersionedDataContainer.java b/src/main/java/net/querz/mca/VersionedDataContainer.java new file mode 100644 index 00000000..b0fe5871 --- /dev/null +++ b/src/main/java/net/querz/mca/VersionedDataContainer.java @@ -0,0 +1,42 @@ +package net.querz.mca; + +/** + * Interface for any NBT data container which has the "DataVersion" tag. + */ +public interface VersionedDataContainer { + + /** + * @return The exact data version of this container. + */ + int getDataVersion(); + + /** + * Sets the data version value for this container. This does not check if the data of this container + * conforms to that of the data version specified, that is the responsibility of the developer. + * @param dataVersion The numeric data version to be set. + */ + void setDataVersion(int dataVersion); + + /** + * Indicates if the held data version has been set. + */ + default boolean hasDataVersion() { + return getDataVersionEnum() != DataVersion.UNKNOWN; + } + + /** + * Equivalent to calling {@link DataVersion#bestFor(int)} with {@link #getDataVersion()}. + * @return The best matching {@link DataVersion} of this chunk. + */ + default DataVersion getDataVersionEnum() { + return DataVersion.bestFor(getDataVersion()); + } + + /** + * Equivalent to calling {@link #setDataVersion(int)} with {@link DataVersion#id()}. + * @param dataVersion The {@link DataVersion} to set. + */ + default void setDataVersion(DataVersion dataVersion) { + setDataVersion(dataVersion.id()); + } +} diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 67088ddb..656fa9d1 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -227,7 +227,6 @@ protected File getNewTmpFile(String name) { if (!dir.exists()) { dir.mkdirs(); } - System.out.println("TEMP FILE: " + tmpPath); return tmpPath.toFile(); } diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java index f601c981..cc025b30 100644 --- a/src/test/java/net/querz/mca/DataVersionTest.java +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -1,10 +1,7 @@ package net.querz.mca; - import junit.framework.TestCase; -import java.util.Arrays; - public class DataVersionTest extends TestCase { public void testBestForNegativeValue() { @@ -38,6 +35,6 @@ public void testToString() { assertEquals("2724 (1.17)", DataVersion.JAVA_1_17_0.toString()); assertEquals("2730 (1.17.1)", DataVersion.JAVA_1_17_1.toString()); assertEquals("UNKNOWN", DataVersion.UNKNOWN.toString()); - assertEquals("2845 (1.18 21w44a)", DataVersion.JAVA_1_18_21W44A.toString()); + assertEquals("2529 (1.16 20w17a)", DataVersion.JAVA_1_16_20W17A.toString()); } } diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index e1cdc558..a5a08927 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -112,6 +112,7 @@ private Chunk createChunkWithPos() { CompoundTag data = new CompoundTag(); CompoundTag level = new CompoundTag(); data.put("Level", level); + data.putInt("DataVersion", DataVersion.JAVA_1_16_5.id()); return new Chunk(data); } @@ -160,7 +161,7 @@ public void testSetters() { assertEquals(getSomeListTagList(), f.getChunk(1023).getPostProcessing()); f.getChunk(1023).setStructures(getSomeCompoundTag()); assertEquals(getSomeCompoundTag(), f.getChunk(1023).getStructures()); - Section s = new Section(); + Section s = f.getChunk(1023).createSection(); f.getChunk(1023).setSection(0, s); assertEquals(s, f.getChunk(1023).getSection(0)); assertThrowsRuntimeException(() -> f.getChunk(1023).getSection(0).setBlockStates(null), NullPointerException.class); @@ -180,22 +181,33 @@ public void testSetters() { assertThrowsNoRuntimeException(() -> f.getChunk(1023).getSection(0).setSkyLight(null)); } - public void testGetBiomeAt() { + public void testGetBiomeAt2D() { MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertEquals(21, f.getBiomeAt(1024, 1024)); assertEquals(-1, f.getBiomeAt(1040, 1024)); f.setChunk(0, 1, Chunk.newChunk(2201)); assertEquals(-1, f.getBiomeAt(1024, 1040)); - } - public void testSetBiomeAt() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca")), true); + public void testSetBiomeAt_2D_2dBiomeWorld() { + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); f.setBiomeAt(1024, 1024, 20); assertEquals(20, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[0]); f.setBiomeAt(1039, 1039, 47); assertEquals(47, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[255]); f.setBiomeAt(1040, 1024, 20); + + int[] biomes = f.getChunk(65, 64).updateHandle(65, 64).getCompoundTag("Level").getIntArray("Biomes"); + assertEquals(256, biomes.length); + assertEquals(20, biomes[0]); + for (int i = 1; i < 256; i++) { + assertEquals(-1, biomes[i]); + } + } + + public void testSetBiomeAt_2D_3DBiomeWorld() { + MCAFile f = new MCAFile(2, 2, DataVersion.JAVA_1_15_0); + f.setBiomeAt(1040, 1024, 20); int[] biomes = f.getChunk(65, 64).updateHandle(65, 64).getCompoundTag("Level").getIntArray("Biomes"); assertEquals(1024, biomes.length); for (int i = 0; i < 1024; i++) { @@ -246,6 +258,8 @@ public void testMaxAndMinSectionY() { public void testSetBlockDataAt() { MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); + assertEquals(f.getMaxChunkDataVersion(), f.getMinChunkDataVersion()); + assertTrue(f.getDefaultChunkDataVersion() > 0); Section section = f.getChunk(0, 0).getSection(0); assertEquals(10, section.getPalette().size()); assertEquals(0b0001000100010001000100010001000100010001000100010001000100010001L, section.getBlockStates()[0]); @@ -270,6 +284,7 @@ public void testSetBlockDataAt() { assertNull(f.getChunk(1, 0)); f.setBlockStateAt(17, 0, 0, block("minecraft:test"), false); assertNotNull(f.getChunk(1, 0)); + assertEquals(f.getDefaultChunkDataVersion(), f.getChunk(1, 0).getDataVersion()); ListTag s = f.getChunk(1, 0).updateHandle(65, 64).getCompoundTag("Level").getListTag("Sections").asCompoundTagList(); assertEquals(1, s.size()); assertEquals(2, s.get(0).getListTag("Palette").size()); @@ -446,7 +461,7 @@ public void testPartialLoad() { } } - public void test1_15GetBiomeAt() throws IOException { + public void test1_15GetBiomeAt() { MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.0.0.mca"))); assertEquals(162, f.getBiomeAt(31, 0, 63)); assertEquals(4, f.getBiomeAt(16, 0, 48)); From f279206a02c93d887d536933db3a981f7361d039 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 20:02:49 -0500 Subject: [PATCH 009/288] Fix for section Y consistency, always use chunk's section y value when generating NBT --- src/main/java/net/querz/mca/Chunk.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 09b669d0..ce90c18f 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -4,6 +4,7 @@ import net.querz.nbt.tag.ListTag; import java.util.Arrays; +import java.util.Map; import static net.querz.mca.DataVersion.JAVA_1_15_19W36A; import static net.querz.mca.LoadFlags.*; @@ -563,9 +564,9 @@ public CompoundTag updateHandle(int xPos, int zPos) { level.putIfNotNull("Structures", structures); ListTag sections = new ListTag<>(CompoundTag.class); - for (Section section : this.sections.values()) { - if (section != null) { - sections.add(section.updateHandle()); + for (Map.Entry entry : this.sections.entrySet()) { + if (entry.getValue() != null) { + sections.add(entry.getValue().updateHandle(entry.getKey())); } } level.put("Sections", sections); From aa453f8349f723276a8ce966733c126915c5868f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 20:04:54 -0500 Subject: [PATCH 010/288] remove SectionContainer debris (idea which i've abandoned for now) --- src/main/java/net/querz/mca/SectionBase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/querz/mca/SectionBase.java b/src/main/java/net/querz/mca/SectionBase.java index b2575c7a..dcc2b829 100644 --- a/src/main/java/net/querz/mca/SectionBase.java +++ b/src/main/java/net/querz/mca/SectionBase.java @@ -7,7 +7,6 @@ public abstract class SectionBase> implements Comparabl protected static final int NO_HEIGHT_SENTINEL = Integer.MIN_VALUE; protected final CompoundTag data; protected int height = NO_HEIGHT_SENTINEL; - protected SectionContainer container; public SectionBase(CompoundTag sectionRoot) { Objects.requireNonNull(sectionRoot, "sectionRoot must not be null"); From efb961f87aa60da1db1130d093e6cfcd9b4ea48c Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 20:42:13 -0500 Subject: [PATCH 011/288] Add ChunkIterator test --- src/test/java/net/querz/mca/MCAFileTest.java | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index a5a08927..a1170b47 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -1,7 +1,6 @@ package net.querz.mca; import net.querz.nbt.tag.CompoundTag; -import net.querz.nbt.tag.EndTag; import net.querz.nbt.tag.ListTag; import static net.querz.mca.LoadFlags.*; @@ -476,4 +475,33 @@ public void test1_15GetBiomeAt() { assertEquals(4, f.getBiomeAt(16, 106, 63)); assertEquals(162, f.getBiomeAt(31, 106, 48)); } + + public void testChunkIterator() { + MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_18/region/r.15.-9.mca"))); + ChunkIterator iter = mca.iterator(); + assertEquals(-1, iter.currentIndex()); + final int populatedX = 483 & 0x1F; + final int populatedZ = -263 & 0x1F; + int i = 0; + for (int z = 0; z < 32; z++) { + for (int x = 0; x < 32; x++) { + assertTrue(iter.hasNext()); + Chunk chunk = iter.next(); + assertEquals(i, iter.currentIndex()); + assertEquals(x, iter.currentX()); + assertEquals(z, iter.currentZ()); + if (x == populatedX && z == populatedZ) { + assertNotNull(chunk); + } else { + assertNull(chunk); + } + if (i == 1023) { + iter.set(mca.createChunk()); + } + i++; + } + } + assertFalse(iter.hasNext()); + assertNotNull(mca.getChunk(1023)); + } } From 1107e0f44b02991e4b46d6549078fb0bfcdb8bf3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 20:43:30 -0500 Subject: [PATCH 012/288] Add NBTUtil#debugWrite to dump large NBT strings to file - more convenient than using System.out for 100k of text --- src/main/java/net/querz/nbt/io/NBTUtil.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/nbt/io/NBTUtil.java b/src/main/java/net/querz/nbt/io/NBTUtil.java index edd0b869..fd6bdf89 100644 --- a/src/main/java/net/querz/nbt/io/NBTUtil.java +++ b/src/main/java/net/querz/nbt/io/NBTUtil.java @@ -1,18 +1,24 @@ package net.querz.nbt.io; import net.querz.nbt.tag.Tag; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PushbackInputStream; + +import java.io.*; import java.util.zip.GZIPInputStream; public final class NBTUtil { private NBTUtil() {} + /** + * Writes the value returned by {@link Tag#toString()} to the specified file. + *

Useful for looking at large data structures, sorry it's not pretty printed.

+ */ + public static void debugWrite(Tag tag, File file) throws IOException { + try (PrintWriter pw = new PrintWriter(new FileOutputStream(file))) { + pw.write(tag.toString()); + } + } + public static void write(NamedTag tag, File file, boolean compressed) throws IOException { try (FileOutputStream fos = new FileOutputStream(file)) { new NBTSerializer(compressed).toStream(tag, fos); From 22c280cc2ceed8f72d20f4571c4bcd1c22d76985 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 7 Nov 2021 20:43:55 -0500 Subject: [PATCH 013/288] removed incorrect TODO --- src/main/java/net/querz/mca/Section.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index 44501fa3..fc83572b 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -454,7 +454,6 @@ public static Section newSection() { @Override public CompoundTag updateHandle(int y) { checkY(y); - // TODO(1.18): this data type changes in 1.18 data.putByte("Y", (byte) y); if (palette != null) { data.put("Palette", palette); From f35005a076d9d10e4fc398aca09bbc0f4a47125a Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 8 Nov 2021 14:46:10 -0500 Subject: [PATCH 014/288] updating chunk biome javadoc and error messaging to be more clear --- src/main/java/net/querz/mca/Chunk.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index ce90c18f..07f22755 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -118,7 +118,7 @@ public int getBiomeAt(int blockX, int blockZ) { } return biomes[getBlockIndex(blockX, blockZ)]; } else { - throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2203 or higher, use Chunk#getBiomeAt(int,int,int) instead"); + throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2203 or higher (1.15+), use Chunk#getBiomeAt(int,int,int) instead"); } } @@ -145,6 +145,11 @@ public int getBiomeAt(int blockX, int blockY, int blockZ) { } } + /** + * Should only be used for data versions LT 2203 which includes all of 1.14 + * and up until 19w36a (a 1.15 weekly snapshot). + * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead for 1.15 and beyond + */ @Deprecated public void setBiomeAt(int blockX, int blockZ, int biomeID) { checkRaw(); @@ -309,9 +314,9 @@ public int[] getBiomes() { /** * Sets the biome IDs for this chunk. - * @param biomes The biome ID matrix of this chunk. Must have a length of 256. - * @throws IllegalArgumentException When the biome matrix does not have a length of 256 - * or is null + * @param biomes The biome ID matrix of this chunk. Must have a length of {@code 1024} for 1.15+ or {@code 256} + * for prior versions. + * @throws IllegalArgumentException When the biome matrix is {@code null} or does not have a version appropriate length. */ public void setBiomes(int[] biomes) { checkRaw(); From 33687bce12268c3b446139df48923306de3daa1f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 8 Nov 2021 16:35:38 -0500 Subject: [PATCH 015/288] DataVersion added details about chunk data changes in 1.18 snapshot progression Realized 1.18 is making a lot of chunk data changes, backed out partial 1.18 support so far - going to rethink how best approach 1.18 region mca data Strengthened section-y synchronization and enforcement Added a 1.17.1 region mca file - interestingly section y's range from -1 to 15... --- src/main/java/net/querz/mca/Chunk.java | 22 +-- src/main/java/net/querz/mca/DataVersion.java | 33 +++- src/main/java/net/querz/mca/MCAFileBase.java | 8 + src/main/java/net/querz/mca/SectionBase.java | 31 +++- .../java/net/querz/mca/SectionIterator.java | 12 ++ .../net/querz/mca/SectionedChunkBase.java | 172 +++++++++++++++--- src/test/java/net/querz/mca/MCAFileTest.java | 37 +++- src/test/resources/1_17_1/region/r.-3.-2.mca | Bin 0 -> 16384 bytes src/test/resources/ABOUT_TEST_DATA.md | 8 + 9 files changed, 270 insertions(+), 53 deletions(-) create mode 100644 src/main/java/net/querz/mca/SectionIterator.java create mode 100644 src/test/resources/1_17_1/region/r.-3.-2.mca diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 07f22755..deecc90c 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -52,7 +52,7 @@ public Chunk(CompoundTag data) { @Override protected void initReferences(final long loadFlags) { - CompoundTag level = getDataVersionEnum().regionChunksHaveLevelTag() ? data.getCompoundTag("Level") : data; + CompoundTag level = data.getCompoundTag("Level"); if (level == null) { throw new IllegalArgumentException("data does not contain \"Level\" tag"); } @@ -100,7 +100,7 @@ protected void initReferences(final long loadFlags) { for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) { int sectionIndex = section.getNumber("Y").byteValue(); Section newSection = new Section(section, dataVersion, loadFlags); - sections.put(sectionIndex, newSection); + putSection(sectionIndex, newSection, false); } } } @@ -208,7 +208,7 @@ int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { } public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - Section section = sections.get(MCAUtil.blockToChunk(blockY)); + Section section = getSection(MCAUtil.blockToChunk(blockY)); if (section == null) { return null; } @@ -229,9 +229,9 @@ public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { checkRaw(); int sectionIndex = MCAUtil.blockToChunk(blockY); - Section section = sections.get(sectionIndex); + Section section = getSection(sectionIndex); if (section == null) { - sections.put(sectionIndex, section = createSection()); + putSection(sectionIndex, section = createSection(), false); section.setDataVersion(dataVersion); } section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); @@ -250,7 +250,7 @@ public Section createSection() { @Override public void setDataVersion(int dataVersion) { super.setDataVersion(dataVersion); - for (Section section : sections.values()) { + for (Section section : this) { if (section != null) { section.setDataVersion(dataVersion); } @@ -511,7 +511,7 @@ int getBlockIndex(int blockX, int blockZ) { public void cleanupPalettesAndBlockStates() { checkRaw(); - for (Section section : sections.values()) { + for (Section section : this) { if (section != null) { section.cleanupPaletteAndBlockStates(); } @@ -542,7 +542,7 @@ public CompoundTag updateHandle(int xPos, int zPos) { return data; } super.updateHandle(xPos, zPos); - CompoundTag level = getDataVersionEnum().regionChunksHaveLevelTag() ? data.getCompoundTag("Level") : data; + CompoundTag level = data.getCompoundTag("Level"); level.putInt("xPos", xPos); level.putInt("zPos", zPos); level.putLong("LastUpdate", lastUpdate); @@ -569,9 +569,9 @@ public CompoundTag updateHandle(int xPos, int zPos) { level.putIfNotNull("Structures", structures); ListTag sections = new ListTag<>(CompoundTag.class); - for (Map.Entry entry : this.sections.entrySet()) { - if (entry.getValue() != null) { - sections.add(entry.getValue().updateHandle(entry.getKey())); + for (Section section : this) { + if (section != null) { + sections.add(section.updateHandle(section.getHeight() /* contract of iterator assures correctness */)); } } level.put("Sections", sections); diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 132ffb45..e9d2c181 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -59,8 +59,30 @@ public enum DataVersion { JAVA_1_17_1(2730, 17, 1), // fist experimental 1.18 build - // region mca chunk NBT no longer stored inside "Level" tag at some point before/by 21w44a (unsure when exactly) - JAVA_1_18_XS1(2825, 18, -1, "XS1"); + JAVA_1_18_XS1(2825, 18, -1, "XS1"), + + // https://www.minecraft.net/en-us/article/minecraft-snapshot-21w39a + // Level.Sections[].BlockStates & Level.Sections[].Palette have moved to a container structure in Level.Sections[].block_states + // Level.Biomes are now paletted and live in a similar container structure in Level.Sections[].biomes + // Level.CarvingMasks[] is now long[] instead of byte[] + JAVA_1_18_21W39A(2836, 18, -1, "21w39a"), + + // https://www.minecraft.net/en-us/article/minecraft-snapshot-21w43a + // Removed chunk’s Level and moved everything it contained up + // Chunk’s Level.Entities has moved to entities + // Chunk’s Level.TileEntities has moved to block_entities + // Chunk’s Level.TileTicks and Level.ToBeTicked have moved to block_ticks + // Chunk’s Level.LiquidTicks and Level.LiquidsToBeTicked have moved to fluid_ticks + // Chunk’s Level.Sections has moved to sections + // Chunk’s Level.Structures has moved to structures + // Chunk’s Level.Structures.Starts has moved to structures.starts + // Chunk’s Level.Sections[].BlockStates and Level.Sections[].Palette have moved to a container structure in sections[].block_states + // Chunk’s Level.Biomes are now paletted and live in a similar container structure in sections[].biomes + // Added yPos the minimum section y position in the chunk + // Added below_zero_retrogen containing data to support below zero generation + // Added blending_data containing data to support blending new world generation with existing chunks + JAVA_1_18_21W43A(2844, 18, -1, "21w43a"); + private static final int[] ids; private static final DataVersion latestFullReleaseVersion; @@ -181,13 +203,6 @@ public boolean hasEntitiesMca() { return minor >= 17; } - /** - * TRUE before 1.18, FALSE after - */ - public boolean regionChunksHaveLevelTag() { - return minor < 18; - } - public static DataVersion bestFor(int dataVersion) { int found = Arrays.binarySearch(ids, dataVersion); if (found < 0) { diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 20355d07..3fcdd409 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -5,6 +5,7 @@ import java.io.RandomAccessFile; import java.lang.reflect.Array; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -63,6 +64,13 @@ public MCAFileBase(int regionX, int regionZ, DataVersion defaultDataVersion) { this(regionX, regionZ, defaultDataVersion.id()); } + /** + * Gets the count of non-null chunks. + */ + public int count() { + return (int) stream().filter(Objects::nonNull).count(); + } + /** * Get minimum data version of found in loaded chunk data */ diff --git a/src/main/java/net/querz/mca/SectionBase.java b/src/main/java/net/querz/mca/SectionBase.java index dcc2b829..9eb93b93 100644 --- a/src/main/java/net/querz/mca/SectionBase.java +++ b/src/main/java/net/querz/mca/SectionBase.java @@ -4,7 +4,7 @@ import java.util.*; public abstract class SectionBase> implements Comparable { - protected static final int NO_HEIGHT_SENTINEL = Integer.MIN_VALUE; + public static final int NO_HEIGHT_SENTINEL = Integer.MIN_VALUE; protected final CompoundTag data; protected int height = NO_HEIGHT_SENTINEL; @@ -34,13 +34,38 @@ public boolean isEmpty() { } /** - * @return the Y value of this section. Multiply by 16 to get world Y value. - */ + * Gets the height of the bottom of this section relative to Y0 as a section-y value, each 1 section-y is equal to + * 16 blocks. + * This library (as a whole) will attempt to keep the value returned by this function in sync with the actual + * location it has been placed within its chunk. + *

The value returned may be unreliable if this section is placed in multiple chunks at different heights. + * or if user code calls {@link #syncHeight(int)} on a section which is referenced by any chunk.

+ * + * @return The Y value of this section. + * @deprecated Prefer using {@code chunk.getSectionY(section)} which will always be accurate + * within the context of the chunk. + */ public int getHeight() { return height; } + /** + * This method should only be called from a container of Sections such as implementers of + * {@link SectionedChunkBase} in an effort to keep the value accurate, or when building sections prior to adding + * to a chunk where you want to use this section height property for the convenience of not having to track the + * value separately. + * + * @deprecated To set section height (aka section-y) use + * {@code chunk.putSection(int, SectionBase, boolean)} instead of this function. Setting the section height + * by calling this function WILL NOT have any affect upon the sections height in the Chunk or or MCA data when + * serialized. + */ + @Deprecated public void setHeight(int height) { + syncHeight(height); + } + + void syncHeight(int height) { this.height = height; } diff --git a/src/main/java/net/querz/mca/SectionIterator.java b/src/main/java/net/querz/mca/SectionIterator.java new file mode 100644 index 00000000..431aca01 --- /dev/null +++ b/src/main/java/net/querz/mca/SectionIterator.java @@ -0,0 +1,12 @@ +package net.querz.mca; + +import java.util.Iterator; + +public interface SectionIterator> extends Iterator { + /** current section y within chunk */ + int sectionY(); + /** current block world level y of the bottom most block in the current section */ + int sectionBlockMinY(); + /** current block world level y of the top most block in the current section */ + int sectionBlockMaxY(); +} diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index 66042034..c88d352b 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -1,25 +1,18 @@ package net.querz.mca; -import net.querz.nbt.io.NBTDeserializer; -import net.querz.nbt.io.NBTSerializer; -import net.querz.nbt.io.NamedTag; import net.querz.nbt.tag.CompoundTag; -import net.querz.nbt.tag.ListTag; -import java.io.*; import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static net.querz.mca.LoadFlags.*; - /** * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. * @param Concrete type of section. */ public abstract class SectionedChunkBase> extends ChunkBase implements Iterable { - - protected final TreeMap sections = new TreeMap<>(); + private final TreeMap sections = new TreeMap<>(); + private final Map sectionHeightLookup = new HashMap<>(); SectionedChunkBase(int lastMCAUpdate) { super(lastMCAUpdate); @@ -34,54 +27,181 @@ public SectionedChunkBase(CompoundTag data) { } /** - * Fetches the section at the given y-coordinate. + * Sets the section at the specified section-y and synchronizes section-y by calling + * {@code section.setHeight(sectionY);}. + * @param sectionY Section-y to place the section at. It is the developers responsibility to ensure this value is + * reasonable. Remember that sections are 16x16x16 cubes and that 1 section-y equals 16 block-y's. + * @param section Section to set, may be null to remove the section. + * @param moveAllowed If false, and the given section is already present in this chunk {@link IllegalArgumentException} + * is thrown. If ture, and the given section is already present in this chunk its former + * section-y location is set {@code null} and the section is updated to live at the + * specified section-y. + * @return The previous section at section-y, or null if there was none or if the given section was already + * present at sectionY. + * @throws IllegalArgumentException Thrown when the given section is already present in this chunk + * and {@code moveAllowed} is false. + */ + public T putSection(int sectionY, T section, boolean moveAllowed) throws IllegalArgumentException { + checkRaw(); + if (sectionY < Byte.MIN_VALUE || sectionY > Byte.MAX_VALUE) { + throw new IllegalArgumentException( + "sectionY must be in the range of a BYTE [-128..127], given value " + sectionY); + } + if (section != null) { + if (sectionHeightLookup.containsKey(section)) { + final int oldY = sectionHeightLookup.getOrDefault(section, SectionBase.NO_HEIGHT_SENTINEL); + if (sectionY == oldY) return null; + if (!moveAllowed) { + throw new IllegalArgumentException( + String.format("cannot place section at %d, it's already at %d", sectionY, oldY)); + } + final T oldSection = sections.remove(oldY); + sectionHeightLookup.remove(oldSection); + assert(oldSection == section); + } + section.syncHeight(sectionY); + sectionHeightLookup.put(section, sectionY); + return sections.put(sectionY, section); + } else { + final T oldSection = sections.remove(sectionY); + sectionHeightLookup.remove(oldSection); + return oldSection; + } + } + + /** + * Sets the section at the specified section-y and synchronizes section-y by calling + * {@code section.setHeight(sectionY);}. + * @param sectionY Section-y to place the section at. It is the developers responsibility to ensure this value is + * reasonable. Remember that sections are 16x16x16 cubes and that 1 section-y equals 16 block-y's. + * @param section Section to set, may be null to remove the section. + * @return The previous section at section-y, or null if there was none or if the given section was already + * present at sectionY. + * @throws IllegalArgumentException Thrown when the given section is already present in this chunk. + * Call {@code putSection(sectionY, section, true)} to not throw this error and to move the section instead. + */ + public T putSection(int sectionY, T section) { + return putSection(sectionY, section, false); + } + + /** + * Fetches the section at the specified section-y and synchronizes section-y by calling + * {@code section.setHeight(sectionY);} before returning it. * @param sectionY The y-coordinate of the section in this chunk. One section y is equal to 16 world y's * @return The Section. */ public T getSection(int sectionY) { T section = sections.get(sectionY); if (section != null) { - section.setHeight(sectionY); + section.syncHeight(sectionY); } return section; } /** - * Sets a section at a given section y-coordinate + * Alias for {@link #putSection(int, SectionBase)} + *

Sets a section at a given section y-coordinate. * @param sectionY The y-coordinate of the section in this chunk. One section y is equal to 16 world y's - * @param section The section to be set. + * @param section The section to be set. May be null to remove the section. + * @return the previous value associated with {@code sectionY}, or null if there was no section at {@code sectionY} + * or if the section was already at that y. + * @throws IllegalStateException Thrown if adding the given section would result in that section instance occurring + * multiple times in this chunk. Use {@link #putSection} as an alternative to allow moving the section, otherwise + * it is the developers responsibility to first remove the section from this chunk + * ({@code setSection(sectionY, null);}) before placing it at a new section-y. */ - public void setSection(int sectionY, T section) { - checkRaw(); - if (section != null) { - section.setHeight(sectionY); - } - sections.put(sectionY, section); + public T setSection(int sectionY, T section) { + return putSection(sectionY, section, false); + } + + /** + * Looks up the section-y for the given section. This is a safer alternative to using + * {@link SectionBase#getHeight()} as it will always be accurate within the context of this chunk. + * @param section section to lookup the section-y for. + * @return section-y; may be negative for worlds with a min build height below zero. If the given section is + * {@code null} or is not found in this chunk then {@link SectionBase#NO_HEIGHT_SENTINEL} is returned. + */ + public int getSectionY(T section) { + if (section == null) return SectionBase.NO_HEIGHT_SENTINEL; + return sectionHeightLookup.getOrDefault(section, SectionBase.NO_HEIGHT_SENTINEL); } /** * Gets the minimum section y-coordinate. - * @return The y of the lowest section in the chunk. + * @return The y of the lowest populated section in the chunk or {@link SectionBase#NO_HEIGHT_SENTINEL} if there is none. */ public int getMinSectionY() { - return sections.firstKey(); + if (!sections.isEmpty()) { + return sections.firstKey(); + } + return SectionBase.NO_HEIGHT_SENTINEL; } /** * Gets the minimum section y-coordinate. - * @return The y of the highest populated section in the chunk. + * @return The y of the highest populated section in the chunk or {@link SectionBase#NO_HEIGHT_SENTINEL} if there is none. */ public int getMaxSectionY() { - return sections.lastKey(); + if (!sections.isEmpty()) { + return sections.lastKey(); + } + return SectionBase.NO_HEIGHT_SENTINEL; } + /** + * Sections provided by {@link Iterator#next()} are guaranteed to have correct values returned from + * calls to {@link SectionBase#getHeight()}. Also note that the iterator itself can be queried via + * {@link SectionIterator#sectionY()} for the true section-y without calling a deprecated method. + * @return Section iterator. Supports {@link Iterator#remove()}. + */ @Override - public Iterator iterator() { - return sections.values().iterator(); + public SectionIterator iterator() { + return new SectionIteratorImpl(); } public Stream stream() { - return sections.values().stream(); + return StreamSupport.stream(spliterator(), false); } + protected class SectionIteratorImpl implements SectionIterator { + private final Iterator> iter; + private Map.Entry current; + + public SectionIteratorImpl() { + iter = sections.entrySet().iterator(); + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public T next() { + current = iter.next(); + current.getValue().syncHeight(current.getKey()); + return current.getValue(); + } + + @Override + public void remove() { + sectionHeightLookup.remove(current.getValue()); + iter.remove(); + } + + @Override + public int sectionY() { + return current.getKey(); + } + + @Override + public int sectionBlockMinY() { + return sectionY() * 16; + } + + @Override + public int sectionBlockMaxY() { + return sectionY() * 16 + 15; + } + } } diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index a1170b47..17c6b9d1 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -6,6 +6,8 @@ import java.io.*; import java.util.Arrays; +import java.util.Iterator; +import java.util.Objects; public class MCAFileTest extends MCATestCase { @@ -476,12 +478,12 @@ public void test1_15GetBiomeAt() { assertEquals(162, f.getBiomeAt(31, 106, 48)); } - public void testChunkIterator() { - MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_18/region/r.15.-9.mca"))); + public void testMCAFileChunkIterator() { + MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_17_1/region/r.-3.-2.mca"))); ChunkIterator iter = mca.iterator(); assertEquals(-1, iter.currentIndex()); - final int populatedX = 483 & 0x1F; - final int populatedZ = -263 & 0x1F; + final int populatedX = -65 & 0x1F; + final int populatedZ = -42 & 0x1F; int i = 0; for (int z = 0; z < 32; z++) { for (int x = 0; x < 32; x++) { @@ -504,4 +506,31 @@ public void testChunkIterator() { assertFalse(iter.hasNext()); assertNotNull(mca.getChunk(1023)); } + + public void testChunkSectionIterator() { + MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_17_1/region/r.-3.-2.mca"))); + assertEquals(1, mca.count()); + Chunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + final int minY = chunk.getMinSectionY(); + final int maxY = chunk.getMaxSectionY(); + assertNotNull(chunk.getSection(minY)); + assertNotNull(chunk.getSection(maxY)); + Iterator

iter = chunk.iterator(); + for (int y = minY; y <= maxY; y++) { + assertTrue(iter.hasNext()); + Section section = iter.next(); + assertNotNull(section); + assertEquals(y, section.getHeight()); + if (y > maxY - 2) { + iter.remove(); + } + } + assertFalse(iter.hasNext()); + assertEquals(minY, chunk.getMinSectionY()); + assertEquals(maxY - 2, chunk.getMaxSectionY()); + assertNull(chunk.getSection(maxY)); + assertNull(chunk.getSection(maxY - 1)); + assertNotNull(chunk.getSection(maxY - 2)); + } } diff --git a/src/test/resources/1_17_1/region/r.-3.-2.mca b/src/test/resources/1_17_1/region/r.-3.-2.mca new file mode 100644 index 0000000000000000000000000000000000000000..d816f059a8658f52aab968ec3525ed457791d4d3 GIT binary patch literal 16384 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 Date: Mon, 8 Nov 2021 18:29:30 -0500 Subject: [PATCH 016/288] Adding tests to cover chunk#putSection and other minor tweaks/cleanup before sending PR --- src/main/java/net/querz/mca/Chunk.java | 11 +++ src/main/java/net/querz/mca/DataVersion.java | 7 ++ src/main/java/net/querz/mca/Section.java | 8 --- .../net/querz/mca/SectionedChunkBase.java | 28 +++++++- src/test/java/net/querz/mca/MCAFileTest.java | 67 ++++++++++++++++++- 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index deecc90c..2a06f66b 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -50,6 +50,17 @@ public Chunk(CompoundTag data) { super(data); } + /** + * {@inheritDoc} + */ + @Override + public Section createSection(int sectionY) throws IllegalArgumentException { + if (containsSection(sectionY)) throw new IllegalArgumentException("section already exists at section-y " + sectionY); + Section section = createSection(); + putSection(sectionY, section); + return section; + } + @Override protected void initReferences(final long loadFlags) { CompoundTag level = data.getCompoundTag("Level"); diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index e9d2c181..08c8c80f 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -5,6 +5,13 @@ // source: version.json file, found in the root directory of the client and server jars // table of versions can also be found on https://minecraft.fandom.com/wiki/Data_version#List_of_data_versions + +/** + * List of significant MC versions and MCA data versions. + * Non-full release versions are intended for use in data handling logic. + * The set of non-full release versions is not, and does not need to be, the complete set of all versions - only those + * which introduce changes to the MCA data structure are useful. + */ public enum DataVersion { // Kept in ASC order UNKNOWN(0, 0, 0), diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index fc83572b..79d2300d 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -87,14 +87,6 @@ private void assureBlockStates() { if (blockStates == null) blockStates = createBlockStates(); } - private void assureBlockLight() { - if (blockLight == null) blockLight = createBlockLightBuffer(); - } - - private void assureSkyLight() { - if (skyLight == null) skyLight = createSkyLightBuffer(); - } - private void assurePalette() { if (palette == null) palette = new ListTag<>(CompoundTag.class); } diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index c88d352b..3c91f4fd 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -26,6 +26,14 @@ public SectionedChunkBase(CompoundTag data) { super(data); } + public boolean containsSection(int sectionY) { + return sections.containsKey(sectionY); + } + + public boolean containsSection(T section) { + return sectionHeightLookup.containsKey(section); + } + /** * Sets the section at the specified section-y and synchronizes section-y by calling * {@code section.setHeight(sectionY);}. @@ -58,13 +66,18 @@ public T putSection(int sectionY, T section, boolean moveAllowed) throws Illegal final T oldSection = sections.remove(oldY); sectionHeightLookup.remove(oldSection); assert(oldSection == section); + assert(sections.size() == sectionHeightLookup.size()); } section.syncHeight(sectionY); sectionHeightLookup.put(section, sectionY); - return sections.put(sectionY, section); + final T oldSection = sections.put(sectionY, section); + if (oldSection != null) sectionHeightLookup.remove(oldSection); + assert(sections.size() == sectionHeightLookup.size()); + return oldSection; } else { final T oldSection = sections.remove(sectionY); sectionHeightLookup.remove(oldSection); + assert(sections.size() == sectionHeightLookup.size()); return oldSection; } } @@ -123,7 +136,9 @@ public T setSection(int sectionY, T section) { */ public int getSectionY(T section) { if (section == null) return SectionBase.NO_HEIGHT_SENTINEL; - return sectionHeightLookup.getOrDefault(section, SectionBase.NO_HEIGHT_SENTINEL); + int y = sectionHeightLookup.getOrDefault(section, SectionBase.NO_HEIGHT_SENTINEL); + section.syncHeight(y); + return y; } /** @@ -148,6 +163,15 @@ public int getMaxSectionY() { return SectionBase.NO_HEIGHT_SENTINEL; } + /*** + * Creates a new section and places it in this chunk at the specified section-y + * @param sectionY section y + * @return new section + * @throws IllegalArgumentException thrown if the specified y already has a section - basically throwns if + * {@link #containsSection(int)} would return true. + */ + public abstract T createSection(int sectionY) throws IllegalArgumentException; + /** * Sections provided by {@link Iterator#next()} are guaranteed to have correct values returned from * calls to {@link SectionBase#getHeight()}. Also note that the iterator itself can be queried via diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index 17c6b9d1..86418d2e 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -478,6 +478,70 @@ public void test1_15GetBiomeAt() { assertEquals(162, f.getBiomeAt(31, 106, 48)); } + public void testChunkSectionPutSection() { + MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_17_1/region/r.-3.-2.mca"))); + Chunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + final Section section2 = chunk.getSection(2); + assertNull(chunk.putSection(2, section2)); // no error to replace self + assertThrowsException(() -> chunk.putSection(3, section2), IllegalArgumentException.class); // should fail + assertNotSame(section2, chunk.getSection(3)); // shouldn't have updated section 3 + final Section newSection = chunk.createSection(); + final Section prevSection2 = chunk.putSection(2, newSection); // replace existing section 2 with the new one + assertNotNull(prevSection2); + assertSame(section2, prevSection2); // check we got the existing section 2 when we replaced it + assertSame(newSection, chunk.getSection(2)); // verify we put section 2 + assertEquals(2, newSection.getHeight()); // insertion should update section height + final Section section3 = chunk.putSection(3, section2); // should be OK to put old section 2 into section 3 place now + + final Section section1 = chunk.getSection(1); + final Section prevSection5 = chunk.putSection(5, section1, true); // move section 1 into section 5 + assertNotNull(prevSection5); + assertNull(chunk.getSection(1)); // verify we 'moved' section one out + assertNotSame(section1, prevSection5); // make sure the return value isn't stupid + assertNull(chunk.putSection(1, prevSection5, true)); // moving 5 into empty slot is OK + + // guard against section y default(0) case + final Section section0 = chunk.getSection(0); + final Section newSection0 = chunk.createSection(); + assertSame(section0, chunk.putSection(0, newSection0)); + + // and finally direct removal via putting null + assertSame(newSection0, chunk.putSection(0, null)); + assertNull(chunk.getSection(0)); + assertNull(chunk.putSection(0, null)); + chunk.putSection(0, section0); + assertSame(section0, chunk.putSection(0, null, true)); + assertNull(chunk.getSection(0)); + + assertThrowsException(() -> chunk.putSection(Byte.MIN_VALUE - 1, chunk.createSection()), IllegalArgumentException.class); + assertThrowsException(() -> chunk.putSection(Byte.MAX_VALUE + 1, chunk.createSection()), IllegalArgumentException.class); + + assertThrowsNoException(() -> chunk.putSection(Byte.MIN_VALUE, chunk.createSection())); + assertThrowsNoException(() -> chunk.putSection(Byte.MAX_VALUE, chunk.createSection())); + } + + public void testChunkSectionGetSectionY() { + MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_17_1/region/r.-3.-2.mca"))); + Chunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getSectionY(null)); + assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getSectionY(chunk.createSection())); + Section section = chunk.getSection(5); + section.setHeight(-5); + assertEquals(5, chunk.getSectionY(section)); + assertEquals(5, section.getHeight()); // getSectionY should sync Y + } + + public void testChunkSectionMinMaxSectionY() { + Chunk chunk = new Chunk(42); + chunk.setDataVersion(DataVersion.JAVA_1_17_1.id()); + assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getMinSectionY()); + assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getMaxSectionY()); + Section section = chunk.createSection(3); + + } + public void testMCAFileChunkIterator() { MCAFile mca = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("1_17_1/region/r.-3.-2.mca"))); ChunkIterator iter = mca.iterator(); @@ -516,11 +580,12 @@ public void testChunkSectionIterator() { final int maxY = chunk.getMaxSectionY(); assertNotNull(chunk.getSection(minY)); assertNotNull(chunk.getSection(maxY)); - Iterator
iter = chunk.iterator(); + SectionIterator
iter = chunk.iterator(); for (int y = minY; y <= maxY; y++) { assertTrue(iter.hasNext()); Section section = iter.next(); assertNotNull(section); + assertEquals(y, iter.sectionY()); assertEquals(y, section.getHeight()); if (y > maxY - 2) { iter.remove(); From d002cbb4d211f06030bdfe36263273720a88e573 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 08:21:34 -0500 Subject: [PATCH 017/288] Adding demo of auto return typing which will be used for new MCAUtil#read implementations. --- .../java/net/querz/AutoTypingDemoTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/test/java/net/querz/AutoTypingDemoTest.java diff --git a/src/test/java/net/querz/AutoTypingDemoTest.java b/src/test/java/net/querz/AutoTypingDemoTest.java new file mode 100644 index 00000000..991125c5 --- /dev/null +++ b/src/test/java/net/querz/AutoTypingDemoTest.java @@ -0,0 +1,48 @@ +package net.querz; + +import junit.framework.TestCase; + +/** + * Demonstration of auto return typing pattern using the simplest possible constructs for the reader + * to grasp its operation. + *

This pattern does have one key weakness, and that is if the returned type does not match the + * expected type a ClassCastException is thrown from the call site. There is no way for the auto + * function, {@code create(String)} below, to trap this exception and add details or hints. + * The caller needs to be aware of this.

+ */ +public class AutoTypingDemoTest extends TestCase { + + private static abstract class Base {public abstract String str();} + private static class ImplA extends Base {public String str() {return "A";}} + private static class ImplB extends Base {public String str() {return "B";}} + + @SuppressWarnings("unchecked") + private T create(String hint) { + if ("A".equals(hint)) return (T) new ImplA(); + if ("B".equals(hint)) return (T) new ImplB(); + throw new IllegalArgumentException(); + } + + public void testAutoReturnTyping_directAssignmentDemo() { + ImplA a = create("A"); + ImplB b = create("B"); + try { + ImplA bad = create("B"); + fail(); + } catch (ClassCastException expected) { + // Note, it's not possible to trap and clarify this exception within #create(String) + // Ideally I'd like to be able to provide the caller with a more helpful message / hint for correction + // but this is a shortcoming of the pattern + } + } + + public void testAutoReturnTyping_handlingReturnValueWhenCallerDoesntKnowTheTypeAtTimeOfCall() { + ImplA a; + ImplB b; + Base unknown = create("A"); + + if (unknown instanceof ImplA) a = (ImplA) unknown; + else if (unknown instanceof ImplB) b = (ImplB) unknown; + else throw new UnsupportedOperationException(); // or ignore it, or whatever you want + } +} From 0c6d33b292961e18511585a824d022d0264a057b Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 09:16:20 -0500 Subject: [PATCH 018/288] Fixing template args to extend ChunkBase instead of Chunk --- src/main/java/net/querz/mca/ChunkIterator.java | 6 +++--- src/main/java/net/querz/mca/MCAFileBase.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/mca/ChunkIterator.java b/src/main/java/net/querz/mca/ChunkIterator.java index 0099c7bc..41e4e93b 100644 --- a/src/main/java/net/querz/mca/ChunkIterator.java +++ b/src/main/java/net/querz/mca/ChunkIterator.java @@ -3,14 +3,14 @@ import java.util.Iterator; /** - * Enhanced iterator for iterating over {@link Chunk} data. + * Enhanced iterator for iterating over {@link ChunkBase} data. * All 1024 chunks will be returned by successive calls to {@link #next()}, even * those which are {@code null}. * See {@link MCAFileBase#iterator()} */ -public interface ChunkIterator extends Iterator { +public interface ChunkIterator extends Iterator { /** - * Replaces the current chunk with the one given by calling {@link MCAFileBase#setChunk(int, Chunk)} + * Replaces the current chunk with the one given by calling {@link MCAFileBase#setChunk(int, ChunkBase)} * with the {@link #currentIndex()}. Take care as the given chunk is NOT copied by this call. * @param chunk Chunk to set, may be null. */ diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 3fcdd409..e6fdbf55 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -12,7 +12,7 @@ /** * An abstract representation of an mca file. */ -public abstract class MCAFileBase implements Iterable { +public abstract class MCAFileBase implements Iterable { protected int regionX, regionZ; protected T[] chunks; @@ -253,7 +253,7 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx for (int cz = 0; cz < 32; cz++) { for (int cx = 0; cx < 32; cx++) { int index = getChunkIndex(cx, cz); - Chunk chunk = chunks[index]; + T chunk = chunks[index]; if (chunk == null) { continue; } @@ -377,7 +377,7 @@ public Stream stream() { return StreamSupport.stream(spliterator(), false); } - protected static class ChunkIteratorImpl implements ChunkIterator { + protected static class ChunkIteratorImpl implements ChunkIterator { private final MCAFileBase owner; private int currentIndex; From 5171e0c396f5eac9dfac3bd2de0db02ce62ed7bb Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 21:54:02 -0500 Subject: [PATCH 019/288] Fix test tmp dir cleanup to delete recursively --- src/test/java/net/querz/NBTTestCase.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 656fa9d1..9fdcece0 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -239,16 +239,18 @@ protected File copyResourceToTmp(String resource) { return tmpFile; } - protected void cleanupTmpDir() { - String workingDir = System.getProperty("user.dir"); - File tmpDir = new File(workingDir, "tmp"); - File[] tmpFiles = tmpDir.listFiles(); - if (tmpFiles != null && tmpFiles.length != 0) { - for (File file : tmpFiles) { - file.delete(); + private void deleteRecursive(File deleteMe) { + File[] contents = deleteMe.listFiles(); + if (contents != null) { + for (File file : contents) { + deleteRecursive(file); } } - tmpDir.delete(); + deleteMe.delete(); + } + + protected void cleanupTmpDir() { + deleteRecursive(new File(System.getProperty("user.dir"), "tmp")); } protected String calculateFileMD5(File file) { From 3b624a0a06fde95b30e0a1b5a0ff6b4c9810de63 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:47:20 -0500 Subject: [PATCH 020/288] Added CompoundTag#getListTagAutoCast to remove need to always cast result when calling CompoundTag#getListTag --- src/main/java/net/querz/nbt/tag/CompoundTag.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index 651e5800..6a172925 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -141,6 +141,11 @@ public ListTag getListTag(String key) { return get(key, ListTag.class); } + @SuppressWarnings("unchecked") + public > R getListTagAutoCast(String key) { + return (R) get(key, ListTag.class); + } + public CompoundTag getCompoundTag(String key) { return get(key, CompoundTag.class); } From 52fbb7281e388d5d3f7c2a420dd290cd81c97c7e Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:57:54 -0500 Subject: [PATCH 021/288] Adding TagWrapper interface to indicate classes which expose their CompoundTag data object. ChunkBase fixed bug in ctor - should call initReferences0 --- src/main/java/net/querz/mca/ChunkBase.java | 13 +++++++++---- src/main/java/net/querz/mca/TagWrapper.java | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/querz/mca/TagWrapper.java diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java index 9fc347af..18a12741 100644 --- a/src/main/java/net/querz/mca/ChunkBase.java +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -14,7 +14,7 @@ /** * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. */ -public abstract class ChunkBase implements VersionedDataContainer { +public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { protected int dataVersion; protected boolean partial; protected boolean raw; @@ -31,7 +31,7 @@ public abstract class ChunkBase implements VersionedDataContainer { */ public ChunkBase(CompoundTag data) { this.data = data; - initReferences(ALL_DATA); + initReferences0(ALL_DATA); } private void initReferences0(long loadFlags) { @@ -77,7 +77,6 @@ public void setDataVersion(int dataVersion) { this.dataVersion = Math.max(0, dataVersion); } - /** * Serializes this chunk to a RandomAccessFile. * @param raf The RandomAccessFile to be written to. @@ -163,10 +162,16 @@ public CompoundTag getHandle() { return data; } - public CompoundTag updateHandle(int xPos, int zPos) { + public CompoundTag updateHandle() { if (!raw) { data.putInt("DataVersion", dataVersion); } return data; } + + // Note: Not all chunk formats store xz in their NBT, but {@link MCAFileBase} will call this update method + // to give them the chance to record them. + public CompoundTag updateHandle(int xPos, int zPos) { + return updateHandle(); + } } diff --git a/src/main/java/net/querz/mca/TagWrapper.java b/src/main/java/net/querz/mca/TagWrapper.java new file mode 100644 index 00000000..fd603755 --- /dev/null +++ b/src/main/java/net/querz/mca/TagWrapper.java @@ -0,0 +1,18 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +public interface TagWrapper { + /** + * Updates the data tag held by this wrapper and returns it. + * @return A reference to the raw CompoundTag this object is based on. + */ + CompoundTag updateHandle(); + + /** + * Provides a reference to the wrapped data tag. + * May be null for objects which support partial loading such as chunks. + * @return A reference to the raw CompoundTag this object is based on. + */ + CompoundTag getHandle(); +} From efd2be8ef186e60d1f0bd87db5f42077813b3a16 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 09:18:10 -0500 Subject: [PATCH 022/288] Adding POI & ENTITIES mca skeleton classes --- .../java/net/querz/mca/EntitiesChunk.java | 18 ++++++++++ .../java/net/querz/mca/EntitiesMCAFile.java | 33 +++++++++++++++++++ src/main/java/net/querz/mca/PoiChunk.java | 18 ++++++++++ src/main/java/net/querz/mca/PoiMCAFile.java | 33 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 src/main/java/net/querz/mca/EntitiesChunk.java create mode 100644 src/main/java/net/querz/mca/EntitiesMCAFile.java create mode 100644 src/main/java/net/querz/mca/PoiChunk.java create mode 100644 src/main/java/net/querz/mca/PoiMCAFile.java diff --git a/src/main/java/net/querz/mca/EntitiesChunk.java b/src/main/java/net/querz/mca/EntitiesChunk.java new file mode 100644 index 00000000..5d3d020e --- /dev/null +++ b/src/main/java/net/querz/mca/EntitiesChunk.java @@ -0,0 +1,18 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +public class EntitiesChunk extends ChunkBase { + EntitiesChunk(int lastMCAUpdate) { + super(lastMCAUpdate); + } + + public EntitiesChunk(CompoundTag data) { + super(data); + } + + @Override + protected void initReferences(long loadFlags) { + + } +} diff --git a/src/main/java/net/querz/mca/EntitiesMCAFile.java b/src/main/java/net/querz/mca/EntitiesMCAFile.java new file mode 100644 index 00000000..8701dcf4 --- /dev/null +++ b/src/main/java/net/querz/mca/EntitiesMCAFile.java @@ -0,0 +1,33 @@ +package net.querz.mca; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class EntitiesMCAFile extends MCAFileBase { + public EntitiesMCAFile(int regionX, int regionZ) { + super(regionX, regionZ); + } + + public EntitiesMCAFile(int regionX, int regionZ, int defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + public EntitiesMCAFile(int regionX, int regionZ, DataVersion defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + @Override + public Class chunkClass() { + return null; + } + + @Override + public EntitiesChunk createChunk() { + return null; + } + + @Override + protected EntitiesChunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { + return null; + } +} diff --git a/src/main/java/net/querz/mca/PoiChunk.java b/src/main/java/net/querz/mca/PoiChunk.java new file mode 100644 index 00000000..def52f5f --- /dev/null +++ b/src/main/java/net/querz/mca/PoiChunk.java @@ -0,0 +1,18 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +public class PoiChunk extends ChunkBase { + PoiChunk(int lastMCAUpdate) { + super(lastMCAUpdate); + } + + public PoiChunk(CompoundTag data) { + super(data); + } + + @Override + protected void initReferences(long loadFlags) { + + } +} diff --git a/src/main/java/net/querz/mca/PoiMCAFile.java b/src/main/java/net/querz/mca/PoiMCAFile.java new file mode 100644 index 00000000..037b82ee --- /dev/null +++ b/src/main/java/net/querz/mca/PoiMCAFile.java @@ -0,0 +1,33 @@ +package net.querz.mca; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class PoiMCAFile extends MCAFileBase { + public PoiMCAFile(int regionX, int regionZ) { + super(regionX, regionZ); + } + + public PoiMCAFile(int regionX, int regionZ, int defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + public PoiMCAFile(int regionX, int regionZ, DataVersion defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + @Override + public Class chunkClass() { + return null; + } + + @Override + public PoiChunk createChunk() { + return null; + } + + @Override + protected PoiChunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { + return null; + } +} From ea50fcd7055d20de0ba7b4026cb13f704b0e3e57 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 09:58:32 -0500 Subject: [PATCH 023/288] Updating MCAUtil readers to support auto typing of of the various mca types. --- src/main/java/net/querz/mca/MCAUtil.java | 167 ++++++++++++++++++++++- 1 file changed, 161 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index f995bf6a..5a81aa47 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -4,9 +4,15 @@ import java.io.IOException; import java.io.RandomAccessFile; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Provides main and utility functions to read and write .mca files and @@ -16,12 +22,26 @@ public final class MCAUtil { private MCAUtil() {} + private static final Pattern mcaFilePattern = Pattern.compile("^.*\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); + private static final Map>> MCA_CREATORS; + + static { + Map>> map = new HashMap<>(); + map.put("region", MCAFile::new); + map.put("poi", PoiMCAFile::new); + map.put("entities", EntitiesMCAFile::new); + MCA_CREATORS = Collections.unmodifiableMap(map); + } + + // /** * @see MCAUtil#read(File) * @param file The file to read the data from. * @return An in-memory representation of the MCA file with decompressed chunk data. * @throws IOException if something during deserialization goes wrong. + * @deprecated switch to {@link #readAuto(String)} */ + @Deprecated public static MCAFile read(String file) throws IOException { return read(new File(file), LoadFlags.ALL_DATA); } @@ -31,18 +51,22 @@ public static MCAFile read(String file) throws IOException { * @param file The file to read the data from. * @return An in-memory representation of the MCA file with decompressed chunk data. * @throws IOException if something during deserialization goes wrong. + * @deprecated switch to {@link #readAuto(File)} */ + @Deprecated public static MCAFile read(File file) throws IOException { return read(file, LoadFlags.ALL_DATA); } /** - * @see MCAUtil#read(File) + * @see MCAUtil#read(File, long) * @param file The file to read the data from. * @return An in-memory representation of the MCA file with decompressed chunk data. * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded * @throws IOException if something during deserialization goes wrong. + * @deprecated switch to {@link #readAuto(String, long)} */ + @Deprecated public static MCAFile read(String file, long loadFlags) throws IOException { return read(new File(file), loadFlags); } @@ -53,7 +77,9 @@ public static MCAFile read(String file, long loadFlags) throws IOException { * @return An in-memory representation of the MCA file with decompressed chunk data * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded * @throws IOException if something during deserialization goes wrong. + * @deprecated switch to {@link #readAuto(File, long)} */ + @Deprecated public static MCAFile read(File file, long loadFlags) throws IOException { MCAFile mcaFile = newMCAFile(file); try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { @@ -61,7 +87,57 @@ public static MCAFile read(File file, long loadFlags) throws IOException { return mcaFile; } } + // + + // + /** + * @see MCAUtil#readAuto(File) + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @throws IOException if something during deserialization goes wrong. + */ + public static > T readAuto(String file) throws IOException { + return readAuto(new File(file), LoadFlags.ALL_DATA); + } + + /** + * Reads an MCA file and loads all of its chunks. + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @throws IOException if something during deserialization goes wrong. + */ + public static > T readAuto(File file) throws IOException { + return readAuto(file, LoadFlags.ALL_DATA); + } + + /** + * @see MCAUtil#readAuto(File, long) + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException if something during deserialization goes wrong. + */ + public static > T readAuto(String file, long loadFlags) throws IOException { + return readAuto(new File(file), loadFlags); + } + + /** + * Reads an MCA file and loads all of its chunks. + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException if something during deserialization goes wrong. + */ + public static > T readAuto(File file, long loadFlags) throws IOException { + T mcaFile = autoMCAFile(file); + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + mcaFile.deserialize(raf, loadFlags); + return mcaFile; + } + } + // + // /** * Calls {@link MCAUtil#write(MCAFile, File, boolean)} without changing the timestamps. * @see MCAUtil#write(MCAFile, File, boolean) @@ -124,7 +200,9 @@ public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) th } return chunks; } + // + // /** * Turns the chunks coordinates into region coordinates and calls * {@link MCAUtil#createNameFromRegionLocation(int, int)} @@ -156,7 +234,9 @@ public static String createNameFromBlockLocation(int blockX, int blockZ) { public static String createNameFromRegionLocation(int regionX, int regionZ) { return "r." + regionX + "." + regionZ + ".mca"; } + // + // /** * Turns a block coordinate value into a chunk coordinate value. * @param block The block coordinate value. @@ -210,14 +290,89 @@ public static int regionToBlock(int region) { public static int chunkToBlock(int chunk) { return chunk << 4; } + // - private static final Pattern mcaFilePattern = Pattern.compile("^.*r\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); - + /** + * Creates a REGION {@link MCAFile} initialized with its X and Z extracted from the given file name. The file + * does not need to exist. + * @deprecated Legacy helper, switch to {@link #autoMCAFile(Path)} for POI (1.14+) and ENTITIES (1.17+) mca support. + */ + @Deprecated public static MCAFile newMCAFile(File file) { - Matcher m = mcaFilePattern.matcher(file.getName()); + final Matcher m = mcaFilePattern.matcher(file.getName()); + if (!m.find()) { + throw new IllegalArgumentException("invalid mca file name (expect name match '*...mca'): " + + file.getName()); + } + return new MCAFile(Integer.parseInt(m.group("regionX")), Integer.parseInt(m.group("regionZ"))); + } + + + private static void throwCannotDetermineMcaType(Exception cause) { + throw new IllegalArgumentException( + "Unable to determine mca file type. Expect the mca file to have a parent folder with one of the following names: " + + MCA_CREATORS.keySet().stream().sorted().collect(Collectors.joining(", ")), cause); + } + + /** + * @see #autoMCAFile(Path) + */ + public static > T autoMCAFile(File file) { + return autoMCAFile(file.toPath()); + } + + /** + * Detects and creates a concretion (implementer) of {@link MCAFileBase}. The actual type returned is determined by + * the name of the folder containing the .mca file. + *

Usage suggestion when the caller fully controls passed file name: + *

{@code EntitiesMCAFile entitiesMca = MCAUtil.autoMCAFile(Paths.get("entities/r.0.0.mca"));}
+ *

Usage suggestion when the caller expects a specific return type but does not control the passed file name: + *

{@code try {
+	 *   EntitiesMCAFile entitiesMca = MCAUtil.autoMCAFile(filename);
+	 * } catch (ClassCastException expected) {
+	 *   // got an unexpected type
+	 * }}
+ *

Usage suggestion when the caller may not know what return type to expect: + *

{@code MCAFileBase mcaFile = MCAUtil.autoMCAFile(filename);
+	 * if (mcaFile instanceof MCAFile) {
+	 *   // process region mca file
+	 *   MCAFile regionMca = (MCAFile) mcaFile;
+	 * } else if (mcaFile instanceof PoiMCAFile) {
+	 *   // process poi mca file
+	 *   PoiMCAFile poiMca = (PoiMCAFile) mcaFile;
+	 * } else if (mcaFile instanceof EntitiesMCAFile) {
+	 *   // process entities mca file
+	 *   EntitiesMCAFile entitiesMca = (EntitiesMCAFile) mcaFile;
+	 * } else {
+	 *   // unsupported type / don't care about this type, etc.
+	 * }}
+ * + * @param path The file does not need to exist but the given path must have at least 2 parts. + * Required parts: "mca_type/mca_file" + * where mca_type is used to determine the type of {@link MCAFileBase} to return (such as "region", + * "poi", "entities") and mca_file is the .mca file such as "r.0.0.mca". + * @param {@link MCAFileBase} type - do note that any {@link ClassCastException} errors will be thrown + * at the location of assignment, not from within this call. + * @return Instantiated and initialized concretion of {@link MCAFileBase} + * @throws IllegalArgumentException Thrown when the mca type could not be determined from the path or when the + * regions X and Z locations could not be extracted from the filename. + */ + @SuppressWarnings("unchecked") + public static > T autoMCAFile(Path path) { + BiFunction> creator = null; + try { + String hint = path.getParent().getFileName().toString(); + creator = MCA_CREATORS.get(hint); + if (creator == null) throwCannotDetermineMcaType(null); + } catch (Exception ex) { + throwCannotDetermineMcaType(ex); + } + final Matcher m = mcaFilePattern.matcher(path.getFileName().toString()); if (m.find()) { - return new MCAFile(Integer.parseInt(m.group("regionX")), Integer.parseInt(m.group("regionZ"))); + throw new IllegalArgumentException("invalid mca file name (expect name match '*...mca'): " + path); } - throw new IllegalArgumentException("invalid mca file name: " + file.getName()); + final int x = Integer.parseInt(m.group("regionX")); + final int z = Integer.parseInt(m.group("regionZ")); + return (T) creator.apply(x, z); } } From 88379450733a7d7ca7190da6d8ea042cfda91a6a Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 10:09:58 -0500 Subject: [PATCH 024/288] Adding basic test for MCAUtil.autoMCAFile --- src/main/java/net/querz/mca/MCAUtil.java | 2 +- src/test/java/net/querz/mca/MCAUtilTest.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 5a81aa47..7aa147b2 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -368,7 +368,7 @@ public static > T autoMCAFile(Path path) { throwCannotDetermineMcaType(ex); } final Matcher m = mcaFilePattern.matcher(path.getFileName().toString()); - if (m.find()) { + if (!m.find()) { throw new IllegalArgumentException("invalid mca file name (expect name match '*...mca'): " + path); } final int x = Integer.parseInt(m.group("regionX")); diff --git a/src/test/java/net/querz/mca/MCAUtilTest.java b/src/test/java/net/querz/mca/MCAUtilTest.java index 73447a4d..9615f7a9 100644 --- a/src/test/java/net/querz/mca/MCAUtilTest.java +++ b/src/test/java/net/querz/mca/MCAUtilTest.java @@ -1,6 +1,8 @@ package net.querz.mca; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; public class MCAUtilTest extends MCATestCase { @@ -89,4 +91,21 @@ public void testMakeMyCoverageGreatAgain() { assertThrowsNoException(() -> MCAUtil.write(m, target, false)); assertThrowsNoException(() -> MCAUtil.write(m, target, false)); } + + public void testAutoCreateMcaFile() { + MCAFileBase mcaFile = MCAUtil.autoMCAFile(Paths.get("region", "r.1.2.mca")); + assertTrue(mcaFile instanceof MCAFile); + assertEquals(1, mcaFile.getRegionX()); + assertEquals(2, mcaFile.getRegionZ()); + + mcaFile = MCAUtil.autoMCAFile(Paths.get("poi", "r.3.-4.mca")); + assertTrue(mcaFile instanceof PoiMCAFile); + assertEquals(3, mcaFile.getRegionX()); + assertEquals(-4, mcaFile.getRegionZ()); + + mcaFile = MCAUtil.autoMCAFile(Paths.get("entities", "r.-5.6.mca")); + assertTrue(mcaFile instanceof EntitiesMCAFile); + assertEquals(-5, mcaFile.getRegionX()); + assertEquals(6, mcaFile.getRegionZ()); + } } From d132ea4d98ce35179a5c5c318f4a4cd6d374952b Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 09:58:32 -0500 Subject: [PATCH 025/288] Updating MCAUtil readers to support auto typing of of the various mca types. --- src/main/java/net/querz/mca/MCAUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 7aa147b2..5a81aa47 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -368,7 +368,7 @@ public static > T autoMCAFile(Path path) { throwCannotDetermineMcaType(ex); } final Matcher m = mcaFilePattern.matcher(path.getFileName().toString()); - if (!m.find()) { + if (m.find()) { throw new IllegalArgumentException("invalid mca file name (expect name match '*...mca'): " + path); } final int x = Integer.parseInt(m.group("regionX")); From 7afb99da218255f5905aca87212e4b9be3c4b6fc Mon Sep 17 00:00:00 2001 From: BuildTools Date: Tue, 9 Nov 2021 10:09:58 -0500 Subject: [PATCH 026/288] Adding basic test for MCAUtil.autoMCAFile --- src/main/java/net/querz/mca/MCAUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 5a81aa47..7aa147b2 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -368,7 +368,7 @@ public static > T autoMCAFile(Path path) { throwCannotDetermineMcaType(ex); } final Matcher m = mcaFilePattern.matcher(path.getFileName().toString()); - if (m.find()) { + if (!m.find()) { throw new IllegalArgumentException("invalid mca file name (expect name match '*...mca'): " + path); } final int x = Integer.parseInt(m.group("regionX")); From 367f72011e549180057326c23f22555173837567 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:46:01 -0500 Subject: [PATCH 027/288] Updating .17.1 test mca files - added nether portal to captured chunk (nether portals are recorded in POI's) --- .../resources/1_17_1/entities/r.-3.-2.mca | Bin 0 -> 12288 bytes src/test/resources/1_17_1/poi/r.-3.-2.mca | Bin 0 -> 12288 bytes src/test/resources/1_17_1/region/r.-3.-2.mca | Bin 16384 -> 16384 bytes src/test/resources/ABOUT_TEST_DATA.md | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/test/resources/1_17_1/entities/r.-3.-2.mca create mode 100644 src/test/resources/1_17_1/poi/r.-3.-2.mca diff --git a/src/test/resources/1_17_1/entities/r.-3.-2.mca b/src/test/resources/1_17_1/entities/r.-3.-2.mca new file mode 100644 index 0000000000000000000000000000000000000000..f0d92928a1221edf3efa3c0bc66e7415d1d00591 GIT binary patch literal 12288 zcmeIy|2NZn0LSrNmhZY2V+&)))s1XwOY-F=M6GmPWY^Z4NfgMk47AOHk_01yBI zKmZ5;0U+>s1T?<-JRML00zd!=00AHX1b_e#00KY&2mk>f00h32!0C*A?q8}6xcTvw23H)3=_R4j=E3WCtpb1iJ z(WeRNL}HHLVmHs)tAuuKmovVSOoofBaRmM3*WJ8@^LjMS4a&|_yt$5iK}4ZF(#oCB zo@95%b;NZf(h+~94(K&3x}TLX49Kbr+w`0wn;{I=4P{U*-#7+Klh>6q_WA^mm|&@o zxqUu651~j!RUU|UDDyv^-V7m1U$LS>NSF`}wqSnhoA{&KpDh_`eJxb5Pw)PiYG$o- z=_-rg^$wA?Sh_^Qc^%cyJp;Z^4!oy9Ng); z&){7xa;PM5C81BaF*MudNK9R6;&Y2(>L62WuCD~g^1V@Q8)M&0OZ7WxwC~pZG0Pd*i#@-n zd8=Z_QIGg@9vLl=X*$6k-J8G?;a_ID^uHC9q4$*=)i>P@8u#iySnI$z)r;3lCD3d>wBe(2f4{&6aP*peQeJ#lV&%(7Z^Y)k)g%=j4^9v)SK ziRvq|QYK%X6~Fp7om~^^-d5W&_bZ#sQMF#sSRtW*d-B0?E_RZhu(Gx>XE8&~)*Yj( zP@xR*Qwcsm@0P87HPMk-T#KmrsgiQU*rxNJXiD|TStvTj=|ZWtF=FgFVZQP}2)uJb zkuO(O99|=qrjGN)&S@H12kn^5tNvVWQ&s|>+~J9(JPo%&wFD6nne%t_+w7rUs-bbM&kcFo9!51m*x6?`9_rFi?kI<`H0P^fOG zPd@G}aES^#B%SS|(}AWG2M#{M!XB zY}`6;5V_7&4a?OX6{|{^`?`rOrk%B>7~WX`q(pS%V-FUhQMeDjE2By z2#kinXb6mkz-S1JhQMeDjE2By2#kinK!reJw;A(5**$9SXb6mkz-R~nL*O`5#hkSl z_j4UG5McYT{+j((QTJMbxiK*<+g3Y;cfMSCberZ&-F=h%S9Dxi(aiB`%F7*(f7H4^ zV%BSFO_(@gopC7Bzmx6Tl};{OU27rCKG)?*6QBRO0Dgqq6eowDjuG0EX>B{jA$F*QEzP_btBtfZw2sIVdB{T6dN0%k=^?XFv3* zpL6r?WTU#}Q;r;q5BJ)9!ahlFS=)ID>7bk?SGq(ZCp%>a<-DD{==yP{^1bmlM~l+Y p)I1sjqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UnaN002kOWgh?l literal 0 HcmV?d00001 diff --git a/src/test/resources/1_17_1/region/r.-3.-2.mca b/src/test/resources/1_17_1/region/r.-3.-2.mca index d816f059a8658f52aab968ec3525ed457791d4d3..c80ecac2b514d6907c1aa2f97c8bfabcda0f4406 100644 GIT binary patch delta 6298 zcmV;L7-i>xfB}Gj0kC}=1B*61u>pbve;82$c%1EBe{37qeSb$$)KQirQdVp_EjAtL zr15}Zk|;Uzu+A1msj}nRjwEWS|B+9}qr{3586G9sX;H{38_O9zIDi)@f&T~)tC-hn zYk>NX1{oGB0qfR_(Ex)7Y(|&D0rFRDHyF@lOQ(Im@4X}Mjz|59;uOez5&6USf4=Yg zKA-n}?~X^l7pb0*dQy8tomNvK@eJhT+>}P#iK$eohPa9e+0viwJbRCPP`NE#5?g$yc6%lJMm7u6Ys=3 z@lL!G|A!y%Cfg59j>#j*oEjfYe@^f(310uN8sa|1zC`$!dQ#t~CP&9|6LLmtAlr_0 z_jDZ{?mcqsSU>$jgY%=h?3g~gqHzg=p6v4_X~mG64K|69%T-L(AicMtdv#rvfOe~i2HoBN+W z7xnk$+gRLNAMN%%|74jz9@$&shd9y9-+0|EU^`zO`ezS9{YL_!$m88EC?C+vKGE$W zhW5U`P^q5B(Rf;R&vohbzP>$8UibU~hI3tzBUj6(vt12j`(GdHKN1}ti1qh&b@$li z&i4&0&zW-fG0)v@%K5%uf6v_~glabC?m#Jb*K61ulye7Vn!9#F=irW>yN`$|ckk02 z8FTkB%N-lrs=1q6J9qPLm)}jt(c~8~cTty@<}M2Hd1x2M+#NLME)Tie{mo;8hmIW` zjvP7G{p6ve-xxl6Z1_k|*HF)ZIgiFQP&tnB8oXm(gFk=YK971of0cP;Yp|Thb@6}k zYp~Ih$Ms&pPWEQIh?g`BCR1w9WGt=hon-Mi`7NCcIyopeO zc$3-@e3LjfDR_w&(=?b=#x?qW;w9pMs^pUCNt!cJAi}p+2)vacUY!5s^0sYPHAh`&7{RKr3C8#gRq=32$LBK$f#X;{lo(S~I`dMx* zjF3V|?3LV5e|}4Z@rFoU>WiO z;|nZ@14YQYFOTypQho*FLH`KPPedr3f&4(;@?8Olm#M$$1;9|CuP8=@3v(j)A-@qE zx8nYL@V8x<1OIMFIrxLV{KJoj1j!?UF34jj&`bSF;JPGvAfMuMGSoK|A1}5Q6c_Z1 z2;+lxe+!^b@fAW1*nit|tp?!$Q2e__tY`FWhq5B_UZUv9bQ>K_pbaY=+r#^p2d$4&j0_|Gq)|37X)yYow9 z)PMC=t}kzhKmRI=pU)%WlnC>YHmZ;7uVH?we-D=+_;r2z%i}BSchT9W{cD%n3i-PI zZv~1F0)DDrC?J0)>ifzG)OU4hytl0|=ehbS(@*_ZxqiTcGJfJ+;J>)AZK|km_ zf4?-2tp^!gZ{m}m|rm}iMXD9(Ef@2H=$m; z^cyjP`k#Y(51R)%-nYNDA9y1>PTcybLiN2%_0f6q6e2KBg$bcyMArx7K>i8TuTcKB zRq|)h_sgfs^Y6OI*DsEn#c7u$)Q5Lzf3X|uOYKYg;X2&^j)6zVh6#pTAI`)1R|7@N zTY>8mjqOY8uba+qg?{EQ>erWK;QR9k*B7CAWc75OcUiP8m8t&*UH_L5_unkv z-&fPQpS=&W<4PHJ4NIvDPmV_fdI5H9jfJ$t=;U{iX-# zEIj#=dQyKpo6e|N{5S#a1^5GWEjB)Gt&}23}Y4Uy5PC!K~pDCZyYO3UUsO-s2B@%Y?mXb3`&D5RWs5hO( zJmLo;Qu%?1bVS49aM14$qXk1dD7J^!ZQby*uQL{-1uMb}z1={#1Dl3pF&$%3Mu~i( zaBzdXMoXxaZ(jm=_u2DXf2}h%Krt8$n{X|g(e`oV0=)v#-k;$0Vcsrm z;i-yNz7lVO^XV8iGHkR*bjAuh{ci_$=ji&fm`YgPgxT z7=%fR#Rlei|2nQ^rM&BCqX+xq`ETd>Z|8{(GK}@NKN_R9QU3Lqe@&n5W`xwk@CRbC zs_Rp$f@@pnfEi%%!r+qvOz})%Up&|*XHju@oJn;GxL_Qv!Z^0 zqu%ojTRTt87(83$mc>h|A_s_A@f@NMh59P^tg7YKVD}6c5)2s zORUOD3)10hVe9bMf6X6@S#glx-v^^yR)w^$WigSTYn2Tbo8 z517@TQ*0hN)_jVzd#2mqvtY%Ol+BarYEO9hSR~jy7WwV1e~Cdb$Vwc8LCIM3OI4z6 zOtN5b_2yRIATLV1C>i9ei#J#W*U8`hfcf?Ohq;ch-$3{P@~_*D^)poIpv{a{>-ww& z7PIEp%5NReX89}fDOJT7Z=&NyZKrHN$-irw;|yX@N6A0)i~;v_`FC-QJ!8us)n7CP z{b3{O+INmnf83Qw04Dr1(+Mc``r*NxM$gPlQ`}X;8N@D**nr_KR?qa<@WZO+{5l5P zfDia#5)(4#mw_M3Q5pF6Fw8{}cQHg>z`iqpFsbu-#t%sO=XHMbgzxm1FXN^l9VffI z5^@P?9sm;w7!2@DCtxth8!aL5&ikp3`7(~?x}HL8f6hG&c!m+qb6#Fdu)OQ@z$#G3 zi!#HPC4`Jx=#DFbgWV3eZ28TrTNDejy%V%Nh%6#WmE`5EqaF2rFrXa1&Krg^#E;R{soRN zF&*b#f3h|%@o$|D;PW%O67GJ1z3{)V7XMb<5)*8;b~64FV*PF=An_7I^J7u{8jnRA z`JPO7H5(s(G8r|y)sH{c$FOyM#}rRybE`g|;RBP)ZezV5i&46fOgDrjm)o?Vy$7)` z;)C)YNqj&X>3s)5hwlfEQ$3~nE-}*NTQuvte-H)qim2}bRM5D*vA;Y;c%k>ZbvpBv z^-YwA>M+!UJ~z{c;|Uav`tv-$Ma*x*$vn2R_FIZ~Rp^+<`Jr2)WWMTwgupU85MZds zjkGgt1H062PYYs=oxbh>=*vrmlc>Syr|Sb>Jcruo_*nkwx{3H{K78{L-x)kGP$i%2 zf1mf6&s^#v;Qc}3raBI{m{Na zDbK&IuM6k@`vLR(R~xs&kdK@7%f<)ee}{I;ejI8rSY@PLSHO9D)8|FkCjfsh%|AW(Akq10oj3R6!ws9F*sI~e4MY77Tt^Zf0FVXon&AMAo=*{x zjnikR-*<-OA1z=1vxtvZwreYl;enFknY;vY1IvCl)i>8HiQtE;rJz8KSULdFf68?< zm-Hz$%|)!=W6saQTX?|7Z2iM{K|ik7!~MSe_uZEl z#ol&UPk1l`?l*9MZ7FtK#0})VhhhF}n(6%Kg?vO`?|~xIw}08U5Fz=;=>105R};8~ zIv%6CU>yM-?!UOWck3}!Sr6B{ zkl$+?;{Urx5&JJfxpwbi8~@(J)W7&>Rll$pV)2g}`@gK9-XhGOyC9k3Fwb*sbiT2S zcmnHdmy$O9FD21`>!(%y-$wt%%T#}VRsGa|$bS5nZTh`f09~N~u0PLf1zPX9iU(t^ zE`k5a6SrO~xSis)5SKk&KIv<0UT%g zcz(HgXPXESXGuLREc-~5{NTf`g*Eo#%<*jYj6=RBD(>TI+y@-#o}v zdem<2Z2R&gWFVv{{5 z(&^k9J?q|G6<$ToX4G=H;q=JN7_5A&|0{+$EggYBJ)EP*=B-&q-Nc>CPN7y+)^a7~ z+vsHgX&yBUr&dc(!J_2>wUgH9COSu$G&tXm`XlgZt<~l z2d@uRDYr@t4`TTOd##>5*%JmFoE~y_19qzi!-Li6e|_zP(+j7DI*8O=OGr-$%AVKj zhdQKgweE^{vqyH~57Y@Z0(mvsdks&|se% z;a|xkzaqr*^(Er|fWR=}zxRYUJy(~$^XuRJfAbP=^o}87x7am{ze2dBioS!0uIm2X zzAmHajhf2`Js-r*+^iy__sq&WE)eleA{3ObQsr;5O8UBO4v2RIuoCiS<40XP0)HZu z<3*!?ziZRN1xwj`-B0^6Jsf&pz#%MPrFWLt_-WNDej0!2OklGrt30goO5tyA!b%}; ze}9ODqrb9gNGsi&_iu^SeuV$lwAi>}xn0*5Ib8*xWsBVI& zoT1tLp_2`rzWMCC@U_~QoBRJlXgRph*E-e02y1fb%;(Ey9Ny>8f42S; z6%I1{LaL#jkfU92(Lx z*(pUGPO0*=x>|?fE$MKzqdRQ6s@)~DlssbV8_|;Sq+I#-#NK3{wUaoh=El_Qa0c=( zr|9_*cg6V-!@iQ|Cxq=g-PlI=f1MUyf0^z((S0gonft|`kW)h#e&a{yMh$-KFUI$& zR7x-!o2DCKL+V+A6|w0Bd?z1_s%P!?$8I;2br|=IW%);v>9lrMV>3zUUB#E?00+w(8CDqY=<4LM(@bwcN zSXOV0QytT{ze{zTC9`Z^ZeOaHhqcCkj*tHo^45fa7aVS&!TQ>s5b7B83^Cri(VmbM zu^aIe)8Rd5S|96~|5J2le>AF&B6e`z9?m<&c}F?VDd5o&>u0!*v)sSHb(r=?CB8pO zd2xRfDtZQ|%e+t#9voVY*El{M_4;Z$csa~?JGvP!?hivnN4Hw$g^FNzPqpz{^J>hm zuEQ=LoHxjLL+y+=*iCsu?Smy=tnjyYYz6;lnYW4a`WbJN#CZMff0VaL>MrqO1(8Bq z$)Di-F`Q4Rh;}sLw!IV%38=#tZ1T}B)cgYQQu8%^migCthbnmq=Ov7n_!uvd%DmKs z&8&YbZ%=hz!g+P`m3d1hg=qWLb*#qQu{Q5&=4)zNhmKWvL!09*nINn~$11!{o8;Y? zj_SO^XW+G*4>9gffBl4*_N9LP#%=rZ4ZctHE7tz*-Mf|hQ&o-aTixRuX!ka;EFLe_Gkvr<3)VMaKi@Ls5vU<9`CZ!r{duliHY^sEn!Ca@q9c z=vX=xukdZpPC;CppRcsIF}7b#s9F3)Z(0M{&JSZI(vX@I4y1uR^zAA9EpEe^(^+`} zf@!5Vk)E8Iz_X{UO)uP%v3U7{=4p^hHQ(|$*F QKal?i1oNRM%dr8g2M3!AApigX delta 6316 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 diff --git a/src/test/resources/ABOUT_TEST_DATA.md b/src/test/resources/ABOUT_TEST_DATA.md index b7906dbf..4faef583 100644 --- a/src/test/resources/ABOUT_TEST_DATA.md +++ b/src/test/resources/ABOUT_TEST_DATA.md @@ -1,7 +1,7 @@ ## 1.17.1 (DV 2730) ### 1_17_1/r.-3.2.mca (seed: -592955240269541309) -Village, with villager with POI of cartography table and bed +Village, with villager with POI of cartography table and bed as well as nether portal Chunks: - -65 -42 From 42c93f1c6544be458174c405e4051910ecbf2070 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:53:14 -0500 Subject: [PATCH 028/288] MCAFileTest Strengthening setter tests, fixed typo in helper function name assertLoadFLag. Completed test testChunkSectionMinMaxSectionY which was accidentally left incomplete in previous commit. ChunkIterator added currentWorldX/Z getters. Moved deserializeChunk impl from MCAFile into MCAFileBase --- .../java/net/querz/mca/ChunkIterator.java | 10 ++ src/main/java/net/querz/mca/MCAFileBase.java | 17 ++- src/test/java/net/querz/mca/MCAFileTest.java | 107 ++++++++++-------- 3 files changed, 87 insertions(+), 47 deletions(-) diff --git a/src/main/java/net/querz/mca/ChunkIterator.java b/src/main/java/net/querz/mca/ChunkIterator.java index 41e4e93b..0c9b49b1 100644 --- a/src/main/java/net/querz/mca/ChunkIterator.java +++ b/src/main/java/net/querz/mca/ChunkIterator.java @@ -30,4 +30,14 @@ public interface ChunkIterator extends Iterator { * @return Current chunk z within region (in range 0-31) */ int currentZ(); + + /** + * @return Current chunk x in world coordinates (not block coordinates) + */ + int currentWorldX(); + + /** + * @return Current chunk z in world coordinates (not block coordinates) + */ + int currentWorldZ(); } diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index e6fdbf55..834406f6 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -167,7 +167,12 @@ public String createRegionName() { * @return Deserialized chunk. * @throws IOException if something went wrong during deserialization. */ - protected abstract T deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException; + protected T deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { + T chunk = createChunk(); + chunk.setLastMCAUpdate(timestamp); + chunk.deserialize(raf, loadFlags); + return chunk; + } /** * Reads an .mca file from a {@code RandomAccessFile} into this object. @@ -421,5 +426,15 @@ public int currentX() { public int currentZ() { return (currentIndex >> 5) & 0x1F; } + + @Override + public int currentWorldX() { + return currentX() + owner.getRegionX() * 32; + } + + @Override + public int currentWorldZ() { + return currentZ() + owner.getRegionZ() * 32; + } } } diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java index 86418d2e..b320a724 100644 --- a/src/test/java/net/querz/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -5,8 +5,6 @@ import static net.querz.mca.LoadFlags.*; import java.io.*; -import java.util.Arrays; -import java.util.Iterator; import java.util.Objects; public class MCAFileTest extends MCATestCase { @@ -138,32 +136,45 @@ public void testSetters() { f.getChunk(1023).setInhabitedTime(13243546); assertEquals(13243546, f.getChunk(1023).getInhabitedTime()); assertThrowsRuntimeException(() -> f.getChunk(1023).setBiomes(new int[255]), IllegalArgumentException.class); - assertThrowsNoRuntimeException(() -> f.getChunk(1023).setBiomes(new int[256])); - assertTrue(Arrays.equals(new int[256], f.getChunk(1023).getBiomes())); - f.getChunk(1023).setHeightMaps(getSomeCompoundTag()); - assertEquals(getSomeCompoundTag(), f.getChunk(1023).getHeightMaps()); - f.getChunk(1023).setCarvingMasks(getSomeCompoundTag()); - assertEquals(getSomeCompoundTag(), f.getChunk(1023).getCarvingMasks()); - f.getChunk(1023).setEntities(getSomeCompoundTagList()); - assertEquals(getSomeCompoundTagList(), f.getChunk(1023).getEntities()); - f.getChunk(1023).setTileEntities(getSomeCompoundTagList()); - assertEquals(getSomeCompoundTagList(), f.getChunk(1023).getTileEntities()); - f.getChunk(1023).setTileTicks(getSomeCompoundTagList()); - assertEquals(getSomeCompoundTagList(), f.getChunk(1023).getTileTicks()); - f.getChunk(1023).setLiquidTicks(getSomeCompoundTagList()); - assertEquals(getSomeCompoundTagList(), f.getChunk(1023).getLiquidTicks()); - f.getChunk(1023).setLights(getSomeListTagList()); - assertEquals(getSomeListTagList(), f.getChunk(1023).getLights()); - f.getChunk(1023).setLiquidsToBeTicked(getSomeListTagList()); - assertEquals(getSomeListTagList(), f.getChunk(1023).getLiquidsToBeTicked()); - f.getChunk(1023).setToBeTicked(getSomeListTagList()); - assertEquals(getSomeListTagList(), f.getChunk(1023).getToBeTicked()); - f.getChunk(1023).setPostProcessing(getSomeListTagList()); - assertEquals(getSomeListTagList(), f.getChunk(1023).getPostProcessing()); - f.getChunk(1023).setStructures(getSomeCompoundTag()); - assertEquals(getSomeCompoundTag(), f.getChunk(1023).getStructures()); + int[] biomes = new int[256]; + assertThrowsNoRuntimeException(() -> f.getChunk(1023).setBiomes(biomes)); + assertSame(biomes, f.getChunk(1023).getBiomes()); + CompoundTag compoundTag = getSomeCompoundTag(); + f.getChunk(1023).setHeightMaps(compoundTag); + assertSame(compoundTag, f.getChunk(1023).getHeightMaps()); + compoundTag = getSomeCompoundTag(); + f.getChunk(1023).setCarvingMasks(compoundTag); + assertSame(compoundTag, f.getChunk(1023).getCarvingMasks()); + ListTag compoundTagListTag = getSomeCompoundTagList(); + f.getChunk(1023).setEntities(compoundTagListTag); + assertSame(compoundTagListTag, f.getChunk(1023).getEntities()); + compoundTagListTag = getSomeCompoundTagList(); + f.getChunk(1023).setTileEntities(compoundTagListTag); + assertSame(compoundTagListTag, f.getChunk(1023).getTileEntities()); + compoundTagListTag = getSomeCompoundTagList(); + f.getChunk(1023).setTileTicks(compoundTagListTag); + assertSame(compoundTagListTag, f.getChunk(1023).getTileTicks()); + compoundTagListTag = getSomeCompoundTagList(); + f.getChunk(1023).setLiquidTicks(compoundTagListTag); + assertSame(compoundTagListTag, f.getChunk(1023).getLiquidTicks()); + ListTag> listTagListTag = getSomeListTagList(); + f.getChunk(1023).setLights(listTagListTag); + assertSame(listTagListTag, f.getChunk(1023).getLights()); + listTagListTag = getSomeListTagList(); + f.getChunk(1023).setLiquidsToBeTicked(listTagListTag); + assertSame(listTagListTag, f.getChunk(1023).getLiquidsToBeTicked()); + listTagListTag = getSomeListTagList(); + f.getChunk(1023).setToBeTicked(listTagListTag); + assertSame(listTagListTag, f.getChunk(1023).getToBeTicked()); + listTagListTag = getSomeListTagList(); + f.getChunk(1023).setPostProcessing(listTagListTag); + assertSame(listTagListTag, f.getChunk(1023).getPostProcessing()); + compoundTag = getSomeCompoundTag(); + f.getChunk(1023).setStructures(compoundTag); + assertSame(compoundTag, f.getChunk(1023).getStructures()); Section s = f.getChunk(1023).createSection(); f.getChunk(1023).setSection(0, s); + assertEquals(0, s.getHeight()); assertEquals(s, f.getChunk(1023).getSection(0)); assertThrowsRuntimeException(() -> f.getChunk(1023).getSection(0).setBlockStates(null), NullPointerException.class); assertThrowsRuntimeException(() -> f.getChunk(1023).getSection(0).setBlockStates(new long[321]), IllegalArgumentException.class); @@ -330,7 +341,7 @@ public void testSetBlockDataAt2527() { //test "line break" for DataVersion 2527 MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); Chunk p = f.getChunk(0, 0); - p.setDataVersion(3000); + p.setDataVersion(999999); Section section = f.getChunk(0, 0).getSection(0); assertEquals(10, section.getPalette().size()); assertEquals(0b0001000100010001000100010001000100010001000100010001000100010001L, section.getBlockStates()[0]); @@ -390,7 +401,7 @@ public void testChunkInvalidDataTag() { }, IOException.class); } - private void assertLoadFLag(Object field, long flags, long wantedFlag) { + private void assertLoadFlag(Object field, long flags, long wantedFlag) { if((flags & wantedFlag) != 0) { assertNotNull(String.format("Should not be null. Flags=%08x, Wanted flag=%08x", flags, wantedFlag), field); } else { @@ -399,24 +410,24 @@ private void assertLoadFLag(Object field, long flags, long wantedFlag) { } private void assertPartialChunk(Chunk c, long loadFlags) { - assertLoadFLag(c.getBiomes(), loadFlags, BIOMES); - assertLoadFLag(c.getHeightMaps(), loadFlags, HEIGHTMAPS); - assertLoadFLag(c.getEntities(), loadFlags, ENTITIES); - assertLoadFLag(c.getCarvingMasks(), loadFlags, CARVING_MASKS); - assertLoadFLag(c.getLights(), loadFlags, LIGHTS); - assertLoadFLag(c.getPostProcessing(), loadFlags, POST_PROCESSING); - assertLoadFLag(c.getLiquidTicks(), loadFlags, LIQUID_TICKS); - assertLoadFLag(c.getLiquidsToBeTicked(), loadFlags, LIQUIDS_TO_BE_TICKED); - assertLoadFLag(c.getTileTicks(), loadFlags, TILE_TICKS); - assertLoadFLag(c.getTileEntities(), loadFlags, TILE_ENTITIES); - assertLoadFLag(c.getToBeTicked(), loadFlags, TO_BE_TICKED); - assertLoadFLag(c.getSection(0), loadFlags, BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT); + assertLoadFlag(c.getBiomes(), loadFlags, BIOMES); + assertLoadFlag(c.getHeightMaps(), loadFlags, HEIGHTMAPS); + assertLoadFlag(c.getEntities(), loadFlags, ENTITIES); + assertLoadFlag(c.getCarvingMasks(), loadFlags, CARVING_MASKS); + assertLoadFlag(c.getLights(), loadFlags, LIGHTS); + assertLoadFlag(c.getPostProcessing(), loadFlags, POST_PROCESSING); + assertLoadFlag(c.getLiquidTicks(), loadFlags, LIQUID_TICKS); + assertLoadFlag(c.getLiquidsToBeTicked(), loadFlags, LIQUIDS_TO_BE_TICKED); + assertLoadFlag(c.getTileTicks(), loadFlags, TILE_TICKS); + assertLoadFlag(c.getTileEntities(), loadFlags, TILE_ENTITIES); + assertLoadFlag(c.getToBeTicked(), loadFlags, TO_BE_TICKED); + assertLoadFlag(c.getSection(0), loadFlags, BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT); if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0) { Section s = c.getSection(0); assertNotNull(String.format("Section is null. Flags=%08x", loadFlags), s); - assertLoadFLag(s.getBlockStates(), loadFlags, BLOCK_STATES); - assertLoadFLag(s.getBlockLight(), loadFlags, BLOCK_LIGHTS); - assertLoadFLag(s.getSkyLight(), loadFlags, SKY_LIGHT); + assertLoadFlag(s.getBlockStates(), loadFlags, BLOCK_STATES); + assertLoadFlag(s.getBlockLight(), loadFlags, BLOCK_LIGHTS); + assertLoadFlag(s.getSkyLight(), loadFlags, SKY_LIGHT); } } @@ -539,7 +550,9 @@ public void testChunkSectionMinMaxSectionY() { assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getMinSectionY()); assertEquals(SectionBase.NO_HEIGHT_SENTINEL, chunk.getMaxSectionY()); Section section = chunk.createSection(3); - + assertEquals(3, section.getHeight()); + assertEquals(3, chunk.getMinSectionY()); + assertEquals(3, chunk.getMaxSectionY()); } public void testMCAFileChunkIterator() { @@ -549,13 +562,15 @@ public void testMCAFileChunkIterator() { final int populatedX = -65 & 0x1F; final int populatedZ = -42 & 0x1F; int i = 0; - for (int z = 0; z < 32; z++) { - for (int x = 0; x < 32; x++) { + for (int z = 0, wz = -2 * 32; z < 32; z++, wz++) { + for (int x = 0, wx = -3 * 32; x < 32; x++, wx++) { assertTrue(iter.hasNext()); Chunk chunk = iter.next(); assertEquals(i, iter.currentIndex()); assertEquals(x, iter.currentX()); assertEquals(z, iter.currentZ()); + assertEquals(wx, iter.currentWorldX()); + assertEquals(wz, iter.currentWorldZ()); if (x == populatedX && z == populatedZ) { assertNotNull(chunk); } else { From 94c5791980ad53d736ed8cc563437483088e46b7 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:53:53 -0500 Subject: [PATCH 029/288] Add POI_RECORDS to LoadFlags --- src/main/java/net/querz/mca/LoadFlags.java | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/querz/mca/LoadFlags.java b/src/main/java/net/querz/mca/LoadFlags.java index da37a597..3c7e54d2 100644 --- a/src/main/java/net/querz/mca/LoadFlags.java +++ b/src/main/java/net/querz/mca/LoadFlags.java @@ -1,23 +1,26 @@ package net.querz.mca; public final class LoadFlags { + private LoadFlags() {} - public static final long BIOMES = 0x00001; - public static final long HEIGHTMAPS = 0x00002; - public static final long CARVING_MASKS = 0x00004; - public static final long ENTITIES = 0x00008; - public static final long TILE_ENTITIES = 0x00010; - public static final long TILE_TICKS = 0x00040; - public static final long LIQUID_TICKS = 0x00080; - public static final long TO_BE_TICKED = 0x00100; - public static final long POST_PROCESSING = 0x00200; - public static final long STRUCTURES = 0x00400; - public static final long BLOCK_LIGHTS = 0x00800; - public static final long BLOCK_STATES = 0x01000; - public static final long SKY_LIGHT = 0x02000; - public static final long LIGHTS = 0x04000; - public static final long LIQUIDS_TO_BE_TICKED = 0x08000; - public static final long RAW = 0x10000; + public static final long BIOMES = 0x0000_0001; + public static final long HEIGHTMAPS = 0x0000_0002; + public static final long CARVING_MASKS = 0x0000_0004; + public static final long ENTITIES = 0x0000_0008; + public static final long TILE_ENTITIES = 0x0000_0010; + public static final long TILE_TICKS = 0x0000_0040; + public static final long LIQUID_TICKS = 0x0000_0080; + public static final long TO_BE_TICKED = 0x0000_0100; + public static final long POST_PROCESSING = 0x0000_0200; + public static final long STRUCTURES = 0x0000_0400; + public static final long BLOCK_LIGHTS = 0x0000_0800; + public static final long BLOCK_STATES = 0x0000_1000; + public static final long SKY_LIGHT = 0x0000_2000; + public static final long LIGHTS = 0x0000_4000; + public static final long LIQUIDS_TO_BE_TICKED = 0x0000_8000; + public static final long RAW = 0x0001_0000; + public static final long POI_RECORDS = 0x0002_0000; public static final long ALL_DATA = 0xffffffffffffffffL; + } From 5e17a84c1723cd4c9e51c5ab01c98e1383ecd64f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 10:55:18 -0500 Subject: [PATCH 030/288] MCAUtil updated write methods to take MCAFileBase and exposed MCA_CREATORS for customization --- src/main/java/net/querz/mca/MCAUtil.java | 63 +++++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 7aa147b2..1f09c785 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -16,21 +16,29 @@ /** * Provides main and utility functions to read and write .mca files and - * to convert block, chunk and region coordinates. + * to convert block, chunk, and region coordinates. */ public final class MCAUtil { private MCAUtil() {} private static final Pattern mcaFilePattern = Pattern.compile("^.*\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); - private static final Map>> MCA_CREATORS; + + /** + * This map controls the factory creation behavior of the various "auto" functions. When an auto function is + * given an mca file location the folder name which contains the .mca files is used to lookup the correct + * mca file creator from this map. For example, if an auto method were passed the path + * "foo/bar/creator_name/r.4.2.mca" it would call {@code MCA_CREATORS.get("creator_name").apply(4, 2)}. + *

By manipulating this map you can control the factory behavior to support new mca types or to specify + * that a custom creation method should be called which could even return a custom {@link MCAFileBase} + * implementation.

+ */ + public static final Map>> MCA_CREATORS = new HashMap<>(); static { - Map>> map = new HashMap<>(); - map.put("region", MCAFile::new); - map.put("poi", PoiMCAFile::new); - map.put("entities", EntitiesMCAFile::new); - MCA_CREATORS = Collections.unmodifiableMap(map); + MCA_CREATORS.put("region", MCAFile::new); + MCA_CREATORS.put("poi", PoiMCAFile::new); + MCA_CREATORS.put("entities", EntitiesMCAFile::new); } // @@ -139,38 +147,38 @@ public static > T readAuto(File file, long loadFlags) t // /** - * Calls {@link MCAUtil#write(MCAFile, File, boolean)} without changing the timestamps. - * @see MCAUtil#write(MCAFile, File, boolean) + * Calls {@link MCAUtil#write(MCAFileBase, File, boolean)} without changing the timestamps. + * @see MCAUtil#write(MCAFileBase, File, boolean) * @param file The file to write to. * @param mcaFile The data of the MCA file to write. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. */ - public static int write(MCAFile mcaFile, String file) throws IOException { + public static int write(MCAFileBase mcaFile, String file) throws IOException { return write(mcaFile, new File(file), false); } /** - * Calls {@link MCAUtil#write(MCAFile, File, boolean)} without changing the timestamps. - * @see MCAUtil#write(MCAFile, File, boolean) + * Calls {@link MCAUtil#write(MCAFileBase, File, boolean)} without changing the timestamps. + * @see MCAUtil#write(MCAFileBase, File, boolean) * @param file The file to write to. * @param mcaFile The data of the MCA file to write. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. */ - public static int write(MCAFile mcaFile, File file) throws IOException { + public static int write(MCAFileBase mcaFile, File file) throws IOException { return write(mcaFile, file, false); } /** - * @see MCAUtil#write(MCAFile, File, boolean) + * @see MCAUtil#write(MCAFileBase, File, boolean) * @param file The file to write to. * @param mcaFile The data of the MCA file to write. * @param changeLastUpdate Whether to adjust the timestamps of when the file was saved. * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. */ - public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) throws IOException { + public static int write(MCAFileBase mcaFile, String file, boolean changeLastUpdate) throws IOException { return write(mcaFile, new File(file), changeLastUpdate); } @@ -185,7 +193,7 @@ public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. */ - public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) throws IOException { + public static int write(MCAFileBase mcaFile, File file, boolean changeLastUpdate) throws IOException { File to = file; if (file.exists()) { to = File.createTempFile(to.getName(), null); @@ -349,20 +357,23 @@ public static > T autoMCAFile(File file) { * * @param path The file does not need to exist but the given path must have at least 2 parts. * Required parts: "mca_type/mca_file" - * where mca_type is used to determine the type of {@link MCAFileBase} to return (such as "region", - * "poi", "entities") and mca_file is the .mca file such as "r.0.0.mca". + * where mca_type (such as "region", "poi", "entities") is used to determine which + * {@link #MCA_CREATORS} to call and mca_file is the .mca file such as "r.0.0.mca". * @param {@link MCAFileBase} type - do note that any {@link ClassCastException} errors will be thrown * at the location of assignment, not from within this call. - * @return Instantiated and initialized concretion of {@link MCAFileBase} - * @throws IllegalArgumentException Thrown when the mca type could not be determined from the path or when the - * regions X and Z locations could not be extracted from the filename. + * @return Instantiated and initialized concretion of {@link MCAFileBase}. Never Null. + * @throws IllegalArgumentException Thrown when the mca type could not be determined from the path, when there + * is no {@link #MCA_CREATORS} mapped to the mca type, or when the regions X and Z locations could not be + * extracted from the filename. + * @throws NullPointerException Thrown when a custom creator did not produce a result. */ @SuppressWarnings("unchecked") public static > T autoMCAFile(Path path) { BiFunction> creator = null; + String creatorName = null; try { - String hint = path.getParent().getFileName().toString(); - creator = MCA_CREATORS.get(hint); + creatorName = path.getParent().getFileName().toString(); + creator = MCA_CREATORS.get(creatorName); if (creator == null) throwCannotDetermineMcaType(null); } catch (Exception ex) { throwCannotDetermineMcaType(ex); @@ -373,6 +384,10 @@ public static > T autoMCAFile(Path path) { } final int x = Integer.parseInt(m.group("regionX")); final int z = Integer.parseInt(m.group("regionZ")); - return (T) creator.apply(x, z); + T mcaFile = (T) creator.apply(x, z); + if (mcaFile == null) { + throw new NullPointerException("Creator for " + creatorName + " did not produce a result for " + path); + } + return mcaFile; } } From 7d7307d5813aed7fd02886655b36c9f94240a452 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 11:01:17 -0500 Subject: [PATCH 031/288] Refactoring REGION classes to make it easier for users to extend and to be abel to customize the behavior of the "auto" factory methods in MCAUtil --- src/main/java/net/querz/mca/Chunk.java | 551 +---------------- src/main/java/net/querz/mca/MCAFile.java | 10 - .../java/net/querz/mca/RegionChunkBase.java | 562 +++++++++++++++++ .../java/net/querz/mca/RegionMCAFileBase.java | 134 +++++ .../java/net/querz/mca/RegionSectionBase.java | 549 +++++++++++++++++ src/main/java/net/querz/mca/Section.java | 566 +----------------- src/main/java/net/querz/mca/SectionBase.java | 66 +- .../net/querz/mca/SectionedChunkBase.java | 4 +- 8 files changed, 1323 insertions(+), 1119 deletions(-) create mode 100644 src/main/java/net/querz/mca/RegionChunkBase.java create mode 100644 src/main/java/net/querz/mca/RegionMCAFileBase.java create mode 100644 src/main/java/net/querz/mca/RegionSectionBase.java diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 2a06f66b..4f735566 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -1,20 +1,12 @@ package net.querz.mca; import net.querz.nbt.tag.CompoundTag; -import net.querz.nbt.tag.ListTag; - -import java.util.Arrays; -import java.util.Map; - -import static net.querz.mca.DataVersion.JAVA_1_15_19W36A; -import static net.querz.mca.LoadFlags.*; /** * Represents a REGION data mca chunk. Region chunks are composed of a set of {@link Section} where any empty/null * section is filled with air blocks by the game. */ -public class Chunk extends SectionedChunkBase
{ - +public class Chunk extends RegionChunkBase
{ /** * The default chunk data version used when no custom version is supplied. * @deprecated Use {@code DataVersion.latest().id()} instead. @@ -22,511 +14,34 @@ public class Chunk extends SectionedChunkBase
{ @Deprecated public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); - protected long lastUpdate; - protected long inhabitedTime; - protected int[] biomes; - protected CompoundTag heightMaps; - protected CompoundTag carvingMasks; - protected ListTag entities; // never populated for versions >= 1.17 - protected ListTag tileEntities; - protected ListTag tileTicks; - protected ListTag liquidTicks; - protected ListTag> lights; - protected ListTag> liquidsToBeTicked; - protected ListTag> toBeTicked; - protected ListTag> postProcessing; - protected String status; - protected CompoundTag structures; - Chunk(int lastMCAUpdate) { super(lastMCAUpdate); } - /** - * Create a new chunk based on raw base data from a region file. - * @param data The raw base data to be used. - */ public Chunk(CompoundTag data) { super(data); } - /** - * {@inheritDoc} - */ - @Override - public Section createSection(int sectionY) throws IllegalArgumentException { - if (containsSection(sectionY)) throw new IllegalArgumentException("section already exists at section-y " + sectionY); - Section section = createSection(); - putSection(sectionY, section); - return section; - } - @Override - protected void initReferences(final long loadFlags) { - CompoundTag level = data.getCompoundTag("Level"); - if (level == null) { - throw new IllegalArgumentException("data does not contain \"Level\" tag"); - } - inhabitedTime = level.getLong("InhabitedTime"); - lastUpdate = level.getLong("LastUpdate"); - if ((loadFlags & BIOMES) != 0) { - biomes = level.getIntArray("Biomes"); - if (biomes.length == 0) biomes = null; - } - if ((loadFlags & HEIGHTMAPS) != 0) { - heightMaps = level.getCompoundTag("Heightmaps"); - } - if ((loadFlags & CARVING_MASKS) != 0) { - carvingMasks = level.getCompoundTag("CarvingMasks"); - } - if ((loadFlags & ENTITIES) != 0) { - entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null; - } - if ((loadFlags & TILE_ENTITIES) != 0) { - tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null; - } - if ((loadFlags & TILE_TICKS) != 0) { - tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null; - } - if ((loadFlags & LIQUID_TICKS) != 0) { - liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null; - } - if ((loadFlags & LIGHTS) != 0) { - lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null; - } - if ((loadFlags & LIQUIDS_TO_BE_TICKED) != 0) { - liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null; - } - if ((loadFlags & TO_BE_TICKED) != 0) { - toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null; - } - if ((loadFlags & POST_PROCESSING) != 0) { - postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null; - } - status = level.getString("Status"); - if ((loadFlags & STRUCTURES) != 0) { - structures = level.getCompoundTag("Structures"); - } - if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0 && level.containsKey("Sections")) { - for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) { - int sectionIndex = section.getNumber("Y").byteValue(); - Section newSection = new Section(section, dataVersion, loadFlags); - putSection(sectionIndex, newSection, false); - } - } - } - - /** - * May only be used for data versions LT 2203 which includes all of 1.14 - * and up until 19w36a (a 1.15 weekly snapshot). - * @deprecated Use {@link #getBiomeAt(int, int, int)} instead for 1.15 and beyond - */ - @Deprecated - public int getBiomeAt(int blockX, int blockZ) { - if (dataVersion < JAVA_1_15_19W36A.id()) { - if (biomes == null || biomes.length != 256) { - return -1; - } - return biomes[getBlockIndex(blockX, blockZ)]; - } else { - throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2203 or higher (1.15+), use Chunk#getBiomeAt(int,int,int) instead"); - } - } - - /** - * Fetches a biome id at a specific block in this chunk. - * The coordinates can be absolute coordinates or relative to the region or chunk. - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @return The biome id or -1 if the biomes are not correctly initialized. - */ - public int getBiomeAt(int blockX, int blockY, int blockZ) { - if (dataVersion >= JAVA_1_15_19W36A.id()) { - if (biomes == null || biomes.length != 1024) { - return -1; - } - int biomeX = (blockX & 0xF) >> 2; - int biomeY = (blockY & 0xF) >> 2; - int biomeZ = (blockZ & 0xF) >> 2; - - return biomes[getBiomeIndex(biomeX, biomeY, biomeZ)]; - } else { - return getBiomeAt(blockX, blockZ); - } - } - - /** - * Should only be used for data versions LT 2203 which includes all of 1.14 - * and up until 19w36a (a 1.15 weekly snapshot). - * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead for 1.15 and beyond - */ - @Deprecated - public void setBiomeAt(int blockX, int blockZ, int biomeID) { - checkRaw(); - if (dataVersion < JAVA_1_15_19W36A.id()) { - if (biomes == null || biomes.length != 256) { - biomes = new int[256]; - Arrays.fill(biomes, -1); - } - biomes[getBlockIndex(blockX, blockZ)] = biomeID; - } else { - if (biomes == null || biomes.length != 1024) { - biomes = new int[1024]; - Arrays.fill(biomes, -1); - } - - int biomeX = (blockX & 0xF) >> 2; - int biomeZ = (blockZ & 0xF) >> 2; - - for (int y = 0; y < 64; y++) { - biomes[getBiomeIndex(biomeX, y, biomeZ)] = biomeID; - } - } - } - - /** - * Sets a biome id at a specific block column. - * The coordinates can be absolute coordinates or relative to the region or chunk. - * @param blockX The x-coordinate of the block column. - * @param blockZ The z-coordinate of the block column. - * @param biomeID The biome id to be set. - * When set to a negative number, Minecraft will replace it with the block column's default biome. - */ - public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { - checkRaw(); - if (dataVersion >= JAVA_1_15_19W36A.id()) { - if (biomes == null || biomes.length != 1024) { - biomes = new int[1024]; - Arrays.fill(biomes, -1); - } - - int biomeX = (blockX & 0xF) >> 2; - int biomeZ = (blockZ & 0xF) >> 2; - - biomes[getBiomeIndex(biomeX, blockY, biomeZ)] = biomeID; - } else { - if (biomes == null || biomes.length != 256) { - biomes = new int[256]; - Arrays.fill(biomes, -1); - } - biomes[getBlockIndex(blockX, blockZ)] = biomeID; - } - } - - int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { - return biomeY * 16 + biomeZ * 4 + biomeX; - } - - public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - Section section = getSection(MCAUtil.blockToChunk(blockY)); - if (section == null) { - return null; - } - return section.getBlockStateAt(blockX, blockY, blockZ); - } - - /** - * Sets a block state at a specific location. - * The block coordinates can be absolute or relative to the region or chunk. - * @param blockX The x-coordinate of the block. - * @param blockY The y-coordinate of the block. - * @param blockZ The z-coordinate of the block. - * @param state The block state to be set. - * @param cleanup When true, it will cleanup all palettes of this chunk. - * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. - * Recalculating the Palette should only be executed once right before saving the Chunk to file. - */ - public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { - checkRaw(); - int sectionIndex = MCAUtil.blockToChunk(blockY); - Section section = getSection(sectionIndex); - if (section == null) { - putSection(sectionIndex, section = createSection(), false); - section.setDataVersion(dataVersion); - } - section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); - } - - /** - * Creates a new section appropriately initialized for use inside this chunk. - */ - public Section createSection() { - return new Section(dataVersion); + protected Section createSection(CompoundTag section, int dataVersion, long loadFlags) { + return new Section(section, dataVersion, loadFlags); } /** * {@inheritDoc} */ @Override - public void setDataVersion(int dataVersion) { - super.setDataVersion(dataVersion); - for (Section section : this) { - if (section != null) { - section.setDataVersion(dataVersion); - } - } - } - - /** - * @return The generation station of this chunk. - */ - public String getStatus() { - return status; - } - - /** - * Sets the generation status of this chunk. - * @param status The generation status of this chunk. - */ - public void setStatus(String status) { - checkRaw(); - this.status = status; - } - - /** - * @return The timestamp when this chunk was last updated as a UNIX timestamp. - */ - public long getLastUpdate() { - return lastUpdate; - } - - /** - * Sets the time when this chunk was last updated as a UNIX timestamp. - * @param lastUpdate The UNIX timestamp. - */ - public void setLastUpdate(long lastUpdate) { - checkRaw(); - this.lastUpdate = lastUpdate; - } - - /** - * @return The cumulative amount of time players have spent in this chunk in ticks. - */ - public long getInhabitedTime() { - return inhabitedTime; - } - - /** - * Sets the cumulative amount of time players have spent in this chunk in ticks. - * @param inhabitedTime The time in ticks. - */ - public void setInhabitedTime(long inhabitedTime) { - checkRaw(); - this.inhabitedTime = inhabitedTime; - } - - /** - * @return A matrix of biome IDs for all block columns in this chunk. - */ - public int[] getBiomes() { - return biomes; - } - - /** - * Sets the biome IDs for this chunk. - * @param biomes The biome ID matrix of this chunk. Must have a length of {@code 1024} for 1.15+ or {@code 256} - * for prior versions. - * @throws IllegalArgumentException When the biome matrix is {@code null} or does not have a version appropriate length. - */ - public void setBiomes(int[] biomes) { - checkRaw(); - if (biomes != null) { - final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; - if (biomes.length != requiredSize) { - throw new IllegalArgumentException("biomes array must have a length of " + requiredSize); - } + public Section createSection(int sectionY) throws IllegalArgumentException { + if (containsSection(sectionY)) throw new IllegalArgumentException("section already exists at section-y " + sectionY); + Section section = new Section(dataVersion); + if (sectionY != SectionBase.NO_HEIGHT_SENTINEL) { + putSection(sectionY, section); // sets section height & validates range } - this.biomes = biomes; - } - - /** - * @return The height maps of this chunk. - */ - public CompoundTag getHeightMaps() { - return heightMaps; - } - - /** - * Sets the height maps of this chunk. - * @param heightMaps The height maps. - */ - public void setHeightMaps(CompoundTag heightMaps) { - checkRaw(); - this.heightMaps = heightMaps; - } - - /** - * @return The carving masks of this chunk. - */ - public CompoundTag getCarvingMasks() { - return carvingMasks; - } - - /** - * Sets the carving masks of this chunk. - * @param carvingMasks The carving masks. - */ - public void setCarvingMasks(CompoundTag carvingMasks) { - checkRaw(); - this.carvingMasks = carvingMasks; - } - - /** - * @return The entities of this chunk. - */ - public ListTag getEntities() { - return entities; - } - - /** - * Sets the entities of this chunk. - * @param entities The entities. - */ - public void setEntities(ListTag entities) { - checkRaw(); - this.entities = entities; - } - - /** - * @return The tile entities of this chunk. - */ - public ListTag getTileEntities() { - return tileEntities; - } - - /** - * Sets the tile entities of this chunk. - * @param tileEntities The tile entities of this chunk. - */ - public void setTileEntities(ListTag tileEntities) { - checkRaw(); - this.tileEntities = tileEntities; - } - - /** - * @return The tile ticks of this chunk. - */ - public ListTag getTileTicks() { - return tileTicks; - } - - /** - * Sets the tile ticks of this chunk. - * @param tileTicks Thee tile ticks. - */ - public void setTileTicks(ListTag tileTicks) { - checkRaw(); - this.tileTicks = tileTicks; - } - - /** - * @return The liquid ticks of this chunk. - */ - public ListTag getLiquidTicks() { - return liquidTicks; - } - - /** - * Sets the liquid ticks of this chunk. - * @param liquidTicks The liquid ticks. - */ - public void setLiquidTicks(ListTag liquidTicks) { - checkRaw(); - this.liquidTicks = liquidTicks; - } - - /** - * @return The light sources in this chunk. - */ - public ListTag> getLights() { - return lights; - } - - /** - * Sets the light sources in this chunk. - * @param lights The light sources. - */ - public void setLights(ListTag> lights) { - checkRaw(); - this.lights = lights; - } - - /** - * @return The liquids to be ticked in this chunk. - */ - public ListTag> getLiquidsToBeTicked() { - return liquidsToBeTicked; - } - - /** - * Sets the liquids to be ticked in this chunk. - * @param liquidsToBeTicked The liquids to be ticked. - */ - public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { - checkRaw(); - this.liquidsToBeTicked = liquidsToBeTicked; - } - - /** - * @return Stuff to be ticked in this chunk. - */ - public ListTag> getToBeTicked() { - return toBeTicked; - } - - /** - * Sets stuff to be ticked in this chunk. - * @param toBeTicked The stuff to be ticked. - */ - public void setToBeTicked(ListTag> toBeTicked) { - checkRaw(); - this.toBeTicked = toBeTicked; - } - - /** - * @return Things that are in post processing in this chunk. - */ - public ListTag> getPostProcessing() { - return postProcessing; - } - - /** - * Sets things to be post processed in this chunk. - * @param postProcessing The things to be post processed. - */ - public void setPostProcessing(ListTag> postProcessing) { - checkRaw(); - this.postProcessing = postProcessing; - } - - /** - * @return Data about structures in this chunk. - */ - public CompoundTag getStructures() { - return structures; - } - - /** - * Sets data about structures in this chunk. - * @param structures The data about structures. - */ - public void setStructures(CompoundTag structures) { - checkRaw(); - this.structures = structures; - } - - int getBlockIndex(int blockX, int blockZ) { - return (blockZ & 0xF) * 16 + (blockX & 0xF); + return section; } - public void cleanupPalettesAndBlockStates() { - checkRaw(); - for (Section section : this) { - if (section != null) { - section.cleanupPaletteAndBlockStates(); - } - } + public Section createSection() { + return createSection(SectionBase.NO_HEIGHT_SENTINEL); } /** @@ -535,7 +50,7 @@ public void cleanupPalettesAndBlockStates() { */ @Deprecated public static Chunk newChunk() { - return Chunk.newChunk(DataVersion.latest().id()); + return newChunk(DataVersion.latest().id()); } public static Chunk newChunk(int dataVersion) { @@ -546,46 +61,4 @@ public static Chunk newChunk(int dataVersion) { c.status = "mobs_spawned"; return c; } - - @Override - public CompoundTag updateHandle(int xPos, int zPos) { - if (raw) { - return data; - } - super.updateHandle(xPos, zPos); - CompoundTag level = data.getCompoundTag("Level"); - level.putInt("xPos", xPos); - level.putInt("zPos", zPos); - level.putLong("LastUpdate", lastUpdate); - level.putLong("InhabitedTime", inhabitedTime); - if (biomes != null) { - final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; - if (biomes.length != requiredSize) - throw new IllegalStateException( - String.format("Biomes array must be %d bytes for version %d, array size is %d", - requiredSize, dataVersion, biomes.length)); - level.putIntArray("Biomes", biomes); - } - level.putIfNotNull("Heightmaps", heightMaps); - level.putIfNotNull("CarvingMasks", carvingMasks); - level.putIfNotNull("Entities", entities); - level.putIfNotNull("TileEntities", tileEntities); - level.putIfNotNull("TileTicks", tileTicks); - level.putIfNotNull("LiquidTicks", liquidTicks); - level.putIfNotNull("Lights", lights); - level.putIfNotNull("LiquidsToBeTicked", liquidsToBeTicked); - level.putIfNotNull("ToBeTicked", toBeTicked); - level.putIfNotNull("PostProcessing", postProcessing); - level.putString("Status", status); - level.putIfNotNull("Structures", structures); - - ListTag sections = new ListTag<>(CompoundTag.class); - for (Section section : this) { - if (section != null) { - sections.add(section.updateHandle(section.getHeight() /* contract of iterator assures correctness */)); - } - } - level.put("Sections", sections); - return data; - } } diff --git a/src/main/java/net/querz/mca/MCAFile.java b/src/main/java/net/querz/mca/MCAFile.java index 354f2c5d..5bce4c94 100644 --- a/src/main/java/net/querz/mca/MCAFile.java +++ b/src/main/java/net/querz/mca/MCAFile.java @@ -59,16 +59,6 @@ public Chunk createChunk() { return Chunk.newChunk(defaultDataVersion); } - /** - * {@inheritDoc} - */ - @Override - protected Chunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { - Chunk chunk = new Chunk(timestamp); - chunk.deserialize(raf, loadFlags); - return chunk; - } - /** * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead */ diff --git a/src/main/java/net/querz/mca/RegionChunkBase.java b/src/main/java/net/querz/mca/RegionChunkBase.java new file mode 100644 index 00000000..3008688d --- /dev/null +++ b/src/main/java/net/querz/mca/RegionChunkBase.java @@ -0,0 +1,562 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; + +import java.util.Arrays; + +import static net.querz.mca.DataVersion.JAVA_1_15_19W36A; +import static net.querz.mca.LoadFlags.*; + +/** + * Represents a REGION data mca chunk. Region chunks are composed of a set of {@link Section} where any empty/null + * section is filled with air blocks by the game. + */ +public abstract class RegionChunkBase extends SectionedChunkBase { + + protected long lastUpdate; + protected long inhabitedTime; + protected int[] biomes; + protected CompoundTag heightMaps; + protected CompoundTag carvingMasks; + protected ListTag entities; // never populated for chunk versions >= 2724 (1.17) + protected ListTag tileEntities; + protected ListTag tileTicks; + protected ListTag liquidTicks; + protected ListTag> lights; + protected ListTag> liquidsToBeTicked; + protected ListTag> toBeTicked; + protected ListTag> postProcessing; + protected String status; + protected CompoundTag structures; + + RegionChunkBase(int lastMCAUpdate) { + super(lastMCAUpdate); + } + + /** + * Create a new chunk based on raw base data from a region file. + * @param data The raw base data to be used. + */ + public RegionChunkBase(CompoundTag data) { + super(data); + } + + @Override + protected void initReferences(final long loadFlags) { + CompoundTag level = data.getCompoundTag("Level"); + if (level == null) { + throw new IllegalArgumentException("data does not contain \"Level\" tag"); + } + inhabitedTime = level.getLong("InhabitedTime"); + lastUpdate = level.getLong("LastUpdate"); + if ((loadFlags & BIOMES) != 0) { + biomes = level.getIntArray("Biomes"); + if (biomes.length == 0) biomes = null; + } + if ((loadFlags & HEIGHTMAPS) != 0) { + heightMaps = level.getCompoundTag("Heightmaps"); + } + if ((loadFlags & CARVING_MASKS) != 0) { + carvingMasks = level.getCompoundTag("CarvingMasks"); + } + if ((loadFlags & ENTITIES) != 0) { + entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_ENTITIES) != 0) { + tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_TICKS) != 0) { + tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIQUID_TICKS) != 0) { + liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIGHTS) != 0) { + lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null; + } + if ((loadFlags & LIQUIDS_TO_BE_TICKED) != 0) { + liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null; + } + if ((loadFlags & TO_BE_TICKED) != 0) { + toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null; + } + if ((loadFlags & POST_PROCESSING) != 0) { + postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null; + } + status = level.getString("Status"); + if ((loadFlags & STRUCTURES) != 0) { + structures = level.getCompoundTag("Structures"); + } + if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0 && level.containsKey("Sections")) { + for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) { + T newSection = createSection(section, dataVersion, loadFlags); + putSection(newSection.getHeight(), newSection, false); + } + } + } + + protected abstract T createSection(CompoundTag section, int dataVersion, long loadFlags); + + /** + * May only be used for data versions LT 2203 which includes all of 1.14 + * and up until 19w36a (a 1.15 weekly snapshot). + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead for 1.15 and beyond + */ + @Deprecated + public int getBiomeAt(int blockX, int blockZ) { + if (dataVersion < JAVA_1_15_19W36A.id()) { + if (biomes == null || biomes.length != 256) { + return -1; + } + return biomes[getBlockIndex(blockX, blockZ)]; + } else { + throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2203 or higher (1.15+), use Chunk#getBiomeAt(int,int,int) instead"); + } + } + + /** + * Fetches a biome id at a specific block in this chunk. + * The coordinates can be absolute coordinates or relative to the region or chunk. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The biome id or -1 if the biomes are not correctly initialized. + */ + public int getBiomeAt(int blockX, int blockY, int blockZ) { + if (dataVersion >= JAVA_1_15_19W36A.id()) { + if (biomes == null || biomes.length != 1024) { + return -1; + } + int biomeX = (blockX & 0xF) >> 2; + int biomeY = (blockY & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + return biomes[getBiomeIndex(biomeX, biomeY, biomeZ)]; + } else { + return getBiomeAt(blockX, blockZ); + } + } + + /** + * Should only be used for data versions LT 2203 which includes all of 1.14 + * and up until 19w36a (a 1.15 weekly snapshot). + * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead for 1.15 and beyond + */ + @Deprecated + public void setBiomeAt(int blockX, int blockZ, int biomeID) { + checkRaw(); + if (dataVersion < JAVA_1_15_19W36A.id()) { + if (biomes == null || biomes.length != 256) { + biomes = new int[256]; + Arrays.fill(biomes, -1); + } + biomes[getBlockIndex(blockX, blockZ)] = biomeID; + } else { + if (biomes == null || biomes.length != 1024) { + biomes = new int[1024]; + Arrays.fill(biomes, -1); + } + + int biomeX = (blockX & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + for (int y = 0; y < 64; y++) { + biomes[getBiomeIndex(biomeX, y, biomeZ)] = biomeID; + } + } + } + + /** + * Sets a biome id at a specific block column. + * The coordinates can be absolute coordinates or relative to the region or chunk. + * @param blockX The x-coordinate of the block column. + * @param blockZ The z-coordinate of the block column. + * @param biomeID The biome id to be set. + * When set to a negative number, Minecraft will replace it with the block column's default biome. + */ + public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { + checkRaw(); + if (dataVersion >= JAVA_1_15_19W36A.id()) { + if (biomes == null || biomes.length != 1024) { + biomes = new int[1024]; + Arrays.fill(biomes, -1); + } + + int biomeX = (blockX & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + biomes[getBiomeIndex(biomeX, blockY, biomeZ)] = biomeID; + } else { + if (biomes == null || biomes.length != 256) { + biomes = new int[256]; + Arrays.fill(biomes, -1); + } + biomes[getBlockIndex(blockX, blockZ)] = biomeID; + } + } + + int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { + return biomeY * 16 + biomeZ * 4 + biomeX; + } + + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + T section = getSection(MCAUtil.blockToChunk(blockY)); + if (section == null) { + return null; + } + return section.getBlockStateAt(blockX, blockY, blockZ); + } + + /** + * Sets a block state at a specific location. + * The block coordinates can be absolute or relative to the region or chunk. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @param state The block state to be set. + * @param cleanup When true, it will cleanup all palettes of this chunk. + * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Chunk to file. + */ + public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + checkRaw(); + int sectionIndex = MCAUtil.blockToChunk(blockY); + T section = getSection(sectionIndex); + if (section == null) { + putSection(sectionIndex, section = createSection(sectionIndex), false); + section.syncDataVersion(dataVersion); + } + section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); + } + + /** + * {@inheritDoc} + */ + @Override + public void setDataVersion(int dataVersion) { + super.setDataVersion(dataVersion); + for (T section : this) { + if (section != null) { + section.syncDataVersion(dataVersion); + } + } + } + + /** + * @return The generation station of this chunk. + */ + public String getStatus() { + return status; + } + + /** + * Sets the generation status of this chunk. + * @param status The generation status of this chunk. + */ + public void setStatus(String status) { + checkRaw(); + this.status = status; + } + + /** + * @return The timestamp when this chunk was last updated as a UNIX timestamp. + */ + public long getLastUpdate() { + return lastUpdate; + } + + /** + * Sets the time when this chunk was last updated as a UNIX timestamp. + * @param lastUpdate The UNIX timestamp. + */ + public void setLastUpdate(long lastUpdate) { + checkRaw(); + this.lastUpdate = lastUpdate; + } + + /** + * @return The cumulative amount of time players have spent in this chunk in ticks. + */ + public long getInhabitedTime() { + return inhabitedTime; + } + + /** + * Sets the cumulative amount of time players have spent in this chunk in ticks. + * @param inhabitedTime The time in ticks. + */ + public void setInhabitedTime(long inhabitedTime) { + checkRaw(); + this.inhabitedTime = inhabitedTime; + } + + /** + * @return A matrix of biome IDs for all block columns in this chunk. + */ + public int[] getBiomes() { + return biomes; + } + + /** + * Sets the biome IDs for this chunk. + * @param biomes The biome ID matrix of this chunk. Must have a length of {@code 1024} for 1.15+ or {@code 256} + * for prior versions. + * @throws IllegalArgumentException When the biome matrix is {@code null} or does not have a version appropriate length. + */ + public void setBiomes(int[] biomes) { + checkRaw(); + if (biomes != null) { + final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; + if (biomes.length != requiredSize) { + throw new IllegalArgumentException("biomes array must have a length of " + requiredSize); + } + } + this.biomes = biomes; + } + + /** + * @return The height maps of this chunk. + */ + public CompoundTag getHeightMaps() { + return heightMaps; + } + + /** + * Sets the height maps of this chunk. + * @param heightMaps The height maps. + */ + public void setHeightMaps(CompoundTag heightMaps) { + checkRaw(); + this.heightMaps = heightMaps; + } + + /** + * @return The carving masks of this chunk. + */ + public CompoundTag getCarvingMasks() { + return carvingMasks; + } + + /** + * Sets the carving masks of this chunk. + * @param carvingMasks The carving masks. + */ + public void setCarvingMasks(CompoundTag carvingMasks) { + checkRaw(); + this.carvingMasks = carvingMasks; + } + + /** + * @return The entities of this chunk. + */ + public ListTag getEntities() { + return entities; + } + + /** + * Sets the entities of this chunk. + * @param entities The entities. + */ + public void setEntities(ListTag entities) { + checkRaw(); + this.entities = entities; + } + + /** + * @return The tile entities of this chunk. + */ + public ListTag getTileEntities() { + return tileEntities; + } + + /** + * Sets the tile entities of this chunk. + * @param tileEntities The tile entities of this chunk. + */ + public void setTileEntities(ListTag tileEntities) { + checkRaw(); + this.tileEntities = tileEntities; + } + + /** + * @return The tile ticks of this chunk. + */ + public ListTag getTileTicks() { + return tileTicks; + } + + /** + * Sets the tile ticks of this chunk. + * @param tileTicks Thee tile ticks. + */ + public void setTileTicks(ListTag tileTicks) { + checkRaw(); + this.tileTicks = tileTicks; + } + + /** + * @return The liquid ticks of this chunk. + */ + public ListTag getLiquidTicks() { + return liquidTicks; + } + + /** + * Sets the liquid ticks of this chunk. + * @param liquidTicks The liquid ticks. + */ + public void setLiquidTicks(ListTag liquidTicks) { + checkRaw(); + this.liquidTicks = liquidTicks; + } + + /** + * @return The light sources in this chunk. + */ + public ListTag> getLights() { + return lights; + } + + /** + * Sets the light sources in this chunk. + * @param lights The light sources. + */ + public void setLights(ListTag> lights) { + checkRaw(); + this.lights = lights; + } + + /** + * @return The liquids to be ticked in this chunk. + */ + public ListTag> getLiquidsToBeTicked() { + return liquidsToBeTicked; + } + + /** + * Sets the liquids to be ticked in this chunk. + * @param liquidsToBeTicked The liquids to be ticked. + */ + public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { + checkRaw(); + this.liquidsToBeTicked = liquidsToBeTicked; + } + + /** + * @return Stuff to be ticked in this chunk. + */ + public ListTag> getToBeTicked() { + return toBeTicked; + } + + /** + * Sets stuff to be ticked in this chunk. + * @param toBeTicked The stuff to be ticked. + */ + public void setToBeTicked(ListTag> toBeTicked) { + checkRaw(); + this.toBeTicked = toBeTicked; + } + + /** + * @return Things that are in post processing in this chunk. + */ + public ListTag> getPostProcessing() { + return postProcessing; + } + + /** + * Sets things to be post processed in this chunk. + * @param postProcessing The things to be post processed. + */ + public void setPostProcessing(ListTag> postProcessing) { + checkRaw(); + this.postProcessing = postProcessing; + } + + /** + * @return Data about structures in this chunk. + */ + public CompoundTag getStructures() { + return structures; + } + + /** + * Sets data about structures in this chunk. + * @param structures The data about structures. + */ + public void setStructures(CompoundTag structures) { + checkRaw(); + this.structures = structures; + } + + int getBlockIndex(int blockX, int blockZ) { + return (blockZ & 0xF) * 16 + (blockX & 0xF); + } + + public void cleanupPalettesAndBlockStates() { + checkRaw(); + for (T section : this) { + if (section != null) { + section.cleanupPaletteAndBlockStates(); + } + } + } + + + /** + * {@inheritDoc} + */ + @Override + public CompoundTag updateHandle() { + if (raw) { + return data; + } + super.updateHandle(); + CompoundTag level = data.getCompoundTag("Level"); + level.putLong("LastUpdate", lastUpdate); + level.putLong("InhabitedTime", inhabitedTime); + if (biomes != null) { + final int requiredSize = dataVersion <= 0 || dataVersion >= JAVA_1_15_19W36A.id() ? 1024 : 256; + if (biomes.length != requiredSize) + throw new IllegalStateException( + String.format("Biomes array must be %d bytes for version %d, array size is %d", + requiredSize, dataVersion, biomes.length)); + level.putIntArray("Biomes", biomes); + } + level.putIfNotNull("Heightmaps", heightMaps); + level.putIfNotNull("CarvingMasks", carvingMasks); + level.putIfNotNull("Entities", entities); + level.putIfNotNull("TileEntities", tileEntities); + level.putIfNotNull("TileTicks", tileTicks); + level.putIfNotNull("LiquidTicks", liquidTicks); + level.putIfNotNull("Lights", lights); + level.putIfNotNull("LiquidsToBeTicked", liquidsToBeTicked); + level.putIfNotNull("ToBeTicked", toBeTicked); + level.putIfNotNull("PostProcessing", postProcessing); + level.putString("Status", status); + level.putIfNotNull("Structures", structures); + + ListTag sections = new ListTag<>(CompoundTag.class); + for (T section : this) { + if (section != null) { + sections.add(section.updateHandle()); // contract of iterator assures correctness of "height" aka section-y + } + } + level.put("Sections", sections); + return data; + } + + @Override + public CompoundTag updateHandle(int xPos, int zPos) { + if (raw) { + return data; + } + updateHandle(); + CompoundTag level = data.getCompoundTag("Level"); + level.putInt("xPos", xPos); + level.putInt("zPos", zPos); + return data; + } +} diff --git a/src/main/java/net/querz/mca/RegionMCAFileBase.java b/src/main/java/net/querz/mca/RegionMCAFileBase.java new file mode 100644 index 00000000..faf4c701 --- /dev/null +++ b/src/main/java/net/querz/mca/RegionMCAFileBase.java @@ -0,0 +1,134 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +/** + * Represents a REGION data mca file. + */ +public class RegionMCAFileBase extends MCAFileBase implements Iterable { + /** + * The default chunk data version used when no custom version is supplied. + *

Deprecated: use {@code DataVersion.latest().id()} instead. + */ + @Deprecated + public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); + + /** + * {@inheritDoc} + */ + public RegionMCAFileBase(int regionX, int regionZ) { + super(regionX, regionZ); + } + + /** + * {@inheritDoc} + */ + public RegionMCAFileBase(int regionX, int regionZ, int defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + /** + * {@inheritDoc} + */ + public RegionMCAFileBase(int regionX, int regionZ, DataVersion defaultDataVersion) { + super(regionX, regionZ, defaultDataVersion); + } + + /** + * {@inheritDoc} + */ + @Override + public Class chunkClass() { + return Chunk.class; + } + + /** + * {@inheritDoc} + */ + @Override + public Chunk createChunk() { + return Chunk.newChunk(defaultDataVersion); + } + + /** + * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead + */ + @Deprecated + public void setBiomeAt(int blockX, int blockZ, int biomeID) { + createChunkIfMissing(blockX, blockZ).setBiomeAt(blockX, blockZ, biomeID); + } + + public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { + createChunkIfMissing(blockX, blockZ).setBiomeAt(blockX, blockY, blockZ, biomeID); + } + + /** + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead + */ + @Deprecated + public int getBiomeAt(int blockX, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(getChunkIndex(chunkX, chunkZ)); + if (chunk == null) { + return -1; + } + return chunk.getBiomeAt(blockX, blockZ); + } + + /** + * Fetches the biome id at a specific block. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The biome id if the chunk exists and the chunk has biomes, otherwise -1. + */ + public int getBiomeAt(int blockX, int blockY, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(getChunkIndex(chunkX, chunkZ)); + if (chunk == null) { + return -1; + } + return chunk.getBiomeAt(blockX,blockY, blockZ); + } + + /** + * Set a block state at a specific block location. + * The block coordinates can be absolute coordinates or they can be relative to the region. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @param state The block state to be set. + * @param cleanup Whether the Palette and the BLockStates should be recalculated after adding the block state. + */ + public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + createChunkIfMissing(blockX, blockZ).setBlockStateAt(blockX, blockY, blockZ, state, cleanup); + } + + /** + * Fetches a block state at a specific block location. + * The block coordinates can be absolute coordinates or they can be relative to the region. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The block state or null if the chunk or the section do not exist. + */ + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(chunkX, chunkZ); + if (chunk == null) { + return null; + } + return chunk.getBlockStateAt(blockX, blockY, blockZ); + } + + /** + * Recalculates the Palette and the BlockStates of all chunks and sections of this region. + */ + public void cleanupPalettesAndBlockStates() { + for (Chunk chunk : chunks) { + if (chunk != null) { + chunk.cleanupPalettesAndBlockStates(); + } + } + } +} diff --git a/src/main/java/net/querz/mca/RegionSectionBase.java b/src/main/java/net/querz/mca/RegionSectionBase.java new file mode 100644 index 00000000..e6e20008 --- /dev/null +++ b/src/main/java/net/querz/mca/RegionSectionBase.java @@ -0,0 +1,549 @@ +package net.querz.mca; + +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static net.querz.mca.DataVersion.JAVA_1_16_20W17A; +import static net.querz.mca.LoadFlags.*; + +public abstract class RegionSectionBase extends SectionBase { + + protected Map> valueIndexedPalette = new HashMap<>(); + protected ListTag palette; + protected byte[] blockLight; + protected long[] blockStates; + protected byte[] skyLight; + + public static byte[] createBlockLightBuffer() { + return new byte[2048]; + } + + public static long[] createBlockStates() { + return new long[256]; + } + + public static byte[] createSkyLightBuffer() { + return new byte[2048]; + } + + public RegionSectionBase(CompoundTag sectionRoot, int dataVersion) { + this(sectionRoot, dataVersion, ALL_DATA); + } + + public RegionSectionBase(CompoundTag sectionRoot, int dataVersion, long loadFlags) { + super(sectionRoot, dataVersion); + if (dataVersion <= 0) { + throw new IllegalArgumentException("Invalid data version - must be GT 0"); + } + height = sectionRoot.getNumber("Y").byteValue(); + + ListTag rawPalette = sectionRoot.getListTag("Palette"); + if (rawPalette == null) { + return; + } + palette = rawPalette.asCompoundTagList(); + for (int i = 0; i < palette.size(); i++) { + CompoundTag data = palette.get(i); + putValueIndexedPalette(data, i); + } + + if ((loadFlags & BLOCK_LIGHTS) != 0) { + ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); + if (blockLight != null) this.blockLight = blockLight.getValue(); + } + if ((loadFlags & BLOCK_STATES) != 0) { + LongArrayTag blockStates = sectionRoot.getLongArrayTag("BlockStates"); + if (blockStates != null) this.blockStates = blockStates.getValue(); + } + if ((loadFlags & SKY_LIGHT) != 0) { + ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); + if (skyLight != null) this.skyLight = skyLight.getValue(); + } + } + + public RegionSectionBase(int dataVersion) { + super(dataVersion); + blockLight = createBlockLightBuffer(); + blockStates = createBlockStates(); + skyLight = createSkyLightBuffer(); + palette = new ListTag<>(CompoundTag.class); + CompoundTag air = new CompoundTag(); + air.putString("Name", "minecraft:air"); + palette.add(air); + } + + private void assureBlockStates() { + if (blockStates == null) blockStates = createBlockStates(); + } + + private void assurePalette() { + if (palette == null) palette = new ListTag<>(CompoundTag.class); + } + + void putValueIndexedPalette(CompoundTag data, int index) { + PaletteIndex leaf = new PaletteIndex(data, index); + String name = data.getString("Name"); + List leaves = valueIndexedPalette.get(name); + if (leaves == null) { + leaves = new ArrayList<>(1); + leaves.add(leaf); + valueIndexedPalette.put(name, leaves); + } else { + for (PaletteIndex pal : leaves) { + if (pal.data.equals(data)) { + return; + } + } + leaves.add(leaf); + } + } + + PaletteIndex getValueIndexedPalette(CompoundTag data) { + List leaves = valueIndexedPalette.get(data.getString("Name")); + if (leaves == null) { + return null; + } + for (PaletteIndex leaf : leaves) { + if (leaf.data.equals(data)) { + return leaf; + } + } + return null; + } + + private static class PaletteIndex { + + CompoundTag data; + int index; + + PaletteIndex(CompoundTag data, int index) { + this.data = data; + this.index = index; + } + } + + /** + * Fetches a block state based on a block location from this section. + * The coordinates represent the location of the block inside of this Section. + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @return The block state data of this block. + */ + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + return getBlockStateAt(getBlockIndex(blockX, blockY, blockZ)); + } + + private CompoundTag getBlockStateAt(int index) { + int paletteIndex = getPaletteIndex(index); + return palette.get(paletteIndex); + } + + /** + * Attempts to add a block state for a specific block location in this Section. + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @param state The block state to be set + * @param cleanup When true, it will force a cleanup the palette of this section. + * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Section to file. + * @return True if {@link RegionSectionBase#cleanupPaletteAndBlockStates()} was run as a result of this call. + * Note that it is possible that {@link RegionSectionBase#cleanupPaletteAndBlockStates()} needed to be called even if + * the {@code cleanup} argument was {@code false}. In summary if the last call made to this function returns + * {@code true} you can skip the call to {@link RegionSectionBase#cleanupPaletteAndBlockStates()}. + */ + public boolean setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + assurePalette(); + int paletteSizeBefore = palette.size(); + int paletteIndex = addToPalette(state); + //power of 2 --> bits must increase, but only if the palette size changed + //otherwise we would attempt to update all blockstates and the entire palette + //every time an existing blockstate was added while having 2^x blockstates in the palette + if (paletteSizeBefore != palette.size() && (paletteIndex & (paletteIndex - 1)) == 0) { + assureBlockStates(); + adjustBlockStateBits(null, blockStates); + cleanup = true; + } + + setPaletteIndex(getBlockIndex(blockX, blockY, blockZ), paletteIndex, blockStates); + + if (cleanup) { + cleanupPaletteAndBlockStates(); + return true; + } + return false; + } + + /** + * Returns the index of the block data in the palette. + * @param blockStateIndex The index of the block in this section, ranging from 0-4095. + * @return The index of the block data in the palette. + */ + public int getPaletteIndex(int blockStateIndex) { + assureBlockStates(); + int bits = blockStates.length >> 6; + + if (dataVersion > 0 && dataVersion < JAVA_1_16_20W17A.id()) { + double blockStatesIndex = blockStateIndex / (4096D / blockStates.length); + int longIndex = (int) blockStatesIndex; + int startBit = (int) ((blockStatesIndex - Math.floor(blockStatesIndex)) * 64D); + if (startBit + bits > 64) { + long prev = bitRange(blockStates[longIndex], startBit, 64); + long next = bitRange(blockStates[longIndex + 1], 0, startBit + bits - 64); + return (int) ((next << 64 - startBit) + prev); + } else { + return (int) bitRange(blockStates[longIndex], startBit, startBit + bits); + } + } else { + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockStateIndex / indicesPerLong; + int startBit = (blockStateIndex % indicesPerLong) * bits; + return (int) bitRange(blockStates[blockStatesIndex], startBit, startBit + bits); + } + } + + /** + * Sets the index of the block data in the BlockStates. Does not adjust the size of the BlockStates array. + * @param blockIndex The index of the block in this section, ranging from 0-4095. + * @param paletteIndex The block state to be set (index of block data in the palette). + * @param blockStates The block states to be updated. + */ + public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) { + Objects.requireNonNull(blockStates, "blockStates must not be null"); + int bits = blockStates.length >> 6; + + if (dataVersion < JAVA_1_16_20W17A.id()) { + double blockStatesIndex = blockIndex / (4096D / blockStates.length); + int longIndex = (int) blockStatesIndex; + int startBit = (int) ((blockStatesIndex - Math.floor(longIndex)) * 64D); + if (startBit + bits > 64) { + blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, 64); + blockStates[longIndex + 1] = updateBits(blockStates[longIndex + 1], paletteIndex, startBit - 64, startBit + bits - 64); + } else { + blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, startBit + bits); + } + } else { + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockIndex / indicesPerLong; + int startBit = (blockIndex % indicesPerLong) * bits; + blockStates[blockStatesIndex] = updateBits(blockStates[blockStatesIndex], paletteIndex, startBit, startBit + bits); + } + } + + private void upgradeFromBefore20W17A(final int targetVersion) { + int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); + newBits = Math.max(newBits, 4); + long[] newBlockStates; + + int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; + + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, getPaletteIndex(i), newBlockStates); + } + this.blockStates = newBlockStates; + } + + @Override + protected void syncDataVersion(int newDataVersion) { + if (dataVersion < JAVA_1_16_20W17A.id() && newDataVersion >= JAVA_1_16_20W17A.id()) { + upgradeFromBefore20W17A(newDataVersion); + } else if (dataVersion >= JAVA_1_16_20W17A.id() && newDataVersion < JAVA_1_16_20W17A.id()) { + throw new IllegalArgumentException( + String.format("cannot downgrade data version from %d to %d because it crosses version %s", + dataVersion, newDataVersion, JAVA_1_16_20W17A)); + } + super.syncDataVersion(newDataVersion); + } + + /** + * Fetches the palette of this Section. + * @return The palette of this Section. + */ + public ListTag getPalette() { + return palette; + } + + int addToPalette(CompoundTag data) { + PaletteIndex index; + if ((index = getValueIndexedPalette(data)) != null) { + return index.index; + } + palette.add(data); + putValueIndexedPalette(data, palette.size() - 1); + return palette.size() - 1; + } + + int getBlockIndex(int blockX, int blockY, int blockZ) { + return (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF); + } + + static long updateBits(long n, long m, int i, int j) { + //replace i to j in n with j - i bits of m + long mShifted = i > 0 ? (m & ((1L << j - i) - 1)) << i : (m & ((1L << j - i) - 1)) >>> -i; + return ((n & ((j > 63 ? 0 : (~0L << j)) | (i < 0 ? 0 : ((1L << i) - 1L)))) | mShifted); + } + + static long bitRange(long value, int from, int to) { + int waste = 64 - to; + return (value << waste) >>> (waste + from); + } + + /** + * This method recalculates the palette and its indices. + * This should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Section to file. + */ + public void cleanupPaletteAndBlockStates() { + if (blockStates != null && palette != null) { + Map oldToNewMapping = cleanupPalette(); + adjustBlockStateBits(oldToNewMapping, blockStates); + } + } + + private Map cleanupPalette() { + //create index - palette mapping + Map allIndices = new HashMap<>(); + for (int i = 0; i < 4096; i++) { + int paletteIndex = getPaletteIndex(i); + allIndices.put(paletteIndex, paletteIndex); + } + //delete unused blocks from palette + //start at index 1 because we need to keep minecraft:air + int index = 1; + valueIndexedPalette = new HashMap<>(valueIndexedPalette.size()); + putValueIndexedPalette(palette.get(0), 0); + for (int i = 1; i < palette.size(); i++) { + if (!allIndices.containsKey(index)) { + palette.remove(i); + i--; + } else { + putValueIndexedPalette(palette.get(i), i); + allIndices.put(index, i); + } + index++; + } + + return allIndices; + } + + void adjustBlockStateBits(Map oldToNewMapping, long[] blockStates) { + //increases or decreases the amount of bits used per BlockState + //based on the size of the palette. oldToNewMapping can be used to update indices + //if the palette had been cleaned up before using MCAFile#cleanupPalette(). + + int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); + newBits = Math.max(newBits, 4); + + long[] newBlockStates; + + if (dataVersion < JAVA_1_16_20W17A.id()) { + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; + } else { + int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; + } + if (oldToNewMapping != null) { + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, oldToNewMapping.get(getPaletteIndex(i)), newBlockStates); + } + } else { + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, getPaletteIndex(i), newBlockStates); + } + } + this.blockStates = newBlockStates; + } + + /** + * @return The block light array of this Section + */ + public byte[] getBlockLight() { + return blockLight; + } + + /** + * Sets the block light array for this section. + * @param blockLight The block light array + * @throws IllegalArgumentException When the length of the array is not 2048 + */ + public void setBlockLight(byte[] blockLight) { + if (blockLight != null && blockLight.length != 2048) { + throw new IllegalArgumentException("BlockLight array must have a length of 2048"); + } + this.blockLight = blockLight; + } + + /** + * @return The indices of the block states of this Section. + */ + public long[] getBlockStates() { + return blockStates; + } + + /** + * Sets the block state indices to a custom value. + * @param blockStates The block state indices. + * @throws NullPointerException If blockStates is null + * @throws IllegalArgumentException When blockStates' length is < 256 or > 4096 and is not a multiple of 64 + */ + public void setBlockStates(long[] blockStates) { + if (blockStates == null) { + throw new NullPointerException("BlockStates cannot be null"); + } else if (blockStates.length % 64 != 0 || blockStates.length < 256 || blockStates.length > 4096) { + throw new IllegalArgumentException("BlockStates must have a length > 255 and < 4097 and must be divisible by 64"); + } + this.blockStates = blockStates; + } + + /** + * @return The sky light values of this Section + */ + public byte[] getSkyLight() { + return skyLight; + } + + /** + * Sets the sky light values of this section. + * @param skyLight The custom sky light values + * @throws IllegalArgumentException If the length of the array is not 2048 + */ + public void setSkyLight(byte[] skyLight) { + if (skyLight != null && skyLight.length != 2048) { + throw new IllegalArgumentException("SkyLight array must have a length of 2048"); + } + this.skyLight = skyLight; + } + + /** + * Updates the raw CompoundTag that this Section is based on. + * @return A reference to the raw CompoundTag this Section is based on + */ + @Override + public CompoundTag updateHandle() { + checkY(height); + data.putByte("Y", (byte) height); + if (palette != null) { + data.put("Palette", palette); + } + if (blockLight != null) { + data.putByteArray("BlockLight", blockLight); + } + if (blockStates != null) { + data.putLongArray("BlockStates", blockStates); + } + if (skyLight != null) { + data.putByteArray("SkyLight", skyLight); + } + return data; + } + + /** + * Creates an iterable that iterates over all blocks in this section, in order of their indices. + * XYZ can be calculated with the following formulas: + *

+	 * {@code
+	 * x = index & 0xF;
+	 * z = (index >> 4) & 0xF;
+	 * y = index >> 8;
+	 * }
+	 * 
+ * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to + * modify ALL blocks of the same type in this Section at the same time. + */ + public BlockStateIterator blocksStates() { + return new BlockStateIteratorImpl(this); + } + + protected static class BlockStateIteratorImpl implements BlockStateIterator { + + private final RegionSectionBase section; + private final int sectionWorldY; + private int currentIndex; + private CompoundTag currentTag; + private boolean dirty; + + public BlockStateIteratorImpl(RegionSectionBase section) { + this.section = section; + this.sectionWorldY = section.getHeight() * 16; + currentIndex = -1; + } + + @Override + public boolean hasNext() { + return currentIndex < 4095; + } + + @Override + public CompoundTag next() { + return currentTag = section.getBlockStateAt(++currentIndex); + } + + @Override + public Iterator iterator() { + return this; + } + + @Override + public void setBlockStateAtCurrent(CompoundTag state) { + Objects.requireNonNull(state); + if (currentTag != state) { + dirty = !section.setBlockStateAt(currentX(), currentY(), currentZ(), state, false); + } + } + + @Override + public void cleanupPaletteAndBlockStatesIfDirty() { + if (dirty) section.cleanupPaletteAndBlockStates(); + } + + @Override + public int currentIndex() { + return currentIndex; + } + + @Override + public int currentX() { + return currentIndex & 0xF; + } + + @Override + public int currentZ() { + return (currentIndex >> 4) & 0xF; + } + + @Override + public int currentY() { + return currentIndex >> 8; + } + + @Override + public int currentBlockY() { + return sectionWorldY + (currentIndex >> 8); + } + } + + /** + * Streams all blocks in this section, in order of their indices. + * XYZ can be calculated with the following formulas: + *
+	 * {@code
+	 * x = index & 0xF;
+	 * z = (index >> 4) & 0xF;
+	 * y = index >> 8;
+	 * }
+	 * 
+ * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to + * modify ALL blocks of the same type in this Section at the same time. + */ + public Stream streamBlocksStates() { + return StreamSupport.stream(blocksStates().spliterator(), false); + } +} diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java index 79d2300d..b68c96f6 100644 --- a/src/main/java/net/querz/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -1,566 +1,64 @@ package net.querz.mca; -import static net.querz.mca.DataVersion.JAVA_1_16_20W17A; -import static net.querz.mca.LoadFlags.*; -import net.querz.nbt.tag.ByteArrayTag; import net.querz.nbt.tag.CompoundTag; -import net.querz.nbt.tag.ListTag; -import net.querz.nbt.tag.LongArrayTag; - -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** - * Represents a REGION data chunk section. Sections can be thought of as "sub-chunks" - * which are 16x16x16 block cubes stacked atop each other to create a "chunk". + * Represents a REGION data chunk section. */ -public class Section extends SectionBase
{ - - private int dataVersion; // for internal use only - must be kept in sync with chunk data version - protected Map> valueIndexedPalette = new HashMap<>(); - protected ListTag palette; - protected byte[] blockLight; - protected long[] blockStates; - protected byte[] skyLight; - - public static byte[] createBlockLightBuffer() { - return new byte[2048]; - } - - public static long[] createBlockStates() { - return new long[256]; - } - - public static byte[] createSkyLightBuffer() { - return new byte[2048]; - } +public class Section extends RegionSectionBase { public Section(CompoundTag sectionRoot, int dataVersion) { - this(sectionRoot, dataVersion, ALL_DATA); + super(sectionRoot, dataVersion); } public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { - super(sectionRoot); - if (dataVersion <= 0) { - throw new IllegalArgumentException("Invalid data version - must be GT 0"); - } - this.dataVersion = dataVersion; - height = sectionRoot.getNumber("Y").byteValue(); - - ListTag rawPalette = sectionRoot.getListTag("Palette"); - if (rawPalette == null) { - return; - } - palette = rawPalette.asCompoundTagList(); - for (int i = 0; i < palette.size(); i++) { - CompoundTag data = palette.get(i); - putValueIndexedPalette(data, i); - } - - if ((loadFlags & BLOCK_LIGHTS) != 0) { - ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); - if (blockLight != null) this.blockLight = blockLight.getValue(); - } - if ((loadFlags & BLOCK_STATES) != 0) { - LongArrayTag blockStates = sectionRoot.getLongArrayTag("BlockStates"); - if (blockStates != null) this.blockStates = blockStates.getValue(); - } - if ((loadFlags & SKY_LIGHT) != 0) { - ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); - if (skyLight != null) this.skyLight = skyLight.getValue(); - } + super(sectionRoot, dataVersion, loadFlags); } - Section(int dataVersion) { - this.dataVersion = dataVersion; - blockLight = createBlockLightBuffer(); - blockStates = createBlockStates(); - skyLight = createSkyLightBuffer(); - palette = new ListTag<>(CompoundTag.class); - CompoundTag air = new CompoundTag(); - air.putString("Name", "minecraft:air"); - palette.add(air); - } - - private void assureBlockStates() { - if (blockStates == null) blockStates = createBlockStates(); - } - - private void assurePalette() { - if (palette == null) palette = new ListTag<>(CompoundTag.class); - } - - void putValueIndexedPalette(CompoundTag data, int index) { - PaletteIndex leaf = new PaletteIndex(data, index); - String name = data.getString("Name"); - List leaves = valueIndexedPalette.get(name); - if (leaves == null) { - leaves = new ArrayList<>(1); - leaves.add(leaf); - valueIndexedPalette.put(name, leaves); - } else { - for (PaletteIndex pal : leaves) { - if (pal.data.equals(data)) { - return; - } - } - leaves.add(leaf); - } - } - - PaletteIndex getValueIndexedPalette(CompoundTag data) { - List leaves = valueIndexedPalette.get(data.getString("Name")); - if (leaves == null) { - return null; - } - for (PaletteIndex leaf : leaves) { - if (leaf.data.equals(data)) { - return leaf; - } - } - return null; - } - - private static class PaletteIndex { - - CompoundTag data; - int index; - - PaletteIndex(CompoundTag data, int index) { - this.data = data; - this.index = index; - } + public Section(int dataVersion) { + super(dataVersion); } /** - * Fetches a block state based on a block location from this section. - * The coordinates represent the location of the block inside of this Section. - * @param blockX The x-coordinate of the block in this Section - * @param blockY The y-coordinate of the block in this Section - * @param blockZ The z-coordinate of the block in this Section - * @return The block state data of this block. + * @return An empty Section initialized using the latest full release data version. + * @deprecated Dangerous - prefer using {@link Chunk#createSection(int)} or using the + * {@link #Section(int)} constructor instead. */ - public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - return getBlockStateAt(getBlockIndex(blockX, blockY, blockZ)); - } - - private CompoundTag getBlockStateAt(int index) { - int paletteIndex = getPaletteIndex(index); - return palette.get(paletteIndex); - } - - /** - * Attempts to add a block state for a specific block location in this Section. - * @param blockX The x-coordinate of the block in this Section - * @param blockY The y-coordinate of the block in this Section - * @param blockZ The z-coordinate of the block in this Section - * @param state The block state to be set - * @param cleanup When true, it will force a cleanup the palette of this section. - * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. - * Recalculating the Palette should only be executed once right before saving the Section to file. - * @return True if {@link Section#cleanupPaletteAndBlockStates()} was run as a result of this call. - * Note that it is possible that {@link Section#cleanupPaletteAndBlockStates()} needed to be called even if - * the {@code cleanup} argument was {@code false}. In summary if the last call made to this function returns - * {@code true} you can skip the call to {@link Section#cleanupPaletteAndBlockStates()}. - */ - public boolean setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { - assurePalette(); - int paletteSizeBefore = palette.size(); - int paletteIndex = addToPalette(state); - //power of 2 --> bits must increase, but only if the palette size changed - //otherwise we would attempt to update all blockstates and the entire palette - //every time an existing blockstate was added while having 2^x blockstates in the palette - if (paletteSizeBefore != palette.size() && (paletteIndex & (paletteIndex - 1)) == 0) { - assureBlockStates(); - adjustBlockStateBits(null, blockStates); - cleanup = true; - } - - setPaletteIndex(getBlockIndex(blockX, blockY, blockZ), paletteIndex, blockStates); - - if (cleanup) { - cleanupPaletteAndBlockStates(); - return true; - } - return false; - } - - /** - * Returns the index of the block data in the palette. - * @param blockStateIndex The index of the block in this section, ranging from 0-4095. - * @return The index of the block data in the palette. - */ - public int getPaletteIndex(int blockStateIndex) { - assureBlockStates(); - int bits = blockStates.length >> 6; - - if (dataVersion > 0 && dataVersion < JAVA_1_16_20W17A.id()) { - double blockStatesIndex = blockStateIndex / (4096D / blockStates.length); - int longIndex = (int) blockStatesIndex; - int startBit = (int) ((blockStatesIndex - Math.floor(blockStatesIndex)) * 64D); - if (startBit + bits > 64) { - long prev = bitRange(blockStates[longIndex], startBit, 64); - long next = bitRange(blockStates[longIndex + 1], 0, startBit + bits - 64); - return (int) ((next << 64 - startBit) + prev); - } else { - return (int) bitRange(blockStates[longIndex], startBit, startBit + bits); - } - } else { - int indicesPerLong = (int) (64D / bits); - int blockStatesIndex = blockStateIndex / indicesPerLong; - int startBit = (blockStateIndex % indicesPerLong) * bits; - return (int) bitRange(blockStates[blockStatesIndex], startBit, startBit + bits); - } - } - - /** - * Sets the index of the block data in the BlockStates. Does not adjust the size of the BlockStates array. - * @param blockIndex The index of the block in this section, ranging from 0-4095. - * @param paletteIndex The block state to be set (index of block data in the palette). - * @param blockStates The block states to be updated. - */ - public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) { - Objects.requireNonNull(blockStates, "blockStates must not be null"); - int bits = blockStates.length >> 6; - - if (dataVersion < JAVA_1_16_20W17A.id()) { - double blockStatesIndex = blockIndex / (4096D / blockStates.length); - int longIndex = (int) blockStatesIndex; - int startBit = (int) ((blockStatesIndex - Math.floor(longIndex)) * 64D); - if (startBit + bits > 64) { - blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, 64); - blockStates[longIndex + 1] = updateBits(blockStates[longIndex + 1], paletteIndex, startBit - 64, startBit + bits - 64); - } else { - blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, startBit + bits); - } - } else { - int indicesPerLong = (int) (64D / bits); - int blockStatesIndex = blockIndex / indicesPerLong; - int startBit = (blockIndex % indicesPerLong) * bits; - blockStates[blockStatesIndex] = updateBits(blockStates[blockStatesIndex], paletteIndex, startBit, startBit + bits); - } - } - - private void upgradeFromBefore20W17A(final int targetVersion) { - int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); - newBits = Math.max(newBits, 4); - long[] newBlockStates; - - int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); - newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; - - for (int i = 0; i < 4096; i++) { - setPaletteIndex(i, getPaletteIndex(i), newBlockStates); - } - this.blockStates = newBlockStates; - this.dataVersion = targetVersion; - } - - protected void setDataVersion(int newDataVersion) { - if (newDataVersion <= 0) { - throw new IllegalArgumentException("Invalid data version - must be GT 0"); - } - if (dataVersion < JAVA_1_16_20W17A.id() && newDataVersion >= JAVA_1_16_20W17A.id()) { - upgradeFromBefore20W17A(newDataVersion); - } - dataVersion = newDataVersion; - } - - /** - * Fetches the palette of this Section. - * @return The palette of this Section. - */ - public ListTag getPalette() { - return palette; - } - - int addToPalette(CompoundTag data) { - PaletteIndex index; - if ((index = getValueIndexedPalette(data)) != null) { - return index.index; - } - palette.add(data); - putValueIndexedPalette(data, palette.size() - 1); - return palette.size() - 1; - } - - int getBlockIndex(int blockX, int blockY, int blockZ) { - return (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF); - } - - static long updateBits(long n, long m, int i, int j) { - //replace i to j in n with j - i bits of m - long mShifted = i > 0 ? (m & ((1L << j - i) - 1)) << i : (m & ((1L << j - i) - 1)) >>> -i; - return ((n & ((j > 63 ? 0 : (~0L << j)) | (i < 0 ? 0 : ((1L << i) - 1L)))) | mShifted); - } - - static long bitRange(long value, int from, int to) { - int waste = 64 - to; - return (value << waste) >>> (waste + from); - } - - /** - * This method recalculates the palette and its indices. - * This should only be used moderately to avoid unnecessary recalculation of the palette indices. - * Recalculating the Palette should only be executed once right before saving the Section to file. - */ - public void cleanupPaletteAndBlockStates() { - if (blockStates != null && palette != null) { - Map oldToNewMapping = cleanupPalette(); - adjustBlockStateBits(oldToNewMapping, blockStates); - } - } - - private Map cleanupPalette() { - //create index - palette mapping - Map allIndices = new HashMap<>(); - for (int i = 0; i < 4096; i++) { - int paletteIndex = getPaletteIndex(i); - allIndices.put(paletteIndex, paletteIndex); - } - //delete unused blocks from palette - //start at index 1 because we need to keep minecraft:air - int index = 1; - valueIndexedPalette = new HashMap<>(valueIndexedPalette.size()); - putValueIndexedPalette(palette.get(0), 0); - for (int i = 1; i < palette.size(); i++) { - if (!allIndices.containsKey(index)) { - palette.remove(i); - i--; - } else { - putValueIndexedPalette(palette.get(i), i); - allIndices.put(index, i); - } - index++; - } - - return allIndices; - } - - void adjustBlockStateBits(Map oldToNewMapping, long[] blockStates) { - //increases or decreases the amount of bits used per BlockState - //based on the size of the palette. oldToNewMapping can be used to update indices - //if the palette had been cleaned up before using MCAFile#cleanupPalette(). - - int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); - newBits = Math.max(newBits, 4); - - long[] newBlockStates; - - if (dataVersion < JAVA_1_16_20W17A.id()) { - newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; - } else { - int newLength = (int) Math.ceil(4096D / (Math.floor(64D / newBits))); - newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; - } - if (oldToNewMapping != null) { - for (int i = 0; i < 4096; i++) { - setPaletteIndex(i, oldToNewMapping.get(getPaletteIndex(i)), newBlockStates); - } - } else { - for (int i = 0; i < 4096; i++) { - setPaletteIndex(i, getPaletteIndex(i), newBlockStates); - } - } - this.blockStates = newBlockStates; - } - - /** - * @return The block light array of this Section - */ - public byte[] getBlockLight() { - return blockLight; - } - - /** - * Sets the block light array for this section. - * @param blockLight The block light array - * @throws IllegalArgumentException When the length of the array is not 2048 - */ - public void setBlockLight(byte[] blockLight) { - if (blockLight != null && blockLight.length != 2048) { - throw new IllegalArgumentException("BlockLight array must have a length of 2048"); - } - this.blockLight = blockLight; - } - - /** - * @return The indices of the block states of this Section. - */ - public long[] getBlockStates() { - return blockStates; - } - - /** - * Sets the block state indices to a custom value. - * @param blockStates The block state indices. - * @throws NullPointerException If blockStates is null - * @throws IllegalArgumentException When blockStates' length is < 256 or > 4096 and is not a multiple of 64 - */ - public void setBlockStates(long[] blockStates) { - if (blockStates == null) { - throw new NullPointerException("BlockStates cannot be null"); - } else if (blockStates.length % 64 != 0 || blockStates.length < 256 || blockStates.length > 4096) { - throw new IllegalArgumentException("BlockStates must have a length > 255 and < 4097 and must be divisible by 64"); - } - this.blockStates = blockStates; - } - - /** - * @return The sky light values of this Section - */ - public byte[] getSkyLight() { - return skyLight; - } - - /** - * Sets the sky light values of this section. - * @param skyLight The custom sky light values - * @throws IllegalArgumentException If the length of the array is not 2048 - */ - public void setSkyLight(byte[] skyLight) { - if (skyLight != null && skyLight.length != 2048) { - throw new IllegalArgumentException("SkyLight array must have a length of 2048"); - } - this.skyLight = skyLight; + @Deprecated + public static Section newSection() { + return new Section(DataVersion.latest().id()); } /** - * Creates an empty Section with base values. - * @return An empty Section - * @deprecated Dangerous - prefer using {@link Chunk#createSection()} instead. + * This method should only be used for building sections prior to adding to a chunk where you want to use this + * section height property for the convenience of not having to track the value separately. + * + * @deprecated To set section height (aka section-y) use + * {@code chunk.putSection(int, SectionBase, boolean)} instead of this function. Setting the section height + * by calling this function WILL NOT have any affect upon the sections height in the Chunk or or MCA data when + * serialized. */ @Deprecated - public static Section newSection() { - return new Section(DataVersion.latest().id()); + public void setHeight(int height) { + syncHeight(height); } /** * Updates the raw CompoundTag that this Section is based on. - * This must be called before saving a Section to disk if the Section was manually created - * to set the Y of this Section. - * @param y The Y-value of this Section + * + * @param y The Y-value of this Section to include in the returned tag. + * DOES NOT update this sections height value permanently. * @return A reference to the raw CompoundTag this Section is based on + * @deprecated The holding chunk is the authority on this sections y / height and takes care of all updates to it. */ - @Override + @Deprecated public CompoundTag updateHandle(int y) { - checkY(y); - data.putByte("Y", (byte) y); - if (palette != null) { - data.put("Palette", palette); - } - if (blockLight != null) { - data.putByteArray("BlockLight", blockLight); - } - if (blockStates != null) { - data.putLongArray("BlockStates", blockStates); - } - if (skyLight != null) { - data.putByteArray("SkyLight", skyLight); - } - return data; - } - - /** - * Creates an iterable that iterates over all blocks in this section, in order of their indices. - * XYZ can be calculated with the following formulas: - *
-	 * {@code
-	 * x = index & 0xF;
-	 * z = (index >> 4) & 0xF;
-	 * y = index >> 8;
-	 * }
-	 * 
- * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to - * modify ALL blocks of the same type in this Section at the same time. - */ - public BlockStateIterator blocksStates() { - return new BlockStateIteratorImpl(this); - } - - protected static class BlockStateIteratorImpl implements BlockStateIterator { - - private final Section section; - private final int sectionWorldY; - private int currentIndex; - private CompoundTag currentTag; - private boolean dirty; - - public BlockStateIteratorImpl(Section section) { - this.section = section; - this.sectionWorldY = section.getHeight() * 16; - currentIndex = -1; - } - - @Override - public boolean hasNext() { - return currentIndex < 4095; - } - - @Override - public CompoundTag next() { - return currentTag = section.getBlockStateAt(++currentIndex); + final int oldY = height; + try { + height = y; + return updateHandle(); + } finally { + height = oldY; } - - @Override - public Iterator iterator() { - return this; - } - - @Override - public void setBlockStateAtCurrent(CompoundTag state) { - Objects.requireNonNull(state); - if (currentTag != state) { - dirty = !section.setBlockStateAt(currentX(), currentY(), currentZ(), state, false); - } - } - - @Override - public void cleanupPaletteAndBlockStatesIfDirty() { - if (dirty) section.cleanupPaletteAndBlockStates(); - } - - @Override - public int currentIndex() { - return currentIndex; - } - - @Override - public int currentX() { - return currentIndex & 0xF; - } - - @Override - public int currentZ() { - return (currentIndex >> 4) & 0xF; - } - - @Override - public int currentY() { - return currentIndex >> 8; - } - - @Override - public int currentBlockY() { - return sectionWorldY + (currentIndex >> 8); - } - } - - /** - * Streams all blocks in this section, in order of their indices. - * XYZ can be calculated with the following formulas: - *
-	 * {@code
-	 * x = index & 0xF;
-	 * z = (index >> 4) & 0xF;
-	 * y = index >> 8;
-	 * }
-	 * 
- * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to - * modify ALL blocks of the same type in this Section at the same time. - */ - public Stream streamBlocksStates() { - return StreamSupport.stream(blocksStates().spliterator(), false); } } diff --git a/src/main/java/net/querz/mca/SectionBase.java b/src/main/java/net/querz/mca/SectionBase.java index 9eb93b93..fff13bc7 100644 --- a/src/main/java/net/querz/mca/SectionBase.java +++ b/src/main/java/net/querz/mca/SectionBase.java @@ -3,18 +3,34 @@ import net.querz.nbt.tag.CompoundTag; import java.util.*; -public abstract class SectionBase> implements Comparable { +/** + * Sections can be thought of as "sub-chunks" which are 16x16x16 block cubes + * stacked atop each other to create a "chunk". + */ +public abstract class SectionBase> implements Comparable, TagWrapper { public static final int NO_HEIGHT_SENTINEL = Integer.MIN_VALUE; + /** for internal use only - must be kept in sync with chunk data version */ + protected int dataVersion; protected final CompoundTag data; protected int height = NO_HEIGHT_SENTINEL; - public SectionBase(CompoundTag sectionRoot) { + protected SectionBase(CompoundTag sectionRoot, int dataVersion) { Objects.requireNonNull(sectionRoot, "sectionRoot must not be null"); this.data = sectionRoot; + this.dataVersion = dataVersion; } - public SectionBase() { + protected SectionBase(int dataVersion) { data = new CompoundTag(); + this.dataVersion = dataVersion; + } + + /** section data version must be kept in sync with chunk data version */ + protected void syncDataVersion(int newDataVersion) { + if (newDataVersion <= 0) { + throw new IllegalArgumentException("Invalid data version - must be GT 0"); + } + this.dataVersion = newDataVersion; } @Override @@ -38,34 +54,18 @@ public boolean isEmpty() { * 16 blocks. * This library (as a whole) will attempt to keep the value returned by this function in sync with the actual * location it has been placed within its chunk. - *

The value returned may be unreliable if this section is placed in multiple chunks at different heights. - * or if user code calls {@link #syncHeight(int)} on a section which is referenced by any chunk.

- * + *

The value returned may be unreliable if this section is placed in multiple chunks at different heights + * or if this section is an instance of {@link Section} and user code calls {@link Section#setHeight(int)} + * on a section which is referenced by any chunk.

+ *

Prefer using {@code chunk.getSectionY(section)} which will always be accurate within the context of the + * chunk.

* @return The Y value of this section. - * @deprecated Prefer using {@code chunk.getSectionY(section)} which will always be accurate - * within the context of the chunk. */ public int getHeight() { return height; } - /** - * This method should only be called from a container of Sections such as implementers of - * {@link SectionedChunkBase} in an effort to keep the value accurate, or when building sections prior to adding - * to a chunk where you want to use this section height property for the convenience of not having to track the - * value separately. - * - * @deprecated To set section height (aka section-y) use - * {@code chunk.putSection(int, SectionBase, boolean)} instead of this function. Setting the section height - * by calling this function WILL NOT have any affect upon the sections height in the Chunk or or MCA data when - * serialized. - */ - @Deprecated - public void setHeight(int height) { - syncHeight(height); - } - - void syncHeight(int height) { + protected void syncHeight(int height) { this.height = height; } @@ -73,19 +73,15 @@ protected void checkY(int y) { if (y == NO_HEIGHT_SENTINEL) { throw new IndexOutOfBoundsException("section height not set"); } + if (y < Byte.MIN_VALUE | y > Byte.MAX_VALUE) { + throw new IndexOutOfBoundsException("section height (aka section-y) must be in range of BYTE [-128..127] was: " + y); + } } /** - * Updates the raw CompoundTag that this Section is based on. - * This must be called before saving a Section to disk if the Section was manually created - * to set the Y of this Section. - * @param y The Y-value of this Section to include in the returned tag. - * DOES NOT update this sections height value permanently. - * @return A reference to the raw CompoundTag this Section is based on + * {@inheritDoc} */ - public abstract CompoundTag updateHandle(int y); - - public CompoundTag updateHandle() { - return updateHandle(height); + public CompoundTag getHandle() { + return data; } } diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index 3c91f4fd..841cfbf1 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -164,7 +164,9 @@ public int getMaxSectionY() { } /*** - * Creates a new section and places it in this chunk at the specified section-y + * Creates a new section and places it in this chunk at the specified section-y UNLESS + * the given sectionY is {@link SectionBase#NO_HEIGHT_SENTINEL} in which case the new + * section is not added to this chunk. * @param sectionY section y * @return new section * @throws IllegalArgumentException thrown if the specified y already has a section - basically throwns if From ede849745d2dd43df22e1f8091860832dbd3a9ac Mon Sep 17 00:00:00 2001 From: BuildTools Date: Wed, 10 Nov 2021 11:02:51 -0500 Subject: [PATCH 032/288] Adding boilerplate code to ENTITIES mca classes --- src/main/java/net/querz/mca/EntitiesChunk.java | 5 +++++ src/main/java/net/querz/mca/EntitiesMCAFile.java | 11 ++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/querz/mca/EntitiesChunk.java b/src/main/java/net/querz/mca/EntitiesChunk.java index 5d3d020e..1bd39880 100644 --- a/src/main/java/net/querz/mca/EntitiesChunk.java +++ b/src/main/java/net/querz/mca/EntitiesChunk.java @@ -15,4 +15,9 @@ public EntitiesChunk(CompoundTag data) { protected void initReferences(long loadFlags) { } + + @Override + public CompoundTag updateHandle() { + return data; + } } diff --git a/src/main/java/net/querz/mca/EntitiesMCAFile.java b/src/main/java/net/querz/mca/EntitiesMCAFile.java index 8701dcf4..341edb9c 100644 --- a/src/main/java/net/querz/mca/EntitiesMCAFile.java +++ b/src/main/java/net/querz/mca/EntitiesMCAFile.java @@ -18,16 +18,13 @@ public EntitiesMCAFile(int regionX, int regionZ, DataVersion defaultDataVersion) @Override public Class chunkClass() { - return null; + return EntitiesChunk.class; } @Override public EntitiesChunk createChunk() { - return null; - } - - @Override - protected EntitiesChunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { - return null; + EntitiesChunk chunk = new EntitiesChunk(0); + chunk.setDataVersion(getDefaultChunkDataVersion()); + return chunk; } } From c57dc30b0ad7443deb735cdf1af8b3e4cf379811 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Thu, 11 Nov 2021 21:43:49 -0500 Subject: [PATCH 033/288] deprecating Chunk(int lastMCAUpdate) constructors, default ctor behavior is now to use the current timestamp Adding MCAFileBase#removeChunk Mostly finalized POI support implementation Starting POI support unit tests and started setting up abstract test pattern for ChunkBase --- src/main/java/net/querz/mca/Chunk.java | 9 +- src/main/java/net/querz/mca/ChunkBase.java | 37 ++- .../java/net/querz/mca/EntitiesChunk.java | 4 +- .../java/net/querz/mca/EntitiesMCAFile.java | 2 +- src/main/java/net/querz/mca/MCAFileBase.java | 22 ++ src/main/java/net/querz/mca/PoiChunk.java | 15 +- src/main/java/net/querz/mca/PoiChunkBase.java | 216 +++++++++++++ src/main/java/net/querz/mca/PoiMCAFile.java | 21 +- src/main/java/net/querz/mca/PoiRecord.java | 290 ++++++++++++++++++ .../java/net/querz/mca/RegionChunkBase.java | 8 +- .../net/querz/mca/SectionedChunkBase.java | 4 +- .../java/net/querz/mca/ChunkBaseTest.java | 88 ++++++ .../java/net/querz/mca/PoiChunkBaseTest.java | 11 + src/test/java/net/querz/mca/PoiChunkTest.java | 33 ++ .../java/net/querz/mca/PoiRecordTest.java | 75 +++++ 15 files changed, 791 insertions(+), 44 deletions(-) create mode 100644 src/main/java/net/querz/mca/PoiChunkBase.java create mode 100644 src/main/java/net/querz/mca/PoiRecord.java create mode 100644 src/test/java/net/querz/mca/ChunkBaseTest.java create mode 100644 src/test/java/net/querz/mca/PoiChunkBaseTest.java create mode 100644 src/test/java/net/querz/mca/PoiChunkTest.java create mode 100644 src/test/java/net/querz/mca/PoiRecordTest.java diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 4f735566..8805e986 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -4,7 +4,9 @@ /** * Represents a REGION data mca chunk. Region chunks are composed of a set of {@link Section} where any empty/null - * section is filled with air blocks by the game. + * section is filled with air blocks by the game. When altering existing chunks for MC 1.14+, be sure to have read and + * understood the documentation on {@link PoiRecord} to avoid problems with villagers, nether portal linking, + * lodestones, bees, and probably more as Minecraft continues to evolve. */ public class Chunk extends RegionChunkBase
{ /** @@ -14,8 +16,9 @@ public class Chunk extends RegionChunkBase
{ @Deprecated public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id(); - Chunk(int lastMCAUpdate) { - super(lastMCAUpdate); + @Deprecated + protected Chunk(int lastMCAUpdate) { + setLastMCAUpdate(lastMCAUpdate); } public Chunk(CompoundTag data) { diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java index 18a12741..9694f98a 100644 --- a/src/main/java/net/querz/mca/ChunkBase.java +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -21,8 +21,9 @@ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { protected int lastMCAUpdate; protected CompoundTag data; - ChunkBase(int lastMCAUpdate) { - this.lastMCAUpdate = lastMCAUpdate; + protected ChunkBase() { + dataVersion = DataVersion.latest().id(); + lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); } /** @@ -30,8 +31,17 @@ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { * @param data The raw base data to be used. */ public ChunkBase(CompoundTag data) { + this(data, ALL_DATA); + } + + /** + * Create a new chunk based on raw base data from a region file. + * @param data The raw base data to be used. + * @param loadFlags Union of {@link LoadFlags} to process. + */ + public ChunkBase(CompoundTag data, long loadFlags) { this.data = data; - initReferences0(ALL_DATA); + initReferences0(loadFlags); } private void initReferences0(long loadFlags) { @@ -40,19 +50,18 @@ private void initReferences0(long loadFlags) { if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { raw = true; - return; - } - - if (dataVersion == 0) { - throw new IllegalArgumentException("data does not contain \"DataVersion\" tag"); - } + } else { + if (dataVersion == 0) { + throw new IllegalArgumentException("data does not contain \"DataVersion\" tag"); + } - initReferences(loadFlags); + initReferences(loadFlags); - // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. - if (loadFlags != ALL_DATA) { - data = null; - partial = true; + // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. + if (loadFlags != ALL_DATA) { + data = null; + partial = true; + } } } diff --git a/src/main/java/net/querz/mca/EntitiesChunk.java b/src/main/java/net/querz/mca/EntitiesChunk.java index 1bd39880..f26a359b 100644 --- a/src/main/java/net/querz/mca/EntitiesChunk.java +++ b/src/main/java/net/querz/mca/EntitiesChunk.java @@ -3,9 +3,7 @@ import net.querz.nbt.tag.CompoundTag; public class EntitiesChunk extends ChunkBase { - EntitiesChunk(int lastMCAUpdate) { - super(lastMCAUpdate); - } + protected EntitiesChunk() {} public EntitiesChunk(CompoundTag data) { super(data); diff --git a/src/main/java/net/querz/mca/EntitiesMCAFile.java b/src/main/java/net/querz/mca/EntitiesMCAFile.java index 341edb9c..5e5ef9ee 100644 --- a/src/main/java/net/querz/mca/EntitiesMCAFile.java +++ b/src/main/java/net/querz/mca/EntitiesMCAFile.java @@ -23,7 +23,7 @@ public Class chunkClass() { @Override public EntitiesChunk createChunk() { - EntitiesChunk chunk = new EntitiesChunk(0); + EntitiesChunk chunk = new EntitiesChunk(); chunk.setDataVersion(getDefaultChunkDataVersion()); return chunk; } diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 834406f6..2226f0af 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -346,6 +346,28 @@ public T getChunk(int chunkX, int chunkZ) { return getChunk(getChunkIndex(chunkX, chunkZ)); } + /** + * Removes the chunk at the given index (sets it to null) and returns the previous value. + * @param index chunk index [0..1024) + * @return chunk which was removed, or null if there was none. + */ + public T removeChunk(int index) { + T chunk = chunks[index]; + chunks[index] = null; + return chunk; + } + + /** + * Removes the chunk at the given xz (sets it to null) and returns the previous value. + * Works with absolute and relative coordinates. + * @param chunkX chunk x + * @param chunkZ chunk z + * @return chunk which was removed, or null if there was none. + */ + public T removeChunk(int chunkX, int chunkZ) { + return removeChunk(getChunkIndex(chunkX, chunkZ)); + } + /** * Calculates the index of a chunk from its x- and z-coordinates in this region. * This works with absolute and relative coordinates. diff --git a/src/main/java/net/querz/mca/PoiChunk.java b/src/main/java/net/querz/mca/PoiChunk.java index def52f5f..9850f593 100644 --- a/src/main/java/net/querz/mca/PoiChunk.java +++ b/src/main/java/net/querz/mca/PoiChunk.java @@ -2,17 +2,20 @@ import net.querz.nbt.tag.CompoundTag; -public class PoiChunk extends ChunkBase { - PoiChunk(int lastMCAUpdate) { - super(lastMCAUpdate); - } +public class PoiChunk extends PoiChunkBase{ + + protected PoiChunk() { } public PoiChunk(CompoundTag data) { super(data); } - @Override - protected void initReferences(long loadFlags) { + public PoiChunk(CompoundTag data, long loadData) { + super(data, loadData); + } + @Override + protected PoiRecord createPoiRecord(CompoundTag recordTag) { + return new PoiRecord(recordTag); } } diff --git a/src/main/java/net/querz/mca/PoiChunkBase.java b/src/main/java/net/querz/mca/PoiChunkBase.java new file mode 100644 index 00000000..e9c1f11a --- /dev/null +++ b/src/main/java/net/querz/mca/PoiChunkBase.java @@ -0,0 +1,216 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.Tag; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static net.querz.mca.LoadFlags.*; + +public abstract class PoiChunkBase extends ChunkBase implements Iterable { + protected List records = new ArrayList<>(); + protected Map poiSectionValidity = new HashMap<>(); + + protected PoiChunkBase() { } + + public PoiChunkBase(CompoundTag data) { + super(data); + } + + public PoiChunkBase(CompoundTag data, long loadData) { + super(data, loadData); + } + + @Override + protected void initReferences(long loadFlags) { + if ((loadFlags & POI_RECORDS) != 0) { + CompoundTag sectionsTag = data.getCompoundTag("Sections"); + if (sectionsTag == null) { + throw new IllegalArgumentException("Sections tag not found!"); + } + for (Map.Entry> sectionTag : sectionsTag.entrySet()) { + int sectionY = Integer.parseInt(sectionTag.getKey()); + boolean valid = ((CompoundTag) sectionTag.getValue()).getBoolean("Valid"); + poiSectionValidity.put(sectionY, valid); + ListTag recordTags = ((CompoundTag) sectionTag.getValue()).getListTagAutoCast("Records"); + for (CompoundTag recordTag : recordTags) { + T record = createPoiRecord(recordTag); + records.add(record); + } + } + } + } + + /** + * Called from {@link #initReferences(long)}. Exists to provide a hook for custom implementations to override to + * add support for modded poi's, etc. without having to implement {@link #initReferences(long)} logic fully. + */ + protected abstract T createPoiRecord(CompoundTag recordTag); + + public void add(T record) { + if (record == null || record.getType() == null || record.getType().isEmpty()) { + throw new IllegalArgumentException( + "record must not be null and must have a type specified"); + } + records.add(record); + } + + /** + * Removes the given record from ths poi chunk both by reference and by equality. + * @param record record to remove + * @return true if any record was removed + */ + public boolean remove(T record) { + if (record == null || record.getType() == null || record.getType().isEmpty()) { + return false; + } + return records.removeIf(r -> r == record || r.equals(record)); + } + + /** + * Removes the FIRST PoiRecord at the given xyz. + * @param x world block x + * @param y world block y + * @param z world block z + * @return Removed PoiRecord or null if no such record + */ + public T removeFirst(final int x, final int y, final int z) { + Iterator iter = records.iterator(); + while (iter.hasNext()) { + T record = iter.next(); + if (record.matches(x, y, z)) { + iter.remove(); + return record; + } + } + return null; + } + + /** + * Removes all records at the given xyz. + * @param x world block x + * @param y world block y + * @param z world block z + * @return True if any records were removed + */ + public boolean removeAll(final int x, final int y, final int z) { + return records.removeIf(r -> r.matches(x, y, z)); + } + + /** + * Removes all PoiRecords with the given type. + * @param poiType poi type to remove + * @return true if any records were removed + */ + public boolean removeAll(final String poiType) { + return records.removeIf(r -> r.matches(poiType)); + } + + /** + * Gets the first poi record found with the exact xyz given + * @param x world block x + * @param y world block y + * @param z world block z + * @return poi record if found, otherwise null + */ + public T getFirst(final int x, final int y, final int z) { + return records.stream().filter(r -> r.matches(x, y, z)).findFirst().orElse(null); + } + + /** + * Gets all poi record found with the exact xyz given. Really there should be only one - but nothing is stopping you + * from messing it up. + * @param x world block x + * @param y world block y + * @param z world block z + * @return list of poi records at the given xyz + */ + public List getAll(final int x, final int y, final int z) { + return records.stream().filter(r -> r.matches(x, y, z)).collect(Collectors.toList()); + } + + /** + * Gets all poi records of the given type + * @param poiType poi type + * @return list of poi records matching the given poi type + */ + public List getAll(final String poiType) { + return records.stream().filter(r -> r.matches(poiType)).collect(Collectors.toList()); + } + + /** + * Marks the given subchunk invalid so that Minecraft will recompute POI for it when loaded. + * @param sectionY subchunk section-y to invalidate + */ + public void invalidateSection(int sectionY) { + if (sectionY < Byte.MIN_VALUE || sectionY > Byte.MAX_VALUE) + throw new IllegalArgumentException("sectionY must be in range [-128..127]"); + poiSectionValidity.put(sectionY, false); + } + + /** + * Checks if the given section has been marked invalid either by calling {@link #invalidateSection(int)} or if + * it was already invalidated in the poi mca file. + */ + public boolean isPoiSectionValid(int sectionY) { + return poiSectionValidity.getOrDefault(sectionY, true); + } + + /** + * Checks if the given poi record resides in a section that has been marked invalid either by calling + * {@link #invalidateSection(int)} or was already invalidated in the poi mca file. + */ + public boolean isPoiSectionValid(PoiRecord record) { + return record == null || poiSectionValidity.getOrDefault(record.getSectionY(), true); + } + + /** + * {@inheritDoc} + */ + @Override + public CompoundTag updateHandle() { + if (raw) return data; + super.updateHandle(); + Map> sectionedLists = records.stream().collect(Collectors.groupingBy(PoiRecord::getSectionY)); + // ensure that all invalidated sections are in sectionedLists so we can just do one processing pass + for (int sectionY : poiSectionValidity.keySet()) { + if (!sectionedLists.containsKey(sectionY)) { + sectionedLists.put(sectionY, Collections.emptyList()); + } + } + CompoundTag sectionsTag = new CompoundTag(sectionedLists.size()); + data.put("Sections", sectionsTag); + for (Map.Entry> sectionList : sectionedLists.entrySet()) { + CompoundTag sectionTag = new CompoundTag(); + sectionsTag.put(Integer.toString(sectionList.getKey()), sectionsTag); + ListTag recordsTag = new ListTag<>(CompoundTag.class, sectionList.getValue().size()); + sectionTag.put("Records", recordsTag); + for (PoiRecord record : sectionList.getValue()) { + recordsTag.add(record.updateHandle()); + } + sectionTag.putBoolean("Valid", poiSectionValidity.getOrDefault(sectionList.getKey(), true)); + } + return data; + } + + @Override + public Iterator iterator() { + return records.iterator(); + } + + /** + * Provides an iterator over poi records with the given type. This is a convenience function and does not provide + * any real optimization v.s. iterating over all elements. + * @param poiType poi type + * @return Never null, but may be empty + */ + public Iterator iterator(final String poiType) { + return records.stream().filter(r -> r.matches(poiType)).iterator(); + } + + public Stream stream() { + return records.stream(); + } +} diff --git a/src/main/java/net/querz/mca/PoiMCAFile.java b/src/main/java/net/querz/mca/PoiMCAFile.java index 037b82ee..7aa3c856 100644 --- a/src/main/java/net/querz/mca/PoiMCAFile.java +++ b/src/main/java/net/querz/mca/PoiMCAFile.java @@ -1,8 +1,12 @@ package net.querz.mca; -import java.io.IOException; -import java.io.RandomAccessFile; - +/** + * POI files are best thought of as an INDEX the game uses to be able to quickly locate certain blocks. + * However, the names of the indexed locations is not necessarily a block type but often a description of its usage + * and one poi type may map to multiple block types (e.g. poi of 'minecraft:home' maps to any of the bed blocks). + * + *

See {@link PoiRecord} for more information and for a list of POI types and how they map to blocks.

+ */ public class PoiMCAFile extends MCAFileBase { public PoiMCAFile(int regionX, int regionZ) { super(regionX, regionZ); @@ -18,16 +22,13 @@ public PoiMCAFile(int regionX, int regionZ, DataVersion defaultDataVersion) { @Override public Class chunkClass() { - return null; + return PoiChunk.class; } @Override public PoiChunk createChunk() { - return null; - } - - @Override - protected PoiChunk deserializeChunk(RandomAccessFile raf, long loadFlags, int timestamp) throws IOException { - return null; + PoiChunk chunk = new PoiChunk(); + chunk.setDataVersion(getDefaultChunkDataVersion()); + return chunk; } } diff --git a/src/main/java/net/querz/mca/PoiRecord.java b/src/main/java/net/querz/mca/PoiRecord.java new file mode 100644 index 00000000..d0f1f39b --- /dev/null +++ b/src/main/java/net/querz/mca/PoiRecord.java @@ -0,0 +1,290 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +import java.util.Objects; + +/** + *

+ * In summary, if you have changed the block at a POI location, or altered the blocks in a {@link Chunk} + * in such a way that may have added or removed POI blocks you have a few options (MC 1.14+) + *

    + *
  1. calculate an accurate new poi state yourself by removing and adding poi records on the {@link PoiChunk}, + * and to be truly accurate you must also modify villager "brains", but they will figure things out when + * they try to interact with their poi's and find them of the wrong type.
  2. + *
  3. invalidate the poi sub-chunk within which you have made alterations with {@link PoiChunk#invalidateSection(int)}
  4. + *
  5. remove the poi chunk from the poi file with {@link PoiMCAFile#removeChunk(int)} or {@link PoiMCAFile#removeChunk(int, int)}
  6. + *
  7. delete the entire poi mca file
  8. + *
+ * All of the options, other than calculating poi state yourself, will trigger Minecraft to re-calculate poi records + * without causing errant behavior. The worst thing you can do is to do nothing - Minecraft will eventually notice + * but it may cause "strange behavior" and various WTF's until the game sorts itself out. + *

About this class
+ * A record as found in POI MCA files (points of interest). Hashable and equatable, but does not consider + * {@code freeTickets} in those operations as that field is largely MC internal state. POI mca files were added in + * MC 1.14 to improve villager performance and only contained locations of blocks villagers interacted with. Over time + * POI mca has evolved to include locations of other block types to optimize game performance - such as improving + * nether portal lag by storing portal block locations in the poi files so the game doesn't need to scan every block + * in every chunk until it finds a destination portal. + *

At time of writing, 1.17.1, this class exposes all poi record fields. For now, there is no support for + * reading or storing extra fields which this class does not wrap.

+ *

POI types As of 1.17 + *

    + *
  • minecraft:unemployed - does not map to a block type
  • + *
  • minecraft:armorer - block: blast_furnace
  • + *
  • minecraft:butcher - block: smoker
  • + *
  • minecraft:cartographer - block: cartography_table
  • + *
  • minecraft:cleric - block: brewing_stand
  • + *
  • minecraft:farmer - block: composter
  • + *
  • minecraft:fisherman - block: barrel
  • + *
  • minecraft:fletcher - block: fletching_table
  • + *
  • minecraft:leatherworker - block: any cauldron block
  • + *
  • minecraft:librarian - block: lectern
  • + *
  • minecraft:mason - block: stonecutter
  • + *
  • minecraft:nitwit - does not map to a block type
  • + *
  • minecraft:shepherd - block: loom
  • + *
  • minecraft:toolsmith - block: smithing_table
  • + *
  • minecraft:weaponsmith - block: grindstone
  • + *
  • minecraft:home - block: any bed
  • + *
  • minecraft:meeting - block: bell
  • + *
  • minecraft:beehive - block: beehive
  • + *
  • minecraft:bee_nest - block: bee_nest
  • + *
  • minecraft:nether_portal - block: nether_portal
  • + *
  • minecraft:lodestone - block: lodestone
  • + *
  • minecraft:lightning_rod - block: lightning_rod
  • + *
+ *

+ *
+ * What are "Tickets"? + *

+ * Tickets are only used for blocks/poi's (points of interest) which villagers interact with. Internally + * Minecraft specifies a max tickets for each such poi type. This is the maximum number of villagers which + * can "take a ticket" (aka be using that poi at the same time; aka max number of villagers which + * can claim that poi and store it in their "brain"). For all villager eligible poi's that limit + * is one (1), with the single exception being minecraft:meeting (block minecraft:bell) which has a + * limit of 32. + *

+ * Poi entries which are not for villager interaction such as beehives, nether portals, + * lighting rods, etc., have a max ticket count of zero (0). + *

+ * A truly valid POI Record is one that satisfies all of the following conditions + *

    + *
  • the block at the poi location is appropriate for the poi type
  • + *
  • free tickets is never GT max tickets for that poi type
  • + *
  • {@link #getFreeTickets()} equals the count of all villagers who have stored the poi location in their + * "brain" subtracted from the max tickets for that poi type
  • + *
+ *

+ */ +public class PoiRecord implements TagWrapper, Comparable { + protected String type; + protected int freeTickets; + protected int x; + protected int y; + protected int z; + + public PoiRecord() { } + + public PoiRecord(CompoundTag data) { + this.freeTickets = data.getInt("free_tickets"); + this.type = data.getString("type"); + int[] pos = data.getIntArray("pos"); + this.x = pos[0]; + this.y = pos[1]; + this.z = pos[2]; + } + + /** + * Defaults free tickets to result of passing the given type to {@link #maxFreeTickets(String)} + * @param x world block x + * @param y world block y - must be a within the absolute maximum limit of blocks + * theoretically supportable by chunk sections [-2048..2032) + * @param z world block z + * @param type required, poi type name + */ + public PoiRecord(int x, int y, int z, String type) { + this(x, y, z, type, maxFreeTickets(type)); + } + + /** + * @param x world block x + * @param y world block y - must be a within the absolute maximum limit of blocks + * theoretically supportable by chunk sections [-2048..2048) + * @param z world block z + * @param type required, poi type name + * @param freeTickets must be GT 0 + */ + public PoiRecord(int x, int y, int z, String type, int freeTickets) { + this.type = validateType(type); + this.freeTickets = validateFreeTickets(freeTickets); + this.y = validateY(y); + this.x = x; + this.z = z; + } + + private String validateType(String type) { + if (type == null || type.isEmpty()) { + throw new IllegalArgumentException("poi type must not be null"); + } + return type; + } + + private int validateFreeTickets(int freeTickets) { + if (freeTickets < 0) { + throw new IllegalArgumentException("freeTickets must be GE 0"); + } + return freeTickets; + } + + private int validateY(int y) { + if (y < Byte.MIN_VALUE * 16 || y > Byte.MAX_VALUE * 16 + 15) { + throw new IndexOutOfBoundsException(String.format( + "Given Y value %d is out of range for any legal block. Y must be in range [%d..%d]", + y, Byte.MIN_VALUE * 16, Byte.MAX_VALUE * 16 + 15)); + } + return y; + } + + /** + * Returns a {@link CompoundTag} representing this record. + * The tag returned is newly created and not a reference to a tag held by any other object. This is a different + * behavior than most other {@code getHandle()} implementations. + */ + @Override + public CompoundTag updateHandle() { + CompoundTag data = new CompoundTag(); + data.putInt("free_tickets", freeTickets); + data.putString("type", type); + data.putIntArray("pos", new int[] {x, y, z}); + return data; + } + + /** + * Returns a {@link CompoundTag} representing this record. + * The tag returned is newly created and not a reference to a tag held by any other object. This is a different + * behavior than most other {@code getHandle()} implementations. + * @return data handle, never null + */ + @Override + public CompoundTag getHandle() { + return updateHandle(); + } + + /** + * + */ + public int getFreeTickets() { + return freeTickets; + } + + /** + */ + public void setFreeTickets(int freeTickets) { + this.freeTickets = validateFreeTickets(freeTickets); + } + + /** Type of the point, for example: minecraft:home, minecraft:meeting, minecraft:butcher, minecraft:nether_portal */ + public String getType() { + return type; + } + + /** Type of the point, for example: minecraft:home, minecraft:meeting, minecraft:butcher, minecraft:nether_portal */ + public void setType(String type) { + this.type = validateType(type); + } + + /** world x location */ + public int getX() { + return x; + } + + /** world x location */ + public void setX(int x) { + this.x = x; + } + + /** world y location */ + public int getY() { + return y; + } + + /** + * @param y must be a within the absolute maximum limit of blocks + * theoretically supportable by chunk sections [-2048..2048) + */ + public void setY(int y) { + this.y = validateY(y); + } + + /** world z location */ + public int getZ() { + return z; + } + + /** world z location */ + public void setZ(int z) { + this.z = z; + } + + @Override + public int hashCode() { + return Objects.hash(type, x, y, z); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PoiRecord)) return false; + PoiRecord that = (PoiRecord) other; + return this.y == that.y && this.x == that.x && this.z == that.z && Objects.equals(this.type, that.type); + } + + @Override + public int compareTo(PoiRecord other) { + if (other == null) { + return -1; + } + return Integer.compare(this.y, other.y); + } + + public boolean matches(int x, int y, int z) { + return this.y == y && this.x == x && this.z == z; + } + + public boolean matches(String type) { + return this.type.equals(type); + } + + public int getSectionY() { + return this.y >> 4; + } + + /** + * Gets the default max free tickets for the given poi type. + * @param poiType poi type - NOT block type + * @return default (vanilla) max free tickets for the given type. + */ + public static int maxFreeTickets(String poiType) { + switch (poiType) { + case "minecraft:unemployed": + case "minecraft:armorer": + case "minecraft:butcher": + case "minecraft:cartographer": + case "minecraft:cleric": + case "minecraft:farmer": + case "minecraft:fisherman": + case "minecraft:fletcher": + case "minecraft:leatherworker": + case "minecraft:librarian": + case "minecraft:mason": + case "minecraft:nitwit": + case "minecraft:shepherd": + case "minecraft:toolsmith": + case "minecraft:weaponsmith": + case "minecraft:home": + return 1; + case "minecraft:meeting": + return 32; + } + return 0; + } +} diff --git a/src/main/java/net/querz/mca/RegionChunkBase.java b/src/main/java/net/querz/mca/RegionChunkBase.java index 3008688d..c98b72eb 100644 --- a/src/main/java/net/querz/mca/RegionChunkBase.java +++ b/src/main/java/net/querz/mca/RegionChunkBase.java @@ -10,7 +10,9 @@ /** * Represents a REGION data mca chunk. Region chunks are composed of a set of {@link Section} where any empty/null - * section is filled with air blocks by the game. + * section is filled with air blocks by the game. When altering existing chunks for MC 1.14+, be sure to have read and + * understood the documentation on {@link PoiRecord} to avoid problems with villagers, nether portal linking, + * lodestones, bees, and probably more as Minecraft continues to evolve. */ public abstract class RegionChunkBase extends SectionedChunkBase { @@ -30,9 +32,7 @@ public abstract class RegionChunkBase extends Secti protected String status; protected CompoundTag structures; - RegionChunkBase(int lastMCAUpdate) { - super(lastMCAUpdate); - } + protected RegionChunkBase() { } /** * Create a new chunk based on raw base data from a region file. diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index 841cfbf1..f6316c22 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -14,9 +14,7 @@ public abstract class SectionedChunkBase> extends Chunk private final TreeMap sections = new TreeMap<>(); private final Map sectionHeightLookup = new HashMap<>(); - SectionedChunkBase(int lastMCAUpdate) { - super(lastMCAUpdate); - } + protected SectionedChunkBase() { } /** * Create a new chunk based on raw base data from a region file. diff --git a/src/test/java/net/querz/mca/ChunkBaseTest.java b/src/test/java/net/querz/mca/ChunkBaseTest.java new file mode 100644 index 00000000..a694f83f --- /dev/null +++ b/src/test/java/net/querz/mca/ChunkBaseTest.java @@ -0,0 +1,88 @@ +package net.querz.mca; + +import net.querz.NBTTestCase; +import net.querz.nbt.tag.CompoundTag; +import static net.querz.mca.DataVersion.*; + +/** + * All implementors of {@link ChunkBaseTest} should create a test which inherits this one and add + * tests to cover any additional functionality added by that concretion. + */ +public abstract class ChunkBaseTest extends NBTTestCase { + protected abstract T createChunk(); + protected abstract T createChunk(CompoundTag tag); + protected abstract T createChunk(CompoundTag tag, long loadData); + + public void testChunkBase_defaultConstructor() { + T chunk = createChunk(); + int now = (int)(System.currentTimeMillis() / 1000); + assertEquals(DataVersion.latest().id(), chunk.getDataVersion()); + assertEquals(DataVersion.latest(), chunk.getDataVersionEnum()); + assertTrue(Math.abs(now - chunk.getLastMCAUpdate()) <= 1); + } + + public void testLastMcaUpdated() { + T chunk = createChunk(); + chunk.setLastMCAUpdate(1747522); + assertEquals(1747522, chunk.getLastMCAUpdate()); + } + + public void testDataVersion() { + T chunk = createChunk(createTag(JAVA_1_16_0.id())); + assertEquals(JAVA_1_16_0.id(), chunk.getDataVersion()); + assertEquals(JAVA_1_16_0, chunk.getDataVersionEnum()); + chunk.setDataVersion(JAVA_1_16_1.id()); + assertEquals(JAVA_1_16_1.id(), chunk.getDataVersion()); + assertEquals(JAVA_1_16_1, chunk.getDataVersionEnum()); + chunk.setDataVersion(JAVA_1_16_2); + assertEquals(JAVA_1_16_2.id(), chunk.getDataVersion()); + assertEquals(JAVA_1_16_2, chunk.getDataVersionEnum()); + } + + /** + * @param dataVersion set as "DataVersion" in returned tag IFF GT 0 + */ + protected CompoundTag createTag(int dataVersion) { + CompoundTag tag = new CompoundTag(); + if (dataVersion > 0) tag.putInt("DataVersion", dataVersion); + return tag; + } + + protected abstract void validateAllDataConstructor(); + + final public void testConstructor_allData() { + CompoundTag tag = createTag(DataVersion.JAVA_1_17_1.id()); + assertNotNull(tag); + T chunk = createChunk(tag, LoadFlags.ALL_DATA); + assertEquals(DataVersion.JAVA_1_17_1.id(), chunk.getDataVersion()); + assertEquals(DataVersion.JAVA_1_17_1, chunk.getDataVersionEnum()); + assertFalse(chunk.partial); + assertFalse(chunk.raw); + assertSame(tag, chunk.getHandle()); + validateAllDataConstructor(); + } + + final public void testConstructor_raw() { + CompoundTag tag = createTag(DataVersion.JAVA_1_16_5.id()); + assertNotNull(tag); + T chunk = createChunk(tag, LoadFlags.RAW); + assertEquals(DataVersion.JAVA_1_16_5.id(), chunk.getDataVersion()); + assertEquals(DataVersion.JAVA_1_16_5, chunk.getDataVersionEnum()); + assertFalse(chunk.partial); + assertTrue(chunk.raw); + assertSame(tag, chunk.getHandle()); + assertThrowsException(chunk::checkRaw, UnsupportedOperationException.class); + } + + public void testConstructor_allData_throwsIfDataVersionNotFound() { + CompoundTag tag = createTag(-1); + assertNotNull(tag); + assertThrowsException(() -> createChunk(tag, LoadFlags.ALL_DATA), IllegalArgumentException.class); + } + + public void testConstructor_raw_noThrowIfDataVersionNotFound() { + CompoundTag tag = createTag(-1); + assertNotNull(tag); + assertThrowsNoException(() -> createChunk(tag, LoadFlags.RAW)); + } +} diff --git a/src/test/java/net/querz/mca/PoiChunkBaseTest.java b/src/test/java/net/querz/mca/PoiChunkBaseTest.java new file mode 100644 index 00000000..1cfa0d8b --- /dev/null +++ b/src/test/java/net/querz/mca/PoiChunkBaseTest.java @@ -0,0 +1,11 @@ +package net.querz.mca; + + +/** + * All implementors of {@link PoiChunkBase} should create a test which inherits this one and add + * tests to cover any additional functionality added by that concretion. + */ +public abstract class PoiChunkBaseTest> extends ChunkBaseTest { + + +} diff --git a/src/test/java/net/querz/mca/PoiChunkTest.java b/src/test/java/net/querz/mca/PoiChunkTest.java new file mode 100644 index 00000000..4d63669e --- /dev/null +++ b/src/test/java/net/querz/mca/PoiChunkTest.java @@ -0,0 +1,33 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; + +public class PoiChunkTest extends PoiChunkBaseTest { + + @Override + protected PoiChunk createChunk() { + return new PoiChunk(); + } + + @Override + protected PoiChunk createChunk(CompoundTag tag) { + return new PoiChunk(tag); + } + + @Override + protected PoiChunk createChunk(CompoundTag tag, long loadData) { + return new PoiChunk(tag, loadData); + } + + @Override + protected CompoundTag createTag(int dataVersion) { + CompoundTag tag = super.createTag(dataVersion); + tag.put("Sections", new CompoundTag()); + return tag; + } + + @Override + protected void validateAllDataConstructor() { + + } +} diff --git a/src/test/java/net/querz/mca/PoiRecordTest.java b/src/test/java/net/querz/mca/PoiRecordTest.java new file mode 100644 index 00000000..ec83b165 --- /dev/null +++ b/src/test/java/net/querz/mca/PoiRecordTest.java @@ -0,0 +1,75 @@ +package net.querz.mca; + +import junit.framework.TestCase; +import net.querz.nbt.tag.CompoundTag; + +public class PoiRecordTest extends TestCase { + private CompoundTag makeTag(int tickets, String type, int x, int y, int z) { + CompoundTag tag = new CompoundTag(); + tag.putString("type", type); + tag.putIntArray("pos", new int[] {x, y, z}); + tag.putInt("free_tickets", tickets); + return tag; + } + + public void testConstructor_CompoundTag() { + PoiRecord record = new PoiRecord(makeTag(3, "minecraft:test", 7, -42, 1777)); + assertEquals(3, record.getFreeTickets()); + assertEquals("minecraft:test", record.getType()); + assertEquals(7, record.getX()); + assertEquals(-42, record.getY()); + assertEquals(1777, record.getZ()); + } + + public void testUpdateHandle() { + CompoundTag tag = makeTag(0, "minecraft:test", 1_234_679, 315, -8_546_776); + PoiRecord record = new PoiRecord(tag); + assertEquals(tag, record.updateHandle()); + // if impl changes to hold onto tag this test will need to be updated, this is here to catch that + assertNotSame(tag, record.updateHandle()); + } + + public void testGetHandle() { + CompoundTag tag = makeTag(0, "minecraft:test", 1_234_679, 315, -8_546_776); + PoiRecord record = new PoiRecord(tag); + assertEquals(tag, record.getHandle()); + // if impl changes to hold onto tag this test will need to be updated, this is here to catch that + assertNotSame(tag, record.getHandle()); + } + + public void testGetSectionY() { + PoiRecord record = new PoiRecord(); + record.setY(77); + assertEquals(4, record.getSectionY()); + record.setY(-38); + assertEquals(-3, record.getSectionY()); + } + + public void testEquals() { + PoiRecord record1 = new PoiRecord(1, 2, 3, "foo", 5); + PoiRecord record2 = new PoiRecord(1, 2, 3, "foo", 5); + assertEquals(record1, record2); + record1.setX(0); + assertFalse(record1.equals(record2)); + record1.setX(1); + record1.setY(0); + assertFalse(record1.equals(record2)); + record1.setY(2); + record1.setZ(0); + assertFalse(record1.equals(record2)); + record1.setZ(3); + record1.setFreeTickets(0); + assertTrue(record1.equals(record2)); // free tickets should not be considered in equals + record1.setFreeTickets(5); + record1.setType("bar"); + assertFalse(record1.equals(record2)); + record1.setType("foo"); + assertEquals(record1, record2); // make sure there wasn't a fubar in the test chain + } + + public void testHashCode_ignoresFreeTickets() { + PoiRecord record1 = new PoiRecord(1, 2, 3, "foo", 5); + PoiRecord record2 = new PoiRecord(1, 2, 3, "foo", 7); + assertEquals(record1.hashCode(), record2.hashCode()); + } +} From ef74058f65d11c8c49b525c28c2fd84d3996dfc5 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 09:31:59 -0500 Subject: [PATCH 034/288] adding default value getters for CompoundTag --- .../java/net/querz/nbt/tag/CompoundTag.java | 40 +++++++++++++++++++ .../net/querz/nbt/tag/CompoundTagTest.java | 13 ++++++ 2 files changed, 53 insertions(+) diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index 6a172925..57ed798d 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -155,41 +155,81 @@ public boolean getBoolean(String key) { return t instanceof ByteTag && ((ByteTag) t).asByte() > 0; } + public boolean getBoolean(String key, boolean defaultValue) { + Tag t = get(key); + return t instanceof ByteTag ? ((ByteTag) t).asByte() > 0 : defaultValue; + } + public byte getByte(String key) { ByteTag t = getByteTag(key); return t == null ? ByteTag.ZERO_VALUE : t.asByte(); } + public byte getByte(String key, byte defaultValue) { + ByteTag t = getByteTag(key); + return t == null ? defaultValue : t.asByte(); + } + public short getShort(String key) { ShortTag t = getShortTag(key); return t == null ? ShortTag.ZERO_VALUE : t.asShort(); } + public short getShort(String key, short defaultValue) { + ShortTag t = getShortTag(key); + return t == null ? defaultValue: t.asShort(); + } + public int getInt(String key) { IntTag t = getIntTag(key); return t == null ? IntTag.ZERO_VALUE : t.asInt(); } + public int getInt(String key, int defaultValue) { + IntTag t = getIntTag(key); + return t == null ? defaultValue : t.asInt(); + } + public long getLong(String key) { LongTag t = getLongTag(key); return t == null ? LongTag.ZERO_VALUE : t.asLong(); } + public long getLong(String key, long defaultValue) { + LongTag t = getLongTag(key); + return t == null ? defaultValue : t.asLong(); + } + public float getFloat(String key) { FloatTag t = getFloatTag(key); return t == null ? FloatTag.ZERO_VALUE : t.asFloat(); } + public float getFloat(String key, float defaultValue) { + FloatTag t = getFloatTag(key); + return t == null ? defaultValue : t.asFloat(); + } + public double getDouble(String key) { DoubleTag t = getDoubleTag(key); return t == null ? DoubleTag.ZERO_VALUE : t.asDouble(); } + public double getDouble(String key, double defaultValue) { + DoubleTag t = getDoubleTag(key); + return t == null ? defaultValue: t.asDouble(); + } + public String getString(String key) { StringTag t = getStringTag(key); return t == null ? StringTag.ZERO_VALUE : t.getValue(); } + public String getString(String key, String defaultValue) { + StringTag t = getStringTag(key); + return t == null ? defaultValue: t.getValue(); + } + public byte[] getByteArray(String key) { ByteArrayTag t = getByteArrayTag(key); return t == null ? ByteArrayTag.ZERO_VALUE : t.getValue(); diff --git a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java index e08426f4..91c9a0e4 100644 --- a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java +++ b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java @@ -373,4 +373,17 @@ public void testPutIfNotNull() { assertEquals(1, ct.size()); assertEquals("bar", ct.getString("foo")); } + + public void testDefaultValueGetters() { + CompoundTag ct = new CompoundTag(); + assertTrue(ct.getBoolean("name", true)); + assertFalse(ct.getBoolean("name", false)); + assertEquals((byte)-7, ct.getByte("name", (byte) -7)); + assertEquals((short)13456, ct.getShort("name", (short) 13456)); + assertEquals(13456789, ct.getInt("name", 13456789)); + assertEquals(13456789101112L, ct.getLong("name", 13456789101112L)); + assertEquals(1.23456f, ct.getFloat("name", 1.23456f), 0.5e-5f); + assertEquals(1.234567981019, ct.getDouble("name", 1.234567981019), 0.5e-12f); + assertEquals("hello world", ct.getString("name", "hello world")); + } } From d6910dd372c265e8b1657b7bfe757f691668b609 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:08:46 -0500 Subject: [PATCH 035/288] add test helper NBTTestCase#assertThrowsIllegalArgumentException --- src/test/java/net/querz/NBTTestCase.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 9fdcece0..2a64306e 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -113,6 +113,11 @@ protected T invokeGetValue(Tag tag) { return null; } + + protected void assertThrowsIllegalArgumentException(ExceptionRunnable r) { + assertThrowsException(r, IllegalArgumentException.class); + } + protected void assertThrowsException(ExceptionRunnable r, Class e) { assertThrowsException(r, e, false); } From 39620cbd3e09bf23aca874986553b0dcb67bc900 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:12:50 -0500 Subject: [PATCH 036/288] Adding chunk constructor Chunk(CompoundTag data, long loadFlags) Chunks - Adding initMembers pattern to avoid NPE's from within overrides of initReferences --- src/main/java/net/querz/mca/Chunk.java | 5 ++++ src/main/java/net/querz/mca/ChunkBase.java | 30 ++++++++++++++++++- .../java/net/querz/mca/RegionChunkBase.java | 4 +++ .../net/querz/mca/SectionedChunkBase.java | 4 +++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 8805e986..08ef9897 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -25,6 +25,11 @@ public Chunk(CompoundTag data) { super(data); } + public Chunk(CompoundTag data, long loadFlags) { + super(data, loadFlags); + } + + @Override protected Section createSection(CompoundTag section, int dataVersion, long loadFlags) { return new Section(section, dataVersion, loadFlags); diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java index 9694f98a..b3b25723 100644 --- a/src/main/java/net/querz/mca/ChunkBase.java +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -13,6 +13,20 @@ /** * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. + *

+ * Cautionary note to implementors - DO NOT USE INLINE MEMBER INITIALIZATION IN YOUR CLASSES
+ * Define all member initialization in {@link #initMembers()} or be very confused! + *

+ * Due to how Java initializes objects, this base class will call {@link #initReferences(long)} before any inline + * member initialization has occurred. The symptom of using in line member initialization is that you will get + * very confusing {@link NullPointerException}'s from within {@link #initReferences(long)} for members which + * are accessed by your {@link #initReferences(long)} implementation that you have defined inline initializers for + * because those initializers will not run until AFTER {@link #initReferences(long)} returns. + *

+ * It is however "safe" to use inline member initialization for any members which are not accessed from within + * {@link #initReferences(long)} - but unless you really fully understand the warning above and its full + * ramifications just don't do it. + *

*/ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { protected int dataVersion; @@ -21,9 +35,22 @@ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { protected int lastMCAUpdate; protected CompoundTag data; + /** + * Due to how Java initializes objects and how this class hierarchy is setup it is ill advised to use inline member + * initialization because {@link #initReferences0(long)} may be called before members are initialized which WILL + * result in very confusing {@link NullPointerException}'s being thrown from within {@link #initReferences(long)}. + * This is not a problem that can be solved by moving initialization into your constructors, because you must call + * the super constructor as the first line of your child constructor! + *

So, to get around this hurdle, perform all member initialization you would normally inline in your + * class def, within this method instead. Implementers should never need to call this method themselves + * as ChunkBase will always call it, even from the default constructor.

+ */ + protected void initMembers() { }; + protected ChunkBase() { dataVersion = DataVersion.latest().id(); lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); + initMembers(); } /** @@ -41,6 +68,7 @@ public ChunkBase(CompoundTag data) { */ public ChunkBase(CompoundTag data, long loadFlags) { this.data = data; + initMembers(); initReferences0(loadFlags); } @@ -66,7 +94,7 @@ private void initReferences0(long loadFlags) { } /** - * Child classes should not call this method directly. + * Child classes should not call this method directly, it will be called for them. * Raw and partial data handling is taken care of, this method will not be called if {@code loadFlags} is * {@link LoadFlags#RAW}. */ diff --git a/src/main/java/net/querz/mca/RegionChunkBase.java b/src/main/java/net/querz/mca/RegionChunkBase.java index c98b72eb..25bd259f 100644 --- a/src/main/java/net/querz/mca/RegionChunkBase.java +++ b/src/main/java/net/querz/mca/RegionChunkBase.java @@ -42,6 +42,10 @@ public RegionChunkBase(CompoundTag data) { super(data); } + public RegionChunkBase(CompoundTag data, long loadFlags) { + super(data, loadFlags); + } + @Override protected void initReferences(final long loadFlags) { CompoundTag level = data.getCompoundTag("Level"); diff --git a/src/main/java/net/querz/mca/SectionedChunkBase.java b/src/main/java/net/querz/mca/SectionedChunkBase.java index f6316c22..d256c3cf 100644 --- a/src/main/java/net/querz/mca/SectionedChunkBase.java +++ b/src/main/java/net/querz/mca/SectionedChunkBase.java @@ -24,6 +24,10 @@ public SectionedChunkBase(CompoundTag data) { super(data); } + public SectionedChunkBase(CompoundTag data, long loadFlags) { + super(data, loadFlags); + } + public boolean containsSection(int sectionY) { return sections.containsKey(sectionY); } From c80235a00c8412e46022ef8db69f8cbe3b5963d1 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:13:51 -0500 Subject: [PATCH 037/288] Adding Mutable utility class (currently only planned use is in tests) --- src/main/java/net/querz/util/Mutable.java | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/net/querz/util/Mutable.java diff --git a/src/main/java/net/querz/util/Mutable.java b/src/main/java/net/querz/util/Mutable.java new file mode 100644 index 00000000..3424ec8e --- /dev/null +++ b/src/main/java/net/querz/util/Mutable.java @@ -0,0 +1,24 @@ +package net.querz.util; + +/** + * Simple utility for passing mutable objects into/out of lambdas, like {@link java.util.Optional}, but mutable. + * @param + */ +public class Mutable { + + T value; + + public Mutable() { } + + public Mutable(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + } +} From 09ff5e7c7be06bc333638ce7d22579375227c5c5 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:15:20 -0500 Subject: [PATCH 038/288] PoiRecord - adding copy constructor and changing setters to builder pattern --- src/main/java/net/querz/mca/PoiRecord.java | 52 +++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/mca/PoiRecord.java b/src/main/java/net/querz/mca/PoiRecord.java index d0f1f39b..e3556b44 100644 --- a/src/main/java/net/querz/mca/PoiRecord.java +++ b/src/main/java/net/querz/mca/PoiRecord.java @@ -85,6 +85,17 @@ public class PoiRecord implements TagWrapper, Comparable { public PoiRecord() { } + /** + * copy constructor + */ + public PoiRecord(PoiRecord other) { + this.type = other.type; + this.freeTickets = other.freeTickets; + this.x = other.x; + this.y = other.y; + this.z = other.z; + } + public PoiRecord(CompoundTag data) { this.freeTickets = data.getInt("free_tickets"); this.type = data.getString("type"); @@ -171,16 +182,27 @@ public CompoundTag getHandle() { } /** - * + * See class doc {@link PoiRecord} */ public int getFreeTickets() { return freeTickets; } /** + * See class doc {@link PoiRecord} */ - public void setFreeTickets(int freeTickets) { + public PoiRecord setFreeTickets(int freeTickets) { this.freeTickets = validateFreeTickets(freeTickets); + return this; + } + + /** + * Sets freeTickets to the default max free tickets for this poi type. + * see class doc {@link PoiRecord} + */ + public PoiRecord resetFreeTickets() { + this.freeTickets = maxFreeTickets(this.type); + return this; } /** Type of the point, for example: minecraft:home, minecraft:meeting, minecraft:butcher, minecraft:nether_portal */ @@ -189,8 +211,9 @@ public String getType() { } /** Type of the point, for example: minecraft:home, minecraft:meeting, minecraft:butcher, minecraft:nether_portal */ - public void setType(String type) { + public PoiRecord setType(String type) { this.type = validateType(type); + return this; } /** world x location */ @@ -199,8 +222,9 @@ public int getX() { } /** world x location */ - public void setX(int x) { + public PoiRecord setX(int x) { this.x = x; + return this; } /** world y location */ @@ -212,8 +236,9 @@ public int getY() { * @param y must be a within the absolute maximum limit of blocks * theoretically supportable by chunk sections [-2048..2048) */ - public void setY(int y) { + public PoiRecord setY(int y) { this.y = validateY(y); + return this; } /** world z location */ @@ -222,8 +247,23 @@ public int getZ() { } /** world z location */ - public void setZ(int z) { + public PoiRecord setZ(int z) { + this.z = z; + return this; + } + + /** + * Sets XYZ + * @param x world block x + * @param y world block y + * @param z world block z + * @return self + */ + public PoiRecord setYXZ(int x, int y, int z) { + this.x = x; + this.y = y; this.z = z; + return this; } @Override From 9138a67cb40db53c478095db1ce4608d8546d97f Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:16:10 -0500 Subject: [PATCH 039/288] Finalized implementation of PoiChunk and added full test coverage --- src/main/java/net/querz/mca/PoiChunk.java | 1 + src/main/java/net/querz/mca/PoiChunkBase.java | 268 ++++++++--- .../java/net/querz/mca/ChunkBaseTest.java | 57 ++- .../java/net/querz/mca/PoiChunkBaseTest.java | 442 +++++++++++++++++- src/test/java/net/querz/mca/PoiChunkTest.java | 13 +- .../java/net/querz/mca/PoiRecordTest.java | 2 +- 6 files changed, 692 insertions(+), 91 deletions(-) diff --git a/src/main/java/net/querz/mca/PoiChunk.java b/src/main/java/net/querz/mca/PoiChunk.java index 9850f593..87245d9f 100644 --- a/src/main/java/net/querz/mca/PoiChunk.java +++ b/src/main/java/net/querz/mca/PoiChunk.java @@ -18,4 +18,5 @@ public PoiChunk(CompoundTag data, long loadData) { protected PoiRecord createPoiRecord(CompoundTag recordTag) { return new PoiRecord(recordTag); } + } diff --git a/src/main/java/net/querz/mca/PoiChunkBase.java b/src/main/java/net/querz/mca/PoiChunkBase.java index e9c1f11a..0e6fa2c2 100644 --- a/src/main/java/net/querz/mca/PoiChunkBase.java +++ b/src/main/java/net/querz/mca/PoiChunkBase.java @@ -9,9 +9,16 @@ import java.util.stream.Stream; import static net.querz.mca.LoadFlags.*; -public abstract class PoiChunkBase extends ChunkBase implements Iterable { - protected List records = new ArrayList<>(); - protected Map poiSectionValidity = new HashMap<>(); +public abstract class PoiChunkBase extends ChunkBase implements Collection { + // private to preserve the ability to change how records are stored to optimize lookups later + private List records; + protected Map poiSectionValidity; + + @Override + protected void initMembers() { + records = new ArrayList<>(); + poiSectionValidity = new HashMap<>(); + } protected PoiChunkBase() { } @@ -32,12 +39,17 @@ protected void initReferences(long loadFlags) { } for (Map.Entry> sectionTag : sectionsTag.entrySet()) { int sectionY = Integer.parseInt(sectionTag.getKey()); - boolean valid = ((CompoundTag) sectionTag.getValue()).getBoolean("Valid"); + boolean valid = ((CompoundTag) sectionTag.getValue()).getBoolean("Valid", true); poiSectionValidity.put(sectionY, valid); ListTag recordTags = ((CompoundTag) sectionTag.getValue()).getListTagAutoCast("Records"); - for (CompoundTag recordTag : recordTags) { - T record = createPoiRecord(recordTag); - records.add(record); + if (recordTags != null) { + for (CompoundTag recordTag : recordTags) { + T record = createPoiRecord(recordTag); + if (sectionY != record.getSectionY()) { + poiSectionValidity.put(sectionY, false); + } + records.add(record); + } } } } @@ -49,43 +61,73 @@ protected void initReferences(long loadFlags) { */ protected abstract T createPoiRecord(CompoundTag recordTag); - public void add(T record) { - if (record == null || record.getType() == null || record.getType().isEmpty()) { - throw new IllegalArgumentException( - "record must not be null and must have a type specified"); + @Override + public boolean add(T record) { + if (record == null) { + throw new IllegalArgumentException("record must not be null"); } - records.add(record); + return records.add(record); } /** - * Removes the given record from ths poi chunk both by reference and by equality. - * @param record record to remove - * @return true if any record was removed + * Gets the first poi record found with the exact xyz given + * @param x world block x + * @param y world block y + * @param z world block z + * @return poi record if found, otherwise null */ - public boolean remove(T record) { - if (record == null || record.getType() == null || record.getType().isEmpty()) { - return false; - } - return records.removeIf(r -> r == record || r.equals(record)); + public T getFirst(final int x, final int y, final int z) { + return records.stream().filter(r -> r.matches(x, y, z)).findFirst().orElse(null); } /** - * Removes the FIRST PoiRecord at the given xyz. + * Gets a shallow COPY of the set of poi records in this chunk. + * Modifications to the list will have no affect on this chunk, but modifying items in that list will. + *

However, you can {@link #getAll()} modify the returned list, then call {@link #set(Collection)} + * with your modified list to update the records in this chunk.

+ */ + public List getAll() { + // don't return actual records list, retain the freedom to make it something other than a list for + // optimizations later! + return new ArrayList<>(records); + } + + /** + * Gets all poi record found with the exact xyz given. Really there should be only one - but nothing + * is stopping you from messing it up. * @param x world block x * @param y world block y * @param z world block z - * @return Removed PoiRecord or null if no such record + * @return new list of poi records at the given xyz */ - public T removeFirst(final int x, final int y, final int z) { - Iterator iter = records.iterator(); - while (iter.hasNext()) { - T record = iter.next(); - if (record.matches(x, y, z)) { - iter.remove(); - return record; - } - } - return null; + public List getAll(final int x, final int y, final int z) { + return records.stream().filter(r -> r.matches(x, y, z)).collect(Collectors.toList()); + } + + /** + * Gets all poi records of the given type + * @param poiType poi type + * @return new list of poi records matching the given poi type + */ + public List getAll(final String poiType) { + List list = records.stream().filter(r -> r.matches(poiType)).collect(Collectors.toList()); + return list; + } + + /** + * Removes the given record from ths poi chunk both by reference and by equality. + * @param record record to remove + * @return true if any record was removed + */ + @Override + public boolean remove(Object record) { + if (!(record instanceof PoiRecord)) return false; + return records.removeIf(r -> r == record || r.equals(record)); + } + + @Override + public boolean removeAll(Collection c) { + return records.removeAll(c); } /** @@ -105,39 +147,123 @@ public boolean removeAll(final int x, final int y, final int z) { * @return true if any records were removed */ public boolean removeAll(final String poiType) { + if (poiType == null || poiType.isEmpty()) { + return false; + } return records.removeIf(r -> r.matches(poiType)); } /** - * Gets the first poi record found with the exact xyz given + * Removes the FIRST PoiRecord at the given xyz. * @param x world block x * @param y world block y * @param z world block z - * @return poi record if found, otherwise null + * @return Removed PoiRecord or null if no such record */ - public T getFirst(final int x, final int y, final int z) { - return records.stream().filter(r -> r.matches(x, y, z)).findFirst().orElse(null); + public T removeFirst(final int x, final int y, final int z) { + Iterator iter = records.iterator(); + while (iter.hasNext()) { + T record = iter.next(); + if (record.matches(x, y, z)) { + iter.remove(); + return record; + } + } + return null; + } + + @Override + public boolean containsAll(Collection c) { + return records.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + boolean changed = false; + for (T r : c) { + if (r != null) { + records.add(r); + changed = true; + } + } + return changed; + } + + @Override + public boolean retainAll(Collection c) { + return records.retainAll(c); } /** - * Gets all poi record found with the exact xyz given. Really there should be only one - but nothing is stopping you - * from messing it up. - * @param x world block x - * @param y world block y - * @param z world block z - * @return list of poi records at the given xyz + * Removes all poi record data from this chunk. This WILL NOT provide any signal to Minecraft that the + * poi records for this chunk should be recalculated. Calling this function is only the correct action + * if you have removed all poi blocks from the chunk or if you plan to rebuild the poi records. + *

Also resets all poi chunk section validity flags to indicate "is valid = true".

*/ - public List getAll(final int x, final int y, final int z) { - return records.stream().filter(r -> r.matches(x, y, z)).collect(Collectors.toList()); + @Override + public void clear() { + records.clear(); + poiSectionValidity.clear(); + } + + @Override + public int size() { + return records.size(); + } + + @Override + public boolean isEmpty() { + return records.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return records.contains(o); + } + + @Override + public Iterator iterator() { + return records.iterator(); } /** - * Gets all poi records of the given type - * @param poiType poi type - * @return list of poi records matching the given poi type + * Provides an iterator over poi records with the given type. This is a convenience function and does not provide + * any real optimization v.s. iterating over all elements. + * @param poiType poi type, if null or empty an empty iterator is returned + * @return Never null, but may be empty. Does not support {@link Iterator#remove()} */ - public List getAll(final String poiType) { - return records.stream().filter(r -> r.matches(poiType)).collect(Collectors.toList()); + public Iterator iterator(final String poiType) { + if (poiType == null || poiType.isEmpty()) { + return Collections.emptyIterator(); + } + return records.stream().filter(r -> r.matches(poiType)).iterator(); + } + + public Stream stream() { + return records.stream(); + } + + @Override + public Object[] toArray() { + return records.toArray(); + } + + @Override + public T1[] toArray(T1[] a) { + return records.toArray(a); + } + + /** + * Clears the poi records from this chunk by first calling {@link #clear()}, then repopulates them by + * taking a shallow copy from the given collection. If the collection is null the affect of this + * function is the same as {@link #clear()}. + * @param c collection to shallow copy poi records from, any null entries will be ignored. + */ + public void set(Collection c) { + clear(); + if (c != null) { + addAll(c); + } } /** @@ -155,6 +281,8 @@ public void invalidateSection(int sectionY) { * it was already invalidated in the poi mca file. */ public boolean isPoiSectionValid(int sectionY) { + if (sectionY < Byte.MIN_VALUE || sectionY > Byte.MAX_VALUE) + throw new IllegalArgumentException("sectionY must be in range [-128..127]"); return poiSectionValidity.getOrDefault(sectionY, true); } @@ -180,37 +308,23 @@ public CompoundTag updateHandle() { sectionedLists.put(sectionY, Collections.emptyList()); } } - CompoundTag sectionsTag = new CompoundTag(sectionedLists.size()); - data.put("Sections", sectionsTag); - for (Map.Entry> sectionList : sectionedLists.entrySet()) { + + CompoundTag sectionContainerTag = new CompoundTag(sectionedLists.size()); + data.put("Sections", sectionContainerTag); + for (Map.Entry> entry : sectionedLists.entrySet()) { CompoundTag sectionTag = new CompoundTag(); - sectionsTag.put(Integer.toString(sectionList.getKey()), sectionsTag); - ListTag recordsTag = new ListTag<>(CompoundTag.class, sectionList.getValue().size()); - sectionTag.put("Records", recordsTag); - for (PoiRecord record : sectionList.getValue()) { - recordsTag.add(record.updateHandle()); + List sectionRecords = entry.getValue(); + boolean isValid = poiSectionValidity.getOrDefault(entry.getKey(), true); + if (!isValid || !sectionRecords.isEmpty()) { + sectionContainerTag.put(Integer.toString(entry.getKey()), sectionTag); + ListTag recordsTag = new ListTag<>(CompoundTag.class, sectionRecords.size()); + sectionTag.putBoolean("Valid", isValid); + sectionTag.put("Records", recordsTag); + for (PoiRecord record : sectionRecords) { + recordsTag.add(record.updateHandle()); + } } - sectionTag.putBoolean("Valid", poiSectionValidity.getOrDefault(sectionList.getKey(), true)); } return data; } - - @Override - public Iterator iterator() { - return records.iterator(); - } - - /** - * Provides an iterator over poi records with the given type. This is a convenience function and does not provide - * any real optimization v.s. iterating over all elements. - * @param poiType poi type - * @return Never null, but may be empty - */ - public Iterator iterator(final String poiType) { - return records.stream().filter(r -> r.matches(poiType)).iterator(); - } - - public Stream stream() { - return records.stream(); - } } diff --git a/src/test/java/net/querz/mca/ChunkBaseTest.java b/src/test/java/net/querz/mca/ChunkBaseTest.java index a694f83f..ef6a7424 100644 --- a/src/test/java/net/querz/mca/ChunkBaseTest.java +++ b/src/test/java/net/querz/mca/ChunkBaseTest.java @@ -2,6 +2,10 @@ import net.querz.NBTTestCase; import net.querz.nbt.tag.CompoundTag; +import net.querz.util.Mutable; + +import java.util.function.Consumer; + import static net.querz.mca.DataVersion.*; /** @@ -13,6 +17,19 @@ public abstract class ChunkBaseTest extends NBTTestCase { protected abstract T createChunk(CompoundTag tag); protected abstract T createChunk(CompoundTag tag, long loadData); + protected T createChunk(DataVersion dataVersion) { + T chunk = createChunk(); + chunk.setDataVersion(dataVersion); + return chunk; + } + + /** + * helper equivalent to {@code createChunk(createTag(dataVersion.id()))} + */ + final protected T createFilledChunk(DataVersion dataVersion) { + return createChunk(createTag(dataVersion.id())); + } + public void testChunkBase_defaultConstructor() { T chunk = createChunk(); int now = (int)(System.currentTimeMillis() / 1000); @@ -48,7 +65,41 @@ protected CompoundTag createTag(int dataVersion) { return tag; } - protected abstract void validateAllDataConstructor(); + protected abstract void validateAllDataConstructor(T chunk); + + protected void validateTagRequired(DataVersion dataVersion, String tagName) { + validateTagRequired(dataVersion, tag -> tag.remove(tagName)); + } + + protected void validateTagRequired(DataVersion dataVersion, Consumer tagRemover) { + assertThrowsException(() -> { + CompoundTag tag = createTag(dataVersion.id()); + assertNotNull(tag); + tagRemover.accept(tag); + createChunk(tag, LoadFlags.ALL_DATA); + }, IllegalArgumentException.class); + } + + /** + * @return the resulting chunk + */ + protected T validateTagNotRequired(DataVersion dataVersion, String tagName) { + return validateTagNotRequired(dataVersion, tag -> tag.remove(tagName)); + } + + /** + * @return the resulting chunk + */ + protected T validateTagNotRequired(DataVersion dataVersion, Consumer tagRemover) { + Mutable out = new Mutable<>(); + assertThrowsNoException(() -> { + CompoundTag tag = createTag(dataVersion.id()); + assertNotNull(tag); + tagRemover.accept(tag); + out.set(createChunk(tag, LoadFlags.ALL_DATA)); + }); + return out.get(); + } final public void testConstructor_allData() { CompoundTag tag = createTag(DataVersion.JAVA_1_17_1.id()); @@ -59,7 +110,7 @@ final public void testConstructor_allData() { assertFalse(chunk.partial); assertFalse(chunk.raw); assertSame(tag, chunk.getHandle()); - validateAllDataConstructor(); + validateAllDataConstructor(chunk); } final public void testConstructor_raw() { @@ -85,4 +136,6 @@ public void testConstructor_raw_noThrowIfDataVersionNotFound() { assertNotNull(tag); assertThrowsNoException(() -> createChunk(tag, LoadFlags.RAW)); } + + } diff --git a/src/test/java/net/querz/mca/PoiChunkBaseTest.java b/src/test/java/net/querz/mca/PoiChunkBaseTest.java index 1cfa0d8b..6d9da195 100644 --- a/src/test/java/net/querz/mca/PoiChunkBaseTest.java +++ b/src/test/java/net/querz/mca/PoiChunkBaseTest.java @@ -1,11 +1,451 @@ package net.querz.mca; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.util.Mutable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + /** * All implementors of {@link PoiChunkBase} should create a test which inherits this one and add * tests to cover any additional functionality added by that concretion. */ -public abstract class PoiChunkBaseTest> extends ChunkBaseTest { +public abstract class PoiChunkBaseTest> extends ChunkBaseTest { + protected static final DataVersion DEFAULT_TEST_VERSION = DataVersion.latest(); + + protected abstract RT createPoiRecord(int x, int y, int z, String type); + + @Override + protected CompoundTag createTag(int dataVersion) { + final CompoundTag tag = super.createTag(dataVersion); + final CompoundTag sectionContainerTag = new CompoundTag(); + CompoundTag sectionTag; + ListTag recordsListTag; + tag.put("Sections", sectionContainerTag); + + // r.-3.-2.mca + // chunks -96 -64 to -65 -33 + // blocks -1536 -1024 to -1025 -513 + // + // within chunk -65 -42 + // blocks -1040 -672 to -1025 -657 + + // section marked invalid, + sectionTag = new CompoundTag(); + recordsListTag = new ListTag<>(CompoundTag.class); + recordsListTag.add(PoiRecordTest.makeTag(1, "minecraft:cartographer", -1032, 41, -667)); + recordsListTag.add(PoiRecordTest.makeTag(1, "minecraft:shepherd", -1032, 42, -667)); + recordsListTag.add(PoiRecordTest.makeTag(1, "minecraft:toolsmith", -1032, 43, -667)); + sectionTag.putBoolean("Valid", false); + sectionTag.put("Records", recordsListTag); + sectionContainerTag.put("2", sectionTag); + + // fully valid + sectionTag = new CompoundTag(); + recordsListTag = new ListTag<>(CompoundTag.class); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:home", -1032, 63, -670)); + recordsListTag.add(PoiRecordTest.makeTag(1, "minecraft:cartographer", -1032, 63, -667)); + sectionTag.putBoolean("Valid", true); + sectionTag.put("Records", recordsListTag); + sectionContainerTag.put("3", sectionTag); + + // fully valid + sectionTag = new CompoundTag(); + recordsListTag = new ListTag<>(CompoundTag.class); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 71, -667)); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 71, -668)); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 72, -667)); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 72, -668)); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 73, -667)); + recordsListTag.add(PoiRecordTest.makeTag(0, "minecraft:nether_portal", -1031, 73, -668)); + sectionTag.putBoolean("Valid", true); + sectionTag.put("Records", recordsListTag); + sectionContainerTag.put("4", sectionTag); + + return tag; + } + + @Override + protected void validateAllDataConstructor(T chunk) { + assertTrue(chunk.isPoiSectionValid(0)); + assertFalse(chunk.isPoiSectionValid(2)); + assertTrue(chunk.isPoiSectionValid(3)); + assertTrue(chunk.isPoiSectionValid(5)); + assertEquals(11, chunk.size()); + + RT record = chunk.getFirst(-1032, 63, -667); + assertEquals(new PoiRecord(-1032, 63, -667, "minecraft:cartographer"), record); + assertEquals(1, record.getFreeTickets()); // not part of .equlas check + + List records = chunk.getAll("minecraft:home"); + assertEquals(1, records.size()); + record = records.get(0); + assertEquals(new PoiRecord(-1032, 63, -670, "minecraft:home"), record); + assertEquals(0, record.getFreeTickets()); // not part of .equals check + + records = chunk.getAll("minecraft:nether_portal"); + assertEquals(6, records.size()); + + records = chunk.getAll("minecraft:cartographer"); + assertEquals(2, records.size()); + } + + public void testSectionsTagRequired() { + validateTagRequired(DataVersion.latest(), "Sections"); + } + + public void testValidTagNotRequired() { + T chunk = validateTagNotRequired(DataVersion.latest(), tag -> { + assertNotNull(tag.getCompoundTag("Sections").getCompoundTag("2").remove("Valid")); + }); + assertTrue(chunk.isPoiSectionValid(2)); + } + + @SuppressWarnings("unchecked") + public void testRecordsTagNotRequired() { + Mutable countRemoved = new Mutable<>(); + T chunk = validateTagNotRequired(DataVersion.latest(), tag -> { + ListTag recordsRemovedTag = (ListTag) tag.getCompoundTag("Sections").getCompoundTag("4").remove("Records"); + assertNotNull(recordsRemovedTag); + countRemoved.set(recordsRemovedTag.size()); + }); + assertTrue(countRemoved.get() > 0); + assertEquals(createFilledChunk(DEFAULT_TEST_VERSION).size() - countRemoved.get(), chunk.size()); + } + + public void testAdd() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 64, 0, "A")); + chunk.add(createPoiRecord(0, 64, 1, "B")); + chunk.add(createPoiRecord(1, 64, 0, "C")); + assertEquals(3, chunk.size()); + + assertThrowsIllegalArgumentException(() -> chunk.add(null)); + + assertEquals(3, chunk.size()); + assertEquals("A", chunk.getAll().get(0).getType()); + assertEquals("B", chunk.getAll().get(1).getType()); + assertEquals("C", chunk.getAll().get(2).getType()); + } + + public void testIsEmpty() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + assertTrue(chunk.isEmpty()); + chunk.add(createPoiRecord(0, 64, 0, "A")); + assertFalse(chunk.isEmpty()); + } + + public void testContains() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 64, 0, "A")); + chunk.add(createPoiRecord(0, 64, 1, "B")); + chunk.add(createPoiRecord(1, 64, 0, "C")); + assertTrue(chunk.contains(createPoiRecord(0, 64, 1, "B"))); + assertFalse(chunk.contains(createPoiRecord(0, 64, 1, "D"))); + assertFalse(chunk.contains(null)); + } + + public void testClear() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 64, 0, "A")); + chunk.invalidateSection(-3); + assertFalse(chunk.isPoiSectionValid(-3)); + chunk.clear(); + assertTrue(chunk.isEmpty()); + assertTrue(chunk.isPoiSectionValid(-3)); + } + + public void testSectionInvalidation() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + assertTrue(chunk.isPoiSectionValid(Byte.MIN_VALUE)); + assertTrue(chunk.isPoiSectionValid(Byte.MAX_VALUE)); + chunk.invalidateSection(Byte.MIN_VALUE); + chunk.invalidateSection(Byte.MAX_VALUE); + assertFalse(chunk.isPoiSectionValid(Byte.MIN_VALUE)); + assertFalse(chunk.isPoiSectionValid(Byte.MAX_VALUE)); + assertTrue(chunk.isPoiSectionValid(Byte.MIN_VALUE + 1)); + assertTrue(chunk.isPoiSectionValid(Byte.MAX_VALUE - 1)); + + assertThrowsIllegalArgumentException(() -> chunk.invalidateSection(Byte.MIN_VALUE - 1)); + assertThrowsIllegalArgumentException(() -> chunk.invalidateSection(Byte.MAX_VALUE + 1)); + assertThrowsIllegalArgumentException(() -> chunk.isPoiSectionValid(Byte.MIN_VALUE - 1)); + assertThrowsIllegalArgumentException(() -> chunk.isPoiSectionValid(Byte.MAX_VALUE + 1)); + } + + public void testGetFirstByXYZ() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "C")); + chunk.add(createPoiRecord(1, 2, -3, "D")); + + assertEquals("B", chunk.getFirst(1, 2, 3).getType()); + assertEquals("D", chunk.getFirst(1, 2, -3).getType()); + assertNull(chunk.getFirst(9, 9, 9)); + } + + public void testGetAllByXYZ() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "C")); + chunk.add(createPoiRecord(1, 2, -3, "D")); + + List regions = chunk.getAll(1, 2, 3); + assertEquals(2, regions.size()); + assertEquals("B", regions.get(0).getType()); + assertEquals("C", regions.get(1).getType()); + + regions = chunk.getAll(1, 2, -3); + assertEquals(1, regions.size()); + assertEquals("D", regions.get(0).getType()); + + regions = chunk.getAll(9, 9, 9); + assertTrue(regions.isEmpty()); + } + + public void testGetAllByType() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "C")); + chunk.add(createPoiRecord(1, 2, -3, "A")); + + List regions = chunk.getAll("A"); + assertEquals(2, regions.size()); + assertEquals("A", regions.get(0).getType()); + assertEquals("A", regions.get(1).getType()); + assertNotSame(regions.get(0), regions.get(1)); + + regions = chunk.getAll("X"); + assertTrue(regions.isEmpty()); + } + + public void testRemoveByObject() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + RT recordB = createPoiRecord(1, 2, 3, "B"); + chunk.add(recordB); + chunk.add(createPoiRecord(1, 2, 3, "C")); + + // removes by ref + assertTrue(chunk.remove(recordB)); + assertEquals(2, chunk.size()); + + // not found + assertFalse(chunk.remove(recordB)); + assertEquals(2, chunk.size()); + + // null + assertFalse(chunk.remove(null)); + assertEquals(2, chunk.size()); + + // removes by equality + assertTrue(chunk.remove(createPoiRecord(1, 2, 3, "C"))); + assertEquals(1, chunk.size()); + assertEquals("A", chunk.getAll().get(0).getType()); + } + + public void testRemoveAllByCollection() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 64, 0, "A")); + chunk.add(createPoiRecord(0, 64, 1, "B")); + chunk.add(createPoiRecord(1, 64, 0, "C")); + chunk.add(createPoiRecord(2, 64, 2, "A")); + chunk.add(createPoiRecord(0, 65, 0, "B")); + + List rem = new ArrayList<>(chunk.getAll("A")); + rem.add(createPoiRecord(0, 64, 1, "B")); + rem.add(null); + rem.add(new Object()); + + assertTrue(chunk.removeAll(rem)); + assertEquals(2, chunk.size()); + assertEquals("C", chunk.getAll().get(0).getType()); + assertEquals("B", chunk.getAll().get(1).getType()); + + assertFalse(chunk.removeAll(rem)); + assertEquals(2, chunk.size()); + } + + public void testRemoveAllByXYZ() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "C")); + chunk.add(createPoiRecord(1, 2, -3, "D")); + assertTrue(chunk.removeAll(1, 2, 3)); + assertEquals(2, chunk.size()); + List regions = chunk.getAll(); + assertEquals("A", regions.get(0).getType()); + assertEquals("D", regions.get(1).getType()); + } + + public void testRemoveAllByType() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "A")); + chunk.add(createPoiRecord(1, 2, -3, "D")); + assertTrue(chunk.removeAll("A")); + assertEquals(2, chunk.size()); + assertEquals("B", chunk.getAll().get(0).getType()); + assertEquals("D", chunk.getAll().get(1).getType()); + + // not found + assertFalse(chunk.removeAll("X")); + assertEquals(2, chunk.size()); + + // null + assertFalse(chunk.removeAll((String) null)); + assertEquals(2, chunk.size()); + + // empty + assertFalse(chunk.removeAll("")); + assertEquals(2, chunk.size()); + } + + public void testRemoveFirstByXYZ() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(3, 2, 1, "A")); + chunk.add(createPoiRecord(1, 2, 3, "B")); + chunk.add(createPoiRecord(1, 2, 3, "C")); + assertEquals(3, chunk.size()); + RT record = chunk.removeFirst(1, 2, 3); + assertEquals("B", record.getType()); + assertEquals(2, chunk.size()); + // not found + assertNull(chunk.removeFirst(9, 9, 9)); + assertEquals(2, chunk.size()); + } + + public void testContainsAll() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 64, 0, "A")); + chunk.add(createPoiRecord(0, -653, 1, "B")); + chunk.add(createPoiRecord(1, 1587, 0, "C")); + List ref = new ArrayList<>(); + assertTrue(chunk.containsAll(ref)); + ref.add(createPoiRecord(0, -653, 1, "B")); + assertTrue(chunk.containsAll(ref)); + ref.add(createPoiRecord(0, 0, 0, "A")); + assertFalse(chunk.containsAll(ref)); + ref.remove(0); + assertFalse(chunk.containsAll(ref)); + ref.clear(); + ref.add(null); + assertFalse(chunk.containsAll(ref)); + ref.add(createPoiRecord(0, -653, 1, "B")); + assertFalse(chunk.containsAll(ref)); + } + + public void testAddAll() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 0, 0, "A")); + chunk.add(createPoiRecord(0, 0, 1, "A")); + chunk.add(createPoiRecord(0, 0, 2, "A")); + + List ref = new ArrayList<>(); + assertFalse(chunk.addAll(ref)); + ref.add(createPoiRecord(0, 0, 4, "A")); + ref.add(createPoiRecord(0, 0, 5, "A")); + assertTrue(chunk.addAll(ref)); + assertEquals(5, chunk.size()); + + ref.clear(); + ref.add(null); + assertFalse(chunk.addAll(ref)); + ref.add(createPoiRecord(0, 0, 6, "B")); + assertTrue(chunk.addAll(ref)); + assertEquals(6, chunk.size()); + assertFalse(chunk.stream().anyMatch(Objects::isNull)); + + // currently duplicates are not prevented + // if that changes in the future and this fails then update this test :) + ref.clear(); + ref.add(createPoiRecord(0, 0, 4, "A")); + ref.add(createPoiRecord(0, 0, 5, "A")); + assertTrue(chunk.addAll(ref)); + assertEquals(8, chunk.size()); + } + + public void testRetainAll() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 0, 0, "A")); + chunk.add(createPoiRecord(0, 0, 1, "A")); + chunk.add(createPoiRecord(0, 0, 2, "A")); + + List ref = new ArrayList<>(); + ref.add(createPoiRecord(0, 0, 0, "A")); + ref.add(createPoiRecord(0, 0, 1, "A")); + ref.add(createPoiRecord(0, 0, 2, "A")); + assertFalse(chunk.retainAll(ref)); + assertEquals(3, chunk.size()); + + ref.remove(1); + ref.add(createPoiRecord(0, 0, 9, "B")); + assertTrue(chunk.retainAll(ref)); + assertEquals(2, chunk.size()); + assertEquals(0, chunk.getAll().get(0).getZ()); + assertEquals(2, chunk.getAll().get(1).getZ()); + } + + public void testSet() { + T chunk = createChunk(DEFAULT_TEST_VERSION); + chunk.add(createPoiRecord(0, 0, 0, "A")); + chunk.invalidateSection(2); + + List ref = new ArrayList<>(); + ref.add(createPoiRecord(0, 0, 0, "B")); + ref.add(createPoiRecord(0, 0, 1, "A")); + chunk.set(ref); + assertEquals(2, chunk.size()); + assertFalse(chunk.contains(createPoiRecord(0, 0, 0, "A"))); + assertTrue(chunk.contains(createPoiRecord(0, 0, 0, "B"))); + assertTrue(chunk.contains(createPoiRecord(0, 0, 1, "A"))); + assertTrue(chunk.isPoiSectionValid(2)); + + chunk.invalidateSection(2); + chunk.set(null); + assertEquals(0, chunk.size()); + assertTrue(chunk.isPoiSectionValid(2)); + } + + public void testTypeFilteredIterator() { + T chunk = createFilledChunk(DEFAULT_TEST_VERSION); + final int originalSize = chunk.size(); + Iterator iter = chunk.iterator("minecraft:nether_portal"); + assertNotNull(iter); + for (int i = 0; i < 6; i++) { + assertTrue(iter.hasNext()); + assertNotNull(iter.next()); + } + assertFalse(iter.hasNext()); + + assertNotNull(chunk.iterator("")); + assertNotNull(chunk.iterator(null)); + assertFalse(chunk.iterator("").hasNext()); + assertFalse(chunk.iterator(null).hasNext()); + } + + public void testUpdateHandle() { + // identity + CompoundTag expectedTag = createTag(DEFAULT_TEST_VERSION.id()); + T chunk = createFilledChunk(DEFAULT_TEST_VERSION); + chunk.getHandle().clear(); + assertEquals(expectedTag, chunk.updateHandle()); + // writes empty section if it's marked invalid + chunk.invalidateSection(Byte.MAX_VALUE); + CompoundTag newSection = new CompoundTag(); + newSection.putBoolean("Valid", false); + newSection.put("Records", new ListTag<>(CompoundTag.class)); + expectedTag.getCompoundTag("Sections").put(Integer.toString(Byte.MAX_VALUE), newSection); + assertEquals(expectedTag, chunk.updateHandle()); + } } diff --git a/src/test/java/net/querz/mca/PoiChunkTest.java b/src/test/java/net/querz/mca/PoiChunkTest.java index 4d63669e..4565b177 100644 --- a/src/test/java/net/querz/mca/PoiChunkTest.java +++ b/src/test/java/net/querz/mca/PoiChunkTest.java @@ -2,7 +2,7 @@ import net.querz.nbt.tag.CompoundTag; -public class PoiChunkTest extends PoiChunkBaseTest { +public class PoiChunkTest extends PoiChunkBaseTest { @Override protected PoiChunk createChunk() { @@ -20,14 +20,7 @@ protected PoiChunk createChunk(CompoundTag tag, long loadData) { } @Override - protected CompoundTag createTag(int dataVersion) { - CompoundTag tag = super.createTag(dataVersion); - tag.put("Sections", new CompoundTag()); - return tag; - } - - @Override - protected void validateAllDataConstructor() { - + protected PoiRecord createPoiRecord(int x, int y, int z, String type) { + return new PoiRecord(x, y, z, type); } } diff --git a/src/test/java/net/querz/mca/PoiRecordTest.java b/src/test/java/net/querz/mca/PoiRecordTest.java index ec83b165..244e4533 100644 --- a/src/test/java/net/querz/mca/PoiRecordTest.java +++ b/src/test/java/net/querz/mca/PoiRecordTest.java @@ -4,7 +4,7 @@ import net.querz.nbt.tag.CompoundTag; public class PoiRecordTest extends TestCase { - private CompoundTag makeTag(int tickets, String type, int x, int y, int z) { + public static CompoundTag makeTag(int tickets, String type, int x, int y, int z) { CompoundTag tag = new CompoundTag(); tag.putString("type", type); tag.putIntArray("pos", new int[] {x, y, z}); From 4a9e7ecf76eff70b01c1017bcf79fa9ab0783e21 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 12:49:24 -0500 Subject: [PATCH 040/288] DataVersion adding 1.18 PRE1 DataVersion added test to catch misconfiguration of enum - and fixed a bug it found --- src/main/java/net/querz/mca/DataVersion.java | 22 ++++++++++++++++--- .../java/net/querz/mca/DataVersionTest.java | 19 ++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 08c8c80f..0d1d5dcc 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -46,7 +46,7 @@ public enum DataVersion { // 3D Biomes added. Biomes array in the Level tag for each chunk changed // to contain 1024 integers instead of 256 see {@link Chunk} - JAVA_1_15_19W36A(2203, 10, -1, "19w36a"), + JAVA_1_15_19W36A(2203, 15, -1, "19w36a"), JAVA_1_15_0(2225, 15, 0), JAVA_1_15_1(2227, 15, 1), JAVA_1_15_2(2230, 15, 2), @@ -88,7 +88,8 @@ public enum DataVersion { // Added yPos the minimum section y position in the chunk // Added below_zero_retrogen containing data to support below zero generation // Added blending_data containing data to support blending new world generation with existing chunks - JAVA_1_18_21W43A(2844, 18, -1, "21w43a"); + JAVA_1_18_21W43A(2844, 18, -1, "21w43a"), + JAVA_1_18_PRE1(2847, 18, -1, "PRE1"); private static final int[] ids; @@ -121,7 +122,7 @@ public enum DataVersion { *
  • CT# for combat tests (e.g. CT6, CT6b)
  • *
  • XS# for experimental snapshots(e.g. XS1, XS2)
  • *
  • YYwWWz for weekly builds (e.g. 21w37a, 21w37b)
  • - *
  • PR# for pre-releases (e.g. PR1, PR2)
  • + *
  • PRE# for pre-releases (e.g. PRE1, PRE2)
  • *
  • RC# for release candidates (e.g. RC1, RC2)
  • */ DataVersion(int id, int minor, int patch, String buildDescription) { @@ -145,6 +146,21 @@ public enum DataVersion { } } +// private void checkEnumName() { +// if (id != 0) { +// StringBuilder sb = new StringBuilder("JAVA_1_"); +// sb.append(minor).append('_'); +// if (isFullRelease) { +// sb.append(patch); +// } else { +// sb.append(buildDescription.toUpperCase()); +// } +// if (!name().equals(sb.toString())) { +// throw new IllegalArgumentException(String.format("Expected enum name to be '%s' but was '%s'", sb, name())); +// } +// } +// } + public int id() { return id; } diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java index cc025b30..ceaf08f7 100644 --- a/src/test/java/net/querz/mca/DataVersionTest.java +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -2,7 +2,26 @@ import junit.framework.TestCase; +import java.util.regex.Pattern; + public class DataVersionTest extends TestCase { + private static final Pattern ALLOWED_ENUM_DESCRIPTION_PATTERN = Pattern.compile("^(?:FINAL|\\d{2}w\\d{2}[a-z]|(?:CT|XS|PRE|RC)\\d+|)"); + public void testEnumNamesMatchVersionInformation() { + for (DataVersion dv : DataVersion.values()) { + if (dv.id() != 0) { + StringBuilder sb = new StringBuilder("JAVA_1_"); + sb.append(dv.minor()).append('_'); + if (dv.isFullRelease()) { + sb.append(dv.patch()); + } else { + sb.append(dv.getBuildDescription().toUpperCase()); + } + assertEquals(sb.toString(), dv.name()); + assertTrue("Build description of " + dv.name() + " does not follow convention!", + ALLOWED_ENUM_DESCRIPTION_PATTERN.matcher(dv.getBuildDescription()).matches()); + } + } + } public void testBestForNegativeValue() { assertEquals(DataVersion.UNKNOWN, DataVersion.bestFor(-42)); From 1306ebd89da5942f5996e1fedb36f68c31605a68 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 13:02:50 -0500 Subject: [PATCH 041/288] Add integration test for reading & writing poi mca files for 1.17.1 --- .../java/net/querz/mca/PoiMCAFileTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/net/querz/mca/PoiMCAFileTest.java diff --git a/src/test/java/net/querz/mca/PoiMCAFileTest.java b/src/test/java/net/querz/mca/PoiMCAFileTest.java new file mode 100644 index 00000000..df0d3249 --- /dev/null +++ b/src/test/java/net/querz/mca/PoiMCAFileTest.java @@ -0,0 +1,38 @@ +package net.querz.mca; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PoiMCAFileTest extends MCATestCase { + + public void testPoiMca_1_17_1() { + PoiMCAFile mca = assertThrowsNoException(() -> MCAUtil.readAuto(copyResourceToTmp("1_17_1/poi/r.-3.-2.mca"))); + PoiChunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + assertEquals(DataVersion.JAVA_1_17_1, chunk.getDataVersionEnum()); + assertTrue(chunk.isPoiSectionValid(3)); + assertTrue(chunk.isPoiSectionValid(4)); + + assertFalse(chunk.isEmpty()); + Map> recordsByType = chunk.stream().collect(Collectors.groupingBy(PoiRecord::getType)); + assertTrue(recordsByType.containsKey("minecraft:home")); + assertTrue(recordsByType.containsKey("minecraft:cartographer")); + assertTrue(recordsByType.containsKey("minecraft:nether_portal")); + assertEquals(1, recordsByType.get("minecraft:home").size()); + assertEquals(new PoiRecord(-1032, 63, -670, "minecraft:home"), recordsByType.get("minecraft:home").get(0)); + // it'd be better if we had a bell in this chunk to test a non-zero value here + assertEquals(0, recordsByType.get("minecraft:home").get(0).getFreeTickets()); + + File tmpFile = super.getNewTmpFile("/poi/out.r.-3.-2.mca"); + assertThrowsNoException(() -> MCAUtil.write(mca, tmpFile)); + + PoiMCAFile mca2 = assertThrowsNoException(() -> MCAUtil.readAuto(tmpFile)); + PoiChunk chunk2 = mca2.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk2); + + assertEquals(chunk.data, chunk2.data); + } +} From cd73912297374f97a1171008f8f21c66983639f3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 13:06:58 -0500 Subject: [PATCH 042/288] added additional check to poi 1.17.1 test --- src/test/java/net/querz/mca/PoiMCAFileTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/net/querz/mca/PoiMCAFileTest.java b/src/test/java/net/querz/mca/PoiMCAFileTest.java index df0d3249..d96f11de 100644 --- a/src/test/java/net/querz/mca/PoiMCAFileTest.java +++ b/src/test/java/net/querz/mca/PoiMCAFileTest.java @@ -22,6 +22,7 @@ public void testPoiMca_1_17_1() { assertTrue(recordsByType.containsKey("minecraft:cartographer")); assertTrue(recordsByType.containsKey("minecraft:nether_portal")); assertEquals(1, recordsByType.get("minecraft:home").size()); + assertEquals(6, recordsByType.get("minecraft:nether_portal").size()); assertEquals(new PoiRecord(-1032, 63, -670, "minecraft:home"), recordsByType.get("minecraft:home").get(0)); // it'd be better if we had a bell in this chunk to test a non-zero value here assertEquals(0, recordsByType.get("minecraft:home").get(0).getFreeTickets()); From 7e61a3eef1d4b63c941891fe2d2450bccb86c752 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 13:33:18 -0500 Subject: [PATCH 043/288] Created the start of MCAFileBaseTest abstract test and updated PoiMCAFileTest to extend it. --- .../java/net/querz/mca/MCAFileBaseTest.java | 34 +++++++++++++++++++ .../java/net/querz/mca/PoiMCAFileTest.java | 14 +++----- 2 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 src/test/java/net/querz/mca/MCAFileBaseTest.java diff --git a/src/test/java/net/querz/mca/MCAFileBaseTest.java b/src/test/java/net/querz/mca/MCAFileBaseTest.java new file mode 100644 index 00000000..45f3df88 --- /dev/null +++ b/src/test/java/net/querz/mca/MCAFileBaseTest.java @@ -0,0 +1,34 @@ +package net.querz.mca; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Objects; + +// TODO: implement abstract test pattern for MCAFileBase & refactor MCAFileTest like mad +public class MCAFileBaseTest extends MCATestCase { + + /** + * Reads an mca file, writes it, reads it back in, verifies that the NBT of the first non-empty chunk is identical + * between the reads and that the data versions are correct and that MCAUtil.readAuto returned the correct type. + */ + protected > void validateReadWriteParity(DataVersion expectedDataVersion, String mcaResourcePath, Class clazz) { + FT mcaA = assertThrowsNoException(() -> MCAUtil.readAuto(copyResourceToTmp(mcaResourcePath))); + assertTrue(clazz.isInstance(mcaA)); + CT chunkA = mcaA.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunkA); + assertEquals(expectedDataVersion.id(), mcaA.getDefaultChunkDataVersion()); + assertEquals(expectedDataVersion, chunkA.getDataVersionEnum()); + + File tmpFile = super.getNewTmpFile(Paths.get("out", mcaResourcePath).toString()); + assertThrowsNoException(() -> MCAUtil.write(mcaA, tmpFile)); + + FT mcaB = assertThrowsNoException(() -> MCAUtil.readAuto(tmpFile)); + CT chunkB = mcaB.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertTrue(clazz.isInstance(mcaB)); + assertNotNull(chunkB); + assertEquals(expectedDataVersion.id(), mcaB.getDefaultChunkDataVersion()); + assertEquals(expectedDataVersion, chunkB.getDataVersionEnum()); + + assertEquals(chunkA.data, chunkB.data); + } +} diff --git a/src/test/java/net/querz/mca/PoiMCAFileTest.java b/src/test/java/net/querz/mca/PoiMCAFileTest.java index d96f11de..90c25008 100644 --- a/src/test/java/net/querz/mca/PoiMCAFileTest.java +++ b/src/test/java/net/querz/mca/PoiMCAFileTest.java @@ -1,12 +1,11 @@ package net.querz.mca; -import java.io.File; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -public class PoiMCAFileTest extends MCATestCase { +public class PoiMCAFileTest extends MCAFileBaseTest { public void testPoiMca_1_17_1() { PoiMCAFile mca = assertThrowsNoException(() -> MCAUtil.readAuto(copyResourceToTmp("1_17_1/poi/r.-3.-2.mca"))); @@ -26,14 +25,9 @@ public void testPoiMca_1_17_1() { assertEquals(new PoiRecord(-1032, 63, -670, "minecraft:home"), recordsByType.get("minecraft:home").get(0)); // it'd be better if we had a bell in this chunk to test a non-zero value here assertEquals(0, recordsByType.get("minecraft:home").get(0).getFreeTickets()); + } - File tmpFile = super.getNewTmpFile("/poi/out.r.-3.-2.mca"); - assertThrowsNoException(() -> MCAUtil.write(mca, tmpFile)); - - PoiMCAFile mca2 = assertThrowsNoException(() -> MCAUtil.readAuto(tmpFile)); - PoiChunk chunk2 = mca2.stream().filter(Objects::nonNull).findFirst().orElse(null); - assertNotNull(chunk2); - - assertEquals(chunk.data, chunk2.data); + public void testMcaReadWriteParity_1_17_1() { + validateReadWriteParity(DataVersion.JAVA_1_17_1, "1_17_1/poi/r.-3.-2.mca", PoiMCAFile.class); } } From 4d9bf68a6348208e0e28f30888765cc713cfa331 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 15:05:24 -0500 Subject: [PATCH 044/288] Adding mca files from various MC versions for better integration testing --- .../r.-1.-1.mca => 1_12_2/region/r.0.0.mca} | Bin 16384 -> 16384 bytes src/test/resources/1_13_2/region/r.-2.-2.mca | Bin 0 -> 16384 bytes src/test/resources/1_14_4/poi/r.-1.0.mca | Bin 0 -> 12288 bytes src/test/resources/1_14_4/region/r.-1.0.mca | Bin 0 -> 16384 bytes src/test/resources/1_15_2/poi/r.-1.0.mca | Bin 0 -> 12288 bytes src/test/resources/1_15_2/region/r.-1.0.mca | Bin 0 -> 16384 bytes src/test/resources/1_16_5/poi/r.0.-1.mca | Bin 0 -> 12288 bytes .../r.15.-9.mca => 1_16_5/region/r.0.-1.mca} | Bin 20480 -> 20480 bytes .../resources/1_18_PRE1/entities/r.-2.-3.mca | Bin 0 -> 12288 bytes src/test/resources/1_18_PRE1/poi/r.-2.-3.mca | Bin 0 -> 12288 bytes .../resources/1_18_PRE1/region/r.-2.-3.mca | Bin 0 -> 20480 bytes src/test/resources/1_9_4/region/r.2.-1.mca | Bin 0 -> 12288 bytes 12 files changed, 0 insertions(+), 0 deletions(-) rename src/test/resources/{1_18/region/r.-1.-1.mca => 1_12_2/region/r.0.0.mca} (64%) create mode 100644 src/test/resources/1_13_2/region/r.-2.-2.mca create mode 100644 src/test/resources/1_14_4/poi/r.-1.0.mca create mode 100644 src/test/resources/1_14_4/region/r.-1.0.mca create mode 100644 src/test/resources/1_15_2/poi/r.-1.0.mca create mode 100644 src/test/resources/1_15_2/region/r.-1.0.mca create mode 100644 src/test/resources/1_16_5/poi/r.0.-1.mca rename src/test/resources/{1_18/region/r.15.-9.mca => 1_16_5/region/r.0.-1.mca} (56%) create mode 100644 src/test/resources/1_18_PRE1/entities/r.-2.-3.mca create mode 100644 src/test/resources/1_18_PRE1/poi/r.-2.-3.mca create mode 100644 src/test/resources/1_18_PRE1/region/r.-2.-3.mca create mode 100644 src/test/resources/1_9_4/region/r.2.-1.mca diff --git a/src/test/resources/1_18/region/r.-1.-1.mca b/src/test/resources/1_12_2/region/r.0.0.mca similarity index 64% rename from src/test/resources/1_18/region/r.-1.-1.mca rename to src/test/resources/1_12_2/region/r.0.0.mca index 293c54425102b8de0b433dca0ed0e22a92517389..50c42697780a799710e565d0f518fd47d10af3ee 100644 GIT binary patch delta 5585 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 delta 5727 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 RtCD2$|HPTTwX?AW=?Py(oMHd~ diff --git a/src/test/resources/1_13_2/region/r.-2.-2.mca b/src/test/resources/1_13_2/region/r.-2.-2.mca new file mode 100644 index 0000000000000000000000000000000000000000..9695dfc1cad61bbd4d5e7b4b958788451e73eca3 GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/1_14_4/poi/r.-1.0.mca b/src/test/resources/1_14_4/poi/r.-1.0.mca new file mode 100644 index 0000000000000000000000000000000000000000..c979f83fb68158d8dd2c11901ba2354b996e07cb GIT binary patch literal 12288 zcmeIvKT84u0ETfDJEF-7vnfdlf+!lIlNK>N4h|%cpooTQXtARP!3Gg5qOGEmI7kWm zH>h9`Na56pQG-cSAPtpK5QI}rL4AgXo1XUzJn(x>CWZh42q1s}0tgr>U@;q6qlEwh z2q1s}0tg_000IagfB*u<3(Rhmo{g_kL;wK<5I_KdKY@&8d0(ygTCN6dO;u-FRV7;_ zr6>AY#}2BwQg~`r4c;#lHAlNR8u0Z?6MA&M%PV-clS{6iTJK@J(cvE13dcfodAshh zAKyM$pHfF>+UAe*dZ^%+PwQ*bFK&P5MJO;iuI!wx&CdWAp0R#|0009ILKmY**5I_I{ I1pXoL3kjrB!2kdN literal 0 HcmV?d00001 diff --git a/src/test/resources/1_14_4/region/r.-1.0.mca b/src/test/resources/1_14_4/region/r.-1.0.mca new file mode 100644 index 0000000000000000000000000000000000000000..171d957b3396a83acc627bbfd9578e3f8d758f69 GIT binary patch literal 16384 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 SFS2KWXMtydXMz8h0{;aL(OusF literal 0 HcmV?d00001 diff --git a/src/test/resources/1_15_2/poi/r.-1.0.mca b/src/test/resources/1_15_2/poi/r.-1.0.mca new file mode 100644 index 0000000000000000000000000000000000000000..1ea4d28207b32459f29db37e8be77c5935a816b0 GIT binary patch literal 12288 zcmZQz7zLvtFd71*Aut*OBPRry7)Op@M}0aP0;3@?8UmvsFd71*Aut*OqaiRF0;3@? z8UmvsFd70QGXxU*j@}uWJ|Fe~lx9x@?xP-51j0 znJGJS0skJ2&KtXuxk7(_@6DR^OtDzE_VD_~e#5-p$@(jq@1*MeXS^(?@y6~&!q(Rs z=hrKJW%}{N(_=KQMnhmU1V%$(Gz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E2C4FLeG CE>T7R literal 0 HcmV?d00001 diff --git a/src/test/resources/1_15_2/region/r.-1.0.mca b/src/test/resources/1_15_2/region/r.-1.0.mca new file mode 100644 index 0000000000000000000000000000000000000000..c01d14c28e3ce930ae94c03e9d3f1e99e8fdeb9c GIT binary patch literal 16384 zcmeI%RZtvXn+EUzNw5%t1cE2H3>F-MyEC{9?m9?t3&GtbxD7tIy9N*L?k+(G7-0Xk z-(GxMwfB4R@zm*yx4ZguJ-<44{p!_AUIbnQUIbnQ{&NHn5&v_#zBGLicoBFJcoBFJ zcoBFJcoBFJcoBFJcoBFJ_|FtDjOttc&usqE`bFSH;6>m?;6>m?;6>m?;MFSvJ4Cni z6J7N0L>j-^t4(#1GQ{FexO40|b+Ntxcf{x1`80Av4xY#9=n=n z?-5x{9Y>qOz=yh2-_y&7Gv71#ZPvtq1I4xkhxcY#ic8L-q5Qa+>UM(J1kW0E{dY6yx|CRnI$BjFpB2nNj`hjYy5|6`4j-SmgfUYSt`{7s;ymQYr? z7yW5o8tXLnqbyU<_vv|gV1=R}4rehTMvVw4>YHEcbgSvA@B92gb;WCpE%YWb5}CaG z(XeArs8>-1fmwd#@|}Fs&6H|xMnShv>f*eVN+#0qlww{T8DPXmYUsFdV&9xCf01hw zH$)=U>f8E}GktB=4N2N*n<2Wq?agFDMREb?!E&fLMm5(PSw7YWyU~t9+ezhFN&mYI zH|O*U!NH{M?(K-^n&`bI|0R%jmUb2^Kwwss>Fex2cWy;xX+6hl7rbzX>N$=c_c_ts zGP_I>z+*nkO~}z4gxu3ek7AcLZ;1_;v4=RZy@Rr;uGnfPtR6CviKHua)QmA zLL^U-#Xa7o3Ge9^jMj6IhG^SzD%#-9+U|a-r{P>`EcMNA@V(V+tPpfGK07_?ce#ze zc6N?(!MNnm_>-!WY%XY5*9Q$c1a3*uGrE}8CM032aeac;6Y|{r!1qGfCeEk$3`TNa z3@lMb-Oi9P?QJBNTJ)SU0^uY^AT`m5v+{lJrRjSu!lb%JGrSnWl8HCLH?sH_6*x2P z`ln#h@(bOeBOnb=nZVgbBoo;8ZI{;giPKasc}|0r*&6QO`de}QxAn;HEu&nwsWW^s z2OfFffmD~JwDLMm`NHA6M|FbQ>jBe@8eXmvR0abnE=T!LmFl?evF6f==1c-{^sx9Tk1=t0*hgwXBaHg=Gf` z2I0^nk;A_TX2z?e=YVljmORKD@kYgeYYDd|*jWxEo1HPzp1!th zp!50P61Cz~=Z0yl^0Fn8%;K`f2hcfX|6UzAmLudP*PsRz*?q}4RTzVOmDZ%cUa7{$ z$H|cH){C5dKM+QpPf?w&FwpcaG&ZKt36z)!sK+E>)lo8hQ&9GnW0N%RJ|N!C%9G8T zrK)yyk%`5YdVyl3*p0XdZAMaDI4#>ylOLc_si7KM6N|d+uZ;vPDd%5+3&S0<3lLIh z_jLoRzc_eM2Wp{vArW|Tl$cQzOV+iRl-V=R?u8=P`wDpME}Lh;N-A~-Th=;k3(sfP zo?%g<%-V(5I)`%&pi6%u0S$kv1IIJ3w+v^eI8+!6DEVsR-ZBoL4WaU~-)lM5x!Jgq8AAF{wG z;+0)RFZ#_&IAAAcRC6C-SW8_)2OyxLVK{glQ&#uqB*Efn|KT&mj>|S%4LGsCe>%bJ z>^Sg(k6{Odo^F+K>QrA*q~blex@*KelR1=e_>~@^C;xt=&wxRemrMu>R-5i|~ zyL}q$Syry8 zGew8H0NalD4TV=gR;H0E(jcbZ3T-9=m*k>^@6cNt?|ZQC^RZS>oxtpMT?422X_dmM z{&QbwdWRQ0pv7mbS1dRpf zqjt%vT`_gb$_y$i<1gJLu4ZcnQr(SR&kOT0pFJ{x3)P8HSB#eZX)GRL=CrrqomRttX1VJ&GNV!QnPtcP&h1kl#R;zv6G??Z3FH$5NfQ zxi6{ka+JrgAcE34lRw7x1Mw5<7LugLKrL`^0^>iKYum$U% zIM)(1%vF3&HcIV$CG(J(d5wobWUwd=NEdYxx@spu^h0HBivWu;}{ zA&*7gCdsiC=Lbc*=M8=c*G~1RYf>eh{=9;U`I;r5oDMSZtEx6duG>-RvfgB#Bey#t z#zgi^zM*4wUUDUl%W7V~Q0!zgqRD=)?S95=*GJuA?y*k;gz2?j%H=b3Qwx3O*xNZf z<8sx#jU-#S4P|CLD!2Ycw&^auGg`BEVe80KKgjHJ{CD_4aJ6RUP+GBd@v@NX@!ni# z#P-5%IQaQ`E}Cq2X?wDMaA{R&X&dLvXD#Z;=ceU|***;bVv2)+qn&avfYFZ|s>j^{;Q%Vqwnu9r?Q4I zL>#hb($azq-5QqJPx?q;I&b+Lo(yaPK1I+T^z(Ff_z*V-$%tXB_oEmh90pimza{>C zAT;F?<0tFXbON++I{2;-ZWO8G1f^)yj*`*0X5y09UQMw64Q=R6b(oyPzWhlVgViZX z#v_FdnJ1(rO5RPdNV%{CQuow;#;=p%MrEG;#jYpdkVEcrXGz__*fxBDg?12^SsN%K zsr5T*wCv9=OFWhf_Ka!5%s3=Ua=>A!t(hGyC?$fGhxn(s3>7~rtGH0K((A0cx;jY< ztUq}k`!bRu%|;&23DSf#%DFYwA5CmfX#A4t3?{?p8FOeAs+y~gUxTfzYEl6Cy615?rseiu_8zd~z;t&b- zqdhlz0G}r1_CD!AdVv5*NU!(&Q=Yv~U%cPEI(=~VDKG|sJaone?k5s6j0lUCDDE2^ zM-{vxfNW92b*daYJJdYl1|BBGqGUCc2w2=M@9+bBw3MG-?equNy8<7;94DAESkmrL zI6{X09z*v-)TGRZ9*_$%52n7v<)X;hhf?a$izaYR49u)p{dJws%#d{CT` z18R#b$=~D565e9hSICbnku0^`p+(tWlMl=KeJC{THL}H~Ydo3^!=w86#hCG8KFnUq zWSLu2)n7@au3p=EN4eI>b^xENn!NiY+w&Epx+3+#VeUD~t-%_J3+r*+PLjp_>9S1h<(VczO9mg_{Vh5Hd3|A!{ zeNNxb-yM2{G^N#QsnnC}jDz6*AaB!)e@@S}S=DP2mI@8*kjE-gUc*qmBupra3!Rh# zBoKem@yl9mjWW*en0!UFF^{gLul;SeiA~I)CUBJ)XNrol4Fx~#5V4n*xqynMnLTu{ zYG$1of?Ueq27=#ftM1qRQbA1UkMSXTu2ow?>pb>NSa{yYTY36jV4mFcYKx0<&<>iA zyivr2$x6UdAF~O^Qyk4-+c(<+wp@{FBMO>UA={sdBR0kX7e#<-Rr{7*nvkFNWuIYM zpFtqnWqNFx&5%JLrID3YA={*?30Drn8+UfGhM|HFD?OsBoWH&{?kBJYTu|wJ$Z;96 zJEF51h)LAn!`K<51}|8p+yi>Eq9@@u>Z2JhO`ipKY_&t?gp}rbm}Dbcnp6cD9fUJNUvsU>O4Fh<}uaG3o;dPH8!A4x475O8( z7H8h9j?llspDM~FBZ_e-;CkLQ%}P%oJp>Tq^i$4+yIJrZ67~kfAxHp8PnQYxzA^lK zZ0yW9F7rovzavsSL#SnULq+1y%m$eW_88?3$@E>QS|uq@cL#W|<>Y58o)58_*E@B* zD39Ge^UarZqwxPv4q8y?YWKioWiIGbSZMFAs(bh*tw);Z1X07t_!bK~1^E z6_2|=t72Ul&UEv4d2-stx#8z&54=l<=gIE_@FxX4dsREIv8E6)>+#SE@It(YJV@0) z$n78-a8s5vuY(b;JLmK%naOtB<&lD@@p#w?uwIALo7Pd$RdGs^fO<(6qJC`N1 zY#iOZjV{tLgKRh@dSy4VSH{3zHo|V(4K-nPoZ!r(cq*-b*yP7f&wzyZH*TtnX3AMZ#reESm2V z-y02nxXWB_o^?K`f%xClXJ`Uh6aaX=tr8&?Vs_yI0FFGHl>e$;i(a{7SuXW#eqL*Dxiz?!2H&F z-y#wHucvJ_qK$!LR-pb~r0wV1x0T(4^4%A~VsENOv3nqDsG%k3qPg!ohR@JEZypXN zYO+WlnLMsq_F0eV@-lIfZ5Q>+Rtp|sA=Z<|^ILSv;2ssyqJ|A=kVN#|HE4rn*kFMv z$s6lP>r&Uyqc@M;2|Zf;XvIHTK(We008>Dl1Cl@qeMNiripkXVvv~`w@dE<%?$1BR^W`aYxud|I@AOAUIK}) z!0x&Qcs?Y9JfIiSeo(dzZEVG#w3?1cPaF27g7)EEl^o8y zHTUYczW9`ZBcd(BVaY?2N@2}OWF^AOFVQk;t;K-q02fUqRLh*8 z1`Sa4Z?P@lKJf*LdG7-lerTm*KlQkXe<#$~2ASD_DjZQI z32J;UJzP$y3*VgLiyvij_Q>Gb*E%b50nM_z4+RoB+K=UT#>6FQ6$5Dk01+ZHxCqY@ zkZCtn45qw3l<=?PS>w|sO3aLSEw2X1hV?QNt$-d1y@JvV_1W|JXId7Mmuc&NyQC5b z0&TonO;weELJ11no5^^)s5`? zLC4&Em=`dUnyw4v^cHU(#3Z0OAL9oi>8`+-ry;Trd95AwdP$E-U~U*fSk3#U!AqPA zPj>j0Tof0{xA*jq0;x7&3WBEr7c%Ln?{#V$`P|KIgf!;yx(ZU=Bnv_qR%FHE`$?a| zLYz@dAu{g#5p#;}{3i)>WI3%T6bW49%B!(jQl!gl^rZQ%HIhGWVUJn|>jPy+)1hrm zj+u&}&u7Q6eDMLdDSV#ijei(Ozxmu;JznS`JG^%i=c)n*c_p`taa1$8o$dNy&>tH(`{7%#N7Yqdua5%= z9o`v3&U-~(>m2lFPb-J#PWJW*Lb0w2Jly}CCz!$W&lTV%x(ob7M1Hj=Ae>~x{$D8* zfc3n7|F5VH_5^5?^8e41Y?<6(-hZEv@uNxqwI8<<)r2Vjzfb;Yl9SW?j^F>9 zV*hJ@{lPJWdLBF%49kLG;gHq zCLtXfT7@N!t2&;OE%IoukTMi?J%T&&nmw6q5S1r`mR5)8qZ)AH_uE}uLwdD*co4w_ zW_l8R1zDz)sZ-deECB7ryK!N9<3~1ab#?TQrE704ixoP(@Nj3YsHdvalOy}TQp)6# zn;Dc7$!~2DScy67{t}~vj7}uWqyQ z>m!yviAv6P$WvLynfWbPTQFC-CpdJL&aA{DwcOZe+4brM=VdSU_SOk(-*-qvxz4O5 zG&Spz-_t#(y^~@mbSp|pPir}Exa4&l=RbvRIn5kb<_8}b*V#9G6_5Dl;Cf`)KK`EI z4s*SSFQvakxE77Z)MyBdhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR`5^!R D_|;7U literal 0 HcmV?d00001 diff --git a/src/test/resources/1_18/region/r.15.-9.mca b/src/test/resources/1_16_5/region/r.0.-1.mca similarity index 56% rename from src/test/resources/1_18/region/r.15.-9.mca rename to src/test/resources/1_16_5/region/r.0.-1.mca index bf1c5b0f67f52c058c349815c8a903682b4d736b..f33d8540550ef1cf4ca984ac59d7b9ccf4d60a6d 100644 GIT binary patch literal 20480 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 delta 8808 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

    +qOZ(4gD@ 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 diff --git a/src/test/resources/1_18_PRE1/entities/r.-2.-3.mca b/src/test/resources/1_18_PRE1/entities/r.-2.-3.mca new file mode 100644 index 0000000000000000000000000000000000000000..188141673fce62c75ee244db959fb333530e4b59 GIT binary patch literal 12288 zcmeI$`#aN(AIEX!6gRh0lr56u%;cOLBFyBhjI6S8o196+NYHyo00BS%5C8-K0YCr{00aO5KmZT`1OS15O28}aGx(os0Ubbq zk53qJ7?;O!|0U=I9b~%0Hr_(}!_Aqq|Gzoak!*8#r11UMu$-eR&|nHwO6vTZpMu;QVik1-4+9Hg zy5k>ql4;jlkiPD2TO@GwZmR!pO0B8(6h~}jo6&OEk{W;1MX68ON1iV4NQ3Vfkm2qA zpI~S#h6?#KM4d+IMzKRBmhLU9+WpxJF41?KS?kd&tokC|wO?axx;;){i@U4%UDb6H zfWBZs7m;5SkT&yvGZUEJ&9P()J?$8qR9i_-5WVUTgtJl87k)&n;?S40S1OkK4@3j$ zt`-7V^F@#?L5n3XxvM6oCMzNW3xg|gCtVVhOA!l)dk?8=(Tm{R_IS0Uq5-89DdaRU zNXko6XVnQJQqoVg>yV*ysacVW!)(hb|5Y#JmBi}4+(UOj!PcxN{8B5k@d`l?B^@YU zR*Ukju)ew^O)rlk(5l^LtoN4uBj&mGgblDk5TxbQHxej0T$7gYv2;EsR=pWE->ZV2 zGg7==nFF&+1`($$+^$^8u%{ZqZQ6q2JtJL#-brz6jnK>;%^2!bO8v?4P|I5HOBa5s zISu5*pWTyTznU3|o_sueE3!{ezz(*EKOKf`SL(!(sv4wjKFXc_?Uy_H&>9d?$^Aj# z*US=8=yP-8X|o@AUdvnOm{_;uW1|pye!!M-^XyM<1|C zTasuPQtn1Thgs~e%Re#vY37l@P<1b(=IlKAkR9J%I%a%-%h(J&qHY9hOdzhD}v>pJ5T{9{(jb4 ziAvemtW`W2O}aSg(Ai(R{lqa52Qi3XVon zq!_Ocr`}KLNyvL#ed?2_mO%!I^hSL!Q54${o9=jFfgp!{qC_L7Zl?5ZYJady3F^}dCW`67L~z=n>EQj&MP8@HE5fo4VN|Vx)V!uKFVSi$~-d?Zt*x&t_$(xxY{k6Nj)2ShZs`gb=Ma~)}D1>L>|OWWLk$V zJAiG=#>RzSVxr|S3o4I}i3cjUt-K9UT}>pN*HGoh#~Z*g!f)hExU>38!H_u4CDK6# zL&laD=KppnDaujZwJ(!AP+(S)8@Z)BkrcSc=;!2p3|n@!Mw_Hxb?v;r&22M$k_vI1 zO-yJ%0F6g%;3kjqTw|B;1E!h%XzInab;0+~=4^I$qn#S-3;o)f&%Pr*dn%V~DFCk* za+de-zWavV9p}G}#l&n{u-b9Nkbw}7KWt(`&trr)DEO_~XqVc#XY8e0PU}IN`Qn9L z#B)c2FN;|6b*kYCdCPt zyGAilPMauc%B;wq3}3@;`B@e$)YfV(>O6%u$E_9L_I$BX;L{|dD4;R+-Z%SW|L??N zmR|ZuxHSvkMA?a=&$-rwPmb=P7_)DXrad7a8~tYohuGb!_iB0LGL0LzdE2h5X8XK8 zo^X=m7K(jh^gXluWTeRf<6gaY|L9)v$j~ah>c)cZs4l0~SZMKvf$2KdTndL1^&D=S z{dErI+-i}&+#VESh`d(C+3hKEy|J^ukMc2ROn~hLMGfrnW)Xh)bB1fJd4^49vPn7x z5yja)mzq6?9bVQ(eS>jOj4{LMjIC?AyVdfceB5?WtP5qcdB|T&N3j8mMpZ_8!i<%T z?k3rGsv(L;bhVCqR=@~p0@emzxcwl~cFO95*ctFuZ+M(K>hXV@Pz zn*CXsuu*ZY_f)2TCz^LJYdAZldc!fXIlUdJ8KoEE0vD8IWF6RevDf6{wA2EXbH1h} z7as+?y-+t=>d@Vx&MA1hzI3_XnE^2rCbf;$@2ow z-JcS-Rwl%WHzAJkdwpXjYfSy0=BZtuT|O;bsH6QfEN(Bu&|0;3@?8Umvs gFd71*Aut*OqaiRF0;3@?8UmvsFd71*A%H&w090jNZU6uP literal 0 HcmV?d00001 diff --git a/src/test/resources/1_18_PRE1/region/r.-2.-3.mca b/src/test/resources/1_18_PRE1/region/r.-2.-3.mca new file mode 100644 index 0000000000000000000000000000000000000000..36c5d7a3fabbb4b3f91b1a10af92691f4a5a8623 GIT binary patch literal 20480 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 F_#d;9*+&2X literal 0 HcmV?d00001 diff --git a/src/test/resources/1_9_4/region/r.2.-1.mca b/src/test/resources/1_9_4/region/r.2.-1.mca new file mode 100644 index 0000000000000000000000000000000000000000..6108c9bb5f1761fccf44c9f87b9ded4d342c932b GIT binary patch literal 12288 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 d;Ev|5G*0<7JVnJW{NjuxoPGd_d zwbL*j;#$n#z4+KIrqW~D_4rBUV=3e z-zqyDRuP(|4Rq!P{32c z3b)DS6pYh!l&Gk4JFqF7EcAAEsO5!}w-D*B_xD5$#CW$7bZ?dZgM*}^i3$5`H! zbO@)oPdppTV6d-mxO}#g<7W##AEZm}NK2)x%M9P%=D`lPph@4arh}T9ce=8P^k)3fr-2K zm!F!Y{x629uDJM?w!9Kg%x3F(Pe)(>0KIX`r9Q8E7}B{pp4Ak_F3|i2z54Aa^b$r; zc_sf@ehq~`9=08m4Wk~MV$)@xid<$_lC-OFk+a*?j74Npjm^uUpkPP~)k#_56yUq0ap?pU3 zS5mZ_^@~~XG4b{Z12^;*_g@>>!u*nSEXt^@o@_lLyz#q?;*{E>%eft{SHAFT57S1R zS28S=yRlcTKL*J|v|&G4FE1RvppT}Dssv!qD%S-By{KUQUy-pqE&{0dujW6<-MUoFEvzxCN4o88CJ w=Dd@g1wVOM7lREIC-kCzbu`FkR!gSd38zoXd2(p59qt!tlS%$p|9=tqA8aU9!~g&Q literal 0 HcmV?d00001 From 8ea39c1071f743f334cb4af3216112ca2e2efbee Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 15:47:20 -0500 Subject: [PATCH 045/288] Add POI read/write parity tests for all supported versions --- .../java/net/querz/mca/PoiMCAFileTest.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/test/java/net/querz/mca/PoiMCAFileTest.java b/src/test/java/net/querz/mca/PoiMCAFileTest.java index 90c25008..f70210e7 100644 --- a/src/test/java/net/querz/mca/PoiMCAFileTest.java +++ b/src/test/java/net/querz/mca/PoiMCAFileTest.java @@ -7,6 +7,26 @@ public class PoiMCAFileTest extends MCAFileBaseTest { + public void testMcaReadWriteParity_1_14_4() { + validateReadWriteParity(DataVersion.JAVA_1_14_4, "1_14_4/poi/r.-1.0.mca", PoiMCAFile.class); + } + + public void testMcaReadWriteParity_1_15_2() { + validateReadWriteParity(DataVersion.JAVA_1_15_2, "1_15_2/poi/r.-1.0.mca", PoiMCAFile.class); + } + + public void testMcaReadWriteParity_1_16_5() { + validateReadWriteParity(DataVersion.JAVA_1_16_5, "1_16_5/poi/r.0.-1.mca", PoiMCAFile.class); + } + + public void testMcaReadWriteParity_1_17_1() { + validateReadWriteParity(DataVersion.JAVA_1_17_1, "1_17_1/poi/r.-3.-2.mca", PoiMCAFile.class); + } + + public void testMcaReadWriteParity_1_18_PRE1() { + validateReadWriteParity(DataVersion.JAVA_1_18_PRE1, "1_18_PRE1/poi/r.-2.-3.mca", PoiMCAFile.class); + } + public void testPoiMca_1_17_1() { PoiMCAFile mca = assertThrowsNoException(() -> MCAUtil.readAuto(copyResourceToTmp("1_17_1/poi/r.-3.-2.mca"))); PoiChunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); @@ -26,8 +46,4 @@ public void testPoiMca_1_17_1() { // it'd be better if we had a bell in this chunk to test a non-zero value here assertEquals(0, recordsByType.get("minecraft:home").get(0).getFreeTickets()); } - - public void testMcaReadWriteParity_1_17_1() { - validateReadWriteParity(DataVersion.JAVA_1_17_1, "1_17_1/poi/r.-3.-2.mca", PoiMCAFile.class); - } } From 7f2ca13191db80d8e5ef311e5f65e2f6eda2f6f0 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 12 Nov 2021 15:51:23 -0500 Subject: [PATCH 046/288] Updating notes about test data --- src/test/resources/ABOUT_TEST_DATA.md | 51 ++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/test/resources/ABOUT_TEST_DATA.md b/src/test/resources/ABOUT_TEST_DATA.md index 4faef583..dc0d9b72 100644 --- a/src/test/resources/ABOUT_TEST_DATA.md +++ b/src/test/resources/ABOUT_TEST_DATA.md @@ -1,3 +1,41 @@ +## Older versions + +### 1.9.4 +has pigs in it, nothing special +Chunks: +- 88 -20 + +### 1.12.2 +has sheep about and various villager shoved in a 2x2 hole + +Chunks: +- 10 11 + +### 1.13.2 +has chickens, horses, and villagers + +Chunks: +- -42 -45 + +### 1.14.4 +has villagers, lecturn, bell, bed. The librarian is bound to the lecturn in the chunk + +Chunks: +- -1 16 + +### 1.15.2 +has a horse, villagers, lecturn, bell, bed (and half a bed). + +Chunks: +- -3 11 + +### 1.16.5 +has an iron golumn, villagers, fletching table, bell +berry bushes + +Chunks: +- 4 -27 + ## 1.17.1 (DV 2730) ### 1_17_1/r.-3.2.mca (seed: -592955240269541309) @@ -6,18 +44,13 @@ Village, with villager with POI of cartography table and bed as well as nether p Chunks: - -65 -42 -## 1.18 21w44a (DV 2845) +## 1.18 Vanilla world height from Y -64 to 320 Region chunk NBT data is not wrapped in the "Level" tag. -### 1_18/r.-1.-1.mca (seed: 9014880806510125335) -Village, with POI entries, and villager - -Chunks: -- -24, -12 -### 1_18/r.15.-9.mca (seed: 9014880806510125335) -3D biomes - lush cave in selected chunk around Y0. +### 1_18_PRE1 +villager, lush caves, beds, bell, workstations, chickens, cat, geode Chunks: -- 483, -263 \ No newline at end of file +- -60 -69 From 83e889c608e34c70510a2ea1c200b94a9c2144f2 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 08:31:54 -0500 Subject: [PATCH 047/288] Adding helpers to CompoundTag get/set FloatTagListAsArray and get/set DoubleTagListAsArray and tests --- .../java/net/querz/nbt/tag/CompoundTag.java | 77 +++++++++++++++++-- .../net/querz/nbt/tag/CompoundTagTest.java | 30 ++++++++ 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index 57ed798d..c46b3f37 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -1,11 +1,6 @@ package net.querz.nbt.tag; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.function.BiConsumer; import net.querz.io.MaxDepthIO; @@ -245,6 +240,38 @@ public long[] getLongArray(String key) { return t == null ? LongArrayTag.ZERO_VALUE : t.getValue(); } + /** + * Convenience function to get a ListTag<FloatTag> as an array of floats. + * @param key name of the ListTag + * @return null if key does not exist; empty array if key exists but list was empty; array of values otherwise + */ + public float[] getFloatTagListAsArray(String key) { + ListTag t = getListTagAutoCast(key); + if (t == null) return null; + List floatTagList = t.getValue(); + float[] floats = new float[floatTagList.size()]; + for (int i = 0; i < floats.length; i++) { + floats[i] = floatTagList.get(i).asFloat(); + } + return floats; + } + + /** + * Convenience function to get a ListTag<DoubleTag> as an array of doubles. + * @param key name of the ListTag + * @return null if key does not exist; empty array if key exists but list was empty; array of values otherwise + */ + public double[] getDoubleTagListAsArray(String key) { + ListTag t = getListTagAutoCast(key); + if (t == null) return null; + List doubleTagList = t.getValue(); + double[] doubles = new double[doubleTagList.size()]; + for (int i = 0; i < doubles.length; i++) { + doubles[i] = doubleTagList.get(i).asDouble(); + } + return doubles; + } + public Tag put(String key, Tag tag) { return getValue().put(Objects.requireNonNull(key), Objects.requireNonNull(tag)); } @@ -300,6 +327,44 @@ public Tag putLongArray(String key, long[] value) { return put(key, new LongArrayTag(value)); } + /** + * Convenience function to set a ListTag<FloatTag> from an array. If values is null then + * the specified key is REMOVED. Provide an empty array to indicate that an empty ListTag is desired. + * @param key name of ListTag + * @param values values to set + * @return new ListTag, or null if values was null + */ + public Tag putFloatArrayAsTagList(String key, float[] values) { + if (values == null) { + remove(key); + return null; + } + ListTag listTag = new ListTag<>(FloatTag.class, values.length); + for (float v : values) { + listTag.addFloat(v); + } + return put(key, listTag); + } + + /** + * Convenience function to set a ListTag<DoubleTag> from an array. If values is null then + * the specified key is REMOVED. Provide an empty array to indicate that an empty ListTag is desired. + * @param key name of ListTag + * @param values values to set + * @return new ListTag, or null if values was null + */ + public Tag putDoubleArrayAsTagList(String key, double[] values) { + if (values == null) { + remove(key); + return null; + } + ListTag listTag = new ListTag<>(DoubleTag.class, values.length); + for (double v : values) { + listTag.addDouble(v); + } + return put(key, listTag); + } + @Override public String valueToString(int maxDepth) { StringBuilder sb = new StringBuilder("{"); diff --git a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java index 91c9a0e4..f1c2de60 100644 --- a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java +++ b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java @@ -386,4 +386,34 @@ public void testDefaultValueGetters() { assertEquals(1.234567981019, ct.getDouble("name", 1.234567981019), 0.5e-12f); assertEquals("hello world", ct.getString("name", "hello world")); } + + public void testPutGetFloatArrayAsTagList() { + CompoundTag tag = new CompoundTag(); + float[] values = new float[] {-1.1f, 0f, 1.1f}; + tag.putFloatArrayAsTagList("name", values); + assertTrue(tag.containsKey("name")); + assertArrayEquals(values, tag.getFloatTagListAsArray("name"), 1e-5f); + + tag.putFloatArrayAsTagList("name", new float[0]); + assertTrue(tag.containsKey("name")); + assertEquals(0, tag.getFloatTagListAsArray("name").length); + + tag.putFloatArrayAsTagList("name", null); // should remove tag + assertFalse(tag.containsKey("name")); + } + + public void testPutGetDoubleArrayAsTagList() { + CompoundTag tag = new CompoundTag(); + double[] values = new double[] {-1.1f, 0f, 1.1f}; + tag.putDoubleArrayAsTagList("name", values); + assertTrue(tag.containsKey("name")); + assertArrayEquals(values, tag.getDoubleTagListAsArray("name"), 1e-5f); + + tag.putDoubleArrayAsTagList("name", new double[0]); + assertTrue(tag.containsKey("name")); + assertEquals(0, tag.getDoubleTagListAsArray("name").length); + + tag.putDoubleArrayAsTagList("name", null); // should remove tag + assertFalse(tag.containsKey("name")); + } } From 0b3b00acbff50f9998e186067e732a2bde45330a Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 09:46:13 -0500 Subject: [PATCH 048/288] Adding ArgValidator utility class. --- .../java/net/querz/util/ArgValidator.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/net/querz/util/ArgValidator.java diff --git a/src/main/java/net/querz/util/ArgValidator.java b/src/main/java/net/querz/util/ArgValidator.java new file mode 100644 index 00000000..fd30b098 --- /dev/null +++ b/src/main/java/net/querz/util/ArgValidator.java @@ -0,0 +1,60 @@ +package net.querz.util; + +public class ArgValidator { + private ArgValidator() { } + + public static T requireValue(T value) { + if (value == null) { + throw new IllegalArgumentException(); + } + return value; + } + + public static String requireNotEmpty(String value) { + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException(); + } + return value; + } + + public static T requireValue(T value, String name) { + if (value == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + return value; + } + + public static String requireNotEmpty(String value, String name) { + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException(name + " must not be null or empty"); + } + return value; + } + + public static void check(boolean condition) { + if (!condition) { + throw new IllegalArgumentException(); + } + } + + public static void check(boolean condition, String description) { + if (!condition) { + throw new IllegalArgumentException(description); + } + } + + + public static T check(T value, boolean condition) { + if (!condition) { + throw new IllegalArgumentException(); + } + return value; + } + + public static T check(T value, boolean condition, String description) { + if (!condition) { + throw new IllegalArgumentException(description); + } + return value; + } +} From ceafc590d89bff9d63447fdf0ea3d8e6de07850e Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 09:47:02 -0500 Subject: [PATCH 049/288] Reordering methods in TagWrapper interface so get comes before update --- src/main/java/net/querz/mca/TagWrapper.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/net/querz/mca/TagWrapper.java b/src/main/java/net/querz/mca/TagWrapper.java index fd603755..b9eaa011 100644 --- a/src/main/java/net/querz/mca/TagWrapper.java +++ b/src/main/java/net/querz/mca/TagWrapper.java @@ -3,11 +3,6 @@ import net.querz.nbt.tag.CompoundTag; public interface TagWrapper { - /** - * Updates the data tag held by this wrapper and returns it. - * @return A reference to the raw CompoundTag this object is based on. - */ - CompoundTag updateHandle(); /** * Provides a reference to the wrapped data tag. @@ -15,4 +10,10 @@ public interface TagWrapper { * @return A reference to the raw CompoundTag this object is based on. */ CompoundTag getHandle(); + + /** + * Updates the data tag held by this wrapper and returns it. + * @return A reference to the raw CompoundTag this object is based on. + */ + CompoundTag updateHandle(); } From 6191a95851b65dcb6ad164cea83635146ff1158b Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 09:50:57 -0500 Subject: [PATCH 050/288] Adding EntityUtil (helpers for uuid tags and normalising yaw and pitch values); added tests for same --- .../net/querz/mca/entities/EntityUtil.java | 80 +++++++++++++++++++ .../querz/mca/entities/EntityUtilTest.java | 74 +++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/main/java/net/querz/mca/entities/EntityUtil.java create mode 100644 src/test/java/net/querz/mca/entities/EntityUtilTest.java diff --git a/src/main/java/net/querz/mca/entities/EntityUtil.java b/src/main/java/net/querz/mca/entities/EntityUtil.java new file mode 100644 index 00000000..a5820b93 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/EntityUtil.java @@ -0,0 +1,80 @@ +package net.querz.mca.entities; + +import net.querz.mca.DataVersion; +import net.querz.nbt.tag.CompoundTag; +import net.querz.util.ArgValidator; + +import java.util.*; + +public class EntityUtil { + + public static final UUID ZERO_UUID = new UUID(0, 0); + + private EntityUtil() {} + + /** + * May return null if tag does not contain expected UUID fields or contains ZERO UUID value. + */ + public static UUID getUuid(int dataVersion, CompoundTag tag) { + ArgValidator.requireValue(tag); + long most; + long least; + if (dataVersion >= DataVersion.JAVA_1_16_0.id()) { + int[] bits = tag.getIntArray("UUID"); + if (bits == null || bits.length != 4) return null; + most = ((long)bits[0] << 32) | ((long)bits[1] & 0xFFFF_FFFFL); + least = ((long)bits[2] << 32) | ((long)bits[3] & 0xFFFF_FFFFL); + } else { + most = tag.getLong("UUIDMost"); + least = tag.getLong("UUIDLeast"); + } + if (most != 0 || least != 0) { + return new UUID(most, least); + } else { + return null; + } + } + + /** + * @param dataVersion controls tag format where 1.16+ stores an int array + * and lesser versions store "most" and "least" longs + * @param tag not null + * @param uuid not null, not ZERO_UUID value + */ + public static void setUuid(int dataVersion, CompoundTag tag, UUID uuid) { + ArgValidator.requireValue(tag, "tag"); + ArgValidator.requireValue(uuid, "uuid"); + ArgValidator.check(!ZERO_UUID.equals(uuid), "zero uuid"); + long most = uuid.getMostSignificantBits(); + long least = uuid.getLeastSignificantBits(); + if (dataVersion >= DataVersion.JAVA_1_16_0.id()) { + int[] bits = new int[4]; + bits[0] = (int) (most >> 32); + bits[1] = (int) (most & 0xFFFF_FFFFL); + bits[2] = (int) (least >> 32); + bits[3] = (int) (least & 0xFFFF_FFFFL); + tag.putIntArray("UUID", bits); + } else { + tag.putLong("UUIDMost", most); + tag.putLong("UUIDLeast", least); + } + } + + /** + * Normalizes the given yaw to be in the range [0..360) by removing excessive rotations. + */ + public static float normalizeYaw(float yaw) { + yaw = yaw % 360; + if (yaw < 0) yaw += 360; + return yaw; + } + + /** + * Clamps the given pitch to to [-90..90] + */ + public static float clampPitch(float pitch) { + if (pitch < -90f) return -90f; + if (pitch > 90f) return 90f; + return pitch; + } +} diff --git a/src/test/java/net/querz/mca/entities/EntityUtilTest.java b/src/test/java/net/querz/mca/entities/EntityUtilTest.java new file mode 100644 index 00000000..bb4ed1ef --- /dev/null +++ b/src/test/java/net/querz/mca/entities/EntityUtilTest.java @@ -0,0 +1,74 @@ +package net.querz.mca.entities; + +import junit.framework.TestCase; +import net.querz.mca.DataVersion; +import net.querz.mca.MCATestCase; +import net.querz.nbt.tag.CompoundTag; + +import java.util.UUID; + +public class EntityUtilTest extends MCATestCase { + + public void testUuidSetGet() { + final UUID uuid = UUID.fromString("7a10303f-dacf-4e35-a8be-1ce2818b0372"); // UUID.randomUUID(); + + // test set and get uuid symmetry + CompoundTag tag = new CompoundTag(); + EntityUtil.setUuid(DataVersion.JAVA_1_14_4.id(), tag, uuid); + assertEquals(tag.toString(), uuid, EntityUtil.getUuid(DataVersion.JAVA_1_14_4.id(), tag)); + + tag = new CompoundTag(); + EntityUtil.setUuid(DataVersion.JAVA_1_17_1.id(), tag, uuid); + assertEquals(tag.toString(), uuid, EntityUtil.getUuid(DataVersion.JAVA_1_17_1.id(), tag)); + + // test no uuid fields produces null + tag = new CompoundTag(); + assertNull(EntityUtil.getUuid(DataVersion.JAVA_1_14_4.id(), tag)); + assertNull(EntityUtil.getUuid(DataVersion.JAVA_1_17_1.id(), tag)); + + // test attempt to set zero uuid + assertThrowsIllegalArgumentException(() -> + EntityUtil.setUuid(DataVersion.JAVA_1_14_4.id(), new CompoundTag(), EntityUtil.ZERO_UUID)); + assertThrowsIllegalArgumentException(() -> + EntityUtil.setUuid(DataVersion.JAVA_1_17_1.id(), new CompoundTag(), EntityUtil.ZERO_UUID)); + + // test attempt to set null + assertThrowsIllegalArgumentException(() -> + EntityUtil.setUuid(DataVersion.JAVA_1_14_4.id(), null, EntityUtil.ZERO_UUID)); + assertThrowsIllegalArgumentException(() -> + EntityUtil.setUuid(DataVersion.JAVA_1_17_1.id(), null, EntityUtil.ZERO_UUID)); + + + // test reading zero uuid produces null + tag = new CompoundTag(); + tag.putLong("UUIDMost", 0); + tag.putLong("UUIDLeast", 0); + assertNull(EntityUtil.getUuid(DataVersion.JAVA_1_14_4.id(), tag)); + + tag = new CompoundTag(); + tag.putIntArray("UUID", new int[] {0, 0, 0, 0}); + assertNull(EntityUtil.getUuid(DataVersion.JAVA_1_17_1.id(), tag)); + } + + public void testNormalizeYaw() { + assertEquals(0f, EntityUtil.normalizeYaw(360f), 1e-4f); + assertEquals(0f, EntityUtil.normalizeYaw(-360f), 1e-4f); + assertEquals(270f, EntityUtil.normalizeYaw(-90f), 1e-4f); + assertEquals(72.654f, EntityUtil.normalizeYaw(360f + 72.654f), 1e-4f); + assertEquals(360f - 72.654f, EntityUtil.normalizeYaw(-72.654f), 1e-4f); + assertEquals(30f, EntityUtil.normalizeYaw(-7 * 360f + 30f), 1e-4f); + assertEquals(330f, EntityUtil.normalizeYaw(-7 * 360f - 30f), 1e-4f); + } + + public void testClampPitch() { + assertEquals(-90f, EntityUtil.clampPitch(-1111f), 1e-4f); + assertEquals(-90f, EntityUtil.clampPitch(-90.001f), 1e-4f); + assertEquals(-89.999f, EntityUtil.clampPitch(-89.999f), 1e-4f); + assertEquals(-0.001f, EntityUtil.clampPitch(-0.001f), 1e-4f); + assertEquals(0f, EntityUtil.clampPitch(0f), 1e-4f); + assertEquals(0.001f, EntityUtil.clampPitch(0.001f), 1e-4f); + assertEquals(89.999f, EntityUtil.clampPitch(89.999f), 1e-4f); + assertEquals(90f, EntityUtil.clampPitch(90.001f), 1e-4f); + assertEquals(90f, EntityUtil.clampPitch(1111f), 1e-4f); + } +} From 34cb4c4f4d8142d59deed260db15a885177a653c Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 09:51:24 -0500 Subject: [PATCH 051/288] Adding IllegalEntityTagException class --- .../entities/IllegalEntityTagException.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/net/querz/mca/entities/IllegalEntityTagException.java diff --git a/src/main/java/net/querz/mca/entities/IllegalEntityTagException.java b/src/main/java/net/querz/mca/entities/IllegalEntityTagException.java new file mode 100644 index 00000000..c3996e80 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/IllegalEntityTagException.java @@ -0,0 +1,19 @@ +package net.querz.mca.entities; + +public class IllegalEntityTagException extends IllegalArgumentException { + public IllegalEntityTagException() { + super(); + } + + public IllegalEntityTagException(String message) { + super(message); + } + + public IllegalEntityTagException(String message, Throwable cause) { + super(message, cause); + } + + public IllegalEntityTagException(Throwable cause) { + super(cause); + } +} From 07d04857536ebc3c3a197652e24898c81e492c35 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 13 Nov 2021 16:31:55 -0500 Subject: [PATCH 052/288] Adding utility functions to CompoundTag putStringsAsTagList and getStringTagListValues to make getting / setting values of ListTag easier --- .../java/net/querz/nbt/tag/CompoundTag.java | 48 +++++++++++++-- .../net/querz/nbt/tag/CompoundTagTest.java | 58 ++++++++++++++++--- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index c46b3f37..7a91b691 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -2,6 +2,7 @@ import java.util.*; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import net.querz.io.MaxDepthIO; @@ -241,7 +242,7 @@ public long[] getLongArray(String key) { } /** - * Convenience function to get a ListTag<FloatTag> as an array of floats. + * Convenience function to get the values from a {@code ListTag} as an array of floats. * @param key name of the ListTag * @return null if key does not exist; empty array if key exists but list was empty; array of values otherwise */ @@ -257,7 +258,7 @@ public float[] getFloatTagListAsArray(String key) { } /** - * Convenience function to get a ListTag<DoubleTag> as an array of doubles. + * Convenience function to get the values from a {@code ListTag} as an array of doubles. * @param key name of the ListTag * @return null if key does not exist; empty array if key exists but list was empty; array of values otherwise */ @@ -272,6 +273,20 @@ public double[] getDoubleTagListAsArray(String key) { return doubles; } + /** + * Convenience function to get the values from a {@code ListTag} as a {@code List} + * @see #putStringsAsTagList(String, List) + * @param key name of the ListTag + * @return null if key does not exist; empty list if key exists but list was empty; list of values otherwise + */ + public List getStringTagListValues(String key) { + ListTag t = getListTagAutoCast(key); + if (t == null) return null; + return t.getValue().stream() + .map(StringTag::getValue) + .collect(Collectors.toList()); + } + public Tag put(String key, Tag tag) { return getValue().put(Objects.requireNonNull(key), Objects.requireNonNull(tag)); } @@ -331,10 +346,10 @@ public Tag putLongArray(String key, long[] value) { * Convenience function to set a ListTag<FloatTag> from an array. If values is null then * the specified key is REMOVED. Provide an empty array to indicate that an empty ListTag is desired. * @param key name of ListTag - * @param values values to set + * @param values values to set (may be one or more floats, or a float[]) * @return new ListTag, or null if values was null */ - public Tag putFloatArrayAsTagList(String key, float[] values) { + public Tag putFloatArrayAsTagList(String key, float... values) { if (values == null) { remove(key); return null; @@ -350,10 +365,10 @@ public Tag putFloatArrayAsTagList(String key, float[] values) { * Convenience function to set a ListTag<DoubleTag> from an array. If values is null then * the specified key is REMOVED. Provide an empty array to indicate that an empty ListTag is desired. * @param key name of ListTag - * @param values values to set + * @param values values to set (may be one or more doubles, or a double[]) * @return new ListTag, or null if values was null */ - public Tag putDoubleArrayAsTagList(String key, double[] values) { + public Tag putDoubleArrayAsTagList(String key, double... values) { if (values == null) { remove(key); return null; @@ -365,6 +380,27 @@ public Tag putDoubleArrayAsTagList(String key, double[] values) { return put(key, listTag); } + /** + * Convenience function to set a {@code ListTag} from a {@code List}. If values is null then + * the specified key is REMOVED. Provide an empty List to indicate that an empty ListTag is desired. + * @see #getStringTagListValues(String) + * @see Arrays#asList(Object[]) + * @param key name of ListTag + * @param values values to set + * @return new ListTag, or null if values was null + */ + public Tag putStringsAsTagList(String key, List values) { + if (values == null) { + remove(key); + return null; + } + ListTag listTag = new ListTag<>(StringTag.class, values.size()); + for (String v : values) { + listTag.addString(v); + } + return put(key, listTag); + } + @Override public String valueToString(int maxDepth) { StringBuilder sb = new StringBuilder("{"); diff --git a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java index f1c2de60..f58e8c14 100644 --- a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java +++ b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java @@ -3,9 +3,7 @@ import net.querz.io.MaxDepthReachedException; import net.querz.NBTTestCase; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotEquals; @@ -394,11 +392,23 @@ public void testPutGetFloatArrayAsTagList() { assertTrue(tag.containsKey("name")); assertArrayEquals(values, tag.getFloatTagListAsArray("name"), 1e-5f); - tag.putFloatArrayAsTagList("name", new float[0]); + // put singleton + tag = new CompoundTag(); + tag.putFloatArrayAsTagList("name", 42.7f); + assertTrue(tag.containsKey("name")); + assertArrayEquals(new float[]{42.7f}, tag.getFloatTagListAsArray("name"), 1e-5f); + + // put empty should create empty + values = new float[0]; + tag = new CompoundTag(); + tag.putFloatArrayAsTagList("name", values); assertTrue(tag.containsKey("name")); assertEquals(0, tag.getFloatTagListAsArray("name").length); - tag.putFloatArrayAsTagList("name", null); // should remove tag + // put null should remove tag + values = null; + tag = new CompoundTag(); + tag.putFloatArrayAsTagList("name", values); assertFalse(tag.containsKey("name")); } @@ -409,11 +419,45 @@ public void testPutGetDoubleArrayAsTagList() { assertTrue(tag.containsKey("name")); assertArrayEquals(values, tag.getDoubleTagListAsArray("name"), 1e-5f); - tag.putDoubleArrayAsTagList("name", new double[0]); + // put singleton + tag = new CompoundTag(); + tag.putDoubleArrayAsTagList("name", 42.7); + assertTrue(tag.containsKey("name")); + assertArrayEquals(new double[]{42.7}, tag.getDoubleTagListAsArray("name"), 1e-5f); + + // put empty should create empty + values = new double[0]; + tag = new CompoundTag(); + tag.putDoubleArrayAsTagList("name", values); assertTrue(tag.containsKey("name")); assertEquals(0, tag.getDoubleTagListAsArray("name").length); - tag.putDoubleArrayAsTagList("name", null); // should remove tag + // put null should remove tag + values = null; + tag = new CompoundTag(); + tag.putDoubleArrayAsTagList("name", values); + assertFalse(tag.containsKey("name")); + } + + public void testPutGetStringsAsTagList() { + CompoundTag tag = new CompoundTag(); + List values = Arrays.asList("abba", "dabba"); + + tag.putStringsAsTagList("name", values); + assertTrue(tag.containsKey("name")); + assertEquals(values, tag.getStringTagListValues("name")); + + // put empty should create empty + values = new ArrayList<>(); + tag = new CompoundTag(); + tag.putStringsAsTagList("name", values); + assertTrue(tag.containsKey("name")); + assertEquals(0, tag.getStringTagListValues("name").size()); + + // put null should remove tag + values = null; + tag = new CompoundTag(); + tag.putStringsAsTagList("name", values); assertFalse(tag.containsKey("name")); } } From 3e77fc3c7dd639a37b95d30a40a7a598156c5572 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:45:11 -0500 Subject: [PATCH 053/288] EntityUtil adding removeUuid and normalizeYaw(double) helpers & tests --- .../net/querz/mca/entities/EntityUtil.java | 21 ++++++++++++ .../querz/mca/entities/EntityUtilTest.java | 33 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/querz/mca/entities/EntityUtil.java b/src/main/java/net/querz/mca/entities/EntityUtil.java index a5820b93..4fd84a13 100644 --- a/src/main/java/net/querz/mca/entities/EntityUtil.java +++ b/src/main/java/net/querz/mca/entities/EntityUtil.java @@ -60,6 +60,18 @@ public static void setUuid(int dataVersion, CompoundTag tag, UUID uuid) { } } + /** + * Removes all UUID value tags from the given tag (for all possible tag names across all version). + * @param tag Tag to modify. + * @return Given tag. + */ + public static CompoundTag removeUuid(CompoundTag tag) { + tag.remove("UUID"); + tag.remove("UUIDMost"); + tag.remove("UUIDLeast"); + return tag; + } + /** * Normalizes the given yaw to be in the range [0..360) by removing excessive rotations. */ @@ -69,6 +81,15 @@ public static float normalizeYaw(float yaw) { return yaw; } + /** + * Normalizes the given yaw to be in the range [0..360) by removing excessive rotations. + */ + public static float normalizeYaw(double yaw) { + yaw = yaw % 360; + if (yaw < 0) yaw += 360; + return (float) yaw; + } + /** * Clamps the given pitch to to [-90..90] */ diff --git a/src/test/java/net/querz/mca/entities/EntityUtilTest.java b/src/test/java/net/querz/mca/entities/EntityUtilTest.java index bb4ed1ef..f72ac7c7 100644 --- a/src/test/java/net/querz/mca/entities/EntityUtilTest.java +++ b/src/test/java/net/querz/mca/entities/EntityUtilTest.java @@ -1,6 +1,5 @@ package net.querz.mca.entities; -import junit.framework.TestCase; import net.querz.mca.DataVersion; import net.querz.mca.MCATestCase; import net.querz.nbt.tag.CompoundTag; @@ -50,7 +49,24 @@ public void testUuidSetGet() { assertNull(EntityUtil.getUuid(DataVersion.JAVA_1_17_1.id(), tag)); } - public void testNormalizeYaw() { + public void testRemoveUuid() { + final UUID uuid = UUID.fromString("7a10303f-dacf-4e35-a8be-1ce2818b0372"); // UUID.randomUUID(); + + // test set and get uuid symmetry + CompoundTag tag = new CompoundTag(); + EntityUtil.setUuid(DataVersion.JAVA_1_14_4.id(), tag, uuid); + EntityUtil.setUuid(DataVersion.JAVA_1_17_1.id(), tag, uuid); + assertTrue(tag.containsKey("UUIDMost")); + assertTrue(tag.containsKey("UUIDLeast")); + assertTrue(tag.containsKey("UUID")); + + EntityUtil.removeUuid(tag); + assertFalse(tag.containsKey("UUIDMost")); + assertFalse(tag.containsKey("UUIDLeast")); + assertFalse(tag.containsKey("UUID")); + } + + public void testNormalizeYaw_floats() { assertEquals(0f, EntityUtil.normalizeYaw(360f), 1e-4f); assertEquals(0f, EntityUtil.normalizeYaw(-360f), 1e-4f); assertEquals(270f, EntityUtil.normalizeYaw(-90f), 1e-4f); @@ -58,6 +74,18 @@ public void testNormalizeYaw() { assertEquals(360f - 72.654f, EntityUtil.normalizeYaw(-72.654f), 1e-4f); assertEquals(30f, EntityUtil.normalizeYaw(-7 * 360f + 30f), 1e-4f); assertEquals(330f, EntityUtil.normalizeYaw(-7 * 360f - 30f), 1e-4f); + assertTrue(Float.isNaN(EntityUtil.normalizeYaw(Float.NaN))); + } + + public void testNormalizeYaw_doubles() { + assertEquals(0f, EntityUtil.normalizeYaw(360d), 1e-4f); + assertEquals(0f, EntityUtil.normalizeYaw(-360d), 1e-4f); + assertEquals(270f, EntityUtil.normalizeYaw(-90d), 1e-4f); + assertEquals(72.654f, EntityUtil.normalizeYaw(360d + 72.654d), 1e-4f); + assertEquals(360f - 72.654f, EntityUtil.normalizeYaw(-72.654d), 1e-4f); + assertEquals(30f, EntityUtil.normalizeYaw(-7d * 360d + 30d), 1e-4f); + assertEquals(330f, EntityUtil.normalizeYaw(-7d * 360d - 30d), 1e-4f); + assertTrue(Float.isNaN(EntityUtil.normalizeYaw(Double.NaN))); } public void testClampPitch() { @@ -70,5 +98,6 @@ public void testClampPitch() { assertEquals(89.999f, EntityUtil.clampPitch(89.999f), 1e-4f); assertEquals(90f, EntityUtil.clampPitch(90.001f), 1e-4f); assertEquals(90f, EntityUtil.clampPitch(1111f), 1e-4f); + assertTrue(Float.isNaN(EntityUtil.clampPitch(Float.NaN))); } } From efa8dce970f5d7010d6a67adee46a32f7d8f3587 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:47:09 -0500 Subject: [PATCH 054/288] RegionChunkBase adding ability to read/write BIOME data from MCA files older than MC 1.13 --- .../java/net/querz/mca/RegionChunkBase.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/querz/mca/RegionChunkBase.java b/src/main/java/net/querz/mca/RegionChunkBase.java index 25bd259f..60060b06 100644 --- a/src/main/java/net/querz/mca/RegionChunkBase.java +++ b/src/main/java/net/querz/mca/RegionChunkBase.java @@ -55,7 +55,15 @@ protected void initReferences(final long loadFlags) { inhabitedTime = level.getLong("InhabitedTime"); lastUpdate = level.getLong("LastUpdate"); if ((loadFlags & BIOMES) != 0) { - biomes = level.getIntArray("Biomes"); + if (dataVersion >= DataVersion.JAVA_1_13_0.id()) { + biomes = level.getIntArray("Biomes"); + } else { + byte[] byteBiomes = level.getByteArray("Biomes"); + biomes = new int[byteBiomes.length]; + for (int i = 0; i < biomes.length; i++) { + biomes[i] = byteBiomes[i]; + } + } if (biomes.length == 0) biomes = null; } if ((loadFlags & HEIGHTMAPS) != 0) { @@ -527,7 +535,16 @@ public CompoundTag updateHandle() { throw new IllegalStateException( String.format("Biomes array must be %d bytes for version %d, array size is %d", requiredSize, dataVersion, biomes.length)); - level.putIntArray("Biomes", biomes); + + if (dataVersion >= DataVersion.JAVA_1_13_0.id()) { + level.putIntArray("Biomes", biomes); + } else { + byte[] byteBiomes = new byte[biomes.length]; + for (int i = 0; i < biomes.length; i++) { + byteBiomes[i] = (byte) biomes[i]; + } + level.putByteArray("Biomes", byteBiomes); + } } level.putIfNotNull("Heightmaps", heightMaps); level.putIfNotNull("CarvingMasks", carvingMasks); From 3c6aa6dad17d3f9f08e3fc8632a7ff82256f99bd Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:47:45 -0500 Subject: [PATCH 055/288] Trivial doc / error message changes to PoiRecord --- src/main/java/net/querz/mca/PoiRecord.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/querz/mca/PoiRecord.java b/src/main/java/net/querz/mca/PoiRecord.java index e3556b44..5f72e24d 100644 --- a/src/main/java/net/querz/mca/PoiRecord.java +++ b/src/main/java/net/querz/mca/PoiRecord.java @@ -26,7 +26,7 @@ * POI mca has evolved to include locations of other block types to optimize game performance - such as improving * nether portal lag by storing portal block locations in the poi files so the game doesn't need to scan every block * in every chunk until it finds a destination portal. - *

    At time of writing, 1.17.1, this class exposes all poi record fields. For now, there is no support for + *

    At time of writing, 1.14 to 1.17.1, this class exposes all poi record fields. For now, there is no support for * reading or storing extra fields which this class does not wrap.

    *

    POI types As of 1.17 *

      @@ -135,7 +135,7 @@ public PoiRecord(int x, int y, int z, String type, int freeTickets) { private String validateType(String type) { if (type == null || type.isEmpty()) { - throw new IllegalArgumentException("poi type must not be null"); + throw new IllegalArgumentException("poi type must not be null or empty"); } return type; } From 4acc2838d5ba204217a2a856b91fd45fff2c3e92 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:48:31 -0500 Subject: [PATCH 056/288] Add class javadoc for PoiChunkBase --- src/main/java/net/querz/mca/PoiChunkBase.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/net/querz/mca/PoiChunkBase.java b/src/main/java/net/querz/mca/PoiChunkBase.java index 0e6fa2c2..026709d2 100644 --- a/src/main/java/net/querz/mca/PoiChunkBase.java +++ b/src/main/java/net/querz/mca/PoiChunkBase.java @@ -9,6 +9,11 @@ import java.util.stream.Stream; import static net.querz.mca.LoadFlags.*; +/** + * Provides all the basic functionality necessary for this type of chunk with abstraction hooks + * making it easy to extend this class and modify the factory behavior of {@link MCAUtil} to create + * instances of your custom class. + */ public abstract class PoiChunkBase extends ChunkBase implements Collection { // private to preserve the ability to change how records are stored to optimize lookups later private List records; From a80996bd160dae70a9dfde0b79b676b70b5bae62 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:48:58 -0500 Subject: [PATCH 057/288] Add copy constructor test for PoiRecord --- src/test/java/net/querz/mca/PoiRecordTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/net/querz/mca/PoiRecordTest.java b/src/test/java/net/querz/mca/PoiRecordTest.java index 244e4533..6b169dc9 100644 --- a/src/test/java/net/querz/mca/PoiRecordTest.java +++ b/src/test/java/net/querz/mca/PoiRecordTest.java @@ -21,6 +21,16 @@ public void testConstructor_CompoundTag() { assertEquals(1777, record.getZ()); } + public void testCopyConstructor() { + CompoundTag tag = makeTag(3, "minecraft:test", 1_234_679, 315, -8_546_776); + PoiRecord recordA = new PoiRecord(tag); + PoiRecord recordB = new PoiRecord(recordA); + assertEquals(recordA, recordB); + assertEquals(recordA.getFreeTickets(), recordB.getFreeTickets()); + assertEquals(recordA.getHandle(), recordB.getHandle()); + assertNotSame(recordA.getHandle(), recordB.getHandle()); + } + public void testUpdateHandle() { CompoundTag tag = makeTag(0, "minecraft:test", 1_234_679, 315, -8_546_776); PoiRecord record = new PoiRecord(tag); From 1e4b9b48aca2c6f867af6357e2c3e27891f563c0 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:50:28 -0500 Subject: [PATCH 058/288] NBTTestCase - fix assertEquals argument order within assertThrowsException so the expected exception type is in the "expected" position. --- src/test/java/net/querz/NBTTestCase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 2a64306e..25288780 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -113,7 +113,6 @@ protected T invokeGetValue(Tag tag) { return null; } - protected void assertThrowsIllegalArgumentException(ExceptionRunnable r) { assertThrowsException(r, IllegalArgumentException.class); } @@ -130,7 +129,7 @@ protected void assertThrowsException(ExceptionRunnable if (printStackTrace) { ex.printStackTrace(); } - TestCase.assertEquals(ex.getClass(), e); + TestCase.assertEquals(e, ex.getClass()); } } From a2a2c3da765d83130b44669531f49c9fe081639c Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 10:53:00 -0500 Subject: [PATCH 059/288] MCAFileBase - adding DataValue enum getter/setter helpers to manipulate the default data version --- src/main/java/net/querz/mca/MCAFileBase.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/net/querz/mca/MCAFileBase.java b/src/main/java/net/querz/mca/MCAFileBase.java index 2226f0af..6f36007b 100644 --- a/src/main/java/net/querz/mca/MCAFileBase.java +++ b/src/main/java/net/querz/mca/MCAFileBase.java @@ -93,6 +93,10 @@ public int getDefaultChunkDataVersion() { return defaultDataVersion; } + public DataVersion getDefaultChunkDataVersionEnum() { + return DataVersion.bestFor(defaultDataVersion); + } + /** * Set chunk version which will be used when automatically creating new chunks * and for chunks created by {@link #createChunk()}. @@ -101,6 +105,10 @@ public void setDefaultChunkDataVersion(int defaultDataVersion) { this.defaultDataVersion = defaultDataVersion; } + public void setDefaultChunkDataVersion(DataVersion defaultDataVersion) { + this.defaultDataVersion = defaultDataVersion.id(); + } + /** * @return The x-value currently set for this mca file in region coordinates. */ From a82c4228e05a3863464a761d9e2483c3433f06d3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 13:40:09 -0500 Subject: [PATCH 060/288] Adding utility classes for 2d rectangular bounding box logic - namely for providing the ability to support setting absolute coords from outside a chunk as absolute coords inside that chunk... see ChunkBoundingRectangle#relocateX & Z --- .../util/BlockAlignedBoundingRectangle.java | 51 ++ .../querz/util/ChunkBoundingRectangle.java | 58 ++ .../mca/entities/EntityBaseImplTest.java | 719 ++++++++++++++++++ .../querz/mca/entities/EntityFactoryTest.java | 225 ++++++ 4 files changed, 1053 insertions(+) create mode 100644 src/main/java/net/querz/util/BlockAlignedBoundingRectangle.java create mode 100644 src/main/java/net/querz/util/ChunkBoundingRectangle.java create mode 100644 src/test/java/net/querz/mca/entities/EntityBaseImplTest.java create mode 100644 src/test/java/net/querz/mca/entities/EntityFactoryTest.java diff --git a/src/main/java/net/querz/util/BlockAlignedBoundingRectangle.java b/src/main/java/net/querz/util/BlockAlignedBoundingRectangle.java new file mode 100644 index 00000000..636d39a1 --- /dev/null +++ b/src/main/java/net/querz/util/BlockAlignedBoundingRectangle.java @@ -0,0 +1,51 @@ +package net.querz.util; + +public class BlockAlignedBoundingRectangle { + final int widthXZ; + + final int minX; + final int minZ; + final int maxX; // exclusive + final int maxZ; // exclusive + + final double minXd; + final double minZd; + final double maxXd; // exclusive + final double maxZd; // exclusive + + public int getMinX() { + return minX; + } + + public int getMinZ() { + return minZ; + } + + public int getMaxX() { + return maxX; + } + + public int getMaxZ() { + return maxZ; + } + + public int getWidthXZ() { + return widthXZ; + } + + public BlockAlignedBoundingRectangle(int minX, int minZ, int widthXZ) { + this.minXd = this.minX = minX; + this.minZd = this.minZ = minZ; + this.maxXd = this.maxX = minX + widthXZ; + this.maxZd = this.maxZ = minZ + widthXZ; + this.widthXZ = widthXZ; + } + + public boolean contains(int x, int z) { + return minX <= x && x < maxX && minZ <= z && z < maxZ; + } + + public boolean contains(double x, double z) { + return minXd <= x && x < maxXd && minZd <= z && z < maxZd; + } +} diff --git a/src/main/java/net/querz/util/ChunkBoundingRectangle.java b/src/main/java/net/querz/util/ChunkBoundingRectangle.java new file mode 100644 index 00000000..aaa49ac1 --- /dev/null +++ b/src/main/java/net/querz/util/ChunkBoundingRectangle.java @@ -0,0 +1,58 @@ +package net.querz.util; + +public class ChunkBoundingRectangle extends BlockAlignedBoundingRectangle { + protected final int chunkX; + protected final int chunkZ; + + public ChunkBoundingRectangle(int chunkX, int chunkZ) { + super(chunkX << 4, chunkZ << 4, 16); + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + /** + * Calculates a new absolute X such that the given absolute X is inside this chunk + * at the same relative location as the given X was relative to its source chunk. + * + * @param x in block coordinates + * @return an X within the bounds of this chunk + */ + public int relocateX(int x) { + return minX | (x & 0xF); + } + + /** + * Calculates a new absolute Z such that the given absolute Z is inside this chunk + * at the same relative location as the given Z was relative to its source chunk. + * + * @param z in block coordinates + * @return an Z within the bounds of this chunk + */ + public int relocateZ(int z) { + return minZ | (z & 0xF); + } + + /** + * Calculates a new absolute X such that the given absolute X is inside this chunk + * at the same relative location as the given X was relative to its source chunk. + * + * @param x in block coordinates + * @return an X within the bounds of this chunk + */ + public double relocateX(double x) { + double bin = x % 16; + return (bin >= 0 ? minX : maxX) + bin; + } + + /** + * Calculates a new absolute Z such that the given absolute Z is inside this chunk + * at the same relative location as the given Z was relative to its source chunk. + * + * @param z in block coordinates + * @return an Z within the bounds of this chunk + */ + public double relocateZ(double z) { + double bin = z % 16; + return (bin >= 0 ? minZ : maxZ) + bin; + } +} diff --git a/src/test/java/net/querz/mca/entities/EntityBaseImplTest.java b/src/test/java/net/querz/mca/entities/EntityBaseImplTest.java new file mode 100644 index 00000000..feba4050 --- /dev/null +++ b/src/test/java/net/querz/mca/entities/EntityBaseImplTest.java @@ -0,0 +1,719 @@ +package net.querz.mca.entities; + +import net.querz.mca.DataVersion; +import net.querz.mca.MCATestCase; +import net.querz.nbt.tag.CompoundTag; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.UUID; + +public class EntityBaseImplTest extends MCATestCase { + + // + + protected CompoundTag makeEntityTag(String id) { + CompoundTag tag = new CompoundTag(); + tag.putString("id", id); + return tag; + } + + protected CompoundTag setBoolean(CompoundTag tag, String name, Boolean value) { + if (value == null) tag.remove(name); + else tag.putBoolean(name, value); + return tag; + } + + protected CompoundTag setInvulnerable(CompoundTag tag, Boolean value) { + return setBoolean(tag, "Invulnerable", value); + } + + protected CompoundTag setSilent(CompoundTag tag, Boolean value) { + return setBoolean(tag, "Silent", value); + } + + protected CompoundTag setGlowing(CompoundTag tag, Boolean value) { + return setBoolean(tag, "Glowing", value); + } + + protected CompoundTag setPos(CompoundTag tag, double x, double y, double z) { + tag.putDoubleArrayAsTagList("Pos", x, y, z); + return tag; + } + + protected CompoundTag setMotion(CompoundTag tag, double dx, double dy, double dz) { + tag.putDoubleArrayAsTagList("Motion", dx, dy, dz); + return tag; + } + + protected CompoundTag setRotation(CompoundTag tag, float yaw, float pitch) { + tag.putFloatArrayAsTagList("Rotation", yaw, pitch); + return tag; + } + + // + + // + + protected void assertPositionEquals(EntityBaseImpl entity, double expectX, double expectY, double expectZ) { + // assertEquals(double...) takes care of non-finite equality checking too! + assertEquals(expectX, entity.getX(), 1e-4); + assertEquals(expectY, entity.getY(), 1e-4); + assertEquals(expectZ, entity.getZ(), 1e-4); + } + + protected void assertMotionEquals(EntityBaseImpl entity, double expectDX, double expectDY, double expectDZ) { + // assertEquals(double...) takes care of non-finite equality checking too! + assertEquals(expectDX, entity.getMotionDX(), 1e-7); + assertEquals(expectDY, entity.getMotionDY(), 1e-7); + assertEquals(expectDZ, entity.getMotionDZ(), 1e-7); + } + + // sets values for everything but passenger and UUID + protected CompoundTag makeTestEntityTag() { + CompoundTag tag = makeEntityTag("pig"); + setPos(tag, 1420.276, 71.0, -317.416); + setRotation(tag, 346.4548f, -40f); + setMotion(tag, -0.01312, 0.117600, 0.05052); + tag.putShort("Air", (short) 147); + tag.putString("CustomName", "{\"text\":\"bob\"}"); + tag.putFloat("FallDistance", 23.787f); + tag.putShort("Fire", (short) -25); + tag.putInt("PortalCooldown", 96); + tag.putStringsAsTagList("Tags", Arrays.asList("T1", "another_one")); + tag.putInt("TicksFrozen", 291); + + tag.putBoolean("CustomNameVisible", true); + tag.putBoolean("Glowing", true); + tag.putBoolean("HasVisualFire", true); + tag.putBoolean("Invulnerable", true); + tag.putBoolean("NoGravity", true); + tag.putBoolean("OnGround", true); + tag.putBoolean("Silent", true); + + return tag; + } + + // + + // + + public void testConstructor_allTags() { + // Constructor for tags containing passengers transitively tested by #testUpdateHandle_withPassengers() + CompoundTag tag = makeTestEntityTag(); + UUID uuid = UUID.randomUUID(); + EntityUtil.setUuid(DataVersion.latest().id(), tag, uuid); + CompoundTag originalTagCopy = tag.clone(); + + EntityBaseImpl entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertEquals(DataVersion.latest().id(), entity.getDataVersion()); + assertEquals("pig", entity.getId()); + assertEquals(uuid, entity.getUuid()); + + assertEquals(1420.276, entity.getX(), 1e-8); + assertEquals(71.0, entity.getY(), 1e-8); + assertEquals(-317.416, entity.getZ(), 1e-8); + + assertEquals(346.4548f, entity.getRotationYaw(), 1e-5f); + assertEquals(-40f, entity.getRotationPitch(), 1e-5f); + + assertEquals(-0.01312, entity.getMotionDX(), 1e-8); + assertEquals(0.117600, entity.getMotionDY(), 1e-8); + assertEquals(0.05052, entity.getMotionDZ(), 1e-8); + + assertEquals((short) 147, entity.getAir()); + assertEquals("{\"text\":\"bob\"}", entity.getCustomName()); + assertEquals(23.787f, entity.getFallDistance(), 1e-5f); + assertEquals((short) -25, entity.getFire()); + + assertEquals(96, entity.getPortalCooldown()); + assertEquals(Arrays.asList("T1", "another_one"), entity.getScoreboardTags()); + assertEquals(291, entity.getTicksFrozen()); + + assertTrue(entity.isCustomNameVisible()); + assertTrue(entity.isGlowing()); + assertTrue(entity.hasVisualFire()); + assertTrue(entity.isInvulnerable()); + assertTrue(entity.hasNoGravity()); + assertTrue(entity.isOnGround()); + assertTrue(entity.isSilent()); + + assertSame(tag, entity.getHandle()); + assertNotSame(originalTagCopy, entity.getHandle()); + assertEquals(originalTagCopy, entity.updateHandle()); + + // now to check for copy-paste errors on booleans + tag.putBoolean("CustomNameVisible", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.isCustomNameVisible()); + + tag.putBoolean("Glowing", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.isGlowing()); + + tag.putBoolean("HasVisualFire", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.hasVisualFire()); + + tag.putBoolean("Invulnerable", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.isInvulnerable()); + + tag.putBoolean("NoGravity", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.hasNoGravity()); + + tag.putBoolean("OnGround", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.isOnGround()); + + tag.putBoolean("Silent", false); + entity = new EntityBaseImpl(tag, DataVersion.latest().id()); + assertFalse(entity.isSilent()); + } + + public void testCopyConstructor_withStackedPassengers() { + EntityBaseImpl pig = new EntityBaseImpl(makeTestEntityTag(), DataVersion.latest().id()); + pig.setPosition(12, 65, -44); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + UUID pigUuid = pig.generateNewUuid(); + UUID chickenUuid = chicken.generateNewUuid(); + UUID zombieUuid = zombie.generateNewUuid(); + chicken.addPassenger(zombie); + pig.addPassenger(chicken); + + EntityBaseImpl pig2 = pig.clone(); + assertNotEquals(pigUuid, pig2.getUuid()); + assertTrue(pig2.hasPassengers()); + assertEquals(1, pig2.getPassengers().size()); + + EntityBaseImpl chicken2 = (EntityBaseImpl) pig2.getPassengers().get(0); + assertNotEquals(chickenUuid, chicken2.getUuid()); + assertTrue(chicken2.hasPassengers()); + assertEquals(1, chicken2.getPassengers().size()); + + EntityBaseImpl zombie2 = (EntityBaseImpl) chicken2.getPassengers().get(0); + assertNotEquals(zombieUuid, zombie2.getUuid()); + assertFalse(zombie2.hasPassengers()); + } + + // + + // + + public void testUpdateHandle_validPositionRequired() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + assertThrowsException(entity::updateHandle, IllegalStateException.class); + entity.setPosition(0, 0, Double.NaN); + assertThrowsException(entity::updateHandle, IllegalStateException.class); + entity.setPosition(0, Double.NaN, 0); + assertThrowsException(entity::updateHandle, IllegalStateException.class); + entity.setPosition(Double.NaN, 0, 0); + assertThrowsException(entity::updateHandle, IllegalStateException.class); + entity.setPosition(0, 0, 0); + assertThrowsNoException(entity::updateHandle); + } + + public void testUpdateHandle_rotationTagNotOutputUnlessRotationIsValid() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + entity.setRotation(0, Float.NaN); + CompoundTag tag = entity.updateHandle(); + assertFalse(tag.containsKey("Rotation")); + + entity.setRotation(Float.NaN, 0); + tag = entity.updateHandle(); + assertFalse(tag.containsKey("Rotation")); + + entity.setRotation(0, 0); + tag = entity.updateHandle(); + assertTrue(tag.containsKey("Rotation")); + } + + public void testUpdateHandle_motionTagNotOutputUnlessMotionIsValid() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + entity.setMotion(0, 0, Double.NaN); + CompoundTag tag = entity.updateHandle(); + assertFalse(tag.containsKey("Motion")); + + entity.setMotion(0, Double.NaN, 0);tag = entity.updateHandle(); + assertFalse(tag.containsKey("Motion")); + + entity.setMotion(Double.NaN, 0, 0); + tag = entity.updateHandle(); + assertFalse(tag.containsKey("Motion")); + + entity.setMotion(0, 0, 0); + tag = entity.updateHandle(); + assertTrue(tag.containsKey("Motion")); + } + + public void testUpdateHandle_airTagNotOutputWhenAirUnset() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + CompoundTag tag = entity.updateHandle(); + assertFalse(tag.containsKey("Air")); + + entity.setAir((short) 500); + tag = entity.updateHandle(); + assertTrue(tag.containsKey("Air")); + + entity.setAir(EntityBase.AIR_UNSET); + tag = entity.updateHandle(); + assertFalse(tag.containsKey("Air")); + } + + public void testUpdateHandle_uuidGeneratedWhenUnset() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + assertNull(entity.getUuid()); + CompoundTag tag = entity.updateHandle(); + assertTrue(tag.containsKey("UUID")); + assertNotNull(entity.getUuid()); + } + + public void testUpdateHandle_withPassengers() { + // transitively tests constructor with passengers + EntityBaseImpl pig = new EntityBaseImpl(makeTestEntityTag(), DataVersion.latest().id()); + pig.setPosition(12, 65, -44); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + EntityBaseImpl skeleton = new EntityBaseImpl(DataVersion.latest().id(), "skeleton"); + pig.addPassenger(chicken); + assertTrue(chicken.isPositionValid()); + pig.addPassenger(skeleton); + assertTrue(skeleton.isPositionValid()); + chicken.addPassenger(zombie); + assertTrue(zombie.isPositionValid()); + + CompoundTag pigTag = pig.updateHandle(); + + assertTrue(pigTag.containsKey("Passengers")); + assertEquals(2, pigTag.getListTag("Passengers").asCompoundTagList().size()); + CompoundTag chickenTag = pigTag.getListTag("Passengers").asCompoundTagList().get(0); + CompoundTag skeletonTag = pigTag.getListTag("Passengers").asCompoundTagList().get(1); + + assertTrue(chickenTag.containsKey("Passengers")); + assertEquals(1, chickenTag.getListTag("Passengers").asCompoundTagList().size()); + CompoundTag zombieTag = chickenTag.getListTag("Passengers").asCompoundTagList().get(0); + + assertFalse(skeletonTag.containsKey("Passengers")); + assertFalse(zombieTag.containsKey("Passengers")); + + assertNotNull(pig.getUuid()); + assertEquals(pig.getUuid(), EntityUtil.getUuid(DataVersion.latest().id(), pigTag)); + assertEquals("pig", pigTag.getString("id")); + + assertNotNull(chicken.getUuid()); + assertEquals(chicken.getUuid(), EntityUtil.getUuid(DataVersion.latest().id(), chickenTag)); + assertEquals("chicken", chickenTag.getString("id")); + + assertNotNull(skeleton.getUuid()); + assertEquals(skeleton.getUuid(), EntityUtil.getUuid(DataVersion.latest().id(), skeletonTag)); + assertEquals("skeleton", skeletonTag.getString("id")); + + assertNotNull(zombie.getUuid()); + assertEquals(zombie.getUuid(), EntityUtil.getUuid(DataVersion.latest().id(), zombieTag)); + assertEquals("zombie", zombieTag.getString("id")); + } + + // + + // + + public void testPassengers_cannotSetSelfAsPassenger() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + assertThrowsIllegalArgumentException(() -> entity.addPassenger(entity)); + assertNull(entity.getPassengers()); + } + + public void testPassengers_cannotAddNullPassenger() { + EntityBaseImpl entity = new EntityBaseImpl(DataVersion.latest().id(), "zombie", 0, 0, 0); + assertThrowsIllegalArgumentException(() -> entity.addPassenger(null)); + assertNull(entity.getPassengers()); + } + + + public void testPassengers_addPassengerSetsRiderPositionIfUnset() { + EntityBaseImpl spider = new EntityBaseImpl(DataVersion.latest().id(), "spider", 42.743, 68, -96.23); + spider.setMotion(0.1, -0.05, 0.008); + EntityBaseImpl skeleton = new EntityBaseImpl(DataVersion.latest().id(), "skeleton"); + assertFalse(skeleton.isPositionValid()); + spider.addPassenger(skeleton); + assertTrue(skeleton.isPositionValid()); + assertEquals(42.743, skeleton.getX(), 1e-8); + assertEquals(68, skeleton.getY(), 1e-8); + assertEquals(-96.23, skeleton.getZ(), 1e-8); + + // also copies motion + assertEquals(0.1, skeleton.getMotionDX(), 1e-8); + assertEquals(-0.05, skeleton.getMotionDY(), 1e-8); + assertEquals(0.008, skeleton.getMotionDZ(), 1e-8); + } + + // + + // + + public void testSetPosition_basic() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setPosition(19.123, 68.5, -47.89); + assertPositionEquals(pig, 19.123, 68.5, -47.89); + + pig.setX(17.45); + assertPositionEquals(pig, 17.45, 68.5, -47.89); + + pig.setY(65.78); + assertPositionEquals(pig, 17.45, 65.78, -47.89); + + pig.setZ(-42.111); + assertPositionEquals(pig, 17.45, 65.78, -42.111); + } + + public void testSetPosition_withPassengersHavingPositions() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setPosition(19.123, 68.5, -47.89); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + chicken.setPosition(19.456, 68.85, -47.87); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + zombie.setPosition(19.789, 69.15, -47.86); + chicken.addPassenger(zombie); + pig.addPassenger(chicken); + + assertPositionEquals(pig, 19.123, 68.5, -47.89); + assertPositionEquals(chicken, 19.456, 68.85, -47.87); + assertPositionEquals(zombie, 19.789, 69.15, -47.86); + + // passengers should move with their mounts + pig.setPosition(0, 0, 0); + assertPositionEquals(pig, 0, 0, 0); + assertPositionEquals(chicken, 19.456 - 19.123, 68.85 - 68.5, -47.87 + 47.89); + assertPositionEquals(zombie, 19.789 - 19.123, 69.15 - 68.5, -47.86 + 47.89); + } + + public void testSetPosition_withPassengersHavingInvalidPositions() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + pig.addPassenger(chicken); + chicken.addPassenger(zombie); + assertFalse(pig.isPositionValid()); + assertFalse(chicken.isPositionValid()); + assertFalse(zombie.isPositionValid()); + + // setting mount position should also set passengers + pig.setPosition(19.123, 68.5, -47.89); + assertTrue(pig.isPositionValid()); + assertTrue(chicken.isPositionValid()); + assertTrue(zombie.isPositionValid()); + assertPositionEquals(pig, 19.123, 68.5, -47.89); + assertPositionEquals(chicken, 19.123, 68.5, -47.89); + assertPositionEquals(zombie, 19.123, 68.5, -47.89); + + // should not move mount position, but should move passenger to also all NaN + chicken.setPosition(Double.NaN, Double.NaN, Double.NaN); + assertTrue(pig.isPositionValid()); + assertFalse(chicken.isPositionValid()); + assertFalse(zombie.isPositionValid()); + assertPositionEquals(pig, 19.123, 68.5, -47.89); + assertPositionEquals(chicken, Double.NaN, Double.NaN, Double.NaN); + assertPositionEquals(zombie, Double.NaN, Double.NaN, Double.NaN); + + // test the rest using XYZ setters to avoid a whitebox testing trap in case someday the impl changes + + zombie.setPosition(pig.getX(), pig.getY(), pig.getZ()); + pig.setX(0); + assertPositionEquals(pig, 0, 68.5, -47.89); + assertPositionEquals(chicken, 0, Double.NaN, Double.NaN); + assertPositionEquals(zombie, 0, 68.5, -47.89); + + pig.setY(Double.NaN); + assertPositionEquals(pig, 0, Double.NaN, -47.89); + assertPositionEquals(chicken, 0, Double.NaN, Double.NaN); + assertPositionEquals(zombie, 0, Double.NaN, -47.89); + + pig.setY(42); + assertPositionEquals(pig, 0, 42, -47.89); + assertPositionEquals(chicken, 0, 42, Double.NaN); + assertPositionEquals(zombie, 0, 42, -47.89); + + zombie.setZ(-47.5); + assertPositionEquals(pig, 0, 42, -47.89); + assertPositionEquals(chicken, 0, 42, Double.NaN); + assertPositionEquals(zombie, 0, 42, -47.5); + + pig.setZ(0); + assertPositionEquals(pig, 0, 42,0); + assertPositionEquals(chicken, 0, 42, 0); + assertPositionEquals(zombie, 0, 42, 0); + } + + public void testMovePosition_basic() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setPosition(19.123, 68.5, -47.89); + assertPositionEquals(pig, 19.123, 68.5, -47.89); + pig.movePosition(-7, -1, 4); + assertPositionEquals(pig, 19.123 - 7, 68.5 - 1, -47.89 + 4); + } + + public void testMovePosition_cannotMoveInvalidPosition() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + assertThrowsException(() -> pig.movePosition(-7, -1, 4), IllegalStateException.class); + pig.setX(0); + assertThrowsException(() -> pig.movePosition(-7, -1, 4), IllegalStateException.class); + pig.setY(0); + assertThrowsException(() -> pig.movePosition(-7, -1, 4), IllegalStateException.class); + pig.setZ(0); + assertThrowsNoException(() -> pig.movePosition(-7, -1, 4)); + assertPositionEquals(pig, -7, -1, 4); + } + + public void testMovePosition_withPassengers() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + chicken.addPassenger(zombie); + pig.addPassenger(chicken); + + pig.setPosition(0, 0, 0); + // add offsets + chicken.movePosition(0.1, 0.5, 0); + zombie.movePosition(0, 0.15, -0.1); + // check everyone is where they should be + assertPositionEquals(pig, 0, 0, 0); + assertPositionEquals(chicken, 0.1, 0.5, 0); + assertPositionEquals(zombie, 0.1, 0.65, -0.1); + + // move the root pig + pig.movePosition(-7, 80, 4); + assertPositionEquals(pig, -7, 80, 4); + // check that offsets are preserved + assertPositionEquals(chicken, -6.9, 80.5, 4); + assertPositionEquals(zombie, -6.9, 80.65, 3.9); + + // now with some invalid positions in the mix + pig.setPosition(0, 0, 0); + chicken.setPosition(Double.NaN, Double.NaN, Double.NaN); + zombie.setPosition(-1, 0.5, 1); + + pig.movePosition(-7, -1, 4); + assertPositionEquals(pig, -7, -1, 4); + assertPositionEquals(chicken, -7, -1, 4); + assertPositionEquals(zombie, -7, -1, 4); + } + + // + + // + + public void testSetRotation_basic() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setRotation(73.23f, -11.55f); + assertEquals(73.23f, pig.getRotationYaw(), 1e-3); + assertEquals(-11.55f, pig.getRotationPitch(), 1e-3); + + // also checks yaw normalization + pig.setRotationYaw(-30.78f - 720); + assertEquals(360 - 30.78f, pig.getRotationYaw(), 1e-3); + + pig.setRotationPitch(45.976f); + assertEquals(45.976f, pig.getRotationPitch(), 1e-3); + + // pitch clamping + pig.setRotationPitch(145.976f); + assertEquals(90f, pig.getRotationPitch(), 1e-3); + pig.setRotationPitch(-145.976f); + assertEquals(-90f, pig.getRotationPitch(), 1e-3); + } + + public void testSetRotation_doesNotAffectPassengers() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setRotation(30f, 45f); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + chicken.setRotation(51f, -17f); + pig.addPassenger(chicken); + assertEquals(30f, pig.getRotationYaw(), 1e-3); + assertEquals(51f, chicken.getRotationYaw(), 1e-3); + assertEquals(45f, pig.getRotationPitch(), 1e-3); + assertEquals(-17f, chicken.getRotationPitch(), 1e-3); + + pig.setRotationYaw(32f); + assertEquals(32f, pig.getRotationYaw(), 1e-3); // changed + assertEquals(51f, chicken.getRotationYaw(), 1e-3); // unchanged + + chicken.setRotationYaw(52f); + assertEquals(32f, pig.getRotationYaw(), 1e-3); // unchanged + assertEquals(52f, chicken.getRotationYaw(), 1e-3); // changed + + pig.setRotationPitch(60f); + assertEquals(60f, pig.getRotationPitch(), 1e-3); // changed + assertEquals(-17f, chicken.getRotationPitch(), 1e-3); // unchanged + + chicken.setRotationPitch(20f); + assertEquals(60f, pig.getRotationPitch(), 1e-3); // unchanged + assertEquals(20f, chicken.getRotationPitch(), 1e-3); // changed + } + + public void testCardinalAngleHelpers() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setFacingCardinalAngle(0); + assertEquals(0f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(180f, pig.getRotationYaw(), 1e-4f); + + pig.setFacingCardinalAngle(90); + assertEquals(90f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(270f, pig.getRotationYaw(), 1e-4f); + + pig.setFacingCardinalAngle(179); + assertEquals(179f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(359f, pig.getRotationYaw(), 1e-4f); + + pig.setFacingCardinalAngle(181); + assertEquals(181f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(1f, pig.getRotationYaw(), 1e-4f); + + pig.setFacingCardinalAngle(361); + assertEquals(1f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(181f, pig.getRotationYaw(), 1e-4f); + + pig.setFacingCardinalAngle(-90); + assertEquals(270f, pig.getFacingCardinalAngle(), 1e-4f); + assertEquals(90f, pig.getRotationYaw(), 1e-4f); + } + + public void testRotate_givenAngleMustBeFinite() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + assertThrowsIllegalArgumentException(() -> pig.rotate(Float.POSITIVE_INFINITY)); + } + + public void testRotate_passingHighExponentArgDoesNotSquashExistingYaw() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setRotationYaw(213.87139458f); + pig.rotate((float)Long.MAX_VALUE); + float yaw = pig.getRotationYaw(); + float fraction = yaw - ((int) yaw); + assertEquals(0.87139458f, fraction, 1e-4); + + pig.setRotationYaw(213.87139458f); + pig.rotate(100000000000.12); + assertEquals(133.99139458f, pig.getRotationYaw(), 1e-3); // yes i did the math by hand to get this number + } + + public void testRotate_noPassengers() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + pig.setRotationYaw(Float.NaN); + assertThrowsException(() -> pig.rotate(42), IllegalStateException.class); + pig.setRotationYaw(10); + pig.rotate(30); + assertEquals(40f, pig.getRotationYaw(), 1e-4f); + pig.rotate(-45); + assertEquals(355f, pig.getRotationYaw(), 1e-4f); + } + + public void testRotate_rotatesPassengers() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + pig.setRotationYaw(90f); + chicken.setRotationYaw(-15f); + zombie.setRotationYaw(22.5f); + + // validate that adding passengers doesn't rotate them based on mount yaw + pig.addPassenger(chicken); + chicken.addPassenger(zombie); + pig.setRotationYaw(177f); + assertEquals(177f, pig.getRotationYaw(), 1e-4f); + assertEquals(345f, chicken.getRotationYaw(), 1e-4f); + assertEquals(22.5f, zombie.getRotationYaw(), 1e-4f); + + // rotating mount should rotate passengers + pig.setRotationYaw(0f); + pig.rotate(45f); + assertEquals(45f, pig.getRotationYaw(), 1e-4f); + assertEquals(45f - 15f, chicken.getRotationYaw(), 1e-4f); + assertEquals(45f + 22.5f, zombie.getRotationYaw(), 1e-4f); + + // rotating middle mount should rotate passengers, but not entity being ridden + chicken.rotate(-10f); + assertEquals(45f, pig.getRotationYaw(), 1e-4f); + assertEquals(45f - 15f - 10f, chicken.getRotationYaw(), 1e-4f); + assertEquals(45f + 22.5f - 10f, zombie.getRotationYaw(), 1e-4f); + } + + + // + + // + + + public void testMotion_basic() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + assertMotionEquals(pig, 0, 0, 0); + pig.setMotion(0.1, -0.12, -0.03); + assertMotionEquals(pig, 0.1, -0.12, -0.03); + + pig.setMotionDX(1); + assertMotionEquals(pig, 1, -0.12, -0.03); + + pig.setMotionDY(2); + assertMotionEquals(pig, 1, 2, -0.03); + + pig.setMotionDZ(-3); + assertMotionEquals(pig, 1, 2, -3); + + assertTrue(pig.isMotionValid()); + pig.setMotionDX(Double.NaN); + assertFalse(pig.isMotionValid()); + pig.setMotionDX(0); + pig.setMotionDY(Double.POSITIVE_INFINITY); + assertFalse(pig.isMotionValid()); + pig.setMotionDY(0); + pig.setMotionDZ(Double.NEGATIVE_INFINITY); + assertFalse(pig.isMotionValid()); + + pig.setMotionDZ(0); + assertTrue(pig.isMotionValid()); + } + + public void testAddPassenger_syncsPassengerMotion() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + + pig.setMotion(0.1, 0.2, 0.3); + + chicken.addPassenger(zombie); + assertMotionEquals(chicken, 0, 0, 0); + assertMotionEquals(zombie, 0, 0, 0); + + pig.addPassenger(chicken); + assertMotionEquals(pig, 0.1, 0.2, 0.3); + assertMotionEquals(chicken, 0.1, 0.2, 0.3); + assertMotionEquals(zombie, 0.1, 0.2, 0.3); + } + // + + public void testGenerateNewUuid() { + EntityBaseImpl pig = new EntityBaseImpl(DataVersion.latest().id(), "pig"); + EntityBaseImpl chicken = new EntityBaseImpl(DataVersion.latest().id(), "chicken"); + EntityBaseImpl zombie = new EntityBaseImpl(DataVersion.latest().id(), "zombie"); + chicken.addPassenger(zombie); + pig.addPassenger(chicken); + + UUID pigUuid = UUID.randomUUID(); + UUID chickenUuid = UUID.randomUUID(); + UUID zombieUuid = UUID.randomUUID(); + + pig.setUuid(pigUuid); + chicken.setUuid(chickenUuid); + zombie.setUuid(zombieUuid); + + pig.generateNewUuid(); + + assertNotNull(pig.getUuid()); + assertNotNull(chicken.getUuid()); + assertNotNull(zombie.getUuid()); + + assertNotEquals(pigUuid, pig.getUuid()); + assertNotEquals(chickenUuid, chicken.getUuid()); + assertNotEquals(zombieUuid, zombie.getUuid()); + } +} diff --git a/src/test/java/net/querz/mca/entities/EntityFactoryTest.java b/src/test/java/net/querz/mca/entities/EntityFactoryTest.java new file mode 100644 index 00000000..7a909ced --- /dev/null +++ b/src/test/java/net/querz/mca/entities/EntityFactoryTest.java @@ -0,0 +1,225 @@ +package net.querz.mca.entities; + +import net.querz.mca.DataVersion; +import net.querz.mca.MCATestCase; +import net.querz.nbt.tag.CompoundTag; + +import java.util.List; + +public class EntityFactoryTest extends MCATestCase { + + // + private static class EntityStub extends EntityBaseImpl { + final String givenNormalizedId; + final EntityCreatorStub creator; + + public EntityStub(EntityCreatorStub creator, String givenNormalizedId, CompoundTag data, int dataVersion) { + super(dataVersion); + this.creator = creator; + this.givenNormalizedId = givenNormalizedId; + this.data = data; + this.dataVersion = dataVersion; + } + } + + private static class EntityCreatorStub implements EntityCreator { + final String name; + boolean returnNull; + RuntimeException throwMe; + public EntityCreatorStub(String name) { + this.name = name; + } + + @Override + public EntityStub create(String normalizedId, CompoundTag tag, int dataVersion) { + if (throwMe != null) throw throwMe; + return returnNull ? null : new EntityStub(this, normalizedId, tag, dataVersion); + } + + @Override + public String toString() { + return name; + } + } + // + + final EntityCreatorStub defaultedCreator = new EntityCreatorStub("TEST DEFAULT CREATOR"); + + // TODO: IDK why @Before isn't working, resolve that and run this as @Before someday + public void reset() { + // Test should not rely on defaults put in place for user convenience! + EntityFactory.clearCreators(); + EntityFactory.clearEntityIdRemap(); + EntityFactory.setDefaultEntityCreator(defaultedCreator); +// System.out.println("RUNNING BEFORE"); + } + + public void testNormalizeId() { + reset(); + assertThrowsIllegalArgumentException(() -> EntityFactory.normalizeId(null)); + assertThrowsNoException(() -> EntityFactory.normalizeId("not_null")); + assertThrowsIllegalArgumentException(() -> EntityFactory.normalizeId("minecraft:")); + + assertEquals("PIG_ZIG", EntityFactory.normalizeId("minecraft:Pig_zig")); + assertEquals("PIGZIG", EntityFactory.normalizeId("minecraft:PigZig")); + } + + public void testNormalizeAndRemapId() { + reset(); + assertThrowsIllegalArgumentException(() -> EntityFactory.normalizeAndRemapId(null)); + assertThrowsNoException(() -> EntityFactory.normalizeAndRemapId("not_null")); + assertThrowsIllegalArgumentException(() -> EntityFactory.normalizeAndRemapId("minecraft:")); + // not mapped + assertEquals("PIGZIG", EntityFactory.normalizeAndRemapId("minecraft:PigZig")); + // remap + EntityFactory.registerIdRemap("pigzig", "pig_zig"); + assertEquals("PIG_ZIG", EntityFactory.normalizeAndRemapId("minecraft:PigZig")); + } + + public void testDefaultCreatorNotNull() { + reset(); + assertNotNull(EntityFactory.getDefaultEntityCreator()); + } + + public void testSetDefaultCreator() { + reset(); + assertNotNull(EntityFactory.getDefaultEntityCreator()); + assertThrowsIllegalArgumentException(() -> EntityFactory.setDefaultEntityCreator(null)); + EntityCreator ec = new EntityCreatorStub(getName()); + EntityFactory.setDefaultEntityCreator(ec); + assertSame(ec, EntityFactory.getDefaultEntityCreator()); + } + + public void testRegisterCreator_basic() { + reset(); + EntityCreator ec = new EntityCreatorStub(getName()); + assertNull(EntityFactory.getCreatorById("FOO")); + EntityFactory.registerCreator(ec, "foo"); + assertSame(ec, EntityFactory.getCreatorById("FOO")); + assertNull(EntityFactory.getCreatorById("BAR")); + } + + public void testRegisterCreator_multiple() { + reset(); + EntityCreator ec = new EntityCreatorStub(getName()); + EntityFactory.registerCreator(ec, "foo", "minecraft:bar"); + assertSame(ec, EntityFactory.getCreatorById("FOO")); + assertSame(ec, EntityFactory.getCreatorById("BAR")); + assertNull(EntityFactory.getCreatorById("BAZ")); + } + + public void testRegisterCreator_throwsOnNullId() { + reset(); + EntityCreator ec = new EntityCreatorStub(getName()); + assertThrowsIllegalArgumentException(() -> EntityFactory.registerCreator(ec, (String) null)); + assertThrowsIllegalArgumentException(() -> EntityFactory.registerCreator(ec, "foo", null, "minecraft:bar")); + } + + public void testRegisterCreator_afterRegisteringRemapping() { + reset(); + EntityCreator fooEc = new EntityCreatorStub(getName()); + EntityFactory.registerIdRemap("bar", "foo"); + EntityFactory.registerIdRemap("baz", "foo"); + EntityFactory.registerCreator(fooEc, "foo"); + assertSame(fooEc, EntityFactory.getCreatorById("FOO")); + assertSame(fooEc, EntityFactory.getCreatorById("BAR")); + assertSame(fooEc, EntityFactory.getCreatorById("BAZ")); + assertNull(EntityFactory.getCreatorById("ZAP")); + } + + public void testRegisteringRemapping_afterRegisterCreator() { + reset(); + EntityCreator fooEc = new EntityCreatorStub(getName()); + EntityFactory.registerCreator(fooEc, "foo"); + EntityFactory.registerIdRemap("bar", "foo"); + EntityFactory.registerIdRemap("baz", "foo"); + assertSame(fooEc, EntityFactory.getCreatorById("FOO")); + assertSame(fooEc, EntityFactory.getCreatorById("BAR")); + assertSame(fooEc, EntityFactory.getCreatorById("BAZ")); + assertNull(EntityFactory.getCreatorById("ZAP")); + } + + public void testReverseIdRemap() { + reset(); + List rev = EntityFactory.reverseIdRemap("foo"); + assertNotNull(rev); + assertTrue(rev.isEmpty()); + + EntityFactory.registerIdRemap("bar", "foo"); + + // this isn't a reverse lookup, it would be forward + rev = EntityFactory.reverseIdRemap("bar"); + assertNotNull(rev); + assertTrue(rev.isEmpty()); + + // one reverse match + rev = EntityFactory.reverseIdRemap("foo"); + assertNotNull(rev); + assertEquals(1, rev.size()); + + + EntityFactory.registerIdRemap("oof", "foo"); + rev = EntityFactory.reverseIdRemap("foo"); + assertNotNull(rev); + assertEquals(2, rev.size()); + } + + public void testGetRegisteredCreatorIdKeys() { + reset(); + assertTrue(EntityFactory.getRegisteredCreatorIdKeys().isEmpty()); + EntityFactory.registerCreator(new EntityCreatorStub("A"), "FOO", "BAR"); + EntityFactory.registerCreator(new EntityCreatorStub("B"), "ZOO"); + assertEquals(3, EntityFactory.getRegisteredCreatorIdKeys().size()); + assertTrue(EntityFactory.getRegisteredCreatorIdKeys().contains("FOO")); + assertTrue(EntityFactory.getRegisteredCreatorIdKeys().contains("BAR")); + assertTrue(EntityFactory.getRegisteredCreatorIdKeys().contains("ZOO")); + } + + public void testCreate() { + reset(); + CompoundTag tag = new CompoundTag(); + tag.putString("id", "whatever"); + EntityStub entityStub = EntityFactory.createAutoCast(tag, DataVersion.latest().id()); + assertNotNull(entityStub); + assertSame(defaultedCreator, entityStub.creator); + assertEquals("WHATEVER", entityStub.givenNormalizedId); + assertSame(tag, entityStub.getHandle()); + + EntityCreatorStub ec = new EntityCreatorStub(getName()); + EntityFactory.registerIdRemap("Muggle", "non_wizard"); + EntityFactory.registerCreator(ec, "non_wizard"); + + // check that the default is still used + entityStub = EntityFactory.createAutoCast(tag, DataVersion.latest().id()); + assertNotNull(entityStub); + assertSame(defaultedCreator, entityStub.creator); + assertEquals("WHATEVER", entityStub.givenNormalizedId); + assertSame(tag, entityStub.getHandle()); + + // check behavior with use of preferred name + tag = new CompoundTag(); + tag.putString("id", "non_wizard"); + entityStub = EntityFactory.createAutoCast(tag, DataVersion.latest().id()); + assertNotNull(entityStub); + assertSame(ec, entityStub.creator); + assertEquals("NON_WIZARD", entityStub.givenNormalizedId); + assertSame(tag, entityStub.getHandle()); + + // check behavior with use of legacy name + tag = new CompoundTag(); + tag.putString("id", "Muggle"); + entityStub = EntityFactory.createAutoCast(tag, DataVersion.latest().id()); + assertNotNull(entityStub); + assertSame(ec, entityStub.creator); + assertEquals("NON_WIZARD", entityStub.givenNormalizedId); + assertSame(tag, entityStub.getHandle()); + } + + public void testCreate_throwsIllegalEntityTagException_whenCreatorReturnsNull() { + reset(); + CompoundTag tag = new CompoundTag(); + tag.putString("id", "whatever"); + defaultedCreator.returnNull = true; + assertThrowsException(() -> EntityFactory.createAutoCast(tag, DataVersion.latest().id()), IllegalEntityTagException.class); + } +} From e9b3d4b6e9fc4078a7df1c6be3fc14162bce74f4 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 13:46:07 -0500 Subject: [PATCH 061/288] Adding MCAUtil#blockAbsoluteToChunkRelative(int) and MCAUtil#blockAbsoluteToChunkRelative(double) --- src/main/java/net/querz/mca/MCAUtil.java | 20 ++++++++++++++++++++ src/test/java/net/querz/mca/MCAUtilTest.java | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java index 1f09c785..e61cee54 100644 --- a/src/main/java/net/querz/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -298,6 +298,26 @@ public static int regionToBlock(int region) { public static int chunkToBlock(int chunk) { return chunk << 4; } + + /** + * Turns an absolute block coordinate into a chunk relative one [0..15] + * @param block The absolute block coordinate. + * @return Block coordinate relative to its chunk. + */ + public static int blockAbsoluteToChunkRelative(int block) { + return block & 0xF; + } + + /** + * Turns an absolute block coordinate into a chunk relative one [0..16) + * @param block The absolute block coordinate. + * @return Block coordinate relative to its chunk. + */ + public static double blockAbsoluteToChunkRelative(double block) { + double bin = block % 16; + return bin >= 0 ? bin : 16 + bin; + } + // /** diff --git a/src/test/java/net/querz/mca/MCAUtilTest.java b/src/test/java/net/querz/mca/MCAUtilTest.java index 9615f7a9..ed729a7d 100644 --- a/src/test/java/net/querz/mca/MCAUtilTest.java +++ b/src/test/java/net/querz/mca/MCAUtilTest.java @@ -44,6 +44,24 @@ public void testLocationConversion() { assertEquals(-32, MCAUtil.chunkToBlock(-2)); } + public void testBlockAbsoluteToChunkRelative_int() { + assertEquals(0, MCAUtil.blockAbsoluteToChunkRelative(0)); + assertEquals(0, MCAUtil.blockAbsoluteToChunkRelative(16 * 81)); + assertEquals(15, MCAUtil.blockAbsoluteToChunkRelative(16 * 81 - 1)); + assertEquals(0, MCAUtil.blockAbsoluteToChunkRelative(-16 * 81)); + assertEquals(15, MCAUtil.blockAbsoluteToChunkRelative(-16 * 81 - 1)); + assertEquals(1, MCAUtil.blockAbsoluteToChunkRelative(-16 * 81 + 1)); + } + + public void testBlockAbsoluteToChunkRelative_double() { + assertEquals(0.0, MCAUtil.blockAbsoluteToChunkRelative(0.0), 1e-10); + assertEquals(16 - 1e-6, MCAUtil.blockAbsoluteToChunkRelative(-1e-6), 1e-10); + assertEquals(3.4567, MCAUtil.blockAbsoluteToChunkRelative(16 * 81 + 3.4567), 1e-10); + assertEquals(16 - 3.4567, MCAUtil.blockAbsoluteToChunkRelative(16 * 81 - 3.4567), 1e-10); + assertEquals(16 - 3.4567, MCAUtil.blockAbsoluteToChunkRelative(-16 * 81 - 3.4567), 1e-10); + assertEquals(3.4567, MCAUtil.blockAbsoluteToChunkRelative(-16 * 81 + 3.4567), 1e-10); + } + public void testCreateNameFromLocation() { assertEquals("r.0.0.mca", MCAUtil.createNameFromBlockLocation(0, 0)); assertEquals("r.0.0.mca", MCAUtil.createNameFromBlockLocation(511, 511)); From b113741fadcd986537f146b14a91438218445799 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 14:12:50 -0500 Subject: [PATCH 062/288] Adding BlockAlignedBoundingRectangleTest & ChunkBoundingRectangleTest --- .../BlockAlignedBoundingRectangleTest.java | 53 +++++++++++++++++++ .../mca/util/ChunkBoundingRectangleTest.java | 36 +++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/test/java/net/querz/mca/util/BlockAlignedBoundingRectangleTest.java create mode 100644 src/test/java/net/querz/mca/util/ChunkBoundingRectangleTest.java diff --git a/src/test/java/net/querz/mca/util/BlockAlignedBoundingRectangleTest.java b/src/test/java/net/querz/mca/util/BlockAlignedBoundingRectangleTest.java new file mode 100644 index 00000000..87983f50 --- /dev/null +++ b/src/test/java/net/querz/mca/util/BlockAlignedBoundingRectangleTest.java @@ -0,0 +1,53 @@ +package net.querz.mca.util; + +import junit.framework.TestCase; +import net.querz.util.BlockAlignedBoundingRectangle; + +public class BlockAlignedBoundingRectangleTest extends TestCase { + + public void testMinMaxXZ() { + BlockAlignedBoundingRectangle cbr = new BlockAlignedBoundingRectangle(16, -16, 16); + assertEquals(16, cbr.getMinX()); + assertEquals(32, cbr.getMaxX()); + assertEquals(-16, cbr.getMinZ()); + assertEquals(0, cbr.getMaxZ()); + } + + public void testContains_int() { + BlockAlignedBoundingRectangle cbr = new BlockAlignedBoundingRectangle(16, -16, 16); + assertTrue(cbr.contains(16 + 8, -8)); + assertFalse(cbr.contains(-8, 8)); + + assertTrue(cbr.contains(16, -1)); + assertFalse(cbr.contains(15, -1)); + assertFalse(cbr.contains(16, 0)); + + assertTrue(cbr.contains(16, -16)); + assertFalse(cbr.contains(15, -16)); + assertFalse(cbr.contains(16, -17)); + + assertTrue(cbr.contains(31, -1)); + assertFalse(cbr.contains(32, -1)); + assertFalse(cbr.contains(31, 0)); + + assertTrue(cbr.contains(31, -16)); + assertFalse(cbr.contains(32, -16)); + assertFalse(cbr.contains(31, -17)); + } + + public void testContains_double() { + BlockAlignedBoundingRectangle cbr = new BlockAlignedBoundingRectangle(16, -16, 16); + assertTrue(cbr.contains(16 + 7.5, -7.5)); + assertFalse(cbr.contains(-8.5, 8.5)); + + assertTrue(cbr.contains(16.0, -1e-14)); + assertTrue(cbr.contains(16.0, -16.0)); + assertTrue(cbr.contains(32 - 1e-14, -1e-14)); + assertTrue(cbr.contains(32 - 1e-14, -16.0)); + + assertFalse(cbr.contains(16 - 1e-14, -7.5)); // off left + assertFalse(cbr.contains(32.0, -7.5)); // off right + assertFalse(cbr.contains(16 + 7.5, 0.0)); // off top + assertFalse(cbr.contains(16 + 7.5, -16.0 - 1e-14)); // off bottom + } +} diff --git a/src/test/java/net/querz/mca/util/ChunkBoundingRectangleTest.java b/src/test/java/net/querz/mca/util/ChunkBoundingRectangleTest.java new file mode 100644 index 00000000..cb134c56 --- /dev/null +++ b/src/test/java/net/querz/mca/util/ChunkBoundingRectangleTest.java @@ -0,0 +1,36 @@ +package net.querz.mca.util; + + +import junit.framework.TestCase; +import net.querz.util.ChunkBoundingRectangle; + +public class ChunkBoundingRectangleTest extends TestCase { + + public void testRelocate_int() { + ChunkBoundingRectangle cbr = new ChunkBoundingRectangle(1, -1); // [16..32), [-16..0) + + assertEquals(16, cbr.relocateX(0)); + assertEquals(16 + 3, cbr.relocateX(3)); + assertEquals(16 + 7, cbr.relocateX(16 + 7)); + assertEquals(16 + 15, cbr.relocateX(-1)); + + assertEquals(-1, cbr.relocateZ(16 * 53 - 1)); + assertEquals(-15, cbr.relocateZ(16 * -53 - 15)); + assertEquals(-16 + 3, cbr.relocateZ(3)); + assertEquals(-16 + 7, cbr.relocateZ(16 + 7)); + assertEquals(-16 + 15, cbr.relocateZ(-1)); + } + + public void testRelocate_double() { + ChunkBoundingRectangle cbr = new ChunkBoundingRectangle(0, 1); // [0..16), [16..32) + + assertEquals(16 - 1e-6, cbr.relocateX(-1e-6), 1e-10); + assertEquals(16 - 0.5, cbr.relocateX(41 * 16 -0.5), 1e-10); + assertEquals(16 - 0.5, cbr.relocateX(-41 * 16 -0.5), 1e-10); + + assertEquals(16 + 1e-6, cbr.relocateZ(1e-6), 1e-10); + assertEquals(16 + 1e-6, cbr.relocateZ(41 * 16 + 1e-6), 1e-10); + assertEquals(16 + 0.5, cbr.relocateZ(41 * 16 + 0.5), 1e-10); + assertEquals(16 + 6.789, cbr.relocateZ(-41 * 16 + 6.789), 1e-10); + } +} From 613f937ec0b6309d37c1cf540b4b4a8c0e34dde5 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 14:24:42 -0500 Subject: [PATCH 063/288] Everything entities! Basic entity mca support complete & tested. --- src/main/java/net/querz/mca/Chunk.java | 15 + src/main/java/net/querz/mca/ChunkBase.java | 58 +- .../java/net/querz/mca/EntitiesChunk.java | 14 +- .../java/net/querz/mca/EntitiesChunkBase.java | 159 ++++ .../java/net/querz/mca/EntitiesMCAFile.java | 3 - src/main/java/net/querz/mca/PoiChunkBase.java | 16 + .../mca/entities/DefaultEntityCreator.java | 12 + .../net/querz/mca/entities/EntityBase.java | 375 +++++++++ .../querz/mca/entities/EntityBaseImpl.java | 737 ++++++++++++++++++ .../net/querz/mca/entities/EntityCreator.java | 20 + .../net/querz/mca/entities/EntityFactory.java | 244 ++++++ .../net/querz/mca/EntitiesMCAFileTest.java | 59 ++ .../java/net/querz/mca/MCAFileBaseTest.java | 4 + 13 files changed, 1703 insertions(+), 13 deletions(-) create mode 100644 src/main/java/net/querz/mca/EntitiesChunkBase.java create mode 100644 src/main/java/net/querz/mca/entities/DefaultEntityCreator.java create mode 100644 src/main/java/net/querz/mca/entities/EntityBase.java create mode 100644 src/main/java/net/querz/mca/entities/EntityBaseImpl.java create mode 100644 src/main/java/net/querz/mca/entities/EntityCreator.java create mode 100644 src/main/java/net/querz/mca/entities/EntityFactory.java create mode 100644 src/test/java/net/querz/mca/EntitiesMCAFileTest.java diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index 08ef9897..c0c3e19f 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -29,6 +29,21 @@ public Chunk(CompoundTag data, long loadFlags) { super(data, loadFlags); } + @Override + public boolean moveChunkImplemented() { + return false; + } + + @Override + public boolean moveChunkHasFullVersionSupport() { + return false; + } + + @Override + public boolean moveChunk(int newChunkX, int newChunkZ, boolean force) { + throw new UnsupportedOperationException(); + } + @Override protected Section createSection(CompoundTag section, int dataVersion, long loadFlags) { diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java index b3b25723..e479dcb4 100644 --- a/src/main/java/net/querz/mca/ChunkBase.java +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -29,7 +29,12 @@ *

      */ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { + public static final int NO_CHUNK_COORD_SENTINEL = Integer.MIN_VALUE; protected int dataVersion; + // TODO: refactor region chunks to support these fields + protected int chunkX = NO_CHUNK_COORD_SENTINEL; + protected int chunkZ = NO_CHUNK_COORD_SENTINEL; + protected boolean partial; protected boolean raw; protected int lastMCAUpdate; @@ -45,7 +50,7 @@ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { * class def, within this method instead. Implementers should never need to call this method themselves * as ChunkBase will always call it, even from the default constructor.

      */ - protected void initMembers() { }; + protected void initMembers() { } protected ChunkBase() { dataVersion = DataVersion.latest().id(); @@ -114,6 +119,57 @@ public void setDataVersion(int dataVersion) { this.dataVersion = Math.max(0, dataVersion); } + /** + * Gets this chunk's chunk-x coordinate. Returns {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + */ + public int getChunkX() { + return chunkX; + } + + /** + * Gets this chunk's chunk-z coordinate. Returns {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + */ + public int getChunkZ() { + return chunkZ; + } + + + /** + * Indicates if this chunk implementation supports calling {@link #moveChunk(int, int, boolean)}. + * @return false if {@link #moveChunk(int, int, boolean)} is not implemented (calling it will always throw). + */ + public abstract boolean moveChunkImplemented(); + + /** + * Indicates if the current chunk can be be moved with confidence or not. If this function returns false + * and {@link #moveChunkImplemented()} returns true then you must use {@code moveChunk(x, z, true)} to attempt + * a best effort move. + */ + public abstract boolean moveChunkHasFullVersionSupport(); + + /** + * Attempts to update all tags that use absolute positions within this chunk. + * If {@code force = true} the result of calling this function cannot be guaranteed to be complete and + * may still throw {@link UnsupportedOperationException}. + * @param newChunkX new chunk-x + * @param newChunkZ new chunk-z + * @param force true to ignore the guidance of {@link #moveChunkHasFullVersionSupport()} and make a best effort + * anyway. + * @return true if any data was changed as a result of this call + * @throws UnsupportedOperationException thrown if this chunk implementation doest support moves, or moves + * for this chunks version (possibly even if force = true). + */ + public abstract boolean moveChunk(int newChunkX, int newChunkZ, boolean force); + + /** + * Calls {@code moveChunk(newChunkX, newChunkZ, false);} + * @see #moveChunk(int, int, boolean) + */ + public boolean moveChunk(int newChunkX, int newChunkZ) { + return moveChunk(newChunkX, newChunkZ, false); + } + + /** * Serializes this chunk to a RandomAccessFile. * @param raf The RandomAccessFile to be written to. diff --git a/src/main/java/net/querz/mca/EntitiesChunk.java b/src/main/java/net/querz/mca/EntitiesChunk.java index f26a359b..2d379698 100644 --- a/src/main/java/net/querz/mca/EntitiesChunk.java +++ b/src/main/java/net/querz/mca/EntitiesChunk.java @@ -1,21 +1,17 @@ package net.querz.mca; +import net.querz.mca.entities.EntityBase; import net.querz.nbt.tag.CompoundTag; -public class EntitiesChunk extends ChunkBase { +public class EntitiesChunk extends EntitiesChunkBase { + protected EntitiesChunk() {} public EntitiesChunk(CompoundTag data) { super(data); } - @Override - protected void initReferences(long loadFlags) { - - } - - @Override - public CompoundTag updateHandle() { - return data; + public EntitiesChunk(CompoundTag data, long loadData) { + super(data, loadData); } } diff --git a/src/main/java/net/querz/mca/EntitiesChunkBase.java b/src/main/java/net/querz/mca/EntitiesChunkBase.java new file mode 100644 index 00000000..ae93ccc0 --- /dev/null +++ b/src/main/java/net/querz/mca/EntitiesChunkBase.java @@ -0,0 +1,159 @@ +package net.querz.mca; + +import net.querz.mca.entities.EntityBase; +import net.querz.mca.entities.EntityFactory; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.util.ArgValidator; +import net.querz.util.ChunkBoundingRectangle; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Provides all the basic functionality necessary for this type of chunk with abstraction hooks + * making it easy to extend this class and modify the factory behavior of {@link MCAUtil} to create + * instances of your custom class. + */ +public abstract class EntitiesChunkBase extends ChunkBase implements Iterable { + protected List entities; + + @Override + protected void initMembers() { + entities = new ArrayList<>(); + } + + protected EntitiesChunkBase() { } + + public EntitiesChunkBase(CompoundTag data) { + super(data); + } + + public EntitiesChunkBase(CompoundTag data, long loadData) { + super(data, loadData); + } + + @Override + protected void initReferences(long loadFlags) { + if (getDataVersionEnum().hasEntitiesMca()) { + int[] posXZ = data.getIntArray("Position"); + if (posXZ == null || posXZ.length != 2) { + throw new IllegalArgumentException("Position tag missing or invalid"); + } + chunkX = posXZ[0]; + chunkZ = posXZ[1]; + } else { + // probably reading a "region" chunk as "entities" + // TODO: extract chunk position information + } + if ((loadFlags & LoadFlags.ENTITIES) > 0) { + ListTag entitiesTag = data.getListTag("Entities").asCompoundTagList(); + if (entitiesTag == null) { + throw new IllegalArgumentException("Entities tag not found"); + } + initEntities(entitiesTag, loadFlags); + } + } + + /** + * Passed the entities tag from wherever it had to be found in the data tag. + * @param entitiesTag not null + */ + @SuppressWarnings("unchecked") + protected void initEntities(ListTag entitiesTag, long loadFlags) { + if (chunkX != NO_CHUNK_COORD_SENTINEL && chunkZ != NO_CHUNK_COORD_SENTINEL) { + ChunkBoundingRectangle cbr = new ChunkBoundingRectangle(chunkX, chunkZ); + for (CompoundTag entityTag : entitiesTag) { + ET entity = (ET) EntityFactory.create(entityTag, dataVersion); + if (!cbr.contains(entity.getX(), entity.getZ())) { + // TODO: consider reporting a warning... somehow... OR add a LoadFlag to control fix/fail/drop + entity.setX(cbr.relocateX(entity.getX())); + entity.setZ(cbr.relocateZ(entity.getZ())); + } + entities.add(entity); + } + } else { + for (CompoundTag entityTag : entitiesTag) { + entities.add((ET) EntityFactory.create(entityTag, dataVersion)); + } + } + } + + public List getEntities() { + return entities; + } + + public void setEntities(List entities) { + ArgValidator.requireValue(entities); + this.entities = entities; + } + + // TODO: implement move chunk logic + @Override + public boolean moveChunkImplemented() { + return false; + } + + @Override + public boolean moveChunkHasFullVersionSupport() { + return false; + } + + @Override + public boolean moveChunk(int newChunkX, int newChunkZ, boolean force) { + throw new UnsupportedOperationException(); + } + + + @Override + public Iterator iterator() { + return entities.iterator(); + } + @Override + public void forEach(Consumer action) { + entities.forEach(action); + } + + @Override + public Spliterator spliterator() { + return entities.spliterator(); + } + + public Stream stream() { + return entities.stream(); + } + + @Override + public CompoundTag updateHandle() { + if (raw) { + return data; + } + super.updateHandle(); + if (chunkX != NO_CHUNK_COORD_SENTINEL && chunkZ != NO_CHUNK_COORD_SENTINEL) { + // TODO: version support + data.putIntArray("Position", new int[]{chunkX, chunkZ}); + } + ListTag entitiesTag = new ListTag<>(CompoundTag.class, entities.size()); + data.put("Entities", entitiesTag); + for (ET entity : entities) { + entitiesTag.add(entity.updateHandle()); + } + return data; + } + + @Override + public CompoundTag updateHandle(int xPos, int zPos) { + if (!raw) { + if (chunkX == NO_CHUNK_COORD_SENTINEL) chunkX = xPos; + if (chunkZ == NO_CHUNK_COORD_SENTINEL) chunkZ = zPos; + ArgValidator.check(xPos == chunkX && zPos == chunkZ, + "Attempted to write chunk with incorrect chunk XZ. Chunk must be moved with moveChunk(..) first."); + updateHandle(); + } + return data; + } +} diff --git a/src/main/java/net/querz/mca/EntitiesMCAFile.java b/src/main/java/net/querz/mca/EntitiesMCAFile.java index 5e5ef9ee..a674a6f5 100644 --- a/src/main/java/net/querz/mca/EntitiesMCAFile.java +++ b/src/main/java/net/querz/mca/EntitiesMCAFile.java @@ -1,8 +1,5 @@ package net.querz.mca; -import java.io.IOException; -import java.io.RandomAccessFile; - public class EntitiesMCAFile extends MCAFileBase { public EntitiesMCAFile(int regionX, int regionZ) { super(regionX, regionZ); diff --git a/src/main/java/net/querz/mca/PoiChunkBase.java b/src/main/java/net/querz/mca/PoiChunkBase.java index 026709d2..0799b362 100644 --- a/src/main/java/net/querz/mca/PoiChunkBase.java +++ b/src/main/java/net/querz/mca/PoiChunkBase.java @@ -60,6 +60,22 @@ protected void initReferences(long loadFlags) { } } + // TODO: support chunk moves + @Override + public boolean moveChunkImplemented() { + return false; + } + + @Override + public boolean moveChunkHasFullVersionSupport() { + return false; + } + + @Override + public boolean moveChunk(int newChunkX, int newChunkZ, boolean force) { + throw new UnsupportedOperationException(); + } + /** * Called from {@link #initReferences(long)}. Exists to provide a hook for custom implementations to override to * add support for modded poi's, etc. without having to implement {@link #initReferences(long)} logic fully. diff --git a/src/main/java/net/querz/mca/entities/DefaultEntityCreator.java b/src/main/java/net/querz/mca/entities/DefaultEntityCreator.java new file mode 100644 index 00000000..a0e22d11 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/DefaultEntityCreator.java @@ -0,0 +1,12 @@ +package net.querz.mca.entities; + +import net.querz.nbt.tag.CompoundTag; + +public class DefaultEntityCreator implements EntityCreator { + + /** @see EntityCreator#create(String, CompoundTag, int) */ + @Override + public EntityBase create(String normalizedId, CompoundTag tag, int dataVersion) { + return new EntityBaseImpl(tag, dataVersion); + } +} diff --git a/src/main/java/net/querz/mca/entities/EntityBase.java b/src/main/java/net/querz/mca/entities/EntityBase.java new file mode 100644 index 00000000..72ee1739 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/EntityBase.java @@ -0,0 +1,375 @@ +package net.querz.mca.entities; + +import net.querz.mca.EntitiesMCAFile; +import net.querz.mca.MCAUtil; +import net.querz.mca.TagWrapper; +import net.querz.util.ArgValidator; + +import java.util.List; +import java.util.UUID; + +/** + * Extremely basic entity interface to allow users of this library to extend {@link EntitiesMCAFile} and + * rewire {@link MCAUtil} for easy integration with existing code. + */ +public interface EntityBase extends TagWrapper { + short AIR_UNSET = Short.MIN_VALUE; + + /** String representation of the entity's ID. Does not exist for the Player entity. */ + String getId(); + + /** @see #getId() */ + void setId(String id); + + /** + * This entity's Universally Unique Identifier. + *

      May be null (but required by game). If uuid is null or ZERO when {@link #updateHandle()} is called, + * a random UUID will be generated and assigned. + */ + UUID getUuid(); + + /** + * This entity's Universally Unique Identifier. + * @param uuid Nullable, but required by game. If uuid is null when {@link #updateHandle()} is called, + * a random UUID will be generated and assigned. Prefer calling {@link #generateNewUuid()} which + * will also clear UUID's of any riders instead of setting null here. + */ + void setUuid(UUID uuid); + + /** + * Generates a new random UUID for this entity and all passengers. + * @return New UUID for this entity. + */ + UUID generateNewUuid(); + + /** + * How much air the entity has, in ticks. Decreases by 1 per tick when unable to breathe + * (except suffocating in a block). Increase by 1 per tick when it can breathe. If -20 while still + * unable to breathe, the entity loses 1 health and its air is reset to 0. Most mobs can have a + * maximum of 300 in air, while dolphins can reach up to 4800, and axolotls have 6000. + *

      {@link #AIR_UNSET} is used as sentinel value (by this library) to indicate no value. + * However, generally MC stores a default of 300 even on things that don't need air.

      + */ + short getAir(); + + /** + * Set to {@link #AIR_UNSET} to indicate that "Air" should not be included in the NBT data. + * @see #getAir() + */ + void setAir(short air); + + /** + * Distance the entity has fallen. Larger values cause more damage when the entity lands. + */ + float getFallDistance(); + + /** @see #getFallDistance() */ + void setFallDistance(float fallDistance); + + /** + * Number of ticks until the fire is put out. Negative values reflect how long the entity can + * stand in fire before burning. Default -20 or -1 when not on fire. + */ + short getFire(); + + /** @see #getFire() */ + void setFire(short fireTicks); + + /** + * Optional. How many ticks the entity has been freezing. Although this tag is defined for + * all entities, it is actually only used by mobs that are not in the freeze_immune_entity_types + * entity type tag. Ticks up by 1 every tick while in powder snow, up to a maximum of 300 + * (15 seconds), and ticks down by 2 while out of it. + */ + int getTicksFrozen(); + + /** @see #getTicksFrozen() */ + void setTicksFrozen(int ticksFrozen); + + /** + * The number of ticks before which the entity may be teleported back through a nether portal. + * Initially starts at 300 ticks (15 seconds) after teleportation and counts down to 0. + */ + int getPortalCooldown(); + + /** @see #getPortalCooldown() */ + void setPortalCooldown(int portalCooldown); + + /** + * The custom name JSON text component of this entity. Appears in player death messages and villager + * trading interfaces, as well as above the entity when the player's cursor is over it. + * May be empty or not exist. + */ + String getCustomName(); + + /** @see #getCustomName() */ + void setCustomName(String customName); + + /** + * if true, and this entity has a custom name, the name always appears above the entity, regardless of + * where the cursor points. If the entity does not have a custom name, a default name is shown. + *

      May not exist. Default NULL

      + */ + boolean isCustomNameVisible(); + + /** @see #isCustomNameVisible() */ + void setCustomNameVisible(boolean visible); + + /** + * true if the entity should not take damage. This applies to living and nonliving entities alike: mobs should + * not take damage from any source (including potion effects), and cannot be moved by fishing rods, attacks, + * explosions, or projectiles, and objects such as vehicles and item frames cannot be destroyed unless their + * supports are removed. + */ + boolean isInvulnerable(); + + /** @see #isInvulnerable() */ + void setInvulnerable(boolean invulnerable); + + /** + * if true, this entity is silenced. + * May not exist. + */ + boolean isSilent(); + + /** @see #isSilent() */ + void setSilent(boolean silent); + + /** true if the entity has a glowing outline. */ + boolean isGlowing(); + + /** @see #isGlowing() */ + void setGlowing(boolean glowing); + + /** If true, the entity does not fall down naturally. Set to true by striders in lava. */ + boolean hasNoGravity(); + + /** @see #hasNoGravity() */ + void setNoGravity(boolean noGravity); + + /** true if the entity is touching the ground. */ + boolean isOnGround(); + + /** @see #isOnGround() */ + void setOnGround(boolean onGround); + + /** If true, the entity visually appears on fire, even if it is not actually on fire. */ + boolean hasVisualFire(); + + /** @see #hasVisualFire() */ + void setHasVisualFire(boolean hasVisualFire); + + double getX(); + void setX(double x); + + double getY(); + void setY(double y); + + double getZ(); + void setZ(double z); + + default void setPosition(double x, double y, double z) { + setX(x); + setY(y); + setZ(z); + } + + default void movePosition(double dx, double dy, double dz) { + if (!isPositionValid()) { + throw new IllegalStateException("cannot move an invalid position"); + } + setX(getX() + dx); + setY(getY() + dy); + setZ(getZ() + dz); + } + + /** + * @return True if x, y, and z all have finite values. Does not check for reasonable finite values. + */ + default boolean isPositionValid() { + return Double.isFinite(getX()) && Double.isFinite(getY()) && Double.isFinite(getZ()); + } + + /** X velocity of the entity in meters per tick. */ + double getMotionDX(); + + /** + * Updates this entities motion and the motion of all passengers. + * @see #getMotionDX() + */ + void setMotionDX(double dx); + + /** Y velocity of the entity in meters per tick. */ + double getMotionDY(); + + /** + * Updates this entities motion and the motion of all passengers. + * @see #getMotionDY() + */ + void setMotionDY(double dy); + + /** Z velocity of the entity in meters per tick. */ + double getMotionDZ(); + + /** + * Updates this entities motion and the motion of all passengers. + * @see #getMotionDZ() + */ + void setMotionDZ(double dz); + + /** + * Updates this entities motion and the motion of all passengers. + */ + default void setMotion(double dx, double dy, double dz) { + setMotionDX(dx); + setMotionDY(dy); + setMotionDZ(dz); + } + + /** + * @return True if dx, dy, and dz all have finite values. Does not check for reasonable finite values. + */ + default boolean isMotionValid() { + return Double.isFinite(getMotionDX()) && Double.isFinite(getMotionDY()) && Double.isFinite(getMotionDZ()); + } + + /** + * The entity's rotation clockwise around the Y axis (called yaw). + * Due south is 0, west is 90, north is 180, east is 270. + * @return yaw in degrees in range [0..360) + * @see #getFacingCardinalAngle() + */ + float getRotationYaw(); + + /** + * Sets entity yaw (clockwise rotation about the y-axis) in degrees. + * Due south is 0, west is 90, north is 180, east is 270. + * @see #getRotationYaw() + * @see #rotate(float) + */ + void setRotationYaw(float yaw); + + /** + * Convenience function for working with yaw values in cardinal angles - + * where 0 is north, 90 is east, 180 is south, 270 is west. + *

      Because dealing with yaw values is error prone and somewhat nonsensical. + * @return The direction the entity is facing in cardinal angle in degrees in range [0..360) + */ + default float getFacingCardinalAngle() { + return EntityUtil.normalizeYaw(getRotationYaw() + 180); + } + + /** + * Convenience function for working with yaw values in cardinal angles - + * where 0 is north, 90 is east, 180 is south, 270 is west. + *

      Because dealing with yaw values is error prone and somewhat nonsensical. + * @param cardinalAngle Cardinal angle in degrees, used to calculate and set a new yaw value. + */ + default void setFacingCardinalAngle(float cardinalAngle) { + setRotationYaw(EntityUtil.normalizeYaw(cardinalAngle - 180)); + } + + /** + * The entity's declination from the horizon (called pitch). Horizontal is 0. Positive values look downward. + * Does not exceed positive or negative 90 degrees. + */ + float getRotationPitch(); + + /** @see #getRotationPitch() */ + void setRotationPitch(float pitch); + + /** + * @see #getRotationYaw() + * @see #getRotationPitch() + * @see #rotate(float) + */ + default void setRotation(float yaw, float pitch) { + setRotationYaw(yaw); + setRotationPitch(pitch); + } + + /** + * Rotates this entity, and all passengers, by the given angelDegrees. + *

      Note that this is different from {@link #setRotationYaw(float)} as this function is relative to + * the current yaw and affects passengers. + * @param angleDegrees Angle in degrees to rotate this entity and all passengers. May be positive or negative. + * @throws IllegalStateException Thrown if current yaw is not finite + */ + default void rotate(float angleDegrees) { + if (angleDegrees == 0f) return; + ArgValidator.check(Float.isFinite(angleDegrees)); + // Given the nature of floating point numbers, an extremely large given angleDegrees might + // squash the current yaw when added to it - this problem can be avoided by first normalizing it. + angleDegrees = EntityUtil.normalizeYaw(angleDegrees); + float currentYaw = getRotationYaw(); + if (!Float.isFinite(currentYaw)) { + throw new IllegalStateException("cannot rotate non-finite yaw"); + } + setRotationYaw(EntityUtil.normalizeYaw(currentYaw + angleDegrees)); + if (hasPassengers()) { + for (EntityBase passenger : getPassengers()) { + passenger.rotate(angleDegrees); + } + } + } + + /** + * Overload taking a double for your convenience and to provide increased accuracy when passing high magnitude + * values - the given double precision angle is normalized into the range [0..360) before passing it to + * {@link #rotate(float)} + * @see #rotate(float) + */ + default void rotate(double angleDegrees) { + rotate(EntityUtil.normalizeYaw(angleDegrees)); + } + + /** + * @return True if yaw and pitch have finite values. Does not check for reasonable finite values. + */ + default boolean isRotationValid() { + return Float.isFinite(getRotationYaw()) && Float.isFinite(getRotationPitch()); + } + + /** + * @see #getRotationYaw() + * @see #getRotationPitch() + */ + default void setPosition(double x, double y, double z, float yaw, float pitch) { + setPosition(x, y, z); + setRotation(yaw, pitch); + } + + /** + * The data of the entity(s) that is riding this entity. Note that both entities control movement and the + * topmost entity controls spawning conditions when created by a mob spawner. + * May be null. + */ + List getPassengers(); + + /** @see #getPassengers() */ + void setPassengers(List passengers); + + /** + * Adds a passenger, initializing the passenger list if necessary + * @param passenger non-null passenger + */ + void addPassenger(EntityBase passenger); + + /** + * Removes all passengers (sets passengers list to null - does not actually clear that list) + */ + void clearPassengers(); + + default boolean hasPassengers() { + return getPassengers() != null && !getPassengers().isEmpty(); + } + + /** + * List of scoreboard tags of this entity. + * Optional - null if not present. + */ + List getScoreboardTags(); + + /** @see #getScoreboardTags() */ + void setScoreboardTags(List scoreboardTags); +} diff --git a/src/main/java/net/querz/mca/entities/EntityBaseImpl.java b/src/main/java/net/querz/mca/entities/EntityBaseImpl.java new file mode 100644 index 00000000..99273d86 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/EntityBaseImpl.java @@ -0,0 +1,737 @@ +package net.querz.mca.entities; + +import net.querz.mca.VersionedDataContainer; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.util.ArgValidator; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +// TODO: update passenger locations when own xyz changed to maintain relative locations +// TODO: guard against passenger infinite recursion - well I guess NBT max depth will also catch it but still +public class EntityBaseImpl implements EntityBase, VersionedDataContainer { + + protected CompoundTag data; + protected int dataVersion; + /** not null */ + protected String id; + /** nullable, if not set {@link #updateHandle()} must calculate a random UUID and assign it. UUID of ZERO must also be treated as unset */ + protected UUID uuid; + /** Note {@link EntityBase#AIR_UNSET} is used as sentinel value indicating no value. */ + protected short air = EntityBase.AIR_UNSET; + protected int portalCooldown; + protected float fallDistance; + protected short fireTicks = -1; + protected int ticksFrozen; + /** nullable, otherwise text nbt */ + protected String customName; + protected boolean isCustomNameVisible; + protected boolean isInvulnerable; + protected boolean isSilent; + protected boolean isGlowing; + protected boolean isOnGround; + protected boolean noGravity; + protected boolean hasVisualFire; + protected double x = Double.NaN; + protected double y = Double.NaN; + protected double z = Double.NaN; + protected float yaw; + protected float pitch; + protected double dx; + protected double dy; + protected double dz; + /** nullable */ + protected List scoreboardTags; + /** nullable */ + protected List passengers; + + public EntityBaseImpl(int dataVersion) { + this.dataVersion = ArgValidator.check(dataVersion, dataVersion >= 0); + this.data = new CompoundTag(); + } + + public EntityBaseImpl(int dataVersion, String id) { + this.dataVersion = ArgValidator.check(dataVersion, dataVersion > 0); + this.id = ArgValidator.requireNotEmpty(id); + this.data = new CompoundTag(); + } + + public EntityBaseImpl(int dataVersion, String id, double x, double y, double z) { + this(dataVersion, id, x, y, z, 0, 0); + } + + public EntityBaseImpl(int dataVersion, String id, double x, double y, double z, float yaw) { + this(dataVersion, id, x, y, z, yaw, 0); + } + + public EntityBaseImpl(int dataVersion, String id, double x, double y, double z, float yaw, float pitch) { + this.dataVersion = ArgValidator.check(dataVersion, dataVersion > 0); + this.id = ArgValidator.requireNotEmpty(id); + this.data = new CompoundTag(); + this.x = x; + this.y = y; + this.z = z; + this.yaw = EntityUtil.normalizeYaw(yaw); + this.pitch = EntityUtil.clampPitch(pitch); + } + + /** + * Copy constructor. + *

        + *
      • Performs a DEEP COPY of this entity and all passengers. + *
      • For passengers, {@link EntityFactory#create(CompoundTag, int)} is invoked to create strongly typed + * entity instances instead of relying on each passengers clone implementation. + *
      • DOES NOT copy UUID's of self or passengers, instead calls {@link #generateNewUuid()} + * to ensure that each gets a new UUID. + *
      • Triggers {@code other.updateHandle()} which may cause {@link #updateHandle()} to throw + * {@link IllegalStateException} if it is not in a valid state. + *
      • New object receives a new {@code data} {@link CompoundTag} cloned from the updated handle of + * {@code other}. + *
      + * @param other Object to clone. + */ + public EntityBaseImpl(EntityBaseImpl other) { + // need to call update handle to make copying passengers clean and tidy + this.data = other.updateHandle().clone(); + this.dataVersion = other.dataVersion; + this.id = other.id; + this.uuid = null; + this.portalCooldown = other.portalCooldown; + this.fallDistance = other.fallDistance; + this.ticksFrozen = other.ticksFrozen; + this.customName = other.customName; + this.isCustomNameVisible = other.isCustomNameVisible; + this.isInvulnerable = other.isInvulnerable; + this.isSilent = other.isSilent; + this.isGlowing = other.isGlowing; + this.isOnGround = other.isOnGround; + this.noGravity = other.noGravity; + this.hasVisualFire = other.hasVisualFire; + this.x = other.x; + this.y = other.y; + this.z = other.z; + this.yaw = other.yaw; + this.pitch = other.pitch; + this.dx = other.dx; + this.dy = other.dy; + this.dz = other.dz; + this.scoreboardTags = other.scoreboardTags == null ? null : new ArrayList<>(other.scoreboardTags); + this.passengers = !this.data.containsKey("Passengers") ? null + : StreamSupport.stream(this.data.getListTag("Passengers").asCompoundTagList().spliterator(), false) + .map(tag -> EntityFactory.create(tag, dataVersion)) + .collect(Collectors.toList()); + + this.generateNewUuid(); + } + + public EntityBaseImpl(CompoundTag data, int dataVersion) { + this.data = data; + this.dataVersion = dataVersion; + this.id = ArgValidator.requireNotEmpty(data.getString("id", null), "id tag"); + this.uuid = EntityUtil.getUuid(dataVersion, data); + this.air = data.getShort("Air", EntityBase.AIR_UNSET); + this.portalCooldown = data.getInt("PortalCooldown"); + this.fallDistance = data.getFloat("FallDistance"); + this.fireTicks = data.getShort("Fire", (short) -1); + this.ticksFrozen = data.getInt("TicksFrozen", 0); + this.customName = data.getString("CustomName", null); + this.isCustomNameVisible = data.getBoolean("CustomNameVisible"); + this.isInvulnerable = data.getBoolean("Invulnerable"); + this.isSilent = data.getBoolean("Silent"); + this.isGlowing = data.getBoolean("Glowing"); + this.hasVisualFire = data.getBoolean("HasVisualFire"); + this.isOnGround = data.getBoolean("OnGround"); + this.noGravity = data.getBoolean("NoGravity"); + double[] pos = data.getDoubleTagListAsArray("Pos"); + if (pos != null && pos.length == 3) { + this.x = pos[0]; + this.y = pos[1]; + this.z = pos[2]; + } + float[] rotation = data.getFloatTagListAsArray("Rotation"); + if (rotation != null && rotation.length == 2) { + this.yaw = rotation[0]; + this.pitch = rotation[1]; + } + double[] motion = data.getDoubleTagListAsArray("Motion"); + if (motion != null && motion.length == 3) { + this.dx = motion[0]; + this.dy = motion[1]; + this.dz = motion[2]; + } + ListTag passengersTag = data.getListTagAutoCast("Passengers"); + if (passengersTag != null && passengersTag.size() > 0) { + this.passengers = new ArrayList<>(passengersTag.size()); + for (CompoundTag ptag : passengersTag) { + this.passengers.add(EntityFactory.create(ptag, dataVersion)); + } + } + this.scoreboardTags = data.getStringTagListValues("Tags"); + } + + // + + @Override + public int getDataVersion() { + return dataVersion; + } + + @Override + public void setDataVersion(int dataVersion) { + this.dataVersion = dataVersion; + } + + /** {@inheritDoc} */ + @Override + public String getId() { + return id; + } + + /** {@inheritDoc} */ + @Override + public void setId(String id) { + this.id = ArgValidator.requireNotEmpty(id); + } + + /** {@inheritDoc} */ + @Override + public UUID getUuid() { + return uuid; + } + + /** {@inheritDoc} */ + @Override + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + /** {@inheritDoc} */ + @Override + public UUID generateNewUuid() { + this.uuid = UUID.randomUUID(); + if (passengers != null) { + for (EntityBase passenger : passengers) { + passenger.generateNewUuid(); + } + } + return uuid; + } + + /** {@inheritDoc} */ + @Override + public short getAir() { + return air; + } + + /** {@inheritDoc} */ + @Override + public void setAir(short air) { + this.air = air; + } + + /** {@inheritDoc} */ + @Override + public float getFallDistance() { + return fallDistance; + } + + /** {@inheritDoc} */ + @Override + public void setFallDistance(float fallDistance) { + this.fallDistance = fallDistance; + } + + /** {@inheritDoc} */ + @Override + public short getFire() { + return fireTicks; + } + + /** {@inheritDoc} */ + @Override + public void setFire(short fireTicks) { + this.fireTicks = fireTicks; + } + + /** {@inheritDoc} */ + @Override + public int getTicksFrozen() { + return ticksFrozen; + } + + /** {@inheritDoc} */ + @Override + public void setTicksFrozen(int ticksFrozen) { + this.ticksFrozen = ticksFrozen; + } + + /** {@inheritDoc} */ + @Override + public int getPortalCooldown() { + return portalCooldown; + } + + /** {@inheritDoc} */ + @Override + public void setPortalCooldown(int portalCooldown) { + this.portalCooldown = portalCooldown; + } + + /** {@inheritDoc} */ + @Override + public String getCustomName() { + return customName; + } + + /** {@inheritDoc} */ + @Override + public void setCustomName(String customName) { + this.customName = customName; + } + + /** {@inheritDoc} */ + @Override + public boolean isCustomNameVisible() { + return isCustomNameVisible; + } + + /** {@inheritDoc} */ + @Override + public void setCustomNameVisible(boolean visible) { + this.isCustomNameVisible = visible; + } + + /** {@inheritDoc} */ + @Override + public boolean isInvulnerable() { + return isInvulnerable; + } + + /** {@inheritDoc} */ + @Override + public void setInvulnerable(boolean invulnerable) { + this.isInvulnerable = invulnerable; + } + + /** {@inheritDoc} */ + @Override + public boolean isSilent() { + return isSilent; + } + + /** {@inheritDoc} */ + @Override + public void setSilent(boolean silent) { + this.isSilent = silent; + } + + /** {@inheritDoc} */ + @Override + public boolean isGlowing() { + return isGlowing; + } + + /** {@inheritDoc} */ + @Override + public void setGlowing(boolean glowing) { + this.isGlowing = glowing; + } + + /** {@inheritDoc} */ + @Override + public boolean hasNoGravity() { + return noGravity; + } + + /** {@inheritDoc} */ + @Override + public void setNoGravity(boolean noGravity) { + this.noGravity = noGravity; + } + + /** {@inheritDoc} */ + @Override + public boolean isOnGround() { + return this.isOnGround; + } + + /** {@inheritDoc} */ + @Override + public void setOnGround(boolean onGround) { + this.isOnGround = onGround; + } + + /** {@inheritDoc} */ + @Override + public boolean hasVisualFire() { + return hasVisualFire; + } + + /** {@inheritDoc} */ + @Override + public void setHasVisualFire(boolean hasVisualFire) { + this.hasVisualFire = hasVisualFire; + } + + /** + * {@inheritDoc} + * May be {@link Double#NaN} if unset. + * @see #isPositionValid() + */ + @Override + public double getX() { + return x; + } + + /** {@inheritDoc} */ + @Override + public void setX(final double x) { + if (passengers != null) { + if (!Double.isFinite(this.x) || !Double.isFinite(x)) { + for (EntityBase passenger : passengers) { + passenger.setX(x); + } + } else { + final double dx = x - this.x; + for (EntityBase passenger : passengers) { + double px = passenger.getX(); + if (Double.isFinite(px)) { + passenger.setX(px + dx); + } else { + passenger.setX(x); + } + } + } + } + this.x = x; + } + + /** + * {@inheritDoc} + * May be {@link Double#NaN} if unset. + * @see #isPositionValid() + */ + @Override + public double getY() { + return y; + } + + /** {@inheritDoc} */ + @Override + public void setY(final double y) { + if (passengers != null) { + if (!Double.isFinite(this.y) || !Double.isFinite(y)) { + for (EntityBase passenger : passengers) { + passenger.setY(y); + } + } else { + final double dy = y - this.y; + for (EntityBase passenger : passengers) { + double py = passenger.getY(); + if (Double.isFinite(py)) { + passenger.setY(py + dy); + } else { + passenger.setY(y); + } + } + } + } + this.y = y; + } + + /** + * {@inheritDoc} + * May be {@link Double#NaN} if unset. + * @see #isPositionValid() + */ + @Override + public double getZ() { + return z; + } + + /** {@inheritDoc} */ + @Override + public void setZ(final double z) { + if (passengers != null) { + if (!Double.isFinite(this.z) || !Double.isFinite(z)) { + for (EntityBase passenger : passengers) { + passenger.setZ(z); + } + } else { + final double dz = z - this.z; + for (EntityBase passenger : passengers) { + double pz = passenger.getZ(); + if (Double.isFinite(pz)) { + passenger.setZ(pz + dz); + } else { + passenger.setZ(z); + } + } + } + } + this.z = z; + } + + /** {@inheritDoc} */ + @Override + public float getRotationYaw() { + return yaw; + } + + /** + * Sets entity yaw (rotation about the y-axis) in degrees, with 0 being due south. The caller does not need + * to worry about passing a {@code yaw} value in the range [0..360), the given value will be normalized + * into the valid range. + * @see #getRotationYaw() + * @see #rotate(float) + */ + @Override + public void setRotationYaw(float yaw) { + this.yaw = EntityUtil.normalizeYaw(yaw); + } + + /** {@inheritDoc} */ + @Override + public float getRotationPitch() { + return pitch; + } + + /** {@inheritDoc} */ + @Override + public void setRotationPitch(float pitch) { + this.pitch = EntityUtil.clampPitch(pitch); + } + + /** {@inheritDoc} */ + @Override + public void setRotation(float yaw, float pitch) { + this.yaw = EntityUtil.normalizeYaw(yaw); + this.pitch = EntityUtil.clampPitch(pitch); + } + + /** {@inheritDoc} */ + @Override + public void setPosition(double x, double y, double z) { + if (!hasPassengers()) { + this.x = x; + this.y = y; + this.z = z; + } else { + // it's just easier to handle the passenger move logic this way + setX(x); + setY(y); + setZ(z); + } + } + /** {@inheritDoc} */ + @Override + public double getMotionDX() { + return dx; + } + + /** {@inheritDoc} */ + @Override + public void setMotionDX(double dx) { + this.dx = dx; + if (passengers != null) { + for (EntityBase passenger : passengers) { + passenger.setMotionDX(dx); + } + } + } + + /** {@inheritDoc} */ + @Override + public double getMotionDY() { + return dy; + } + + /** {@inheritDoc} */ + @Override + public void setMotionDY(double dy) { + this.dy = dy; + if (passengers != null) { + for (EntityBase passenger : passengers) { + passenger.setMotionDY(dy); + } + } + } + + /** {@inheritDoc} */ + @Override + public double getMotionDZ() { + return dz; + } + + /** {@inheritDoc} */ + @Override + public void setMotionDZ(double dz) { + this.dz = dz; + if (passengers != null) { + for (EntityBase passenger : passengers) { + passenger.setMotionDZ(dz); + } + } + } + + /** {@inheritDoc} */ + @Override + public void setMotion(double dx, double dy, double dz) { + this.dx = dx; + this.dy = dy; + this.dz = dz; + if (passengers != null) { + for (EntityBase passenger : passengers) { + passenger.setMotion(dx, dy, dz); + } + } + } + + /** {@inheritDoc} */ + @Override + public List getPassengers() { + return passengers; + } + + /** {@inheritDoc} */ + @Override + public void setPassengers(List passengers) { + this.passengers = passengers; + } + + /** + * {@inheritDoc} + *

      The caller is generally responsible for ensuring that the positions of passengers make sense or can be + * corrected by the game. However, if the given passenger does not satisfy {@link #isPositionValid()} + * then its position will be set to be the same as this entities position (which only helps if this entity + * satisfies {@link #isPositionValid()}).

      + *

      Also sets passenger motion to match this entities motion.

      + */ + @Override + public void addPassenger(EntityBase passenger) { + ArgValidator.requireValue(passenger); + ArgValidator.check(passenger != this); // at least prevent direct recursion + if (passengers == null) { + passengers = new ArrayList<>(); + } + if (!passenger.isPositionValid()) { + passenger.setPosition(x, y, z); + } + passenger.setMotion(dx, dy, dz); + passengers.add(passenger); + } + + /** {@inheritDoc} */ + @Override + public void clearPassengers() { + passengers = null; + } + + /** {@inheritDoc} */ + @Override + public List getScoreboardTags() { + return scoreboardTags; + } + + /** {@inheritDoc} */ + @Override + public void setScoreboardTags(List scoreboardTags) { + this.scoreboardTags= scoreboardTags; + } + + //
      + + /** {@inheritDoc} */ + @Override + public CompoundTag getHandle() { + return data; + } + + /** {@inheritDoc} */ + @Override + public CompoundTag updateHandle() { + // TODO: restrict field outputs to be version appropriate - there's no harm in extra fields but might as well + // be clean about it. + + // TODO: at some point prefixing with "minecraft:" became preferred, find that version and match behavior + // likewise strip that prefix for previous versions + data.putString("id", ArgValidator.requireValue(id, "id")); + if (uuid == null || EntityUtil.ZERO_UUID.equals(uuid)) { + uuid = UUID.randomUUID(); + } + EntityUtil.setUuid(dataVersion, data, uuid); + + if (!isPositionValid()) { + throw new IllegalStateException("X, Y, or Z is non-finite or was not set"); + } + data.putDoubleArrayAsTagList("Pos", x, y, z); + if (isRotationValid()) { + data.putFloatArrayAsTagList("Rotation", yaw, pitch); + } + if (isMotionValid()) { + data.putDoubleArrayAsTagList("Motion", dx, dy, dz); + } + + if (air != EntityBase.AIR_UNSET) { + data.putShort("Air", air); + } else { + data.remove("Air"); + } + + if (customName != null && !customName.isEmpty()) { + data.putString("CustomName", customName); + } + if (isCustomNameVisible || data.containsKey("CustomNameVisible")) { + data.putBoolean("CustomNameVisible", isCustomNameVisible); + } + + data.putFloat("FallDistance", fallDistance); + data.putShort("Fire", fireTicks); + + if (isGlowing || data.containsKey("Glowing")) { + data.putBoolean("Glowing", isGlowing); + } + if (hasVisualFire || data.containsKey("HasVisualFire")) { + data.putBoolean("HasVisualFire", hasVisualFire); + } + if (isInvulnerable || data.containsKey("Invulnerable")) { + data.putBoolean("Invulnerable", isInvulnerable); + } + if (noGravity || data.containsKey("NoGravity")) { + data.putBoolean("NoGravity", noGravity); + } + + data.putBoolean("OnGround", isOnGround); + data.putInt("PortalCooldown", portalCooldown); + + if (isSilent || data.containsKey("Silent")) { + data.putBoolean("Silent", isSilent); + } + + data.putStringsAsTagList("Tags", scoreboardTags); + data.putInt("TicksFrozen", ticksFrozen); + + if (passengers != null && !passengers.isEmpty()) { + ListTag passengersTag = new ListTag<>(CompoundTag.class, passengers.size()); + for (EntityBase passenger : passengers) { + passengersTag.add(passenger.updateHandle()); + } + data.put("Passengers", passengersTag); + } else { + data.remove("Passengers"); + } + return data; + } + + /** + * Calls the copy constructor. + * @return Deep clone of this entity. + * @see EntityBaseImpl#EntityBaseImpl(EntityBaseImpl) + */ + @Override + public EntityBaseImpl clone() { + return new EntityBaseImpl(this); + } +} diff --git a/src/main/java/net/querz/mca/entities/EntityCreator.java b/src/main/java/net/querz/mca/entities/EntityCreator.java new file mode 100644 index 00000000..8f043492 --- /dev/null +++ b/src/main/java/net/querz/mca/entities/EntityCreator.java @@ -0,0 +1,20 @@ +package net.querz.mca.entities; + + +import net.querz.nbt.tag.CompoundTag; + +@FunctionalInterface +public interface EntityCreator { + + /** + * @param normalizedId normalized entity id, with no "minecraft:" prefix and all UPPER CASE. + * Note this is always the result of {@link EntityFactory#normalizeAndRemapId(String)} and may + * not match the id tag found in the nbt data when reading from old chunks. + * @param tag tag containing entity data + * @param dataVersion data version of chunk / tag + * @return NOT NULL, if an implementer cannot process the given tag it should throw an + * {@link IllegalEntityTagException} + * @throws IllegalEntityTagException Thrown if this creator cannot return a result. + */ + T create(String normalizedId, CompoundTag tag, int dataVersion); +} diff --git a/src/main/java/net/querz/mca/entities/EntityFactory.java b/src/main/java/net/querz/mca/entities/EntityFactory.java new file mode 100644 index 00000000..917739eb --- /dev/null +++ b/src/main/java/net/querz/mca/entities/EntityFactory.java @@ -0,0 +1,244 @@ +package net.querz.mca.entities; + +import net.querz.nbt.tag.CompoundTag; +import net.querz.util.ArgValidator; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Provides a way to customize entity data deserialization. + */ +public class EntityFactory { + private EntityFactory() { } + + // TODO: Implement "creator ai" solution which is composed of a list of predicates that, when there is no + // creator in ENTITY_CREATORS_BY_ID for an entity id, are given the nbt tag one after the other and + // once a predicate returns a creator instance, that creator is learned for that id (put in + // ENTITY_CREATORS_BY_ID) and used from that point on. This will provide a self maintaining solution + // that will, for example, be able to work for all mobs in all future versions by checking for mob-only + // tags without having to maintain the list of all mobs. For efficiency it's probably wise to associate + // the default creator when no predicate provides a creator to use. + + /** + * This map controls the factory creation behavior, keys are entity id's (such as "pig"). + * Id names in this map should not contain the "minecraft:" prefix and should be all UPPER CASE. + */ + private static final Map> ENTITY_CREATORS_BY_ID = new HashMap<>(); + private static EntityCreator DEFAULT_ENTITY_CREATOR = new DefaultEntityCreator(); + + private static final Map ID_REMAP; + + static { + ID_REMAP = new HashMap<>(); + resetEntityIdRemap(); + } + + /** + * Clears the entity id remapping table and removes any creators registered to one of the "old id's". + * This should be generally safe, but if you have explicitly associated a creator with an old name, know that + * you need to re-associate it after making this call. + */ + public static void clearEntityIdRemap() { + // contract of supporting functions ensure that no key is also a value in this map, therefore + // it is not possible that we remove an explicit "value" mapping. + ENTITY_CREATORS_BY_ID.keySet().removeAll(ID_REMAP.keySet()); + ID_REMAP.clear(); + } + + /** + * Resets the entity id remapping table to hard-coded defaults. + * @see #clearCreators() + */ + public static void resetEntityIdRemap() { + clearEntityIdRemap(); + // sources: + // https://technical-minecraft.fandom.com/wiki/Entity + // https://minecraft.fandom.com/wiki/Java_Edition_data_values#Entities + registerIdRemap("ArmorStand", "armor_stand"); + registerIdRemap("CaveSpider", "cave_spider"); + registerIdRemap("Dragon", "ender_dragon"); + registerIdRemap("EnderCrystal", "end_crystal"); + registerIdRemap("ender_crystal", "end_crystal"); + registerIdRemap("EnderEye", "eye_of_ender"); + registerIdRemap("EnderPearl", "ender_pearl"); + registerIdRemap("ExpBottle", "experience_bottle"); + registerIdRemap("FallingBlock", "falling_block"); + registerIdRemap("FireworkRocket", "firework_rocket"); + registerIdRemap("GiantZombie", "giant"); + registerIdRemap("IronGolem", "iron_golem"); + registerIdRemap("ItemFrame", "item_frame"); + registerIdRemap("LargeFireball", "fireball"); + registerIdRemap("LeashKnot", "leash_knot"); + registerIdRemap("LightningBolt", "lightning_bolt"); + registerIdRemap("MagmaCube", "magma_cube"); + // TODO: find old name for command_block_minecart - not that anyone is likely to notice + registerIdRemap("MinecartChest", "chest_minecart"); + registerIdRemap("MinecartEmpty", "minecart"); + registerIdRemap("MinecartFurnace", "furnace_minecart"); + registerIdRemap("MinecartHopper", "hopper_minecart"); + registerIdRemap("MinecartMobSpawner", "spawner_minecart"); + registerIdRemap("MinecartTNT", "tnt_minecart"); + registerIdRemap("PigZombie", "zombified_piglin"); + registerIdRemap("zombie_pigman", "zombified_piglin"); + registerIdRemap("SmallFireball", "small_fireball"); + registerIdRemap("Snowman", "snow_golem"); + registerIdRemap("TNTPrimed", "tnt"); + registerIdRemap("WitherSkull", "wither_skull"); + registerIdRemap("XPOrb", "experience_orb"); + } + + /** + * Registers a mapping from an old id name to a new one. + * Chaining of mappings is not supported and is guarded against. + * Maintains ENTITY_CREATORS_BY_ID map to ensure any creator registered for the preferredId is fired when the oldId + * is encountered IFF there is not already a creator registered for the oldId. + *

      Note that creators are ALWAYS passed the preferredId even if the data source used an old id.

      + * @param oldId ID found in entity nbt data for versions of minecraft prior to the preferred version. + * @param preferredId Preferred ID found in the entity nbt data for the most current supported minecraft version. + */ + public static void registerIdRemap(String oldId, String preferredId) { + String oldIdNorm = normalizeId(oldId); + String newIdNorm = normalizeId(preferredId); + ArgValidator.check(!ID_REMAP.containsKey(newIdNorm) && !ID_REMAP.containsValue(oldIdNorm), + String.format("Chaining of mappings not supported. While adding %s -> %s", oldIdNorm, newIdNorm)); + ID_REMAP.put(oldIdNorm, newIdNorm); + if (ENTITY_CREATORS_BY_ID.containsKey(newIdNorm)) { + ENTITY_CREATORS_BY_ID.putIfAbsent(oldIdNorm, ENTITY_CREATORS_BY_ID.get(newIdNorm)); + } + } + + /** + * Performs a reverse lookup in the remap table to find all of the old id's which are mapped to the given one. + * Use infrequently - this is not an optimized operation. + * @param currentId Current ID (will be passed through {@link #normalizeId(String)}) + * @return not null; list of old id's (in normalized form) that are mapped to the given currentID + */ + public static List reverseIdRemap(String currentId) { + final String idNorm = normalizeId(currentId); + return ID_REMAP.entrySet().stream() + .filter(e -> idNorm.equals(e.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + + public static void setDefaultEntityCreator(EntityCreator creator) { + if (creator == null) throw new IllegalArgumentException(); + DEFAULT_ENTITY_CREATOR = creator; + } + + public static EntityCreator getDefaultEntityCreator() { + return DEFAULT_ENTITY_CREATOR; + } + + /** + * Gets the set of NORMALIZED id's which have creators registered. + * @see #normalizeId(String) + * @return Key set from underlying map, modifications to this map will affect the set of registered creators. + */ + public static Set getRegisteredCreatorIdKeys() { + return ENTITY_CREATORS_BY_ID.keySet(); + } + + /** + * Exposed for advanced usage only, most use cases should not need to call this function. + * @param id NORMALIZED IF. You can normalize an id by calling {@link #normalizeId(String)} + * @return Registered creator or null if there is none (does not fall back to the default creator). + */ + public static EntityCreator getCreatorById(String id) { + return ENTITY_CREATORS_BY_ID.get(id); + } + + /** + * Clears this factories creators map and restores the {@link DefaultEntityCreator} as the default. + * Does NOT reset the entity id remap - call {@link #resetEntityIdRemap()} to do that. + */ + public static void clearCreators() { + ENTITY_CREATORS_BY_ID.clear(); + DEFAULT_ENTITY_CREATOR = new DefaultEntityCreator(); + } + + /** + * Checks that the given id has a value then normalizes it by removing any "minecraft:" + * prefix and making them ALL CAPS for ease of use with custom enum lookups by name. + * + *

      This function DOES NOT perform any name remapping for id's from old versions. + * If that is what you are looking for, use {@link #normalizeAndRemapId(String)} instead.

      + * + * @param id Entity ID + * @return Normalized entity ID - DO NOT set this value as the value of an "id" tag, + * that's not what this function is for. + * @throws IllegalArgumentException Thrown when ID is null or empty (after removing any "minecraft:" prefix) + * or when dataVersion LE 0. + */ + public static String normalizeId(String id) { + ArgValidator.requireValue(id); + id = id.toUpperCase(); + if (id.startsWith("MINECRAFT:")) id = id.substring(10); + ArgValidator.requireNotEmpty(id); + return id; + } + + /** + * @param id ID + * @return Remapped normalized id if there is one, otherwise same as calling {@link #normalizeId(String)} + */ + public static String normalizeAndRemapId(String id) { + final String idNorm = normalizeId(id); + return ID_REMAP.getOrDefault(idNorm, idNorm); + } + + /** + * Registers a creator for one or more entity id's. If there is already a creator registered for the id + * it is silently replaced. There is no need to list all current and legacy id's, only the current ones. + * This function will also register the given creator for all mapped legacy id's which do not already + * have a creator associated. + * @param creator Entity creator + * @param entityId One or more entity id's. ID matching is performed case-insensitive and any "minecraft:" + * prefixes are stripped (therefore do not need to be included). + */ + public static void registerCreator(EntityCreator creator, String... entityId) { + if (creator == null) throw new IllegalArgumentException("creator must not be null"); + for (String id : entityId) { + String idNorm = normalizeId(id); + ENTITY_CREATORS_BY_ID.put(idNorm, creator); + for (String legacyId : reverseIdRemap(idNorm)) { + ENTITY_CREATORS_BY_ID.putIfAbsent(legacyId, creator); + } + } + } + + /** + * Creates and initializes an entity from the given information. + * @param tag must not be null; must contain an "id" tag representation of the entity's ID + * @param dataVersion chunk data version to pass along to the creator + * @return new entity object; never null + * @throws IllegalEntityTagException if the creator failed to create an instance + */ + public static EntityBase create(CompoundTag tag, int dataVersion) { + if (tag == null) throw new IllegalArgumentException("tag must not be null"); + String idNorm = normalizeId(tag.getString("id", null)); + String idPreferredNorm = ID_REMAP.getOrDefault(idNorm, idNorm); + EntityCreator creator = ENTITY_CREATORS_BY_ID.getOrDefault(idNorm, DEFAULT_ENTITY_CREATOR); + EntityBase entity = creator.create(idPreferredNorm, tag, dataVersion); + if (entity == null) { + throw new IllegalEntityTagException(String.format( + "creator %s for %s returned null (it should throw IllegalEntityTagException itself, but didn't)", + creator.getClass().getSimpleName(), + idNorm)); + } + return entity; + } + + /** + * Use this method when you know the return type - for example if you have your own base class and have + * reconfigured this factory with creators which always return that. + * Any casting exceptions which result will be thrown from the call site - not from within this function. + * @see #create(CompoundTag, int) + */ + @SuppressWarnings("unchecked") + public static T createAutoCast(CompoundTag tag, int dataVersion) { + return (T) create(tag, dataVersion); + } +} diff --git a/src/test/java/net/querz/mca/EntitiesMCAFileTest.java b/src/test/java/net/querz/mca/EntitiesMCAFileTest.java new file mode 100644 index 00000000..fbf5151d --- /dev/null +++ b/src/test/java/net/querz/mca/EntitiesMCAFileTest.java @@ -0,0 +1,59 @@ +package net.querz.mca; + +import net.querz.mca.entities.EntityBase; +import net.querz.mca.entities.EntityBaseImpl; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class EntitiesMCAFileTest extends MCAFileBaseTest { + + public void testMcaReadWriteParity_1_17_1() { + validateReadWriteParity(DataVersion.JAVA_1_17_1, "1_17_1/entities/r.-3.-2.mca", EntitiesMCAFile.class); + } + + public void testMcaReadWriteParity_1_18_PRE1() { + validateReadWriteParity(DataVersion.JAVA_1_18_PRE1, "1_18_PRE1/entities/r.-2.-3.mca", EntitiesMCAFile.class); + } + + public void testReadEntities_1_17_1() throws IOException { + EntitiesMCAFile mca = MCAUtil.readAuto(copyResourceToTmp("1_17_1/entities/r.-3.-2.mca")); + assertNotNull(mca); + EntitiesChunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + List entities = chunk.getEntities(); + assertNotNull(entities); + assertFalse(entities.isEmpty()); + + // mca specific checks (will need to be changed if mca file changes in meaningful ways) + assertEquals(1, entities.size()); + Map> entitiesByType = chunk.stream().collect(Collectors.groupingBy(EntityBase::getId)); + assertEquals(1, entitiesByType.size()); + assertTrue(entitiesByType.containsKey("minecraft:villager")); + assertEquals(1, entitiesByType.get("minecraft:villager").size()); + } + + public void testReadEntities_1_18_PRE1() throws IOException { + EntitiesMCAFile mca = MCAUtil.readAuto(copyResourceToTmp("1_18_PRE1/entities/r.-2.-3.mca")); + assertNotNull(mca); + EntitiesChunk chunk = mca.stream().filter(Objects::nonNull).findFirst().orElse(null); + assertNotNull(chunk); + List entities = chunk.getEntities(); + assertNotNull(entities); + assertFalse(entities.isEmpty()); + + // mca specific checks (will need to be changed if mca file changes in meaningful ways) + assertEquals(7, entities.size()); + Map> entitiesByType = chunk.stream().collect(Collectors.groupingBy(EntityBase::getId)); + assertEquals(3, entitiesByType.size()); + assertTrue(entitiesByType.containsKey("minecraft:villager")); + assertEquals(4, entitiesByType.get("minecraft:villager").size()); + assertTrue(entitiesByType.containsKey("minecraft:chicken")); + assertEquals(2, entitiesByType.get("minecraft:chicken").size()); + assertTrue(entitiesByType.containsKey("minecraft:cat")); + assertEquals(1, entitiesByType.get("minecraft:cat").size()); + } +} diff --git a/src/test/java/net/querz/mca/MCAFileBaseTest.java b/src/test/java/net/querz/mca/MCAFileBaseTest.java index 45f3df88..6c881264 100644 --- a/src/test/java/net/querz/mca/MCAFileBaseTest.java +++ b/src/test/java/net/querz/mca/MCAFileBaseTest.java @@ -7,6 +7,7 @@ // TODO: implement abstract test pattern for MCAFileBase & refactor MCAFileTest like mad public class MCAFileBaseTest extends MCATestCase { + // TODO: scan resources folder and auto-run this test for each mca file and type so we don't need to update tests every time we add a new test region file. /** * Reads an mca file, writes it, reads it back in, verifies that the NBT of the first non-empty chunk is identical * between the reads and that the data versions are correct and that MCAUtil.readAuto returned the correct type. @@ -29,6 +30,9 @@ protected > void validateReadWr assertEquals(expectedDataVersion.id(), mcaB.getDefaultChunkDataVersion()); assertEquals(expectedDataVersion, chunkB.getDataVersionEnum()); + assertEquals(chunkA.getLastMCAUpdate(), chunkB.getLastMCAUpdate()); assertEquals(chunkA.data, chunkB.data); } + + public void testNullTest() { } } From 35b8e5c951e94b9f531913a420ed32a1aa900a36 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 14 Nov 2021 21:16:47 -0500 Subject: [PATCH 064/288] Adding stream support to NBT tags that make sense to have it. --- .../java/net/querz/nbt/tag/CompoundTag.java | 10 ++++++++++ .../java/net/querz/nbt/tag/IntArrayTag.java | 5 +++++ src/main/java/net/querz/nbt/tag/ListTag.java | 17 +++++++++++------ .../java/net/querz/nbt/tag/LongArrayTag.java | 5 +++++ .../net/querz/nbt/tag/CompoundTagTest.java | 8 ++++++++ .../net/querz/nbt/tag/IntArrayTagTest.java | 6 ++++++ .../java/net/querz/nbt/tag/ListTagTest.java | 18 ++++++++++++++++++ .../net/querz/nbt/tag/LongArrayTagTest.java | 6 ++++++ 8 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java index 7a91b691..38dce2ff 100644 --- a/src/main/java/net/querz/nbt/tag/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -3,6 +3,7 @@ import java.util.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import net.querz.io.MaxDepthIO; @@ -69,6 +70,15 @@ public Iterator>> iterator() { return entrySet().iterator(); } + @Override + public Spliterator>> spliterator() { + return entrySet().spliterator(); + } + + public Stream>> stream() { + return getValue().entrySet().stream(); + } + public void forEach(BiConsumer> action) { getValue().forEach(action); } diff --git a/src/main/java/net/querz/nbt/tag/IntArrayTag.java b/src/main/java/net/querz/nbt/tag/IntArrayTag.java index 1799c93c..12a03a6e 100644 --- a/src/main/java/net/querz/nbt/tag/IntArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/IntArrayTag.java @@ -1,6 +1,7 @@ package net.querz.nbt.tag; import java.util.Arrays; +import java.util.stream.IntStream; public class IntArrayTag extends ArrayTag implements Comparable { @@ -39,4 +40,8 @@ public int compareTo(IntArrayTag other) { public IntArrayTag clone() { return new IntArrayTag(Arrays.copyOf(getValue(), length())); } + + public IntStream stream() { + return Arrays.stream(getValue()); + } } diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java index 955ad1a4..e33c6d7c 100644 --- a/src/main/java/net/querz/nbt/tag/ListTag.java +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -1,12 +1,8 @@ package net.querz.nbt.tag; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.function.Consumer; +import java.util.stream.Stream; import net.querz.io.MaxDepthIO; @@ -126,11 +122,20 @@ public Iterator iterator() { return getValue().iterator(); } + @Override + public Spliterator spliterator() { + return getValue().spliterator(); + } + @Override public void forEach(Consumer action) { getValue().forEach(action); } + public Stream stream() { + return getValue().stream(); + } + public T set(int index, T t) { return getValue().set(index, Objects.requireNonNull(t)); } diff --git a/src/main/java/net/querz/nbt/tag/LongArrayTag.java b/src/main/java/net/querz/nbt/tag/LongArrayTag.java index e0528dd1..e8a1a088 100644 --- a/src/main/java/net/querz/nbt/tag/LongArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/LongArrayTag.java @@ -1,6 +1,7 @@ package net.querz.nbt.tag; import java.util.Arrays; +import java.util.stream.LongStream; public class LongArrayTag extends ArrayTag implements Comparable { @@ -39,4 +40,8 @@ public int compareTo(LongArrayTag other) { public LongArrayTag clone() { return new LongArrayTag(Arrays.copyOf(getValue(), length())); } + + public LongStream stream() { + return Arrays.stream(getValue()); + } } diff --git a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java index f58e8c14..0b1d8e4a 100644 --- a/src/test/java/net/querz/nbt/tag/CompoundTagTest.java +++ b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java @@ -4,6 +4,7 @@ import net.querz.NBTTestCase; import java.util.*; +import java.util.stream.Collectors; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotEquals; @@ -363,6 +364,13 @@ public void testIterator() { assertEquals(3, ct.size()); } + public void testStream() { + CompoundTag ct = createCompoundTag(); + List keys = ct.stream().map(Map.Entry::getKey).collect(Collectors.toList()); + assertEquals(ct.size(), keys.size()); + assertTrue(keys.containsAll(Arrays.asList("b", "str", "list"))); + } + public void testPutIfNotNull() { CompoundTag ct = new CompoundTag(); assertEquals(0, ct.size()); diff --git a/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java b/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java index 1458a732..487675ff 100644 --- a/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java +++ b/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java @@ -61,4 +61,10 @@ public void testCompareTo() { assertTrue(0 > t4.compareTo(t)); assertThrowsRuntimeException(() -> t.compareTo(null), NullPointerException.class); } + + public void testStream() { + IntArrayTag tag = new IntArrayTag(new int[] {12, 7, 42, -69, 148, -187876}); + assertEquals(-187876, tag.stream().min().getAsInt()); + assertEquals(148, tag.stream().max().getAsInt()); + } } diff --git a/src/test/java/net/querz/nbt/tag/ListTagTest.java b/src/test/java/net/querz/nbt/tag/ListTagTest.java index af2b72fb..fb69e040 100644 --- a/src/test/java/net/querz/nbt/tag/ListTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ListTagTest.java @@ -6,6 +6,8 @@ import java.util.Arrays; import java.util.Comparator; +import java.util.stream.Collectors; + import static org.junit.Assert.assertNotEquals; public class ListTagTest extends NBTTestCase { @@ -349,4 +351,20 @@ public void testAdd() { assertEquals(1, la.size()); assertTrue(Arrays.equals(new long[] {Long.MIN_VALUE, 0, Long.MAX_VALUE}, la.get(0).getValue())); } + + public void testStream() { + ListTag tag = new ListTag<>(StringTag.class); + tag.addString("A"); + tag.addString("quick"); + tag.addString("brown"); + tag.addString("fox"); + tag.addString("jumps"); + tag.addString("over"); + tag.addString("the"); + tag.addString("lazy"); + tag.addString("dog"); + + assertEquals("A quick brown fox jumps over the lazy dog", + tag.stream().map(StringTag::getValue).collect(Collectors.joining(" "))); + } } diff --git a/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java b/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java index dfa2a3f3..9acab67a 100644 --- a/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java +++ b/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java @@ -61,4 +61,10 @@ public void testCompareTo() { assertTrue(0 > t4.compareTo(t)); assertThrowsRuntimeException(() -> t.compareTo(null), NullPointerException.class); } + + public void testStream() { + LongArrayTag tag = new LongArrayTag(new long[] {12, 7, 42, -69, 148, -1878769999999L}); + assertEquals(-1878769999999L, tag.stream().min().getAsLong()); + assertEquals(148, tag.stream().max().getAsLong()); + } } From 1d553501a69708a092a5d24dba86af127da0ee47 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 19 Nov 2021 07:33:07 -0500 Subject: [PATCH 065/288] DataVersion#isCrossedByTransition added --- src/main/java/net/querz/mca/DataVersion.java | 107 +++++++++++------- .../java/net/querz/mca/DataVersionTest.java | 48 +++++--- 2 files changed, 102 insertions(+), 53 deletions(-) diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 0d1d5dcc..a44fd43e 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -37,21 +37,23 @@ public enum DataVersion { JAVA_1_13_1(1628, 13, 1), JAVA_1_13_2(1631, 13, 2), - // poi/r.X.Z.mca files introduced + /** /poi/r.X.Z.mca files introduced */ JAVA_1_14_0(1952, 14, 0), JAVA_1_14_1(1957, 14, 1), JAVA_1_14_2(1963, 14, 2), JAVA_1_14_3(1968, 14, 3), JAVA_1_14_4(1976, 14, 4), - // 3D Biomes added. Biomes array in the Level tag for each chunk changed - // to contain 1024 integers instead of 256 see {@link Chunk} + /** + * 3D Biomes added. Biomes array in the Level tag for each chunk changed + * to contain 1024 integers instead of 256 see {@link Chunk} + */ JAVA_1_15_19W36A(2203, 15, -1, "19w36a"), JAVA_1_15_0(2225, 15, 0), JAVA_1_15_1(2227, 15, 1), JAVA_1_15_2(2230, 15, 2), - // block pallet packing changed in this version - see {@link Section} + /** Block pallet packing changed in this version - see {@link Section} */ JAVA_1_16_20W17A(2529, 16, -1, "20w17a"), JAVA_1_16_0(2566, 16, 0), JAVA_1_16_1(2567, 16, 1), @@ -60,34 +62,47 @@ public enum DataVersion { JAVA_1_16_4(2584, 16, 4), JAVA_1_16_5(2586, 16, 5), - // entities/r.X.Z.mca files introduced - // entities no longer inside region/r.X.Z.mca - except in un-migrated chunks of course + /** + * /entities/r.X.Z.mca files introduced. + * Entities no longer inside region/r.X.Z.mca - except in un-migrated chunks of course. + *

      https://www.minecraft.net/en-us/article/minecraft-snapshot-20w45a

      + */ + JAVA_1_17_20W45A(2681, 17, -1, "20w45a"), + JAVA_1_17_0(2724, 17, 0), JAVA_1_17_1(2730, 17, 1), // fist experimental 1.18 build JAVA_1_18_XS1(2825, 18, -1, "XS1"), - // https://www.minecraft.net/en-us/article/minecraft-snapshot-21w39a - // Level.Sections[].BlockStates & Level.Sections[].Palette have moved to a container structure in Level.Sections[].block_states - // Level.Biomes are now paletted and live in a similar container structure in Level.Sections[].biomes - // Level.CarvingMasks[] is now long[] instead of byte[] + /** + * https://www.minecraft.net/en-us/article/minecraft-snapshot-21w39a + *
        + *
      • Level.Sections[].BlockStates & Level.Sections[].Palette have moved to a container structure in Level.Sections[].block_states + *
      • Level.Biomes are now paletted and live in a similar container structure in Level.Sections[].biomes + *
      • Level.CarvingMasks[] is now long[] instead of byte[] + *
      + */ JAVA_1_18_21W39A(2836, 18, -1, "21w39a"), - // https://www.minecraft.net/en-us/article/minecraft-snapshot-21w43a - // Removed chunk’s Level and moved everything it contained up - // Chunk’s Level.Entities has moved to entities - // Chunk’s Level.TileEntities has moved to block_entities - // Chunk’s Level.TileTicks and Level.ToBeTicked have moved to block_ticks - // Chunk’s Level.LiquidTicks and Level.LiquidsToBeTicked have moved to fluid_ticks - // Chunk’s Level.Sections has moved to sections - // Chunk’s Level.Structures has moved to structures - // Chunk’s Level.Structures.Starts has moved to structures.starts - // Chunk’s Level.Sections[].BlockStates and Level.Sections[].Palette have moved to a container structure in sections[].block_states - // Chunk’s Level.Biomes are now paletted and live in a similar container structure in sections[].biomes - // Added yPos the minimum section y position in the chunk - // Added below_zero_retrogen containing data to support below zero generation - // Added blending_data containing data to support blending new world generation with existing chunks + /** + * https://www.minecraft.net/en-us/article/minecraft-snapshot-21w43a + *
        + *
      • Removed chunk’s Level and moved everything it contained up + *
      • Chunk’s Level.Entities has moved to entities -- WTF, when are entities still put in region chunks? + *
      • Chunk’s Level.TileEntities has moved to block_entities + *
      • Chunk’s Level.TileTicks and Level.ToBeTicked have moved to block_ticks + *
      • Chunk’s Level.LiquidTicks and Level.LiquidsToBeTicked have moved to fluid_ticks + *
      • Chunk’s Level.Sections has moved to sections + *
      • Chunk’s Level.Structures has moved to structures + *
      • Chunk’s Level.Structures.Starts has moved to structures.starts + *
      • Chunk’s Level.Sections[].BlockStates and Level.Sections[].Palette have moved to a container structure in sections[].block_states + *
      • Chunk’s Level.Biomes are now paletted and live in a similar container structure in sections[].biomes + *
      • Added yPos the minimum section y position in the chunk + *
      • Added below_zero_retrogen containing data to support below zero generation + *
      • Added blending_data containing data to support blending new world generation with existing chunks + *
      + */ JAVA_1_18_21W43A(2844, 18, -1, "21w43a"), JAVA_1_18_PRE1(2847, 18, -1, "PRE1"); @@ -146,21 +161,6 @@ public enum DataVersion { } } -// private void checkEnumName() { -// if (id != 0) { -// StringBuilder sb = new StringBuilder("JAVA_1_"); -// sb.append(minor).append('_'); -// if (isFullRelease) { -// sb.append(patch); -// } else { -// sb.append(buildDescription.toUpperCase()); -// } -// if (!name().equals(sb.toString())) { -// throw new IllegalArgumentException(String.format("Expected enum name to be '%s' but was '%s'", sb, name())); -// } -// } -// } - public int id() { return id; } @@ -246,4 +246,33 @@ public static DataVersion latest() { public String toString() { return str; } + + /** + * Indicates if this version would be crossed by the transition between versionA and versionB. + * This is useful for determining if a data upgrade or downgrade would be required to support + * changing from versionA to versionB. The order of A and B don't matter. + * + *

      When using this function, call it on the data version in which a change exists. For + * example if you need to know if changing from A to B would require changing to/from 3D + * biomes then use {@code JAVA_1_15_19W36A.isCrossedByTransition(A, B)} as + * {@link #JAVA_1_15_19W36A} is the version which added 3D biomes.

      + * + *

      In short, if this function returns true then the act of changing data versions from A + * to B can be said to "cross" this version which is an indication that such a change should + * either be considered illegal or that upgrade/downgrade action is required.

      + * + * @param versionA older or newer data version than B + * @param versionB older or newer data version than A + * @return true if chaining from version A to version B, or form B to A, would result in + * crossing this version. This version is considered to be crossed if {@code A != B} and + * {@code min(A, B) < this.id <= max(A, B)} + */ + public boolean isCrossedByTransition(int versionA, int versionB) { + if (versionA == versionB) return false; + if (versionA < versionB) { + return versionA < id && id <= versionB; + } else { + return versionB < id && id <= versionA; + } + } } diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java index ceaf08f7..b8960c02 100644 --- a/src/test/java/net/querz/mca/DataVersionTest.java +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -4,10 +4,12 @@ import java.util.regex.Pattern; +import static net.querz.mca.DataVersion.*; + public class DataVersionTest extends TestCase { private static final Pattern ALLOWED_ENUM_DESCRIPTION_PATTERN = Pattern.compile("^(?:FINAL|\\d{2}w\\d{2}[a-z]|(?:CT|XS|PRE|RC)\\d+|)"); public void testEnumNamesMatchVersionInformation() { - for (DataVersion dv : DataVersion.values()) { + for (DataVersion dv : values()) { if (dv.id() != 0) { StringBuilder sb = new StringBuilder("JAVA_1_"); sb.append(dv.minor()).append('_'); @@ -24,36 +26,54 @@ public void testEnumNamesMatchVersionInformation() { } public void testBestForNegativeValue() { - assertEquals(DataVersion.UNKNOWN, DataVersion.bestFor(-42)); + assertEquals(UNKNOWN, bestFor(-42)); } public void testBestForExactFirst() { - assertEquals(DataVersion.UNKNOWN, DataVersion.bestFor(0)); + assertEquals(UNKNOWN, bestFor(0)); } public void testBestForExactArbitrary() { - assertEquals(DataVersion.JAVA_1_15_0, DataVersion.bestFor(2225)); + assertEquals(JAVA_1_15_0, bestFor(2225)); } public void testBestForBetween() { - assertEquals(DataVersion.JAVA_1_10_2, DataVersion.bestFor(DataVersion.JAVA_1_11_0.id() - 1)); - assertEquals(DataVersion.JAVA_1_11_0, DataVersion.bestFor(DataVersion.JAVA_1_11_0.id() + 1)); + assertEquals(JAVA_1_10_2, bestFor(JAVA_1_11_0.id() - 1)); + assertEquals(JAVA_1_11_0, bestFor(JAVA_1_11_0.id() + 1)); } public void testBestForExactLast() { - final DataVersion last = DataVersion.values()[DataVersion.values().length - 1]; - assertEquals(last, DataVersion.bestFor(last.id())); + final DataVersion last = values()[values().length - 1]; + assertEquals(last, bestFor(last.id())); } public void testBestForAfterLast() { - final DataVersion last = DataVersion.values()[DataVersion.values().length - 1]; - assertEquals(last, DataVersion.bestFor(last.id() + 123)); + final DataVersion last = values()[values().length - 1]; + assertEquals(last, bestFor(last.id() + 123)); } public void testToString() { - assertEquals("2724 (1.17)", DataVersion.JAVA_1_17_0.toString()); - assertEquals("2730 (1.17.1)", DataVersion.JAVA_1_17_1.toString()); - assertEquals("UNKNOWN", DataVersion.UNKNOWN.toString()); - assertEquals("2529 (1.16 20w17a)", DataVersion.JAVA_1_16_20W17A.toString()); + assertEquals("2724 (1.17)", JAVA_1_17_0.toString()); + assertEquals("2730 (1.17.1)", JAVA_1_17_1.toString()); + assertEquals("UNKNOWN", UNKNOWN.toString()); + assertEquals("2529 (1.16 20w17a)", JAVA_1_16_20W17A.toString()); + } + + public void testIsCrossedByTransition() { + assertFalse(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id(), JAVA_1_15_19W36A.id())); + assertFalse(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_0.id(), JAVA_1_15_1.id())); + assertFalse(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_14_3.id(), JAVA_1_14_4.id())); + + assertFalse(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id(), JAVA_1_15_19W36A.id() + 1)); + assertFalse(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id() + 1, JAVA_1_15_19W36A.id())); + + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id() - 1, JAVA_1_15_19W36A.id())); + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id(), JAVA_1_15_19W36A.id() - 1)); + + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id() - 1, JAVA_1_15_19W36A.id() + 1)); + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_15_19W36A.id() + 1, JAVA_1_15_19W36A.id() - 1)); + + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_14_4.id(), JAVA_1_16_0.id())); + assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_16_0.id(), JAVA_1_14_4.id())); } } From 5e730eb510c974114b2850dbef524e15415a44ba Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 19 Nov 2021 07:57:35 -0500 Subject: [PATCH 066/288] Adding UnsupportedVersionChangeException --- .../UnsupportedVersionChangeException.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/net/querz/mca/UnsupportedVersionChangeException.java diff --git a/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java b/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java new file mode 100644 index 00000000..d5d09d09 --- /dev/null +++ b/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java @@ -0,0 +1,23 @@ +package net.querz.mca; + +/** + * Thrown when the requested data version change is not supported because it would require a data upgrade or downgrade + * that is itself, not supported. + */ +public class UnsupportedVersionChangeException extends IllegalArgumentException { + public UnsupportedVersionChangeException() { + super(); + } + + public UnsupportedVersionChangeException(String message) { + super(message); + } + + public UnsupportedVersionChangeException(String message, Throwable cause) { + super(message, cause); + } + + public UnsupportedVersionChangeException(Throwable cause) { + super(cause); + } +} From 2218f69d5d0617da10906abbe61d9442c343044c Mon Sep 17 00:00:00 2001 From: BuildTools Date: Fri, 19 Nov 2021 08:15:38 -0500 Subject: [PATCH 067/288] Adding DataVersion#throwUnsupportedVersionChangeIfCrossed --- src/main/java/net/querz/mca/DataVersion.java | 10 ++++++++++ .../querz/mca/UnsupportedVersionChangeException.java | 5 +++++ src/test/java/net/querz/mca/DataVersionTest.java | 8 +++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index a44fd43e..3f53f745 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -275,4 +275,14 @@ public boolean isCrossedByTransition(int versionA, int versionB) { return versionB < id && id <= versionA; } } + + /** + * Throws {@link UnsupportedVersionChangeException} if {@link #isCrossedByTransition(int, int)} + * were to return true for the given arguments. + */ + public void throwUnsupportedVersionChangeIfCrossed(int versionA, int versionB) { + if (isCrossedByTransition(versionA, versionB)) { + throw new UnsupportedVersionChangeException(this, versionA, versionB); + } + } } diff --git a/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java b/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java index d5d09d09..59465faf 100644 --- a/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java +++ b/src/main/java/net/querz/mca/UnsupportedVersionChangeException.java @@ -9,6 +9,11 @@ public UnsupportedVersionChangeException() { super(); } + public UnsupportedVersionChangeException(DataVersion criticalCrossing, int fromVersion, int toVersion) { + super(String.format("Migrating data version from %d to %d not supported because it crosses %s", + fromVersion, toVersion, criticalCrossing)); + } + public UnsupportedVersionChangeException(String message) { super(message); } diff --git a/src/test/java/net/querz/mca/DataVersionTest.java b/src/test/java/net/querz/mca/DataVersionTest.java index b8960c02..d71e5402 100644 --- a/src/test/java/net/querz/mca/DataVersionTest.java +++ b/src/test/java/net/querz/mca/DataVersionTest.java @@ -6,7 +6,7 @@ import static net.querz.mca.DataVersion.*; -public class DataVersionTest extends TestCase { +public class DataVersionTest extends MCATestCase { private static final Pattern ALLOWED_ENUM_DESCRIPTION_PATTERN = Pattern.compile("^(?:FINAL|\\d{2}w\\d{2}[a-z]|(?:CT|XS|PRE|RC)\\d+|)"); public void testEnumNamesMatchVersionInformation() { for (DataVersion dv : values()) { @@ -76,4 +76,10 @@ public void testIsCrossedByTransition() { assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_14_4.id(), JAVA_1_16_0.id())); assertTrue(JAVA_1_15_19W36A.isCrossedByTransition(JAVA_1_16_0.id(), JAVA_1_14_4.id())); } + + public void testThrowUnsupportedVersionChangeIfCrossed() { + assertThrowsException(() -> JAVA_1_15_19W36A.throwUnsupportedVersionChangeIfCrossed(JAVA_1_14_4.id(), JAVA_1_16_0.id()), + UnsupportedVersionChangeException.class); + assertThrowsNoException(() -> JAVA_1_15_19W36A.throwUnsupportedVersionChangeIfCrossed(JAVA_1_15_19W36A.id(), JAVA_1_15_19W36A.id())); + } } From 45cceba506f4ded89c51bf7c164ccc2ac3a8dc20 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 21 Nov 2021 08:36:16 -0500 Subject: [PATCH 068/288] Add ListTag#isEmpty --- src/main/java/net/querz/nbt/tag/ListTag.java | 4 ++++ src/test/java/net/querz/nbt/tag/ListTagTest.java | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java index e33c6d7c..a974c312 100644 --- a/src/main/java/net/querz/nbt/tag/ListTag.java +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -97,6 +97,10 @@ public int size() { return getValue().size(); } + public boolean isEmpty() { + return getValue().isEmpty(); + } + public T remove(int index) { return getValue().remove(index); } diff --git a/src/test/java/net/querz/nbt/tag/ListTagTest.java b/src/test/java/net/querz/nbt/tag/ListTagTest.java index fb69e040..89fcd572 100644 --- a/src/test/java/net/querz/nbt/tag/ListTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ListTagTest.java @@ -25,6 +25,15 @@ private ListTag createListTag() { return bl; } + public void testIsEmpty() { + ListTag tag = new ListTag<>(IntTag.class); + assertTrue(tag.isEmpty()); + tag.addInt(1); + assertFalse(tag.isEmpty()); + tag.clear(); + assertTrue(tag.isEmpty()); + } + public void testStringConversion() { ListTag bl = createListTag(); assertTrue(3 == bl.size()); From 512db8abca5282fc266b6cb94215915a635f0f56 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 21 Nov 2021 09:26:28 -0500 Subject: [PATCH 069/288] Improving remaining assertThrows... helpers to include useful stack traces instead of having to add printStackTrace flag. --- src/test/java/net/querz/NBTTestCase.java | 61 ++++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/test/java/net/querz/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java index 25288780..52130906 100644 --- a/src/test/java/net/querz/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -1,6 +1,7 @@ package net.querz; import junit.framework.AssertionFailedError; +import junit.framework.ComparisonFailure; import junit.framework.TestCase; import net.querz.nbt.io.NBTDeserializer; import net.querz.nbt.io.NBTSerializer; @@ -117,22 +118,29 @@ protected void assertThrowsIllegalArgumentException(ExceptionRunnable void assertThrowsException(ExceptionRunnable r, Class e) { - assertThrowsException(r, e, false); + protected void assertThrowsUnsupportedOperationException(ExceptionRunnable r) { + assertThrowsException(r, UnsupportedOperationException.class); } - protected void assertThrowsException(ExceptionRunnable r, Class e, boolean printStackTrace) { + protected void assertThrowsException(ExceptionRunnable r, Class e) { try { r.run(); TestCase.fail(); } catch (Exception ex) { - if (printStackTrace) { - ex.printStackTrace(); + if (!e.equals(ex.getClass())) { + throw new WrongExceptionThrownException(e, ex); } - TestCase.assertEquals(e, ex.getClass()); } } + /** + * @deprecated replaced by improved {@link #assertThrowsException(ExceptionRunnable, Class)} + */ + @Deprecated + protected void assertThrowsException(ExceptionRunnable r, Class e, boolean printStackTrace) { + assertThrowsException(r, e); + } + protected void assertThrowsNoException(ExceptionRunnable r) { try { r.run(); @@ -143,18 +151,28 @@ protected void assertThrowsNoException(ExceptionRunnable void assertThrowsException(ExceptionSupplier r, Class e) { - assertThrowsException(r, e, false); - } - - protected void assertThrowsException(ExceptionSupplier r, Class e, boolean printStackTrace) { try { r.run(); TestCase.fail(); } catch (Exception ex) { - if (printStackTrace) { - ex.printStackTrace(); + if (!e.equals(ex.getClass())) { + throw new WrongExceptionThrownException(e, ex); } - TestCase.assertEquals(ex.getClass(), e); + } + } + + /** + * @deprecated replaced by improved {@link #assertThrowsException(ExceptionSupplier, Class)} + */ + @Deprecated + protected void assertThrowsException(ExceptionSupplier r, Class e, boolean printStackTrace) { + assertThrowsException(r, e); + } + + private static class WrongExceptionThrownException extends ComparisonFailure { + public WrongExceptionThrownException(Class expectedType, Exception actual) { + super("", expectedType.getTypeName(), actual.getClass().getTypeName()); + this.setStackTrace(actual.getStackTrace()); } } @@ -174,21 +192,24 @@ protected T assertThrowsNoException(ExceptionSupplier e) { - assertThrowsRuntimeException(r, e, false); - } - - protected void assertThrowsRuntimeException(Runnable r, Class e, boolean printStackTrace) { try { r.run(); TestCase.fail(); } catch (Exception ex) { - if (printStackTrace) { - ex.printStackTrace(); + if (!e.equals(ex.getClass())) { + throw new WrongExceptionThrownException(e, ex); } - TestCase.assertEquals(e, ex.getClass()); } } + /** + * @deprecated replaced by improved {@link #assertThrowsRuntimeException(Runnable, Class)} + */ + @Deprecated + protected void assertThrowsRuntimeException(Runnable r, Class e, boolean printStackTrace) { + assertThrowsRuntimeException(r, e); + } + protected void assertThrowsRuntimeException(Runnable r, boolean printStackTrace) { try { r.run(); From a2c9e71c7d33fe2ac92ab9a7d2a84c917fa84279 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 22 Nov 2021 16:13:14 -0500 Subject: [PATCH 070/288] Making a pass over javadoc's Renaming "RegionXXX" to "TerrainXXX" Adding xz chunk constructors (not yet fully baked) Modified EntitiesChunkBase#updateHandle to modify data entities tag instead of resetting it - supports more use cases this way Made EntitiesChunkBase respect raw read behavior (to refuse to make changes and throw) MCAUtil - added readEntities functions for reading EntitiesMCAFile's, these methods can be used to read pre-1.17 terrain mca files which contain the entities tag Adding IntPointXZ class so we can pass around an x and a z together Added EntitiesChunkBaseTest Added EntitiesChunkTest Added EntitiesMCAFileTest#testLoadingOldRegionMcaAsEntityMca --- src/main/java/net/querz/mca/Chunk.java | 17 +- src/main/java/net/querz/mca/ChunkBase.java | 60 +++- src/main/java/net/querz/mca/DataVersion.java | 18 +- .../java/net/querz/mca/EntitiesChunk.java | 16 +- .../java/net/querz/mca/EntitiesChunkBase.java | 310 +++++++++++++++--- .../java/net/querz/mca/EntitiesMCAFile.java | 8 +- src/main/java/net/querz/mca/MCAFile.java | 16 +- src/main/java/net/querz/mca/MCAFileBase.java | 2 +- src/main/java/net/querz/mca/MCAUtil.java | 113 ++++++- src/main/java/net/querz/mca/PoiChunk.java | 8 +- src/main/java/net/querz/mca/PoiChunkBase.java | 8 +- src/main/java/net/querz/mca/PoiMCAFile.java | 3 +- src/main/java/net/querz/mca/Section.java | 6 +- .../java/net/querz/mca/SectionIterator.java | 6 +- .../net/querz/mca/SectionedChunkBase.java | 4 +- ...onChunkBase.java => TerrainChunkBase.java} | 16 +- ...AFileBase.java => TerrainMCAFileBase.java} | 10 +- ...ctionBase.java => TerrainSectionBase.java} | 24 +- .../mca/entities/DefaultEntityCreator.java | 4 + .../net/querz/mca/entities/EntityBase.java | 22 +- .../querz/mca/entities/EntityBaseImpl.java | 91 ++++- .../net/querz/mca/entities/EntityCreator.java | 3 + .../net/querz/mca/entities/EntityFactory.java | 90 ++++- .../net/querz/mca/entities/EntityUtil.java | 3 + .../entities/IllegalEntityTagException.java | 21 +- .../util/BlockAlignedBoundingRectangle.java | 4 + .../querz/util/ChunkBoundingRectangle.java | 8 + .../java/net/querz/util/IdentityHelper.java | 50 +++ src/main/java/net/querz/util/IntPointXZ.java | 92 ++++++ .../java/net/querz/mca/ChunkBaseTest.java | 39 +-- .../java/net/querz/mca/DataVersionTest.java | 21 ++ .../net/querz/mca/EntitiesChunkBaseTest.java | 280 ++++++++++++++++ .../java/net/querz/mca/EntitiesChunkTest.java | 86 +++++ .../net/querz/mca/EntitiesMCAFileTest.java | 37 +++ .../java/net/querz/mca/PoiChunkBaseTest.java | 16 +- src/test/java/net/querz/mca/PoiChunkTest.java | 4 +- .../mca/entities/EntityBaseImplTest.java | 167 ++++++++-- .../querz/mca/entities/EntityFactoryTest.java | 88 +++-- .../BlockAlignedBoundingRectangleTest.java | 12 + .../net/querz/mca/util/IntPointXZTest.java | 153 +++++++++ 40 files changed, 1688 insertions(+), 248 deletions(-) rename src/main/java/net/querz/mca/{RegionChunkBase.java => TerrainChunkBase.java} (96%) rename src/main/java/net/querz/mca/{RegionMCAFileBase.java => TerrainMCAFileBase.java} (90%) rename src/main/java/net/querz/mca/{RegionSectionBase.java => TerrainSectionBase.java} (94%) create mode 100644 src/main/java/net/querz/util/IdentityHelper.java create mode 100644 src/main/java/net/querz/util/IntPointXZ.java create mode 100644 src/test/java/net/querz/mca/EntitiesChunkBaseTest.java create mode 100644 src/test/java/net/querz/mca/EntitiesChunkTest.java create mode 100644 src/test/java/net/querz/mca/util/IntPointXZTest.java diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java index c0c3e19f..8ec4ac5c 100644 --- a/src/main/java/net/querz/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -3,12 +3,18 @@ import net.querz.nbt.tag.CompoundTag; /** - * Represents a REGION data mca chunk. Region chunks are composed of a set of {@link Section} where any empty/null + * Represents a TERRAIN data mca chunk (from mca files that come from the /region save folder). + * Terrain chunks are composed of a set of {@link Section} where any empty/null * section is filled with air blocks by the game. When altering existing chunks for MC 1.14+, be sure to have read and * understood the documentation on {@link PoiRecord} to avoid problems with villagers, nether portal linking, * lodestones, bees, and probably more as Minecraft continues to evolve. + * + *

      It is my (Ross / Ens) hope that in the future this class can be repurposed to serve as an abstraction + * layer over all the various chunk types (terrain, poi, entity - at the time of writing) and that it + * can take care of keeping them all in sync. But I've already put a lot of time into this library and need + * to return to other things so for now that goal must remain unrealized.

      */ -public class Chunk extends RegionChunkBase
      { +public class Chunk extends TerrainChunkBase
      { /** * The default chunk data version used when no custom version is supplied. * @deprecated Use {@code DataVersion.latest().id()} instead. @@ -18,6 +24,12 @@ public class Chunk extends RegionChunkBase
      { @Deprecated protected Chunk(int lastMCAUpdate) { + super(DEFAULT_DATA_VERSION); + setLastMCAUpdate(lastMCAUpdate); + } + + protected Chunk(int dataVersion, int lastMCAUpdate) { + super(dataVersion); setLastMCAUpdate(lastMCAUpdate); } @@ -80,6 +92,7 @@ public static Chunk newChunk(int dataVersion) { Chunk c = new Chunk(0); c.dataVersion = dataVersion; c.data = new CompoundTag(); + // TODO(1.18): update for 1.18 needed c.data.put("Level", new CompoundTag()); c.status = "mobs_spawned"; return c; diff --git a/src/main/java/net/querz/mca/ChunkBase.java b/src/main/java/net/querz/mca/ChunkBase.java index e479dcb4..2b00e620 100644 --- a/src/main/java/net/querz/mca/ChunkBase.java +++ b/src/main/java/net/querz/mca/ChunkBase.java @@ -12,7 +12,8 @@ import static net.querz.mca.LoadFlags.RAW; /** - * Abstraction for the base of all chunk types which represent chunks composed of sub-chunks {@link SectionBase}. + * Abstraction for the base of all chunk types. Not all chunks types are sectioned, that layer comes further up + * the hierarchy. *

      * Cautionary note to implementors - DO NOT USE INLINE MEMBER INITIALIZATION IN YOUR CLASSES
      * Define all member initialization in {@link #initMembers()} or be very confused! @@ -29,12 +30,12 @@ *

      */ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { + public static final int NO_CHUNK_COORD_SENTINEL = Integer.MIN_VALUE; + protected int dataVersion; - // TODO: refactor region chunks to support these fields protected int chunkX = NO_CHUNK_COORD_SENTINEL; protected int chunkZ = NO_CHUNK_COORD_SENTINEL; - protected boolean partial; protected boolean raw; protected int lastMCAUpdate; @@ -42,19 +43,28 @@ public abstract class ChunkBase implements VersionedDataContainer, TagWrapper { /** * Due to how Java initializes objects and how this class hierarchy is setup it is ill advised to use inline member - * initialization because {@link #initReferences0(long)} may be called before members are initialized which WILL + * initialization because {@link #initReferences(long)} will be called before members are initialized which WILL * result in very confusing {@link NullPointerException}'s being thrown from within {@link #initReferences(long)}. * This is not a problem that can be solved by moving initialization into your constructors, because you must call * the super constructor as the first line of your child constructor! *

      So, to get around this hurdle, perform all member initialization you would normally inline in your * class def, within this method instead. Implementers should never need to call this method themselves - * as ChunkBase will always call it, even from the default constructor.

      + * as ChunkBase will always call it, even from the default constructor. Remember to call {@code super();} + * from your default constructors to maintain this behavior.

      */ protected void initMembers() { } - protected ChunkBase() { - dataVersion = DataVersion.latest().id(); - lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); + protected ChunkBase(int dataVersion) { + this.dataVersion = dataVersion; + this.lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); + initMembers(); + } + + public ChunkBase(int dataVersion, int chunkX, int chunkZ) { + this.dataVersion = dataVersion; + this.lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); + this.chunkX = chunkX; + this.chunkZ = chunkZ; initMembers(); } @@ -121,6 +131,7 @@ public void setDataVersion(int dataVersion) { /** * Gets this chunk's chunk-x coordinate. Returns {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + * @see #moveChunk(int, int, boolean) */ public int getChunkX() { return chunkX; @@ -128,6 +139,7 @@ public int getChunkX() { /** * Gets this chunk's chunk-z coordinate. Returns {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + * @see #moveChunk(int, int, boolean) */ public int getChunkZ() { return chunkZ; @@ -149,24 +161,27 @@ public int getChunkZ() { /** * Attempts to update all tags that use absolute positions within this chunk. - * If {@code force = true} the result of calling this function cannot be guaranteed to be complete and + *

      Call {@link #moveChunkImplemented()} to check if this implementation supports chunk relocation. Also + * check the result of {@link #moveChunkHasFullVersionSupport()} to get an idea of the level of support + * this implementation has for the current chunk. + *

      If {@code force = true} the result of calling this function cannot be guaranteed to be complete and * may still throw {@link UnsupportedOperationException}. - * @param newChunkX new chunk-x - * @param newChunkZ new chunk-z + * @param chunkX new absolute chunk-x + * @param chunkZ new absolute chunk-z * @param force true to ignore the guidance of {@link #moveChunkHasFullVersionSupport()} and make a best effort * anyway. * @return true if any data was changed as a result of this call * @throws UnsupportedOperationException thrown if this chunk implementation doest support moves, or moves * for this chunks version (possibly even if force = true). */ - public abstract boolean moveChunk(int newChunkX, int newChunkZ, boolean force); + public abstract boolean moveChunk(int chunkX, int chunkZ, boolean force); /** * Calls {@code moveChunk(newChunkX, newChunkZ, false);} * @see #moveChunk(int, int, boolean) */ - public boolean moveChunk(int newChunkX, int newChunkZ) { - return moveChunk(newChunkX, newChunkZ, false); + public boolean moveChunk(int chunkX, int chunkZ) { + return moveChunk(chunkX, chunkZ, false); } @@ -241,9 +256,24 @@ public void setLastMCAUpdate(int lastMCAUpdate) { this.lastMCAUpdate = lastMCAUpdate; } + /** + * @throws UnsupportedOperationException thrown if raw is true + */ protected void checkRaw() { if (raw) { - throw new UnsupportedOperationException("cannot update field when working with raw data"); + throw new UnsupportedOperationException("Cannot update field when working with raw data"); + } + } + + protected void checkPartial() { + if (data == null) { + throw new UnsupportedOperationException("Chunk was only partially loaded due to LoadFlags used"); + } + } + + protected void checkChunkXZ() { + if (chunkX == NO_CHUNK_COORD_SENTINEL || chunkZ == NO_CHUNK_COORD_SENTINEL) { + throw new UnsupportedOperationException("This chunk doesn't know its XZ location"); } } diff --git a/src/main/java/net/querz/mca/DataVersion.java b/src/main/java/net/querz/mca/DataVersion.java index 3f53f745..cc9aafbe 100644 --- a/src/main/java/net/querz/mca/DataVersion.java +++ b/src/main/java/net/querz/mca/DataVersion.java @@ -10,10 +10,11 @@ * List of significant MC versions and MCA data versions. * Non-full release versions are intended for use in data handling logic. * The set of non-full release versions is not, and does not need to be, the complete set of all versions - only those - * which introduce changes to the MCA data structure are useful. + * which introduce changes to the MCA data structure are useful. BUT - we humans do love completeness so + * the list might be made complete some day and completeness would be useful for map viewers / editors. */ public enum DataVersion { - // Kept in ASC order + // Kept in ASC order (unit test enforced) UNKNOWN(0, 0, 0), JAVA_1_9_0(169, 9, 0), JAVA_1_9_1(175, 9, 1), @@ -104,7 +105,11 @@ public enum DataVersion { *

    */ JAVA_1_18_21W43A(2844, 18, -1, "21w43a"), - JAVA_1_18_PRE1(2847, 18, -1, "PRE1"); + JAVA_1_18_PRE1(2847, 18, -1, "PRE1"), + JAVA_1_18_PRE2(2848, 18, -1, "PRE2"), + JAVA_1_18_PRE3(2849, 18, -1, "PRE3"), + JAVA_1_18_PRE4(2850, 18, -1, "PRE4"), + JAVA_1_18_PRE5(2851, 18, -1, "PRE5"); private static final int[] ids; @@ -132,8 +137,8 @@ public enum DataVersion { * @param id data version * @param minor minor version * @param patch patch number, LT0 to indicate this data version is not a full release version - * @param buildDescription Suggested convention: