NOTE: {@link #readTextNbtFile(File)}, and its variants, can read both uncompressed (plain text) and GZ + * compressed files (usually ending in .gz file extension - but the extension itself is not evaluated, instead + * the gzip magic number/bom is looked for).
+ *NOTE: {@link #writeTextNbtFile(File, Tag)}, and its variants, can write both uncompressed (plain text) and GZ + * compressed files. If the given file name ends in the '.gz' extension it will be written as a compressed file, + * otherwise it will be written as plain text.
+ */ +public final class TextNbtHelpers { + private TextNbtHelpers() {} + + //Traps and rethrows any checked {@link ParseException}'s as a runtime + * {@link ParseException.SilentParseException}.
+ */ + @SuppressWarnings("unchecked") + public static
+ * 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 inline 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. + *
+ * @see SectionedChunkBase + */ +public abstract class ChunkBase implements VersionedDataContainer, TagWrapperSo, 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. Remember to call {@code super();} + * from your default constructors to maintain this behavior.
+ */ + protected void initMembers() { } + + protected ChunkBase(int dataVersion) { + this.dataVersion = dataVersion; + this.originalLoadFlags = LoadFlags.LOAD_ALL_DATA; + this.lastMCAUpdate = (int)(System.currentTimeMillis() / 1000); + initMembers(); + } + + /** + * 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, LoadFlags.LOAD_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; + this.originalLoadFlags = loadFlags; + initMembers(); + initReferences0(loadFlags); + } + + private void initReferences0(long loadFlags) { + Objects.requireNonNull(data, "data cannot be null"); + if ((loadFlags & LoadFlags.RAW) != 0) { + dataVersion = data.getInt("DataVersion"); + raw = true; + } else { + final ObservedCompoundTag observedData = new ObservedCompoundTag(data); + dataVersion = observedData.getInt("DataVersion"); + if (dataVersion == 0) { + throw new IllegalArgumentException("data does not contain \"DataVersion\" tag"); + } + + data = observedData; + initReferences(loadFlags); + if (data != observedData) { + throw new IllegalStateException("this.data was replaced during initReferences execution - this breaks unreadDataTagKeys behavior!"); + } + unreadDataTagKeys = observedData.unreadKeys(); + + if ((loadFlags & LoadFlags.RELEASE_CHUNK_DATA_TAG) != 0) { + data = null; + // this is questionable... maybe if we also check that data version is within the known bounds too + // (to count it as non-partial) we could be reasonably confidant that the saved chunk would at least + // have a vanilla level of data. + if ((loadFlags & LoadFlags.LOAD_ALL_DATA) != LoadFlags.LOAD_ALL_DATA) partial = true; + } else { + // stop observing the data tag + data = observedData.wrappedTag(); + } + } + } + + /** + * 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}. + */ + protected abstract void initReferences(final long loadFlags); + + /** + * @return one of: region, entities, poi + */ + public abstract String getMcaType(); + + /** + * {@inheritDoc} + */ + public int getDataVersion() { + return dataVersion; + } + + /** + * {@inheritDoc} + */ + 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. + * @see #moveChunk(int, int, long, boolean) + */ + public int getChunkX() { + return chunkX; + } + + /** + * Gets this chunk's chunk-z coordinate. Returns {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + * @see #moveChunk(int, int, long, boolean) + */ + public int getChunkZ() { + return chunkZ; + } + + /** + * Gets this chunk's chunk-xz coordinates. Returns x = z = {@link #NO_CHUNK_COORD_SENTINEL} if not supported or unknown. + * @see #moveChunk(int, int, long, boolean) + */ + public IntPointXZ getChunkXZ() { + return new IntPointXZ(getChunkX(), getChunkZ()); + } + + /** + * Indicates if this chunk implementation supports calling {@link #moveChunk(int, int, long, boolean)}. + * @return false if {@link #moveChunk(int, int, long, 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. + *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 absolute chunk-x
+ * @param newChunkZ new absolute chunk-z
+ * @param moveChunkFlags {@link MoveChunkFlags} OR'd together to control move chunk behavior.
+ * @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, long moveChunkFlags, boolean force);
+
+ /**
+ * Calls {@code moveChunk(newChunkX, newChunkZ, moveChunkFlags, false);}
+ * @see #moveChunk(int, int, long, boolean)
+ */
+ public boolean moveChunk(int chunkX, int chunkZ, long moveChunkFlags) {
+ return moveChunk(chunkX, chunkZ, moveChunkFlags, false);
+ }
+
+ /**
+ * Serializes this chunk to a DataOutput
sink.
+ * @param sink The DataOutput to be written to.
+ * @param xPos The x-coordinate of the chunk.
+ * @param zPos The z-coordinate of the chunk.
+ * @param compressionType Chunk compression strategy to use.
+ * @param writeByteLengthPrefixInt when true the first thing written to the sink will be the total bytes written
+ * (a value equal to 4 less than the return value).
+ * @return The amount of bytes written to the DataOutput.
+ * @throws UnsupportedOperationException When something went wrong during writing.
+ * @throws IOException When something went wrong during writing.
+ */
+ public int serialize(DataOutput sink, int xPos, int zPos, CompressionType compressionType, boolean writeByteLengthPrefixInt) throws IOException {
+ if (partial) {
+ throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized");
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
+ new BinaryNbtSerializer(compressionType).toStream(new NamedTag(null, updateHandle(xPos, zPos)), baos);
+// try (BufferedOutputStream nbtOut = new BufferedOutputStream(compressionType.compress(baos))) {
+// new BinaryNbtSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut);
+// }
+ byte[] rawData = baos.toByteArray();
+ if (writeByteLengthPrefixInt)
+ sink.writeInt(rawData.length + 1); // including the byte to store the compression type
+ sink.writeByte(compressionType.getID());
+ sink.write(rawData);
+ return rawData.length + (writeByteLengthPrefixInt ? 5 : 1);
+ }
+
+ /**
+ * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position.
+ *
It is expected that the byte size int has already been read and the next byte indicates the compression + * used. Essentially this method is symmetrical to {@link #serialize(DataOutput, int, int, CompressionType, boolean)} + * when passing writeByteLengthPrefixInt=false
+ * @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 + * @param lastMCAUpdateTimestamp Last mca update timestamp - epoch seconds. If LT0 the current system timestamp will be used. + * @param chunkAbsXHint The absolute chunk x-coord which should be used if the nbt data doesn't contain this information. + * @param chunkAbsZHint The absolute chunk z-coord which should be used if the nbt data doesn't contain this information. + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf, long loadFlags, int lastMCAUpdateTimestamp, int chunkAbsXHint, int chunkAbsZHint) throws IOException { + deserialize(new FileInputStream(raf.getFD()), loadFlags, lastMCAUpdateTimestamp, chunkAbsXHint, chunkAbsZHint); + } + + /** + * Reads chunk data from an InputStream. The InputStream must already be at the correct position. + *It is expected that the byte size int has already been read and the next byte indicates the compression + * used. Essentially this method is symmetrical to {@link #serialize(DataOutput, int, int, CompressionType, boolean)} + * when passing writeByteLengthPrefixInt=false
+ * @param inputStream The stream to read the chunk data from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @param lastMCAUpdateTimestamp Last mca update timestamp - epoch seconds. If LT0 the current system timestamp will be used. + * @param chunkAbsXHint The absolute chunk x-coord which should be used if the nbt data doesn't contain this information. + * @param chunkAbsZHint The absolute chunk z-coord which should be used if the nbt data doesn't contain this information. + * @throws IOException When something went wrong during reading. + */ + public void deserialize(InputStream inputStream, long loadFlags, int lastMCAUpdateTimestamp, int chunkAbsXHint, int chunkAbsZHint) throws IOException { + int compressionTypeByte = inputStream.read(); + if (compressionTypeByte < 0) + throw new EOFException(); + CompressionType compressionType = CompressionType.getFromID((byte) compressionTypeByte); + if (compressionType == null) { + throw new IOException("invalid compression type " + compressionTypeByte); + } + NamedTag tag = new BinaryNbtDeserializer(compressionType).fromStream(inputStream); + if (tag != null && tag.getTag() instanceof CompoundTag) { + data = (CompoundTag) tag.getTag(); + this.lastMCAUpdate = lastMCAUpdateTimestamp >= 0 ? lastMCAUpdateTimestamp : (int)(System.currentTimeMillis() / 1000); + this.chunkX = chunkAbsXHint; + this.chunkZ = chunkAbsZHint; + 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; + } + + /** + * @throws UnsupportedOperationException thrown if raw is true + */ + protected void checkRaw() { + if (raw) { + throw new UnsupportedOperationException("Cannot use helpers for this 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"); + } + } + + /** + * 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() { + if (data == null) { + throw new UnsupportedOperationException( + "Cannot updateHandle() because data tag is null. This is probably because "+ + "the LoadFlag RELEASE_CHUNK_DATA_TAG was specified"); + } + 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(); + } + + + /** + * @param vaPath version aware nbt path + * @param{@code long myLong = getTagValue(vaPath, LongTag::asLong, 0L);}+ * @param vaPath version aware nbt path + * @param evaluator value provider, given the tag (iff not null) + * @param defaultValue value to return if the tag specified by vaPath does not exist + * @param Tag Type + * @param
+ * TODO: weekly builds don't really fit with having a version but it's annoying to not have a version too - what to do? + *
+ */ +public enum DataVersion { + // TODO: document change history by digging through net.minecraft.util.datafix.DataConverterRegistry + // Kept in ASC order (unit test enforced) + UNKNOWN(0, 0, 0), + JAVA_1_9_15W32A(100, 9, 0, "15w32a"), + JAVA_1_9_0(169, 9, 0), + JAVA_1_9_1_PRE1(170, 9, 1, "PRE1"), + JAVA_1_9_1_PRE2(171, 9, 1, "PRE2"), + JAVA_1_9_1_PRE3(172, 9, 1, "PRE3"), + JAVA_1_9_1(175, 9, 1), + JAVA_1_9_2(176, 9, 2), + JAVA_1_9_3_16W14A(177, 9, 3, "16w14a"), + JAVA_1_9_3_16W15A(178, 9, 3, "16w15a"), + JAVA_1_9_3_16W15B(179, 9, 3, "16w15b"), + JAVA_1_9_3_PRE1(180, 9, 3, "PRE1"), + JAVA_1_9_3_PRE2(181, 9, 3, "PRE2"), + JAVA_1_9_3_PRE3(182, 9, 3, "PRE3"), + JAVA_1_9_3(183, 9, 3), + JAVA_1_9_4(184, 9, 4), + JAVA_1_10_16W20A(501, 10, 0, "16w20a"), + JAVA_1_10_16W21A(503, 10, 0, "16w21a"), + JAVA_1_10_16W21B(504, 10, 0, "16w21b"), + JAVA_1_10_PRE1(506, 10, 0, "PRE1"), + JAVA_1_10_PRE2(507, 10, 0, "PRE2"), + JAVA_1_10_0(510, 10, 0), + JAVA_1_10_1(511, 10, 1), + JAVA_1_10_2(512, 10, 2), + JAVA_1_11_16W32A(800, 11, 0, "16w32a"), + JAVA_1_11_16W32B(801, 11, 0, "16w32b"), + JAVA_1_11_16W33A(802, 11, 0, "16w33a"), + JAVA_1_11_16W35A(803, 11, 0, "16w35a"), + JAVA_1_11_16W36A(805, 11, 0, "16w36a"), + JAVA_1_11_16W38A(807, 11, 0, "16w38a"), + JAVA_1_11_16W39A(809, 11, 0, "16w39a"), + JAVA_1_11_16W39B(811, 11, 0, "16w39b"), + JAVA_1_11_16W39C(812, 11, 0, "16w39c"), + JAVA_1_11_16W40A(813, 11, 0, "16w40a"), + JAVA_1_11_16W41A(814, 11, 0, "16w41a"), + JAVA_1_11_16W42A(815, 11, 0, "16w42a"), + JAVA_1_11_16W43A(816, 11, 0, "16w43a"), + JAVA_1_11_16W44A(817, 11, 0, "16w44a"), + JAVA_1_11_PRE1(818, 11, 0, "PRE1"), + JAVA_1_11_0(819, 11, 0), + JAVA_1_11_1_16W50A(920, 11, 1, "16w50a"), + JAVA_1_11_1(921, 11, 1), + JAVA_1_11_2(922, 11, 2), + JAVA_1_12_17W06A(1022, 12, 0, "17w06a"), + JAVA_1_12_17W13A(1122, 12, 0, "17w13a"), + JAVA_1_12_17W13B(1123, 12, 0, "17w13b"), + JAVA_1_12_17W14A(1124, 12, 0, "17w14a"), + JAVA_1_12_17W15A(1125, 12, 0, "17w15a"), + JAVA_1_12_17W16A(1126, 12, 0, "17w16a"), + JAVA_1_12_17W16B(1127, 12, 0, "17w16b"), + JAVA_1_12_17W17A(1128, 12, 0, "17w17a"), + JAVA_1_12_17W17B(1129, 12, 0, "17w17b"), + JAVA_1_12_17W18A(1130, 12, 0, "17w18a"), + JAVA_1_12_17W18B(1131, 12, 0, "17w18b"), + JAVA_1_12_PRE1(1132, 12, 0, "PRE1"), + JAVA_1_12_PRE2(1133, 12, 0, "PRE2"), + JAVA_1_12_PRE3(1134, 12, 0, "PRE3"), + JAVA_1_12_PRE4(1135, 12, 0, "PRE4"), + JAVA_1_12_PRE5(1136, 12, 0, "PRE5"), + JAVA_1_12_PRE6(1137, 12, 0, "PRE6"), + JAVA_1_12_PRE7(1138, 12, 0, "PRE7"), + JAVA_1_12_0(1139, 12, 0), + JAVA_1_12_1_17W31A(1239, 12, 1, "17w31a"), + JAVA_1_12_1_PRE1(1240, 12, 1, "PRE1"), + JAVA_1_12_1(1241, 12, 1), + JAVA_1_12_2_PRE1(1341, 12, 2, "PRE1"), + JAVA_1_12_2_PRE2(1342, 12, 2, "PRE2"), + JAVA_1_12_2(1343, 12, 2), + JAVA_1_13_17W43A(1444, 13, 0, "17w43a"), + JAVA_1_13_17W43B(1445, 13, 0, "17w43b"), + JAVA_1_13_17W45A(1447, 13, 0, "17w45a"), + JAVA_1_13_17W45B(1448, 13, 0, "17w45b"), + JAVA_1_13_17W46A(1449, 13, 0, "17w46a"), + /** "Blocks" and "Data" were replaced with block palette */ + JAVA_1_13_17W47A(1451, 13, 0, "17w47a"), + JAVA_1_13_17W47B(1452, 13, 0, "17w47b"), + JAVA_1_13_17W48A(1453, 13, 0, "17w48a"), + JAVA_1_13_17W49A(1454, 13, 0, "17w49a"), + JAVA_1_13_17W49B(1455, 13, 0, "17w49b"), + JAVA_1_13_17W50A(1457, 13, 0, "17w50a"), + JAVA_1_13_18W01A(1459, 13, 0, "18w01a"), + JAVA_1_13_18W02A(1461, 13, 0, "18w02a"), + JAVA_1_13_18W03A(1462, 13, 0, "18w03a"), + JAVA_1_13_18W03B(1463, 13, 0, "18w03b"), + JAVA_1_13_18W05A(1464, 13, 0, "18w05a"), + /** + * Biome data now stored in IntArrayTag instead of ByteArrayTag (still 2D using only 256 entries). + *Tags Removed
+ *Tags Added
+ *Tags Added
+ *Tags Removed
+ *Tags Added
+ *Tags Added
+ *Tags Added
+ *Tags Removed
+ *Tags Added
+ *Temporary POI Structure
+ *Villagers got brains ({@code Entities[].Brain}) in the region file data.
+ */ + JAVA_1_14_19W11A(1937, 14, 0, "19w11a"), + JAVA_1_14_19W11B(1938, 14, 0, "19w11b"), + JAVA_1_14_19W12A(1940, 14, 0, "19w12a"), + JAVA_1_14_19W12B(1941, 14, 0, "19w12b"), + JAVA_1_14_19W13A(1942, 14, 0, "19w13a"), + JAVA_1_14_19W13B(1943, 14, 0, "19w13b"), + JAVA_1_14_19W14A(1944, 14, 0, "19w14a"), + JAVA_1_14_19W14B(1945, 14, 0, "19w14b"), + /** + * POI tag structure changed. Begin this library's support of POI files. + *Final POI Structure
+ *Tags Removed
+ *Tags Removed
+ *Tags Added
+ *Example: Level.Structures.References.Desert_Pyramid became Level.Structures.References.desert_pyramid
+ *Example: Level.Structures.Starts.Desert_Pyramid became Level.Structures.Starts.desert_pyramid
+ * + */ + JAVA_1_16_20W21A(2554, 16, 0, "20w21a"), + JAVA_1_16_20W22A(2555, 16, 0, "20w22a"), + /** + *Tags Removed
+ *Tags Added
+ *https://www.minecraft.net/en-us/article/minecraft-snapshot-20w45a
+ */ + JAVA_1_17_20W45A(2681, 17, 0, "20w45a"), + JAVA_1_17_20W46A(2682, 17, 0, "20w46a"), + JAVA_1_17_20W48A(2683, 17, 0, "20w48a"), + JAVA_1_17_20W49A(2685, 17, 0, "20w49a"), + JAVA_1_17_20W51A(2687, 17, 0, "20w51a"), + JAVA_1_17_21W03A(2689, 17, 0, "21w03a"), + JAVA_1_17_21W05A(2690, 17, 0, "21w05a"), + JAVA_1_17_21W05B(2692, 17, 0, "21w05b"), + JAVA_1_17_21W06A(2694, 17, 0, "21w06a"), + JAVA_1_17_21W07A(2695, 17, 0, "21w07a"), + JAVA_1_17_21W08A(2697, 17, 0, "21w08a"), + JAVA_1_17_21W08B(2698, 17, 0, "21w08b"), + JAVA_1_17_21W10A(2699, 17, 0, "21w10a"), +// JAVA_1_17_CT6(2701, 17, 0, "CT6"), +// JAVA_1_17_CT7(2702, 17, 0, "CT7"), + JAVA_1_17_21W11A(2703, 17, 0, "21w11a"), +// JAVA_1_17_CT7B(2703, 17, 0, "CT7b"), -- ambiguous data version +// JAVA_1_17_CT7C(2704, 17, 0, "CT7c"), + JAVA_1_17_21W13A(2705, 17, 0, "21w13a"), +// JAVA_1_17_CT8(2705, 17, 0, "CT8"), -- ambiguous data version + JAVA_1_17_21W14A(2706, 17, 0, "21w14a"), +// JAVA_1_17_CT8B(2706, 17, 0, "CT8b"), -- ambiguous data version +// JAVA_1_17_CT8C(2707, 17, 0, "CT8c"), + JAVA_1_17_21W15A(2709, 17, 0, "21w15a"), + JAVA_1_17_21W16A(2711, 17, 0, "21w16a"), + JAVA_1_17_21W17A(2712, 17, 0, "21w17a"), + JAVA_1_17_21W18A(2713, 17, 0, "21w18a"), + JAVA_1_17_21W19A(2714, 17, 0, "21w19a"), + JAVA_1_17_21W20A(2715, 17, 0, "21w20a"), + JAVA_1_17_PRE1(2716, 17, 0, "PRE1"), + JAVA_1_17_PRE2(2718, 17, 0, "PRE2"), + JAVA_1_17_PRE3(2719, 17, 0, "PRE3"), + JAVA_1_17_PRE4(2720, 17, 0, "PRE4"), + JAVA_1_17_PRE5(2721, 17, 0, "PRE5"), + JAVA_1_17_RC1(2722, 17, 0, "RC1"), + JAVA_1_17_RC2(2723, 17, 0, "RC2"), + JAVA_1_17_0(2724, 17, 0), + JAVA_1_17_1_PRE1(2725, 17, 1, "PRE1"), + JAVA_1_17_1_PRE2(2726, 17, 1, "PRE2"), + JAVA_1_17_1_PRE3(2727, 17, 1, "PRE3"), + JAVA_1_17_1_RC1(2728, 17, 1, "RC1"), + JAVA_1_17_1_RC2(2729, 17, 1, "RC2"), + JAVA_1_17_1(2730, 17, 1), +// JAVA_1_18_XS1(2825, 18, 0, "XS1"), +// JAVA_1_18_XS2(2826, 18, 0, "XS2"), +// JAVA_1_18_XS3(2827, 18, 0, "XS3"), +// JAVA_1_18_XS4(2828, 18, 0, "XS4"), +// JAVA_1_18_XS5(2829, 18, 0, "XS5"), +// JAVA_1_18_XS6(2830, 18, 0, "XS6"), +// JAVA_1_18_XS7(2831, 18, 0, "XS7"), + /** + * article 21w39a + * (yes, they didn't document these changes until a later weekly snapshot). + *Tags Removed
+ *Tags Added
+ *About the New Biome Palette
+ *This version is also the first time the mca scan data shows the `entities` tag being present in region chunks + * again (probably during some stage(s) of world generation). I find it unlikely that the scanned mca versions + * between {@link #JAVA_1_18_21W43A} and this one just happen to not have any entities in the right state to be + * stored in the region mca file - that was 20 * 25 world spawns generated and scanned between these 2 versions!
+ */ + JAVA_1_18_2_22W03A(2966, 18, 2, "22w03a"), + JAVA_1_18_2_22W05A(2967, 18, 2, "22w05a"), + JAVA_1_18_2_22W06A(2968, 18, 2, "22w06a"), + /** + * `structures.References.*` and `structures.starts.*` entry name format changed to include the "minecraft:" prefix. + * Ex. old: "buried_treasure", new: "minecraft:buried_treasure" + */ + JAVA_1_18_2_22W07A(2969, 18, 2, "22w07a"), + JAVA_1_18_2_PRE1(2971, 18, 2, "PRE1"), + JAVA_1_18_2_PRE2(2972, 18, 2, "PRE2"), + JAVA_1_18_2_PRE3(2973, 18, 2, "PRE3"), + JAVA_1_18_2_RC1(2974, 18, 2, "RC1"), + JAVA_1_18_2(2975, 18, 2), +// JAVA_1_19_XS1(3066, 19, 0, "XS1"), + JAVA_1_19_22W11A(3080, 19, 0, "22w11a"), + JAVA_1_19_22W12A(3082, 19, 0, "22w12a"), + JAVA_1_19_22W13A(3085, 19, 0, "22w13a"), + JAVA_1_19_22W14A(3088, 19, 0, "22w14a"), + JAVA_1_19_22W15A(3089, 19, 0, "22w15a"), + JAVA_1_19_22W16A(3091, 19, 0, "22w16a"), + JAVA_1_19_22W16B(3092, 19, 0, "22w16b"), + JAVA_1_19_22W17A(3093, 19, 0, "22w17a"), + JAVA_1_19_22W18A(3095, 19, 0, "22w18a"), + JAVA_1_19_22W19A(3096, 19, 0, "22w19a"), + JAVA_1_19_PRE1(3098, 19, 0, "PRE1"), + JAVA_1_19_PRE2(3099, 19, 0, "PRE2"), + JAVA_1_19_PRE3(3100, 19, 0, "PRE3"), + JAVA_1_19_PRE4(3101, 19, 0, "PRE4"), + JAVA_1_19_PRE5(3102, 19, 0, "PRE5"), + JAVA_1_19_RC1(3103, 19, 0, "RC1"), + JAVA_1_19_RC2(3104, 19, 0, "RC2"), + JAVA_1_19_0(3105, 19, 0), + JAVA_1_19_1_22W24A(3106, 19, 1, "22w24a"), + JAVA_1_19_1_PRE1(3107, 19, 1, "PRE1"), + JAVA_1_19_1_RC1(3109, 19, 1, "RC1"), + JAVA_1_19_1_PRE2(3110, 19, 1, "PRE2"), + JAVA_1_19_1_PRE3(3111, 19, 1, "PRE3"), + JAVA_1_19_1_PRE4(3112, 19, 1, "PRE4"), + JAVA_1_19_1_PRE5(3113, 19, 1, "PRE5"), + JAVA_1_19_1_PRE6(3114, 19, 1, "PRE6"), + JAVA_1_19_1_RC2(3115, 19, 1, "RC2"), + JAVA_1_19_1_RC3(3116, 19, 1, "RC3"), + JAVA_1_19_1(3117, 19, 1), + JAVA_1_19_2_RC1(3118, 19, 2, "RC1"), + JAVA_1_19_2_RC2(3119, 19, 2, "RC2"), + JAVA_1_19_2(3120, 19, 2), + JAVA_1_19_3_22W42A(3205, 19, 3, "22w42a"), + JAVA_1_19_3_22W43A(3206, 19, 3, "22w43a"), + /** {@code Entities[].listener.selector} appears for the first time. */ + JAVA_1_19_3_22W44A(3207, 19, 3, "22w44a"), + JAVA_1_19_3_22W45A(3208, 19, 3, "22w45a"), + JAVA_1_19_3_22W46A(3210, 19, 3, "22w46a"), + JAVA_1_19_3_PRE1(3211, 19, 3, "PRE1"), + JAVA_1_19_3_PRE2(3212, 19, 3, "PRE2"), + JAVA_1_19_3_PRE3(3213, 19, 3, "PRE3"), + JAVA_1_19_3_RC1(3215, 19, 3, "RC1"), + JAVA_1_19_3_RC2(3216, 19, 3, "RC2"), + JAVA_1_19_3_RC3(3217, 19, 3, "RC3"), + JAVA_1_19_3(3218, 19, 3), + JAVA_1_19_4_23W03A(3320, 19, 4, "23w03a"), + JAVA_1_19_4_23W04A(3321, 19, 4, "23w04a"), + JAVA_1_19_4_23W05A(3323, 19, 4, "23w05a"), + JAVA_1_19_4_23W06A(3326, 19, 4, "23w06a"), + JAVA_1_19_4_23W07A(3329, 19, 4, "23w07a"), + JAVA_1_19_4_PRE1(3330, 19, 4, "PRE1"), + JAVA_1_19_4_PRE2(3331, 19, 4, "PRE2"), + JAVA_1_19_4_PRE3(3332, 19, 4, "PRE3"), + JAVA_1_19_4_PRE4(3333, 19, 4, "PRE4"), + JAVA_1_19_4_RC1(3334, 19, 4, "RC1"), + JAVA_1_19_4_RC2(3335, 19, 4, "RC2"), + JAVA_1_19_4_RC3(3336, 19, 4, "RC3"), + JAVA_1_19_4(3337, 19, 4), + JAVA_1_20_23W12A(3442, 20, 0, "23w12a"), + JAVA_1_20_23W13A(3443, 20, 0, "23w13a"), + JAVA_1_20_23W14A(3445, 20, 0, "23w14a"), + JAVA_1_20_23W16A(3449, 20, 0, "23w16a"), + JAVA_1_20_23W17A(3452, 20, 0, "23w17a"), + JAVA_1_20_23W18A(3453, 20, 0, "23w18a"), + JAVA_1_20_PRE1(3454, 20, 0, "PRE1"), + JAVA_1_20_PRE2(3455, 20, 0, "PRE2"), + JAVA_1_20_PRE3(3456, 20, 0, "PRE3"), + JAVA_1_20_PRE4(3457, 20, 0, "PRE4"), + JAVA_1_20_PRE5(3458, 20, 0, "PRE5"), + JAVA_1_20_PRE6(3460, 20, 0, "PRE6"), + JAVA_1_20_PRE7(3461, 20, 0, "PRE7"), + JAVA_1_20_RC1(3462, 20, 0, "RC1"), + JAVA_1_20_0(3463, 20, 0), + JAVA_1_20_1_RC1(3464, 20, 1, "RC1"), + JAVA_1_20_1(3465, 20, 1), + JAVA_1_20_2_23W31A(3567, 20, 2, "23w31a"), + JAVA_1_20_2_23W32A(3569, 20, 2, "23w32a"), + JAVA_1_20_2_23W33A(3570, 20, 2, "23w33a"), + JAVA_1_20_2_23W35A(3571, 20, 2, "23w35a"), + JAVA_1_20_2_PRE1(3572, 20, 2, "PRE1"), + JAVA_1_20_2_PRE2(3573, 20, 2, "PRE2"), + JAVA_1_20_2_PRE3(3574, 20, 2, "PRE3"), + JAVA_1_20_2_PRE4(3575, 20, 2, "PRE4"), + JAVA_1_20_2_RC1(3576, 20, 2, "RC1"), + JAVA_1_20_2_RC2(3577, 20, 2, "RC2"), + JAVA_1_20_2(3578, 20, 2), + JAVA_1_20_3_23W40A(3679, 20, 3, "23w40a"), + JAVA_1_20_3_23W41A(3681, 20, 3, "23w41a"), + JAVA_1_20_3_23W42A(3684, 20, 3, "23w42a"), + JAVA_1_20_3_23W43A(3686, 20, 3, "23w43a"), + JAVA_1_20_3_23W43B(3687, 20, 3, "23w43b"), + JAVA_1_20_3_23W44A(3688, 20, 3, "23w44a"), + JAVA_1_20_3_23W45A(3690, 20, 3, "23w45a"), + JAVA_1_20_3_23W46A(3691, 20, 3, "23w46a"), + JAVA_1_20_3_PRE1(3693, 20, 3, "PRE1"), + JAVA_1_20_3_PRE2(3694, 20, 3, "PRE2"), + JAVA_1_20_3_PRE3(3695, 20, 3, "PRE3"), + JAVA_1_20_3_PRE4(3696, 20, 3, "PRE4"), + JAVA_1_20_3_RC1(3697, 20, 3, "RC1"), + JAVA_1_20_3(3698, 20, 3), + JAVA_1_20_4_RC1(3699, 20, 4, "RC1"), + JAVA_1_20_4(3700, 20, 4), + JAVA_1_20_5_23W51A(3801, 20, 5, "23w51a"), + JAVA_1_20_5_23W51B(3802, 20, 5, "23w51b"), + JAVA_1_20_5_24W03A(3804, 20, 5, "24w03a"), + JAVA_1_20_5_24W03B(3805, 20, 5, "24w03b"), + JAVA_1_20_5_24W04A(3806, 20, 5, "24w04a"), + JAVA_1_20_5_24W05A(3809, 20, 5, "24w05a"), + JAVA_1_20_5_24W05B(3811, 20, 5, "24w05b"), + JAVA_1_20_5_24W06A(3815, 20, 5, "24w06a"), + JAVA_1_20_5_24W07A(3817, 20, 5, "24w07a"), + JAVA_1_20_5_24W09A(3819, 20, 5, "24w09a"), + JAVA_1_20_5_24W10A(3821, 20, 5, "24w10a"), + JAVA_1_20_5_24W11A(3823, 20, 5, "24w11a"), + JAVA_1_20_5_24W12A(3824, 20, 5, "24w12a"), + JAVA_1_20_5_24W13A(3826, 20, 5, "24w13a"), + JAVA_1_20_5_24W14A(3827, 20, 5, "24w14a"), + JAVA_1_20_5_PRE1(3829, 20, 5, "PRE1"), + JAVA_1_20_5_PRE2(3830, 20, 5, "PRE2"), + JAVA_1_20_5_PRE3(3831, 20, 5, "PRE3"), + JAVA_1_20_5_PRE4(3832, 20, 5, "PRE4"), + JAVA_1_20_5_RC1(3834, 20, 5, "RC1"), + JAVA_1_20_5_RC2(3835, 20, 5, "RC2"), + JAVA_1_20_5_RC3(3836, 20, 5, "RC3"), + JAVA_1_20_5(3837, 20, 5), + JAVA_1_20_6_RC1(3838, 20, 6, "RC1"), + JAVA_1_20_6(3839, 20, 6), + JAVA_1_21_24W18A(3940, 21, 0, "24w18a"), + JAVA_1_21_24W19A(3941, 21, 0, "24w19a"), + JAVA_1_21_24W19B(3942, 21, 0, "24w19b"), + JAVA_1_21_24W20A(3944, 21, 0, "24w20a"), + JAVA_1_21_24W21A(3946, 21, 0, "24w21a"), + JAVA_1_21_24W21B(3947, 21, 0, "24w21b"), + JAVA_1_21_PRE1(3948, 21, 0, "PRE1"), + JAVA_1_21_PRE2(3949, 21, 0, "PRE2"), + JAVA_1_21_PRE3(3950, 21, 0, "PRE3"), + JAVA_1_21_PRE4(3951, 21, 0, "PRE4"), + JAVA_1_21_RC1(3952, 21, 0, "RC1"), + JAVA_1_21_0(3953, 21, 0), + JAVA_1_21_1_RC1(3954, 21, 1, "RC1"), + JAVA_1_21_1(3955, 21, 1), + JAVA_1_21_2_24W33A(4058, 21, 2, "24w33a"), + JAVA_1_21_2_24W34A(4060, 21, 2, "24w34a"), + JAVA_1_21_2_24W35A(4062, 21, 2, "24w35a"), + JAVA_1_21_2_24W36A(4063, 21, 2, "24w36a"), + JAVA_1_21_2_24W37A(4065, 21, 2, "24w37a"), + JAVA_1_21_2_24W38A(4066, 21, 2, "24w38a"), + JAVA_1_21_2_24W39A(4069, 21, 2, "24w39a"), + JAVA_1_21_2_24W40A(4072, 21, 2, "24w40a"), + JAVA_1_21_2_PRE1(4073, 21, 2, "PRE1"), + JAVA_1_21_2_PRE2(4074, 21, 2, "PRE2"), + JAVA_1_21_2_PRE3(4075, 21, 2, "PRE3"), + JAVA_1_21_2_PRE4(4076, 21, 2, "PRE4"), + JAVA_1_21_2_PRE5(4077, 21, 2, "PRE5"), + JAVA_1_21_2_RC1(4078, 21, 2, "RC1"), + JAVA_1_21_2_RC2(4079, 21, 2, "RC2"), + JAVA_1_21_2(4080, 21, 2), + JAVA_1_21_3(4082, 21, 3), + JAVA_1_21_4_24W44A(4174, 21, 4, "24w44a"), + JAVA_1_21_4_24W45A(4177, 21, 4, "24w45a"), + JAVA_1_21_4_24W46A(4178, 21, 4, "24w46a"), + JAVA_1_21_4_PRE1(4179, 21, 4, "PRE1"), + JAVA_1_21_4_PRE2(4182, 21, 4, "PRE2"), + JAVA_1_21_4_PRE3(4183, 21, 4, "PRE3"), + JAVA_1_21_4_RC1(4184, 21, 4, "RC1"), + JAVA_1_21_4_RC2(4186, 21, 4, "RC2"), + JAVA_1_21_4_RC3(4188, 21, 4, "RC3"), + JAVA_1_21_4(4189, 21, 4), + JAVA_1_21_5_25W02A(4298, 21, 5, "25w02a"), + JAVA_1_21_5_25W03A(4304, 21, 5, "25w03a"), + JAVA_1_21_5_25W04A(4308, 21, 5, "25w04a"), + JAVA_1_21_5_25W05A(4310, 21, 5, "25w05a"), + JAVA_1_21_5_25W06A(4313, 21, 5, "25w06a"), + JAVA_1_21_5_25W07A(4315, 21, 5, "25w07a"), + JAVA_1_21_5_25W08A(4316, 21, 5, "25w08a"), + JAVA_1_21_5_25W09A(4317, 21, 5, "25w09a"), + JAVA_1_21_5_25W09B(4318, 21, 5, "25w09b"), + JAVA_1_21_5_25W10A(4319, 21, 5, "25w10a"), + JAVA_1_21_5_PRE1(4320, 21, 5, "PRE1"),; + + private static final int[] ids; + private static final DataVersion latestFullReleaseVersion; + private final int id; + private final int minor; + private final int patch; + private final boolean isFullRelease; + private final boolean isWeeklyRelease; + private final String buildDescription; + private final String str; + private final String simpleStr; + + static { + // enum is maintained in order with a unit test to enforce the convention - so no need to sort + ids = Arrays.stream(values()).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, null); + } + + /** + * @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 (unit test enforced):Convention used:
Technically poi files were introduced with {@link #JAVA_1_14_19W11A} but the nbt structure was quickly + * changed and this 3 week span of weekly versions isn't worth the hassle of supporting.
+ * @since {@link #JAVA_1_14_PRE1} + */ + public boolean hasPoiMca() { + return this.id >= JAVA_1_14_PRE1.id; + } + + /** + * TRUE as of 1.17 + * Entities were pulled out of terrain 'region/r.X.Z.mca' files into their own .mca files. E.g. 'entities/r.0.0.mca' + */ + public boolean hasEntitiesMca() { + return this.id >= JAVA_1_17_20W45A.id; + } + + 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]; + } + + /** + * @param simpleVersionStr such as "1.12", "21w13a", "1.19.1-pre3" + * @return exact match or null + */ + public static DataVersion find(String simpleVersionStr) { + final String seeking = simpleVersionStr.toLowerCase(Locale.ENGLISH); + return Arrays.stream(values()).filter(v -> v.simpleStr.equals(seeking)).findFirst().orElse(null); + } + + /** + * @return The previous known data version or null if there is none. + */ + public DataVersion previous() { + if (this.ordinal() > 0) + return values()[this.ordinal() - 1]; + else + return null; + } + + /** + * @return The next known data version or null if there is none. + */ + public DataVersion next() { + if (this.ordinal() < ids.length - 1) + return values()[this.ordinal() + 1]; + else + return null; + } + + /** + * @return The latest full release (non-weekly, non pre-release, etc) version defined. + */ + public static DataVersion latest() { + return latestFullReleaseVersion; + } + + @Override + public String toString() { + return str; + } + + public String toSimpleString() { + return simpleStr; + } + + /** + * 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)} + * @see #throwUnsupportedVersionChangeIfCrossed(int, int) + */ + 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; + } + } + + /** + * 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/io/github/ensgijs/nbt/mca/EntitiesChunk.java b/src/main/java/io/github/ensgijs/nbt/mca/EntitiesChunk.java new file mode 100644 index 00000000..7c83a6ee --- /dev/null +++ b/src/main/java/io/github/ensgijs/nbt/mca/EntitiesChunk.java @@ -0,0 +1,32 @@ +package io.github.ensgijs.nbt.mca; + +import io.github.ensgijs.nbt.mca.io.McaFileHelpers; +import io.github.ensgijs.nbt.tag.CompoundTag; +import io.github.ensgijs.nbt.mca.entities.Entity; +import io.github.ensgijs.nbt.mca.entities.EntityFactory; + +/** + * Thin default implementation of {@link EntitiesChunkBase}. + * + * @see EntitiesChunkBase + * @see EntityFactory + * @see McaFileHelpers#MCA_CREATORS + */ +public class EntitiesChunk extends EntitiesChunkBaseIf performance is everything for you, but you would still like to work with higher level objects + * than nbt tags, you can use {@link #getEntitiesTag()}, find an entity record you want to manipulate + * and use {@link EntityFactory#create(CompoundTag, int)} to get an entity instance then call + * {@link Entity#updateHandle()} to apply your changes all the way back to the entities tag held + * for this chunk.
+ */ + public ListDoes not trigger a handle update. The result of calling {@link #getEntitiesTag()} + * will not change until {@link #updateHandle()} has been called.
+ * @param entities Entities to set, not null, may be empty. + * @throws UnsupportedOperationException if loaded in raw mode + * @see #clearEntities() + */ + public void setEntities(ListRaw mode behavior: supported!
+ * Sets the given tag in the held nbt data handle in its version correct place. Does not make calling
+ * {@link #getEntitiesTag()} or {@link #getEntities()} legal for chunks loaded in raw mode.
+ *
Moving while in RAW mode is supported.
+ * @param newChunkX new absolute chunk-x + * @param newChunkZ new absolute chunk-z + * @param moveChunkFlags {@link MoveChunkFlags} OR'd together to control move chunk behavior. + * @param force unused + * @return true if any data was changed as a result of this call + * @throws UnsupportedOperationException if loaded in raw mode + */ + @Override + public boolean moveChunk(int newChunkX, int newChunkZ, long moveChunkFlags, boolean force) { + if (!moveChunkImplemented()) + throw new UnsupportedOperationException("Missing the data required to move this chunk!"); + if (!RegionBoundingRectangle.MAX_WORLD_BORDER_BOUNDS.containsChunk(newChunkX, newChunkZ)) { + throw new IllegalArgumentException("Chunk XZ must be within the maximum world bounds."); + } + if (this.chunkX == newChunkX && this.chunkZ == newChunkZ) return false; + this.chunkX = newChunkX; + this.chunkZ = newChunkZ; + if (raw) { + setTag(POSITION_PATH, new IntArrayTag(newChunkX, newChunkZ)); + } + if (fixEntityLocations(moveChunkFlags)) { + if ((moveChunkFlags & MoveChunkFlags.AUTOMATICALLY_UPDATE_HANDLE) > 0) { + updateHandle(); + } + } + return true; + } + + /** + * Scans all entities and moves any which are outside this chunks bounds into it preserving their + * relative location from their source chunk. + *Fixing entity locations while in RAW mode is supported.
+ * @return true if any entity locations were changed; false if no changes were made. + * @throws UnsupportedOperationException if loaded in raw mode + */ + public boolean fixEntityLocations(long moveChunkFlags) { + if (!moveChunkImplemented()) + throw new UnsupportedOperationException("Missing the data required to move this chunk!"); + if (this.chunkX == NO_CHUNK_COORD_SENTINEL || this.chunkZ == NO_CHUNK_COORD_SENTINEL) { + throw new IllegalStateException("Chunk XZ not known"); + } + boolean changed = false; + if (entities != null) { + final NbtPath brainMemoriesPath = ENTITIES_BRAIN_MEMORIES_PATH.get(dataVersion); + final NbtPath memoryPosPath = NbtPath.of("value.pos"); + final ChunkBoundingRectangle cbr = new ChunkBoundingRectangle(chunkX, chunkZ); + for (ET entity : entities) { + if (!cbr.containsBlock(entity.getX(), entity.getZ())) { + entity.setX(cbr.relocateX(entity.getX())); + entity.setZ(cbr.relocateZ(entity.getZ())); + if ((moveChunkFlags & MoveChunkFlags.RANDOMIZE_ENTITY_UUID) > 0) { + entity.setUuid(UUID.randomUUID()); + } + changed = true; + } + if (brainMemoriesPath.exists(entity.getHandle())) { + CompoundTag memoriesTag = brainMemoriesPath.getTag(entity.getHandle()); + for (NamedTag entry : memoriesTag) { + int[] pos = memoryPosPath.getIntArray(entry.getTag()); + if (pos != null && !cbr.containsBlock(pos[0], pos[2])) { + // TODO: dimension is also in this data + pos[0] = cbr.relocateX(pos[0]); + pos[2] = cbr.relocateZ(pos[2]); + changed = true; + } + } + } + } + } else if (entitiesTag != null) { + changed = fixEntityLocations(dataVersion, moveChunkFlags, entitiesTag, new ChunkBoundingRectangle(chunkX, chunkZ)); + } else if (raw) { + ListTagUse 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.
+ */
+ public McaFileBase(int regionX, int regionZ) {
+ this.regionX = regionX;
+ 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());
+ }
+
+ /**
+ * 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
+ */
+ 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;
+ }
+
+ 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()}.
+ */
+ 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.
+ * @see #moveRegion(int, int, long, boolean)
+ */
+ public int getRegionX() {
+ return regionX;
+ }
+
+ /**
+ * @return The z-value currently set for this mca file in region coordinates.
+ * @see #moveRegion(int, int, long, boolean)
+ */
+ public int getRegionZ() {
+ return regionZ;
+ }
+
+ /**
+ * Returns result of calling {@link McaFileHelpers#createNameFromRegionLocation(int, int)}
+ * with current region coordinate values.
+ * @return A mca filename in the format "r.{regionX}.{regionZ}.mca"
+ */
+ public String createRegionName() {
+ return McaFileHelpers.createNameFromRegionLocation(regionX, regionZ);
+ }
+
+ /**
+ * @return type of chunk this MCA File holds
+ */
+ public abstract Class See {@link PoiRecord} for more information and for a list of POI types and how they map to blocks. Deprecated: use {@code DataVersion.latest().id()} instead.
+ */
+ @Deprecated
+ public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id();
+
+ /**
+ * {@inheritDoc}
+ */
+ public McaRegionFile(int regionX, int regionZ) {
+ super(regionX, regionZ);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public McaRegionFile(int regionX, int regionZ, int defaultDataVersion) {
+ super(regionX, regionZ, defaultDataVersion);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public McaRegionFile(int regionX, int regionZ, DataVersion defaultDataVersion) {
+ super(regionX, regionZ, defaultDataVersion);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class 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. Also resets all poi chunk section validity flags to indicate "is valid = true".
+ * In summary, if you have changed the block at a POI location, or altered the blocks in a {@link TerrainChunk}
+ * in such a way that may have added or removed POI blocks you have a few options (MC 1.14+)
+ * About this class 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
+ *
+ * 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
+ * AKA: "height" 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. Remember to call {@code super();}
+ * from your default constructors to maintain this behavior. 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 TerrainSection} and user code calls {@link TerrainSection#setHeight(int)}
+ * on a section which is referenced by any chunk. Prefer using {@link TerrainChunk#getSectionY(SectionBase)} which will always be accurate within the context of the
+ * chunk. 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. 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 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#getSectionY()} 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_SECTION_Y_SENTINEL} is returned.
+ */
+ public int getSectionY(T section) {
+ if (section == null) return SectionBase.NO_SECTION_Y_SENTINEL;
+ int y = sectionHeightLookup.getOrDefault(section, SectionBase.NO_SECTION_Y_SENTINEL);
+ section.syncHeight(y);
+ return y;
+ }
+
+ /**
+ * Gets the minimum section y-coordinate.
+ * NOTE: fully generated terrain chunks MAY have a dummy section -1 below the world, the returned value
+ * WILL be this value - {@link TerrainSection} will exist for this Y but it will be completely empty of the
+ * standard tags you would expect to see (blocks, biomes, etc). 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. Note: 2D biomes have a resolution of 1x256x1 blocks. Note: 3D biomes have a resolution of 4x4x4 blocks. Note: 2D biomes have a resolution of 1x256x1 blocks. Note: 3D biomes have a resolution of 4x4x4 blocks. Note: 2D biomes have a resolution of 1x256x1 blocks. Note: 3D biomes have a resolution of 4x4x4 blocks. Modifying the returned value can be done safely, it will have no effect on this chunk. To avoid the overhead of making a copy use {@link #getBiomeAtByRef(int, int, int)} instead. Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. WARNING if the returned value is modified it modifies every value which references the same palette
+ * entry within the same chunk section! Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. Modifying the returned value can be done safely, it will have no effect on this chunk. To avoid the overhead of making a copy use {@link #getBlockAtByRef(int, int, int)} instead. Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. WARNING if the returned value is modified it modifies every value which references the same palette
+ * entry within the same chunk section! Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. Never throws IndexOutOfBoundsException. XYZ are always wrapped into bounds. Deprecated: use {@code DataVersion.latest().id()} instead.
+// */
+// @Deprecated
+// public static final int DEFAULT_DATA_VERSION = DataVersion.latest().id();
+//
+// /**
+// * {@inheritDoc}
+// */
+// public TerrainMCAFileBase(int regionX, int regionZ) {
+// super(regionX, regionZ);
+// }
+//
+// /**
+// * {@inheritDoc}
+// */
+// public TerrainMCAFileBase(int regionX, int regionZ, int defaultDataVersion) {
+// super(regionX, regionZ, defaultDataVersion);
+// }
+//
+// /**
+// * {@inheritDoc}
+// */
+// public TerrainMCAFileBase(int regionX, int regionZ, DataVersion defaultDataVersion) {
+// super(regionX, regionZ, defaultDataVersion);
+// }
+//
+// /**
+// * {@inheritDoc}
+// */
+// @Override
+// public Classnull
if the chunk or the section do not exist.
+// */
+// public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
+// int chunkX = McaFileHelpers.blockToChunk(blockX), chunkZ = McaFileHelpers.blockToChunk(blockZ);
+// TerrainChunk 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 (TerrainChunk chunk : chunks) {
+// if (chunk != null) {
+// chunk.cleanupPalettesAndBlockStates();
+// }
+// }
+// }
+}
diff --git a/src/main/java/io/github/ensgijs/nbt/mca/PoiChunk.java b/src/main/java/io/github/ensgijs/nbt/mca/PoiChunk.java
new file mode 100644
index 00000000..bcf3538f
--- /dev/null
+++ b/src/main/java/io/github/ensgijs/nbt/mca/PoiChunk.java
@@ -0,0 +1,28 @@
+package io.github.ensgijs.nbt.mca;
+
+import io.github.ensgijs.nbt.tag.CompoundTag;
+
+public class PoiChunk extends PoiChunkBase
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ *
+ *
+ * What are "Tickets"?
+ *
+ *
+ */
+public class PoiRecord implements TagWrapper
+ *
+ * @since {@link DataVersion#JAVA_1_13_18W06A}
+ * @see LongArrayTagPackedIntegers
+ */
+ public static final VersionAware
+ *
+ * @return {@link LongArrayTagPackedIntegers} configured to yield block Y values.
+ * @since {@link DataVersion#JAVA_1_13_18W06A}
+ */
+ public LongArrayTagPackedIntegers getHeightMap(String name) {
+ if (getHeightMaps() == null)
+ return null;
+ var hm = getHeightMaps().getLongArrayTag(name);
+ if (hm == null)
+ return null;
+ final int minY = getWorldMinBlockY() - 1;
+ final int maxY = getWorldMaxBlockY();
+ return LongArrayTagPackedIntegers.builder()
+ .dataVersion(dataVersion)
+ .minBitsPerValue(Math.max(9, LongArrayTagPackedIntegers.calculateBitsRequired(maxY - minY)))
+ .valueOffset(minY)
+ .length(256)
+ .build(hm);
+ }
+
+ public IntArrayTag getLegacyHeightMap() {
+ return legacyHeightMap;
+ }
+
+ public void setLegacyHeightMap(IntArrayTag legacyHeightMap) {
+ this.legacyHeightMap = legacyHeightMap;
+ }
+
+ /**
+ * Returns a copy of the palette value at the specified position in this chunk.
+ * null
if the chunk or the section do not exist.
+//// */
+//// public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
+//// int chunkX = McaFileHelpers.blockToChunk(blockX), chunkZ = McaFileHelpers.blockToChunk(blockZ);
+//// TerrainChunk 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 (TerrainChunk chunk : chunks) {
+//// if (chunk != null) {
+//// chunk.cleanupPalettesAndBlockStates();
+//// }
+//// }
+//// }
+//}
diff --git a/src/main/java/io/github/ensgijs/nbt/mca/TerrainSection.java b/src/main/java/io/github/ensgijs/nbt/mca/TerrainSection.java
new file mode 100644
index 00000000..77a9a0c9
--- /dev/null
+++ b/src/main/java/io/github/ensgijs/nbt/mca/TerrainSection.java
@@ -0,0 +1,65 @@
+package io.github.ensgijs.nbt.mca;
+
+import io.github.ensgijs.nbt.tag.CompoundTag;
+
+/**
+ * Represents a Terrain data chunk section. See notes on {@link TerrainChunk} for possible
+ * future repurposing ideas.
+ */
+public class TerrainSection extends TerrainSectionBase {
+
+ public TerrainSection(CompoundTag sectionRoot, int dataVersion) {
+ super(sectionRoot, dataVersion);
+ }
+
+ public TerrainSection(CompoundTag sectionRoot, int dataVersion, long loadFlags) {
+ super(sectionRoot, dataVersion, loadFlags);
+ }
+
+ public TerrainSection(int dataVersion) {
+ super(dataVersion);
+ }
+
+ /**
+ * @return An empty Section initialized using the latest full release data version.
+ * @deprecated Dangerous - prefer using {@link TerrainChunk#createSection(int)} or using the
+ * {@link #TerrainSection(int)} constructor instead.
+ */
+ @Deprecated
+ public static TerrainSection newSection() {
+ return new TerrainSection(DataVersion.latest().id());
+ }
+
+ /**
+ * 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 void setHeight(int height) {
+ syncHeight(height);
+ }
+
+ /**
+ * Updates the raw CompoundTag that this Section is based on.
+ *
+ * @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.
+ */
+ @Deprecated
+ public CompoundTag updateHandle(int y) {
+ final int oldY = sectionY;
+ try {
+ sectionY = y;
+ return updateHandle();
+ } finally {
+ sectionY = oldY;
+ }
+ }
+}
diff --git a/src/main/java/io/github/ensgijs/nbt/mca/TerrainSectionBase.java b/src/main/java/io/github/ensgijs/nbt/mca/TerrainSectionBase.java
new file mode 100644
index 00000000..e7c2999d
--- /dev/null
+++ b/src/main/java/io/github/ensgijs/nbt/mca/TerrainSectionBase.java
@@ -0,0 +1,288 @@
+package io.github.ensgijs.nbt.mca;
+
+import io.github.ensgijs.nbt.io.TextNbtParser;
+import io.github.ensgijs.nbt.tag.*;
+import io.github.ensgijs.nbt.mca.util.PalettizedCuboid;
+
+import static io.github.ensgijs.nbt.mca.DataVersion.*;
+import static io.github.ensgijs.nbt.mca.io.LoadFlags.*;
+
+/**
+ * Provides the base for all terrain section classes.
+ */
+public abstract class TerrainSectionBase extends SectionBase