From 0b33fa87570482ff41f3f579608534feac5afb74 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 9 Jun 2022 11:48:35 +0100 Subject: [PATCH] Allow NBT stored in DiskOptimizedClipboards to be written to disk as a compressed byte array at the end of the file (#1745) * Allow NBT stored in DiskOptimizedClipboards to be written to disk as a compressed byte array at the end of the file * Add some deprecations/javadocs and provide the expected clipboard version on error * Javadoc since tags and add location of clipboard folder to error * Refactor load-from-file method into DOC class * Refactor nbt loading code into separate method in DOC --- .../core/configuration/Settings.java | 5 + .../clipboard/DiskOptimizedClipboard.java | 286 ++++++++++++++++-- ...FaweClipboardVersionMismatchException.java | 53 +++- .../internal/io/ByteBufferInputStream.java | 38 +++ .../java/com/sk89q/jnbt/NBTInputStream.java | 43 +++ .../com/sk89q/worldedit/entity/Player.java | 4 +- .../src/main/resources/lang/strings.json | 2 +- 7 files changed, 406 insertions(+), 25 deletions(-) create mode 100644 worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/ByteBufferInputStream.java diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java index 8a86a5422..b019f3d38 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java @@ -721,6 +721,11 @@ public class Settings extends Config { "If a player's clipboard should be deleted upon logout" }) public boolean DELETE_ON_LOGOUT = false; + @Comment({ + "Allows NBT stored in a clipboard to be written to disk", + " - Requires clipboard.use-disk to be enabled" + }) + public boolean SAVE_CLIPBOARD_NBT_TO_DISK = true; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java index 69aa98b7e..80c49754c 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java @@ -3,19 +3,23 @@ package com.fastasyncworldedit.core.extent.clipboard; import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.configuration.Settings; import com.fastasyncworldedit.core.internal.exception.FaweClipboardVersionMismatchException; +import com.fastasyncworldedit.core.internal.io.ByteBufferInputStream; import com.fastasyncworldedit.core.jnbt.streamer.IntValueReader; import com.fastasyncworldedit.core.math.IntTriple; import com.fastasyncworldedit.core.util.MainUtil; import com.fastasyncworldedit.core.util.ReflectionUtils; import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.DoubleTag; import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.NBTOutputStream; import com.sk89q.jnbt.Tag; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.world.biome.BiomeType; @@ -27,6 +31,7 @@ import com.sk89q.worldedit.world.block.BlockTypes; import org.apache.logging.log4j.Logger; import javax.annotation.Nullable; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; @@ -52,21 +57,27 @@ import java.util.stream.Collectors; */ public class DiskOptimizedClipboard extends LinearClipboard { + public static final int VERSION = 2; private static final Logger LOGGER = LogManagerCompat.getLogger(); - - private static final int VERSION = 1; - private static final int HEADER_SIZE = 22; + private static final int HEADER_SIZE = 27; // Current header size + private static final int VERSION_1_HEADER_SIZE = 22; // Header size of "version 1" + private static final int VERSION_2_HEADER_SIZE = 27; // Header size of "version 2" i.e. when NBT/entities could be saved private final HashMap nbtMap; private final File file; + private final int headerSize; private RandomAccessFile braf; private MappedByteBuffer byteBuffer; private FileChannel fileChannel; - private boolean hasBiomes; + private boolean hasBiomes = false; private boolean canHaveBiomes = true; + private int nbtBytesRemaining; + /** + * Creates a new DiskOptimizedClipboard for the given region. Creates or overwrites a file using the given UUID as a name. + */ public DiskOptimizedClipboard(Region region, UUID uuid) { this( region.getDimensions(), @@ -79,6 +90,14 @@ public class DiskOptimizedClipboard extends LinearClipboard { setOrigin(region.getMinimumPoint()); } + /** + * Creates a new DiskOptimizedClipboard with the given dimensions. Creates a new file with a random UUID name. + * + * @deprecated Use {@link DiskOptimizedClipboard#DiskOptimizedClipboard(Region, UUID)} or + * {@link DiskOptimizedClipboard#DiskOptimizedClipboard(BlockVector3, File)} to avoid creating a large number of clipboard + * files that won't be cleaned until `clipboard.delete-after-days` and a server restart. + */ + @Deprecated(forRemoval = true, since = "TODO") public DiskOptimizedClipboard(BlockVector3 dimensions) { this( dimensions, @@ -89,12 +108,17 @@ public class DiskOptimizedClipboard extends LinearClipboard { ); } + /** + * New DiskOptimizedClipboard. If the file specified exists, then it will be completely overwritten. To load an existing + * clipboard, use {@link DiskOptimizedClipboard#DiskOptimizedClipboard(File)}. + */ public DiskOptimizedClipboard(BlockVector3 dimensions, File file) { super(dimensions, BlockVector3.ZERO); - if (HEADER_SIZE + ((long) getVolume() << 1) >= Integer.MAX_VALUE) { + headerSize = HEADER_SIZE; + if (headerSize + ((long) getVolume() << 1) >= Integer.MAX_VALUE) { throw new IllegalArgumentException( "Dimensions too large for this clipboard format. Use //lazycopy for large selections."); - } else if (HEADER_SIZE + ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1) >= Integer.MAX_VALUE) { + } else if (headerSize + ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1) >= Integer.MAX_VALUE) { LOGGER.error("Dimensions are too large for biomes to be stored in a DiskOptimizedClipboard"); canHaveBiomes = false; } @@ -113,9 +137,10 @@ public class DiskOptimizedClipboard extends LinearClipboard { e.printStackTrace(); } this.braf = new RandomAccessFile(file, "rw"); - long fileLength = (long) getVolume() * 2L + (long) HEADER_SIZE; + long fileLength = (long) getVolume() * 2L + (long) headerSize; braf.setLength(0); braf.setLength(fileLength); + this.nbtBytesRemaining = Integer.MAX_VALUE - (int) fileLength; init(); // write getLength() etc byteBuffer.putChar(2, (char) (VERSION)); @@ -127,15 +152,40 @@ public class DiskOptimizedClipboard extends LinearClipboard { } } + /** + * Load an existing file as a DiskOptimizedClipboard. The file MUST exist and MUST be created as a DiskOptimizedClipboard + * with data written to it. + */ public DiskOptimizedClipboard(File file) { - super(readSize(file), BlockVector3.ZERO); + this(file, VERSION); + } + + /** + * Load an existing file as a DiskOptimizedClipboard. The file MUST exist and MUST be created as a DiskOptimizedClipboard + * with data written to it. + * + * @param file File to read from + * @param versionOverride An override version to allow loading of older clipboards if required + */ + public DiskOptimizedClipboard(File file, int versionOverride) { + super(readSize(file, versionOverride), BlockVector3.ZERO); + headerSize = getHeaderSizeOverrideFromVersion(versionOverride); nbtMap = new HashMap<>(); try { this.file = file; this.braf = new RandomAccessFile(file, "rw"); braf.setLength(file.length()); + this.nbtBytesRemaining = Integer.MAX_VALUE - (int) file.length(); init(); - if (braf.length() - HEADER_SIZE == ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1)) { + long biomeLength = (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1); + if (headerSize >= VERSION_2_HEADER_SIZE) { + readBiomeStatusFromHeader(); + int nbtCount = readNBTSavedCountFromHeader(); + int entitiesCount = readEntitiesSavedCountFromHeader(); + if (Settings.settings().CLIPBOARD.SAVE_CLIPBOARD_NBT_TO_DISK && nbtCount + entitiesCount > 0) { + loadNBTFromFileFooter(nbtCount, entitiesCount, biomeLength); + } + } else if (braf.length() - headerSize == ((long) getVolume() << 1) + biomeLength) { hasBiomes = true; } getAndSetOffsetAndOrigin(); @@ -144,12 +194,30 @@ public class DiskOptimizedClipboard extends LinearClipboard { } } - private static BlockVector3 readSize(File file) { + /** + * Attempt to load a file into a new {@link DiskOptimizedClipboard} instance. Will attempt to recover on version mismatch + * failure. + * + * @param file File to load + * @return new {@link DiskOptimizedClipboard} instance. + */ + public static DiskOptimizedClipboard loadFromFile(final File file) { + DiskOptimizedClipboard doc; + try { + doc = new DiskOptimizedClipboard(file); + } catch (FaweClipboardVersionMismatchException e) { // Attempt to recover + int version = e.getClipboardVersion(); + doc = new DiskOptimizedClipboard(file, version); + } + return doc; + } + + private static BlockVector3 readSize(File file, int expectedVersion) { try (DataInputStream is = new DataInputStream(new FileInputStream(file))) { is.skipBytes(2); int version = is.readChar(); - if (version != VERSION) { - throw new FaweClipboardVersionMismatchException(); + if (version != expectedVersion) { + throw new FaweClipboardVersionMismatchException(expectedVersion, version); } return BlockVector3.at(is.readChar(), is.readChar(), is.readChar()); } catch (IOException e) { @@ -158,6 +226,57 @@ public class DiskOptimizedClipboard extends LinearClipboard { } } + private void loadNBTFromFileFooter(int nbtCount, int entitiesCount, long biomeLength) throws IOException { + int biomeBlocksLength = headerSize + (getVolume() << 1) + (hasBiomes ? (int) biomeLength : 0); + MappedByteBuffer tmp = fileChannel.map(FileChannel.MapMode.READ_ONLY, biomeBlocksLength, braf.length()); + try (NBTInputStream nbtIS = new NBTInputStream(MainUtil.getCompressedIS(new ByteBufferInputStream(tmp)))) { + Iterator iter = nbtIS.toIterator(); + while (nbtCount > 0 && iter.hasNext()) { // TileEntities are stored "before" entities + CompoundTag tag = iter.next(); + int x = tag.getInt("x"); + int y = tag.getInt("y"); + int z = tag.getInt("z"); + IntTriple pos = new IntTriple(x, y, z); + nbtMap.put(pos, tag); + nbtCount--; + } + while (entitiesCount > 0 && iter.hasNext()) { + CompoundTag tag = iter.next(); + Tag posTag = tag.getValue().get("Pos"); + if (posTag == null) { + LOGGER.warn("Missing pos tag: {}", tag); + return; + } + List pos = (List) posTag.getValue(); + double x = pos.get(0).getValue(); + double y = pos.get(1).getValue(); + double z = pos.get(2).getValue(); + BaseEntity entity = new BaseEntity(tag); + BlockArrayClipboard.ClipboardEntity clipboardEntity = new BlockArrayClipboard.ClipboardEntity( + this, + x, + y, + z, + 0f, + 0f, + entity + ); + this.entities.add(clipboardEntity); + entitiesCount--; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private int getHeaderSizeOverrideFromVersion(int versionOverride) { + return switch (versionOverride) { + case 1 -> VERSION_1_HEADER_SIZE; + case 2 -> VERSION_2_HEADER_SIZE; + default -> HEADER_SIZE; + }; + } + @Override public URI getURI() { return file.toURI(); @@ -185,8 +304,11 @@ public class DiskOptimizedClipboard extends LinearClipboard { close(); this.braf = new RandomAccessFile(file, "rw"); // Since biomes represent a 4x4x4 cube, we store fewer biome bytes that volume at 1 byte per biome - // +1 to each too allow for cubes that lie across the region boundary - this.braf.setLength(HEADER_SIZE + ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1)); + // +1 to each to allow for cubes that lie across the region boundary + long length = + headerSize + ((long) getVolume() << 1) + (long) ((getHeight() >> 2) + 1) * ((getLength() >> 2) + 1) * ((getWidth() >> 2) + 1); + this.braf.setLength(length); + this.nbtBytesRemaining = Integer.MAX_VALUE - (int) length; init(); } catch (IOException e) { e.printStackTrace(); @@ -216,7 +338,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { public void setBiome(int index, BiomeType biome) { if (initBiome()) { try { - byteBuffer.put(HEADER_SIZE + (getVolume() << 1) + index, (byte) biome.getInternalId()); + byteBuffer.put(headerSize + (getVolume() << 1) + index, (byte) biome.getInternalId()); } catch (IndexOutOfBoundsException e) { LOGGER.info((long) (getHeight() >> 2) * (getLength() >> 2) * (getWidth() >> 2)); LOGGER.info(index); @@ -230,7 +352,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { if (!hasBiomes()) { return null; } - int biomeId = byteBuffer.get(HEADER_SIZE + (getVolume() << 1) + index) & 0xFF; + int biomeId = byteBuffer.get(headerSize + (getVolume() << 1) + index) & 0xFF; return BiomeTypes.get(biomeId); } @@ -239,7 +361,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { if (!hasBiomes()) { return; } - int mbbIndex = HEADER_SIZE + (getVolume() << 1); + int mbbIndex = headerSize + (getVolume() << 1); try { for (int y = 0; y < getHeight(); y++) { for (int z = 0; z < getLength(); z++) { @@ -325,11 +447,45 @@ public class DiskOptimizedClipboard extends LinearClipboard { ReflectionUtils.getUnsafe().invokeCleaner(cb); } + private void writeBiomeStatusToHeader() { + byteBuffer.put(22, (byte) (hasBiomes ? 1 : 0)); + } + + private void writeNBTSavedCountToHeader(int count) { + byteBuffer.putChar(23, (char) count); + } + + private void writeEntitiesSavedCountToHeader(int count) { + byteBuffer.putChar(25, (char) count); + } + + private boolean readBiomeStatusFromHeader() { + return this.hasBiomes = byteBuffer.get(22) == 1; + } + + private int readNBTSavedCountFromHeader() { + return byteBuffer.getChar(23); + } + + private int readEntitiesSavedCountFromHeader() { + return byteBuffer.getChar(25); + } + @Override @SuppressWarnings("ResultOfMethodCallIgnored") public void close() { try { if (byteBuffer != null) { + if (headerSize >= VERSION_2_HEADER_SIZE) { + if (Settings.settings().CLIPBOARD.SAVE_CLIPBOARD_NBT_TO_DISK) { + try { + writeNBTToDisk(); + } catch (Exception e) { + LOGGER.error("Unable to save NBT data to disk.", e); + } + } + writeBiomeStatusToHeader(); + } byteBuffer.force(); fileChannel.close(); braf.close(); @@ -344,6 +500,94 @@ public class DiskOptimizedClipboard extends LinearClipboard { } } + private void writeNBTToDisk() throws IOException { + if (!nbtMap.isEmpty() || !entities.isEmpty()) { + byte[] output = null; + boolean entitiesFit = false; + // Closing a BAOS does nothing + ByteArrayOutputStream baOS = new ByteArrayOutputStream(); + try (NBTOutputStream nbtOS = new NBTOutputStream(MainUtil.getCompressedOS( + baOS, + Settings.settings().CLIPBOARD.COMPRESSION_LEVEL + ))) { + if (!nbtMap.isEmpty()) { + try { + for (CompoundTag tag : nbtMap.values()) { + nbtOS.writeTag(tag); + } + } catch (IOException e) { + e.printStackTrace(); + } + nbtOS.flush(); + if (baOS.size() > nbtBytesRemaining) { + LOGGER.warn( + "Clipboard file {} does not have enough remaining space to store NBT data on disk.", + file.getName() + ); + writeNBTSavedCountToHeader(0); + writeEntitiesSavedCountToHeader(0); + return; + } else { + writeNBTSavedCountToHeader(nbtMap.size()); + nbtBytesRemaining -= baOS.size(); + } + output = baOS.toByteArray(); //Keep this in case entities are unable to fit. + } + + if (!entities.isEmpty()) { + try { + for (BlockArrayClipboard.ClipboardEntity entity : entities) { + if (entity.getState() != null && entity.getState().getNbtData() != null) { + CompoundTag data = entity.getState().getNbtData(); + HashMap value = new HashMap<>(data.getValue()); + List pos = new ArrayList<>(3); + pos.add(new DoubleTag(entity.getLocation().getX())); + pos.add(new DoubleTag(entity.getLocation().getX())); + pos.add(new DoubleTag(entity.getLocation().getX())); + value.put("Pos", new ListTag(DoubleTag.class, pos)); + nbtOS.writeTag(new CompoundTag(value)); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + if (baOS.size() > nbtBytesRemaining) { + LOGGER.warn( + "Clipboard file {} does not have enough remaining space to store entity data on disk.", + file.getName() + ); + writeEntitiesSavedCountToHeader(0); + } else { + entitiesFit = true; + writeEntitiesSavedCountToHeader(entities.size()); + } + } + } + + if (output == null) { + return; + } + + if (entitiesFit) { + output = baOS.toByteArray(); + } + + long currentLength = this.braf.length(); + this.braf.setLength(currentLength + baOS.size()); + MappedByteBuffer tempBuffer = fileChannel.map( + FileChannel.MapMode.READ_WRITE, + currentLength, + baOS.size() + ); + tempBuffer.put(output); + tempBuffer.force(); + closeDirectBuffer(tempBuffer); + } else { + writeNBTSavedCountToHeader(0); + writeEntitiesSavedCountToHeader(0); + } + } + @Override public Collection getTileEntities() { return nbtMap.values(); @@ -403,7 +647,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { @Override public BlockState getBlock(int index) { try { - int diskIndex = HEADER_SIZE + (index << 1); + int diskIndex = headerSize + (index << 1); char ordinal = byteBuffer.getChar(diskIndex); return BlockState.getFromOrdinal(ordinal); } catch (IndexOutOfBoundsException ignored) { @@ -431,7 +675,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { @Override public > boolean setBlock(int x, int y, int z, B block) { try { - int index = HEADER_SIZE + (getIndex(x, y, z) << 1); + int index = headerSize + (getIndex(x, y, z) << 1); char ordinal = block.getOrdinalChar(); if (ordinal == 0) { ordinal = 1; @@ -452,7 +696,7 @@ public class DiskOptimizedClipboard extends LinearClipboard { public > boolean setBlock(int i, B block) { try { char ordinal = block.getOrdinalChar(); - int index = HEADER_SIZE + (i << 1); + int index = headerSize + (i << 1); byteBuffer.putChar(index, ordinal); boolean hasNbt = block instanceof BaseBlock && block.hasNbtData(); if (hasNbt) { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweClipboardVersionMismatchException.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweClipboardVersionMismatchException.java index 616361ff2..a23db252d 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweClipboardVersionMismatchException.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweClipboardVersionMismatchException.java @@ -1,11 +1,62 @@ package com.fastasyncworldedit.core.internal.exception; +import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.configuration.Caption; +import com.fastasyncworldedit.core.configuration.Settings; +import com.fastasyncworldedit.core.extent.clipboard.DiskOptimizedClipboard; + +import java.io.File; public class FaweClipboardVersionMismatchException extends FaweException { + private final int expected; + private final int version; + + /** + * @deprecated Use {@link FaweClipboardVersionMismatchException#FaweClipboardVersionMismatchException(int, int)} + */ + @Deprecated(forRemoval = true, since = "TODO") public FaweClipboardVersionMismatchException() { - super(Caption.of("fawe.error.clipboard.on.disk.version.mismatch"), Type.CLIPBOARD); + this(DiskOptimizedClipboard.VERSION, -1); + } + + /** + * New exception specifying a version mismatch between that supported and that loaded. + * + * @param version version of clipboard attempting to be loaded + * @param expected expected version of clipboard + * @since TODO + */ + public FaweClipboardVersionMismatchException(int expected, int version) { + super( + Caption.of( + "fawe.error.clipboard.on.disk.version.mismatch", + expected, + version, + Fawe.platform().getDirectory().getName() + File.separator + Settings.settings().PATHS.CLIPBOARD + ), + Type.CLIPBOARD + ); + this.expected = expected; + this.version = version; + } + + /** + * Get the version specified in the clipboard attempting to be loaded. + * + * @since TODO + */ + public int getClipboardVersion() { + return version; + } + + /** + * Get the version that was expected of the clipboard + * + * @since TODO + */ + public int getExpectedVersion() { + return expected; } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/ByteBufferInputStream.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/ByteBufferInputStream.java new file mode 100644 index 000000000..26f297b42 --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/ByteBufferInputStream.java @@ -0,0 +1,38 @@ +package com.fastasyncworldedit.core.internal.io; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * https://stackoverflow.com/questions/4332264/wrapping-a-bytebuffer-with-an-inputstream + */ +public class ByteBufferInputStream extends InputStream { + + ByteBuffer buf; + + public ByteBufferInputStream(ByteBuffer buf) { + this.buf = buf; + } + + @Override + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + @Override + public int read(@Nonnull byte[] bytes, int off, int len) throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java b/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java index 697adc003..fca025212 100644 --- a/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java +++ b/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java @@ -29,6 +29,7 @@ import java.io.InputStream; import java.util.AbstractMap; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -652,4 +653,46 @@ public final class NBTInputStream implements Closeable { is.close(); } + //FAWE start - Copied from FaweStreamChangeSet + public Iterator toIterator() { + return new Iterator() { + private CompoundTag last = read(); + + public CompoundTag read() { + try { + return (CompoundTag) NBTInputStream.this.readTag(); + } catch (Exception ignored) { + // Assume input is complete + } + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public boolean hasNext() { + return last != null || ((last = read()) != null); + } + + @Override + public CompoundTag next() { + CompoundTag tmp = last; + if (tmp == null) { + tmp = read(); + } + last = null; + return tmp; + } + + @Override + public void remove() { + throw new IllegalArgumentException("CANNOT REMOVE"); + } + }; + } + //FAWE end + } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java index 5e44f2053..781b02ae2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/entity/Player.java @@ -470,13 +470,13 @@ public interface Player extends Entity, Actor { } } catch (EmptyClipboardException ignored) { } - DiskOptimizedClipboard doc = new DiskOptimizedClipboard(file); + DiskOptimizedClipboard doc = DiskOptimizedClipboard.loadFromFile(file); Clipboard clip = doc.toClipboard(); ClipboardHolder holder = new ClipboardHolder(clip); session.setClipboard(holder); } } catch (FaweClipboardVersionMismatchException e) { - print(Caption.of("fawe.error.clipboard.on.disk.version.mismatch")); + print(e.getComponent()); } catch (RuntimeException e) { print(Caption.of("fawe.error.clipboard.invalid")); e.printStackTrace(); diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index 3560b7949..5c180980e 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -126,7 +126,7 @@ "fawe.error.clipboard.invalid": "====== INVALID CLIPBOARD ======", "fawe.error.clipboard.invalid.info": "File: {0} (len: {1})", "fawe.error.clipboard.load.failure": "Could not load clipboard. Possible that the clipboard is still being written to from another server?!", - "fawe.error.clipboard.on.disk.version.mismatch": "Clipboard version mismatch. Please delete your clipboards folder and restart the server.", + "fawe.error.clipboard.on.disk.version.mismatch": "Clipboard version mismatch: expected {0} but got {1}. It is recommended you delete the clipboard folder and restart the server.\nYour clipboard folder is located at {2}.", "fawe.error.limit.disallowed-block": "Your limit disallows use of block '{0}'", "fawe.error.limit.disallowed-property": "Your limit disallows use of property '{0}'", "fawe.error.region-mask-invalid": "Invalid region mask: {0}",