diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAChunk.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAChunk.java index a19306480..116e6c6ba 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAChunk.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAChunk.java @@ -588,7 +588,7 @@ public class MCAChunk implements IChunk { } public boolean isModified() { - return modified; + return modified || deleted; } public boolean isDeleted() { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAFile.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAFile.java index 8e86c1cb6..51bb5a1e5 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAFile.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAFile.java @@ -1,10 +1,11 @@ package com.fastasyncworldedit.core.anvil; +import com.fastasyncworldedit.core.FaweCache; +import com.fastasyncworldedit.core.internal.exception.FaweException; import com.fastasyncworldedit.core.math.FastBitSet; import com.fastasyncworldedit.core.util.task.RunnableVal4; import com.plotsquared.core.util.task.RunnableVal; import com.sk89q.jnbt.NBTInputStream; -import com.sk89q.worldedit.MissingWorldException; import com.sk89q.worldedit.internal.util.LogManagerCompat; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -53,26 +54,71 @@ public class MCAFile implements Closeable, Flushable { private boolean closed = false; private volatile boolean init = false; - public MCAFile(Path file) { - this.file = file; - if (!Files.exists(file)) { - throw new MissingWorldException(); - } - String[] split = file.getFileName().toString().split("\\."); - X = Integer.parseInt(split[1]); - Z = Integer.parseInt(split[2]); - offsetMap = new Int2IntOpenHashMap(); - offsetMap.defaultReturnValue(Integer.MAX_VALUE); - init(); - } - - public MCAFile(int mcrX, int mcrZ, Path file) { + private MCAFile(int mcrX, int mcrZ, Path file) { this.file = file; X = mcrX; Z = mcrZ; offsetMap = new Int2IntOpenHashMap(); offsetMap.defaultReturnValue(Integer.MAX_VALUE); - init(); + } + + /** + * Create a new mca file + * + * @param mcrX region x + * @param mcrZ region z + * @param directory directory to create mca file in + * @return new mca file + * @throws IOException on io error + */ + public static MCAFile create(int mcrX, int mcrZ, Path directory) throws IOException { + Files.createDirectories(directory); + Path file = directory.resolve("r." + mcrX + "." + mcrZ + ".mca"); + Files.createFile(file); + MCAFile mca = new MCAFile(mcrX, mcrZ, file); + mca.initEmpty(); + return mca; + } + + /** + * Load an mca file + * + * @param mcrX region x + * @param mcrZ region z + * @param directory directory to load mca file from + * @return loaded mca file + * @throws IOException on io error + */ + @Nullable + public static MCAFile load(int mcrX, int mcrZ, Path directory) throws IOException { + Path file = directory.resolve("r." + mcrX + "." + mcrZ + ".mca"); + if (!Files.exists(file)) { + return null; + } + MCAFile mca = new MCAFile(mcrX, mcrZ, file); + mca.init(); + return mca; + } + + /** + * Load or create an mca file + * + * @param mcrX region x + * @param mcrZ region z + * @param directory directory to load or create mca file in + * @return mca file + * @throws IOException on io error + */ + public static MCAFile loadOrCreate(int mcrX, int mcrZ, Path directory) throws IOException { + Path file = directory.resolve("r." + mcrX + "." + mcrZ + ".mca"); + MCAFile mca = new MCAFile(mcrX, mcrZ, file); + if (!Files.exists(file)) { + Files.createFile(file); + mca.initEmpty(); + } else { + mca.init(); + } + return mca; } public void clear() { @@ -80,7 +126,7 @@ public class MCAFile implements Closeable, Flushable { try { raf.close(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error clearing MCAFile", e); } } synchronized (chunks) { @@ -94,9 +140,7 @@ public class MCAFile implements Closeable, Flushable { * Set if the file should be delete */ public void setDeleted(boolean deleted) { - if (!init) { - init(); - } + checkInit(); this.deleted = deleted; } @@ -107,49 +151,68 @@ public class MCAFile implements Closeable, Flushable { return deleted; } + protected void checkInit() { + if (!init) { + try { + init(); + } catch (IOException e) { + throw new FaweException("Error initialising MCA file", FaweException.Type.ANVIL_IO, false, e); + } + } + } + /** * Initialises the RandomAccessFile and loads the location header from disk if not done yet */ - public synchronized void init() { - try { - if (raf == null) { - this.offsets = new int[SECTOR_INTS]; - if (file != null) { - this.raf = new RandomAccessFile(file.toFile(), "rw"); - final int nSectors = (int) Math.round(Math.ceil((double) raf.length() / SECTOR_BYTES)); - sectorFree = new FastBitSet(nSectors); - sectorFree.setAll(); - sectorFree.set(0, false); - sectorFree.set(1, false); - if (raf.length() < 8192) { - raf.setLength(8192); - } else { - if ((raf.length() & 0xFFF) != 0) { - raf.setLength(((raf.length() + 0xFFF) >> 12) << 12); - } - raf.seek(0); - for (int i = 0; i < SECTOR_INTS; i++) { - final int offset = raf.readInt(); - offsets[i] = offset; - int sectorStart = offset >> 8; - int numSectors = offset & 0xFF; - if (offset != 0 && sectorStart + numSectors <= sectorFree.size()) { - offsetMap.put(offset, i); - for (int sectorNum = 0; sectorNum < (offset & 0xFF); sectorNum++) { - sectorFree.set((offset >> 8) + sectorNum, false); - } + private synchronized void init() throws IOException { + if (raf == null) { + this.offsets = new int[SECTOR_INTS]; + if (file != null) { + this.raf = new RandomAccessFile(file.toFile(), "rw"); + final int nSectors = (int) Math.round(Math.ceil((double) raf.length() / SECTOR_BYTES)); + sectorFree = new FastBitSet(nSectors); + sectorFree.setAll(); + sectorFree.set(0, false); + sectorFree.set(1, false); + if (raf.length() < 8192) { + raf.setLength(8192); + } else { + if ((raf.length() & 0xFFF) != 0) { + raf.setLength(((raf.length() + 0xFFF) >> 12) << 12); + } + raf.seek(0); + for (int i = 0; i < SECTOR_INTS; i++) { + final int offset = raf.readInt(); + offsets[i] = offset; + int sectorStart = offset >> 8; + int numSectors = offset & 0xFF; + if (offset != 0 && sectorStart + numSectors <= sectorFree.size()) { + offsetMap.put(offset, i); + for (int sectorNum = 0; sectorNum < (offset & 0xFF); sectorNum++) { + sectorFree.set((offset >> 8) + sectorNum, false); } } } } - init = true; - closed = false; } - } catch (Throwable e) { - e.printStackTrace(); + init = true; + closed = false; } } + private synchronized void initEmpty() throws IOException { + this.offsets = new int[SECTOR_INTS]; + this.raf = new RandomAccessFile(file.toFile(), "rw"); + + // Initialises with all zeroes + this.raf.setLength(8192L); + this.raf.seek(0); + this.raf.write(FaweCache.INSTANCE.BYTE_BUFFER_8192.get()); + sectorFree = new FastBitSet(2); + init = true; + closed = false; + } + /** * Get the region file X */ @@ -183,9 +246,7 @@ public class MCAFile implements Closeable, Flushable { */ @Nullable public MCAChunk getCachedChunk(int cx, int cz) { - if (!init) { - init(); - } + checkInit(); short pair = (short) ((cx & 31) + ((cz & 31) << 5)); synchronized (chunks) { return chunks.get(pair); @@ -196,9 +257,7 @@ public class MCAFile implements Closeable, Flushable { * Create a new empty {@link MCAChunk}. */ public MCAChunk newChunk(int cx, int cz) { - if (!init) { - init(); - } + checkInit(); short pair = (short) ((cx & 31) + ((cz & 31) << 5)); MCAChunk chunk; synchronized (chunks) { @@ -211,9 +270,7 @@ public class MCAFile implements Closeable, Flushable { * Insert a {@link MCAChunk} into the cache. */ public void setChunk(MCAChunk chunk) { - if (!init) { - init(); - } + checkInit(); int cx = chunk.getX(); int cz = chunk.getZ(); short pair = (short) ((cx & 31) + ((cz & 31) << 5)); @@ -226,9 +283,7 @@ public class MCAFile implements Closeable, Flushable { * Load data from the mca region into the given {@link MCAChunk}. */ public void loadIntoChunkFromFile(MCAChunk chunk) throws IOException { - if (!init) { - init(); - } + checkInit(); int cx = chunk.getX(); int cz = chunk.getZ(); int i = (cx & 31) + ((cz & 31) << 5); @@ -238,37 +293,13 @@ public class MCAFile implements Closeable, Flushable { chunk.setEmpty(true); return; } - chunk.loadFromNIS(getChunkIS(offset >> 8), false); - if (offset == 0) { - return; - } - if (i < 2) { - int length; - byte version; - byte[] data; - synchronized (this) { - raf.seek((long) (offset >> 8) << 12); - length = raf.readInt(); - version = raf.readByte(); - data = new byte[length - 1]; - raf.read(data); - } - FastByteArrayInputStream bais = new FastByteArrayInputStream(data); - BufferedInputStream bis = switch (version) { - case VERSION_GZIP -> new BufferedInputStream(new GZIPInputStream(bais)); - case VERSION_DEFLATE -> new BufferedInputStream(new InflaterInputStream(bais)); - case VERSION_UNCOMPRESSED -> new BufferedInputStream(bais); - default -> throw new IllegalStateException("Unexpected compression version: " + version); - }; - } + chunk.loadFromNIS(new NBTInputStream(getChunkBIS(offset >> 8)), false); } } @Nonnull - public MCAChunk getChunk(int cx, int cz) throws IOException { - if (!init) { - init(); - } + public MCAChunk getChunk(int cx, int cz) { + checkInit(); MCAChunk cached = getCachedChunk(cx, cz); if (cached != null) { return cached; @@ -277,10 +308,8 @@ public class MCAFile implements Closeable, Flushable { } } - public MCAChunk readChunk(int cx, int cz) throws IOException { - if (!init) { - init(); - } + public MCAChunk readChunk(int cx, int cz) { + checkInit(); int i = (cx & 31) + ((cz & 31) << 5); int offset = offsets[i]; if (offset == 0) { @@ -289,26 +318,8 @@ public class MCAFile implements Closeable, Flushable { try { MCAChunk chunk; synchronized (this) { - chunk = getChunkIS(offset >> 8, cx, cz); - } - if (i < 2) { - int length; - byte version; - byte[] data; - synchronized (this) { - raf.seek((long) (offset >> 8) << 12); - length = raf.readInt(); - version = raf.readByte(); - data = new byte[length - 1]; - raf.read(data); - } - FastByteArrayInputStream bais = new FastByteArrayInputStream(data); - BufferedInputStream bis = switch (version) { - case VERSION_GZIP -> new BufferedInputStream(new GZIPInputStream(bais)); - case VERSION_DEFLATE -> new BufferedInputStream(new InflaterInputStream(bais)); - case VERSION_UNCOMPRESSED -> new BufferedInputStream(bais); - default -> throw new IllegalStateException("Unexpected compression version: " + version); - }; + NBTInputStream nis = new NBTInputStream(getChunkBIS(offset >> 8)); + chunk = new MCAChunk(this, nis, cx, cz, false); } short pair = (short) ((cx & 31) + ((cz & 31) << 5)); synchronized (chunks) { @@ -322,12 +333,10 @@ public class MCAFile implements Closeable, Flushable { } /** - * @param onEach cx, cz, offset, size (in kB) + * @param onEach cx, cz, sector, sectorsAllocated (in kB) */ public void forEachChunk(RunnableVal4 onEach) { - if (!init) { - init(); - } + checkInit(); int i = 0; for (int z = 0; z < 32; z++) { for (int x = 0; x < 32; x++, i += 4) { @@ -341,9 +350,7 @@ public class MCAFile implements Closeable, Flushable { } public void forEachChunk(RunnableVal onEach) { - if (!init) { - init(); - } + checkInit(); int rx = X << 5; int rz = Z << 5; for (int z = 0; z < 32; z++) { @@ -359,60 +366,90 @@ public class MCAFile implements Closeable, Flushable { } } - private NBTInputStream getChunkIS(int offset) throws IOException { - int length = -1; - byte version = -1; - byte[] data; - try { - if (offset == 0) { - return null; - } - synchronized (this) { - raf.seek((long) offset << 12); - length = raf.readInt(); - version = raf.readByte(); - data = new byte[length - 1]; - raf.read(data); - } - FastByteArrayInputStream bais = new FastByteArrayInputStream(data); - BufferedInputStream bis = switch (version) { - case VERSION_GZIP -> new BufferedInputStream(new GZIPInputStream(bais)); - case VERSION_DEFLATE -> new BufferedInputStream(new InflaterInputStream(bais)); - case VERSION_UNCOMPRESSED -> new BufferedInputStream(bais); - default -> throw new IllegalStateException("Unexpected compression version: " + version); - }; - return new NBTInputStream(bis); - } catch (IOException e) { - throw new IOException("Length: " + length + ", version: " + version + ", offset: " + offset, e); - } + @Nullable + protected BufferedInputStream getChunkIS(int cx, int cz) throws IOException { + checkInit(); + int offset = offsets[(cx & 31) + ((cz & 31) << 5)]; + return getChunkBIS(offset); } - private MCAChunk getChunkIS(int offset, int cx, int cz) throws IOException { - int length = -1; - byte version = -1; - byte[] data; - try { - if (offset == 0) { - return null; - } - synchronized (this) { - raf.seek((long) offset << 12); - length = raf.readInt(); - version = raf.readByte(); - data = new byte[length - 1]; - raf.read(data); - } - FastByteArrayInputStream bais = new FastByteArrayInputStream(data); - BufferedInputStream bis = switch (version) { - case VERSION_GZIP -> new BufferedInputStream(new GZIPInputStream(bais)); - case VERSION_DEFLATE -> new BufferedInputStream(new InflaterInputStream(bais)); - case VERSION_UNCOMPRESSED -> new BufferedInputStream(bais); - default -> throw new IllegalStateException("Unexpected compression version: " + version); - }; - return new MCAChunk(this, new NBTInputStream(bis), cx, cz, false); - } catch (Exception e) { - throw new IOException("Length: " + length + ", version: " + version + ", offset: " + offset, e); + @Nullable + private BufferedInputStream getChunkBIS(long offset) throws IOException { + if (offset == 0) { + return null; } + byte version; + byte[] data; + synchronized (this) { + raf.seek(offset << 12); + int length = raf.readInt(); + version = raf.readByte(); + data = new byte[length - 1]; + raf.read(data); + } + FastByteArrayInputStream bais = new FastByteArrayInputStream(data); + return switch (version) { + case VERSION_GZIP -> new BufferedInputStream(new GZIPInputStream(bais)); + case VERSION_DEFLATE -> new BufferedInputStream(new InflaterInputStream(bais)); + case VERSION_UNCOMPRESSED -> new BufferedInputStream(bais); + default -> throw new IllegalStateException("Unexpected compression version: " + version); + }; + } + + public byte[] getFullChunkBytes(int cx, int cz) throws IOException { + checkInit(); + int offset = offsets[(cx & 31) + ((cz & 31) << 5)]; + int sectorNumber = offset >> 8; + int sectorsAllocated = offset & 0xFF; + byte[] data = new byte[sectorsAllocated << 12]; + raf.seek((long) sectorNumber << 12); + raf.read(data); + return data; + } + + /** + * Writes chunk bytes directly to the MCAFile and returns the next sector after the chunk + * + * @param cx chunk x + * @param cz chunk z + * @param sectorNumber sector to write chunk from + * @param bytes byte to write + * @return next sector after the chunk + * @throws IOException on IO error + */ + public synchronized int writeFullChunkBytes(int cx, int cz, int sectorNumber, byte[] bytes) throws IOException { + checkInit(); + int allocate = (bytes.length >> 12) + 1; + int offset = (sectorNumber << 8) | (allocate & 0xFF); + setOffset(cx, cz, offset); + raf.seek((long) sectorNumber << 12); + raf.write(bytes); + int newSector = sectorNumber + allocate; + sectorFree.expandTo(newSector, false); + return newSector; + } + + public synchronized void replaceChunkBytes(int cx, int cz, byte[] bytes) throws IOException { + checkInit(); + int currOffset = offsets[(cx & 31) + ((cz & 31) << 5)]; + int allocate = (bytes.length >> 12) + 1; + int sectorNumber = sectorFree.size() + 1; + if (currOffset != 0) { + int currSector = currOffset >> 8; + int currAllocate = currOffset & 0xFF; + sectorFree.setRange(currSector, currSector + currAllocate); + if (allocate <= currAllocate) { + sectorNumber = currSector; + sectorFree.clearRange(currSector, currSector + allocate); + } + System.out.println(sectorNumber); + } + int offset = (sectorNumber << 8) | (allocate & 0xFF); + setOffset(cx, cz, offset); + raf.seek((long) sectorNumber << 12); + raf.write(bytes); + int newSector = sectorNumber + allocate; + sectorFree.expandTo(newSector, false); } public List getCachedChunks() { @@ -428,16 +465,17 @@ public class MCAFile implements Closeable, Flushable { } } - @Override - public synchronized void close() throws IOException { + public synchronized void close(boolean flush) throws IOException { if (raf == null || closed) { return; } - flush(); + if (flush) { + flush(); + } try { raf.close(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error closing MCAFile", e); } raf = null; offsets = null; @@ -446,6 +484,11 @@ public class MCAFile implements Closeable, Flushable { init = false; } + @Override + public synchronized void close() throws IOException { + close(true); + } + public boolean isModified() { if (isDeleted()) { return true; @@ -453,7 +496,7 @@ public class MCAFile implements Closeable, Flushable { synchronized (chunks) { for (Int2ObjectMap.Entry entry : chunks.int2ObjectEntrySet()) { MCAChunk chunk = entry.getValue(); - if (chunk.isModified() || chunk.isDeleted()) { + if (chunk.isModified()) { return true; } } @@ -461,15 +504,19 @@ public class MCAFile implements Closeable, Flushable { return false; } - public synchronized void setOffset(final int x, final int z, final int offset) + public int getOffset(int cx, int cz) { + return offsets[cx + (cz << 5)]; + } + + public synchronized void setOffset(int cx, int cz, final int offset) throws IOException { - int i = (x & 31) + ((z & 31) << 5); + int i = (cx & 31) + ((cz & 31) << 5); if (offset == 0) { offsetMap.remove(offsets[i]); } else { offsetMap.put(offset, i); } - offsets[x + (z << 5)] = offset; + offsets[cx + (cz << 5)] = offset; raf.seek((long) i << 2); raf.writeInt(offset); } @@ -479,6 +526,7 @@ public class MCAFile implements Closeable, Flushable { */ @Override public synchronized void flush() throws IOException { + System.out.println(sectorFree); boolean delete = true; int currentSector = 2; Queue offsets = @@ -577,11 +625,12 @@ public class MCAFile implements Closeable, Flushable { int size = 0; for (int i = sectorFree.size(); i > 0; i--) { if (!sectorFree.get(i)) { - size = i + 1; + size = i + 1; // + 1 to fully encompass break; } } - raf.setLength((long) (size + 1) * SECTOR_BYTES); + System.out.println(sectorFree); + raf.setLength((long) size << 12); if (delete || size < 3) { clear(); Files.delete(file); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAWorld.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAWorld.java index f06b15182..70f4e0791 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAWorld.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/anvil/MCAWorld.java @@ -1,6 +1,7 @@ package com.fastasyncworldedit.core.anvil; import com.fastasyncworldedit.core.Fawe; +import com.fastasyncworldedit.core.internal.exception.FaweException; import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.implementation.packet.ChunkPacket; import com.fastasyncworldedit.core.util.MathMan; @@ -64,7 +65,9 @@ public class MCAWorld extends AbstractWorld { * @param folder World file/folder */ public static synchronized MCAWorld of(String name, Path folder) { - if (Fawe.platform().isWorldLoaded(name)) { + if (folder.getParent().toAbsolutePath().equals(Fawe.platform().getWorldsFolder().toAbsolutePath()) && Fawe + .platform() + .isWorldLoaded(name)) { throw new IllegalStateException("World " + name + " is loaded. Anvil operations cannot be completed on a loaded world."); } // World could be the same name but in a different folder @@ -78,10 +81,18 @@ public class MCAWorld extends AbstractWorld { short regionX = Short.parseShort(split[1]); short regionZ = Short.parseShort(split[2]); int paired = MathMan.pair(regionX, regionZ); - mcaFileCache.computeIfAbsent( - paired, - (i) -> new MCAFile(regionX, regionZ, regionFolder.resolve("r." + regionX + "." + regionZ + ".mca")) - ); + mcaFileCache.computeIfAbsent(paired, (i) -> { + try { + return MCAFile.load(regionX, regionZ, regionFolder); + } catch (IOException e) { + throw new FaweException( + "Error loading MCA file: `" + file.getFileName() + "`", + FaweException.Type.ANVIL_IO, + false, + e + ); + } + }); }); return mcaFileCache.values(); } @@ -95,6 +106,29 @@ public class MCAWorld extends AbstractWorld { } } + /** + * Get or create MCA file for the given region coordinates + * + * @param mcrX x region coordinate + * @param mcrZ z region coordinate + * @return mca file + */ + public MCAFile getOrCreateMCA(int mcrX, int mcrZ) { + int pair = MathMan.pair((short) mcrX, (short) mcrZ); + return mcaFileCache.computeIfAbsent(pair, (i) -> { + try { + return MCAFile.loadOrCreate(mcrX, mcrZ, regionFolder); + } catch (IOException e) { + throw new FaweException( + "Error loading MCA file " + mcrX + "," + mcrZ + " in folder " + regionFolder, + FaweException.Type.ANVIL_IO, + false, + e + ); + } + }); + } + @Override public boolean setTile(final int x, final int y, final int z, final CompoundTag tile) throws WorldEditException { return false; @@ -114,6 +148,10 @@ public class MCAWorld extends AbstractWorld { return folder; } + public Path getRegionFolder() { + return regionFolder; + } + @Override public > boolean setBlock( final BlockVector3 position, @@ -173,14 +211,20 @@ public class MCAWorld extends AbstractWorld { int paired = MathMan.pair(regionX, regionZ); MCAFile mca = mcaFileCache.computeIfAbsent( paired, - (i) -> new MCAFile(regionX, regionZ, regionFolder.resolve("r." + regionX + "." + regionZ + ".mca")) + (i) -> { + try { + return MCAFile.loadOrCreate(regionX, regionZ, regionFolder); + } catch (IOException e) { + throw new FaweException( + "Error loading MCA file " + regionX + "," + regionZ + " in folder " + regionFolder, + FaweException.Type.ANVIL_IO, + false, + e + ); + } + } ); - try { - return mca.getChunk(chunkX, chunkZ); - } catch (IOException e) { - LOGGER.error("Error loading chunk. Creating empty chunk.", e); - return mca.newChunk(chunkX, chunkZ); - } + return mca.getChunk(chunkX, chunkZ); } @Override @@ -192,11 +236,31 @@ public class MCAWorld extends AbstractWorld { public synchronized void flush() { for (MCAFile mca : mcaFileCache.values()) { try { - mca.close(); + mca.flush(); } catch (IOException e) { - LOGGER.error("Could not flush MCAFile {}", mca.getFile().getFileName(), e); + LOGGER.error("Could not close MCAFile {}", mca.getFile().getFileName(), e); } } } + public synchronized void close(boolean flush) { + for (MCAFile mca : mcaFileCache.values()) { + try { + mca.close(flush); + } catch (IOException e) { + LOGGER.error("Could not close MCAFile {}", mca.getFile().getFileName(), e); + } + } + } + + /** + * Create a new MCA file for the given region x,z + * + * @param rX region X + * @param rZ region Z + */ + public synchronized MCAFile createMCA(int rX, int rZ) throws IOException { + return MCAFile.create(rX, rZ, regionFolder); + } + } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/LimitExtent.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/LimitExtent.java index 983a280fa..8662a601e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/LimitExtent.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/LimitExtent.java @@ -97,7 +97,7 @@ public final class LimitExtent extends AbstractDelegateExtent implements IBatchP } private void handleException(FaweException e) { - if (e.ignorable() || !limit.MAX_FAILS()) { + if (!e.ignorable() || !limit.MAX_FAILS()) { throw e; } if (!faweExceptionReasonsUsed[e.getType().ordinal()]) { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweException.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweException.java index 7a71b831b..3f257ec03 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweException.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/exception/FaweException.java @@ -23,6 +23,26 @@ public class FaweException extends RuntimeException { this(TextComponent.of(reason)); } + /** + * New instance. Defaults to {@link FaweException.Type#OTHER}. + */ + public FaweException(Component reason, Type type, boolean ignorable, Exception e) { + super(e); + this.message = reason; + this.type = type; + this.ignorable = ignorable; + } + + /** + * New instance. Defaults to {@link FaweException.Type#OTHER}. + */ + public FaweException(String reason, Type type, boolean ignorable, Exception e) { + super(e); + this.message = TextComponent.of(reason); + this.type = type; + this.ignorable = ignorable; + } + /** * New instance. Defaults to {@link FaweException.Type#OTHER}. */ @@ -114,6 +134,7 @@ public class FaweException extends RuntimeException { ACTOR_REQUIRED, CLIPBOARD, HISTORY, + ANVIL_IO, OTHER } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/BufferedRandomAccessFile.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/BufferedRandomAccessFile.java deleted file mode 100644 index b5b26f56b..000000000 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/BufferedRandomAccessFile.java +++ /dev/null @@ -1,434 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.fastasyncworldedit.core.internal.io; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Arrays; - - -/** - * A BufferedRandomAccessFile is like a - * RandomAccessFile, but it uses a private buffer so that most - * operations do not require a disk access. - *

- * - * Note: The operations on this class are unmonitored. Also, the correct - * functioning of the RandomAccessFile methods that are not - * overridden here relies on the implementation of those methods in the - * superclass. - * Author : Avinash Lakshman ( alakshman@facebook.com) & Prashant Malik ( pmalik@facebook.com ) - */ - -public class BufferedRandomAccessFile extends RandomAccessFile { - static final int LogBuffSz_ = 16; // 64K buffer - public static final int BuffSz_ = (1 << LogBuffSz_); - static final long BuffMask_ = ~(((long) BuffSz_) - 1L); - - /* - * This implementation is based on the buffer implementation in Modula-3's - * "Rd", "Wr", "RdClass", and "WrClass" interfaces. - */ - private boolean dirty_; // true iff unflushed bytes exist - private boolean closed_; // true iff the file is closed - private long curr_; // current position in file - private long lo_, hi_; // bounds on characters in "buff" - private byte[] buff_; // local buffer - private long maxHi_; // this.lo + this.buff.length - private boolean hitEOF_; // buffer contains last file block? - private long diskPos_; // disk position - - /* - * To describe the above fields, we introduce the following abstractions for - * the file "f": - * - * len(f) the length of the file curr(f) the current position in the file - * c(f) the abstract contents of the file disk(f) the contents of f's - * backing disk file closed(f) true iff the file is closed - * - * "curr(f)" is an index in the closed interval [0, len(f)]. "c(f)" is a - * character sequence of length "len(f)". "c(f)" and "disk(f)" may differ if - * "c(f)" contains unflushed writes not reflected in "disk(f)". The flush - * operation has the effect of making "disk(f)" identical to "c(f)". - * - * A file is said to be *valid* if the following conditions hold: - * - * V1. The "closed" and "curr" fields are correct: - * - * f.closed == closed(f) f.curr == curr(f) - * - * V2. The current position is either contained in the buffer, or just past - * the buffer: - * - * f.lo <= f.curr <= f.hi - * - * V3. Any (possibly) unflushed characters are stored in "f.buff": - * - * (forall i in [f.lo, f.curr): c(f)[i] == f.buff[i - f.lo]) - * - * V4. For all characters not covered by V3, c(f) and disk(f) agree: - * - * (forall i in [f.lo, len(f)): i not in [f.lo, f.curr) => c(f)[i] == - * disk(f)[i]) - * - * V5. "f.dirty" is true iff the buffer contains bytes that should be - * flushed to the file; by V3 and V4, only part of the buffer can be dirty. - * - * f.dirty == (exists i in [f.lo, f.curr): c(f)[i] != f.buff[i - f.lo]) - * - * V6. this.maxHi == this.lo + this.buff.length - * - * Note that "f.buff" can be "null" in a valid file, since the range of - * characters in V3 is empty when "f.lo == f.curr". - * - * A file is said to be *ready* if the buffer contains the current position, - * i.e., when: - * - * R1. !f.closed && f.buff != null && f.lo <= f.curr && f.curr < f.hi - * - * When a file is ready, reading or writing a single byte can be performed - * by reading or writing the in-memory buffer without performing a disk - * operation. - */ - - /** - * Open a new BufferedRandomAccessFile on file - * in mode mode, which should be "r" for reading only, or - * "rw" for reading and writing. - */ - public BufferedRandomAccessFile(File file, String mode) throws IOException { - super(file, mode); - this.init(0); - } - - public BufferedRandomAccessFile(File file, String mode, int size) throws IOException { - super(file, mode); - this.init(size); - } - - /** - * Open a new BufferedRandomAccessFile on the file named - * name in mode mode, which should be "r" for - * reading only, or "rw" for reading and writing. - */ - public BufferedRandomAccessFile(String name, String mode) throws IOException { - super(name, mode); - this.init(0); - } - - public BufferedRandomAccessFile(String name, String mode, int size) throws FileNotFoundException { - super(name, mode); - this.init(size); - } - - public BufferedRandomAccessFile(File file, String mode, byte[] buf) throws FileNotFoundException { - super(file, mode); - this.dirty_ = this.closed_ = false; - this.lo_ = this.curr_ = this.hi_ = 0; - this.buff_ = buf; - this.maxHi_ = (long) BuffSz_; - this.hitEOF_ = false; - this.diskPos_ = 0L; - } - - private void init(int size) { - this.dirty_ = this.closed_ = false; - this.lo_ = this.curr_ = this.hi_ = 0; - this.buff_ = (size > BuffSz_) ? new byte[size] : new byte[BuffSz_]; - this.maxHi_ = (long) BuffSz_; - this.hitEOF_ = false; - this.diskPos_ = 0L; - } - - @Override - public void close() throws IOException { - this.flush(); - this.closed_ = true; - super.close(); - } - - /** - * Flush any bytes in the file's buffer that have not yet been written to - * disk. If the file was created read-only, this method is a no-op. - */ - public void flush() throws IOException { - this.flushBuffer(); - } - - /* Flush any dirty bytes in the buffer to disk. */ - private void flushBuffer() throws IOException { - if (this.dirty_) { - if (this.diskPos_ != this.lo_) - super.seek(this.lo_); - int len = (int) (this.curr_ - this.lo_); - super.write(this.buff_, 0, len); - this.diskPos_ = this.curr_; - this.dirty_ = false; - } - } - - /* - * Read at most "this.buff.length" bytes into "this.buff", returning the - * number of bytes read. If the return result is less than - * "this.buff.length", then EOF was read. - */ - private int fillBuffer() throws IOException { - int cnt = 0; - int rem = this.buff_.length; - while (rem > 0) { - int n = super.read(this.buff_, cnt, rem); - if (n < 0) - break; - cnt += n; - rem -= n; - } - if ((cnt < 0) && (this.hitEOF_ = (cnt < this.buff_.length))) { - // make sure buffer that wasn't read is initialized with -1 - Arrays.fill(this.buff_, cnt, this.buff_.length, (byte) 0xff); - } - this.diskPos_ += cnt; - return cnt; - } - - /* - * This method positions this.curr at position pos. - * If pos does not fall in the current buffer, it flushes the - * current buffer and loads the correct one.

- * - * On exit from this routine this.curr == this.hi iff pos - * is at or past the end-of-file, which can only happen if the file was - * opened in read-only mode. - */ - @Override - public void seek(long pos) throws IOException { - if (pos >= this.hi_ || pos < this.lo_) { - // seeking outside of current buffer -- flush and read - this.flushBuffer(); - this.lo_ = pos & BuffMask_; // start at BuffSz boundary - this.maxHi_ = this.lo_ + (long) this.buff_.length; - if (this.diskPos_ != this.lo_) { - super.seek(this.lo_); - this.diskPos_ = this.lo_; - } - int n = this.fillBuffer(); - this.hi_ = this.lo_ + (long) n; - } else { - // seeking inside current buffer -- no read required - if (pos < this.curr_) { - // if seeking backwards, we must flush to maintain V4 - this.flushBuffer(); - } - } - this.curr_ = pos; - } - - /* - * Does not maintain V4 (i.e. buffer differs from disk contents if previously written to) - * - Assumes no writes were made - * @param pos - * @throws IOException - */ - public void seekUnsafe(long pos) throws IOException { - if (pos >= this.hi_ || pos < this.lo_) { - // seeking outside of current buffer -- flush and read - this.flushBuffer(); - this.lo_ = pos & BuffMask_; // start at BuffSz boundary - this.maxHi_ = this.lo_ + (long) this.buff_.length; - if (this.diskPos_ != this.lo_) { - super.seek(this.lo_); - this.diskPos_ = this.lo_; - } - int n = this.fillBuffer(); - this.hi_ = this.lo_ + (long) n; - } - this.curr_ = pos; - } - - @Override - public long getFilePointer() { - return this.curr_; - } - - @Override - public long length() throws IOException { - return Math.max(this.curr_, super.length()); - } - - @Override - public int read() throws IOException { - if (this.curr_ >= this.hi_) { - // test for EOF - // if (this.hi < this.maxHi) return -1; - if (this.hitEOF_) - return -1; - - // slow path -- read another buffer - this.seek(this.curr_); - if (this.curr_ == this.hi_) - return -1; - } - byte res = this.buff_[(int) (this.curr_ - this.lo_)]; - this.curr_++; - return ((int) res) & 0xFF; // convert byte -> int - } - - public byte read1() throws IOException { - if (this.curr_ >= this.hi_) { - // test for EOF - // if (this.hi < this.maxHi) return -1; - if (this.hitEOF_) - return -1; - - // slow path -- read another buffer - this.seek(this.curr_); - if (this.curr_ == this.hi_) - return -1; - } - byte res = this.buff_[(int) (this.curr_ - this.lo_)]; - this.curr_++; - return res; - } - - @Override - public int read(byte[] b) throws IOException { - return this.read(b, 0, b.length); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (this.curr_ >= this.hi_) { - // test for EOF - // if (this.hi < this.maxHi) return -1; - if (this.hitEOF_) - return -1; - - // slow path -- read another buffer - this.seek(this.curr_); - if (this.curr_ == this.hi_) - return -1; - } - len = Math.min(len, (int) (this.hi_ - this.curr_)); - int buffOff = (int) (this.curr_ - this.lo_); - System.arraycopy(this.buff_, buffOff, b, off, len); - this.curr_ += len; - return len; - } - - public byte readCurrent() throws IOException { - if (this.curr_ >= this.hi_) { - // test for EOF - // if (this.hi < this.maxHi) return -1; - if (this.hitEOF_) - return -1; - - // slow path -- read another buffer - this.seek(this.curr_); - if (this.curr_ == this.hi_) - return -1; - } - byte res = this.buff_[(int) (this.curr_ - this.lo_)]; - return res; - } - - public void writeCurrent(byte b) throws IOException { - if (this.curr_ >= this.hi_) { - if (this.hitEOF_ && this.hi_ < this.maxHi_) { - // at EOF -- bump "hi" - this.hi_++; - } else { - // slow path -- write current buffer; read next one - this.seek(this.curr_); - if (this.curr_ == this.hi_) { - // appending to EOF -- bump "hi" - this.hi_++; - } - } - } - this.buff_[(int) (this.curr_ - this.lo_)] = (byte) b; - this.dirty_ = true; - } - - public void writeUnsafe(int b) throws IOException { - this.buff_[(int) (this.curr_ - this.lo_)] = (byte) b; - this.curr_++; - this.dirty_ = true; - } - - @Override - public void write(int b) throws IOException { - if (this.curr_ >= this.hi_) { - if (this.hitEOF_ && this.hi_ < this.maxHi_) { - // at EOF -- bump "hi" - this.hi_++; - } else { - // slow path -- write current buffer; read next one - this.seek(this.curr_); - if (this.curr_ == this.hi_) { - // appending to EOF -- bump "hi" - this.hi_++; - } - } - } - this.buff_[(int) (this.curr_ - this.lo_)] = (byte) b; - this.curr_++; - this.dirty_ = true; - } - - @Override - public void write(byte[] b) throws IOException { - this.write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - while (len > 0) { - int n = this.writeAtMost(b, off, len); - off += n; - len -= n; - this.dirty_ = true; - } - } - - /* - * Write at most "len" bytes to "b" starting at position "off", and return - * the number of bytes written. - */ - private int writeAtMost(byte[] b, int off, int len) throws IOException { - if (this.curr_ >= this.hi_) { - if (this.hitEOF_ && this.hi_ < this.maxHi_) { - // at EOF -- bump "hi" - this.hi_ = this.maxHi_; - } else { - // slow path -- write current buffer; read next one - this.seek(this.curr_); - if (this.curr_ == this.hi_) { - // appending to EOF -- bump "hi" - this.hi_ = this.maxHi_; - } - } - } - len = Math.min(len, (int) (this.hi_ - this.curr_)); - int buffOff = (int) (this.curr_ - this.lo_); - System.arraycopy(b, off, this.buff_, buffOff, len); - this.curr_ += len; - return len; - } -} diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java index e163330e2..323f258cc 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java @@ -95,10 +95,9 @@ public class FastBitSet { } public void expandTo(int newSize, boolean value) { - //System.out.println(newSize); int newLength = (newSize + 64) >> 6; if (newLength <= this.bits.length) { - if (this.size > newSize) { + if (this.size < newSize) { this.size = newSize; } return; diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index 8f04af855..7caf0901f 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -31,8 +31,11 @@ "fawe.worldedit.paste.command.paste": "The clipboard has been pasted at {0}", "fawe.worldedit.history.command.undo.disabled": "Undo disabled, use: //fast", "fawe.worldedit.selection.selection.count": "Counted {0} blocks.", - "fawe.worldedit.anvil.world.is.loaded": "The world shouldn't be in use when executing. Unload the world, or use -f to override (save first)", + "fawe.worldedit.anvil.error.mca.init.failed": "Failed to initialise MCA file.", + "fawe.worldedit.anvil.error.world.loaded": "The world shouldn't be in use when executing. Unload the world, or use -f to override (save first)", "fawe.worldedit.anvil.replaceall.complete": "Replaceall with anvil completed.", + "fawe.worldedit.anvil.replace.complete": "Replace with anvil completed.", + "fawe.worldedit.anvil.set.complete": "Set with anvil completed.", "fawe.worldedit.brush.brush.reset": "Reset your brush. (SHIFT + Click)", "fawe.worldedit.brush.brush.none": "You aren't holding a brush!", "fawe.worldedit.brush.brush.scroll.action.set": "Set scroll action to {0}",