From 57696f25f45742f0026b978369bcddf22b439cea Mon Sep 17 00:00:00 2001 From: Jesse Boyd Date: Tue, 26 Apr 2016 04:14:41 +1000 Subject: [PATCH] Finish clipboard on disk --- README.md | 27 +- core/src/main/java/com/boydti/fawe/Fawe.java | 3 + .../main/java/com/boydti/fawe/config/BBC.java | 4 + .../java/com/boydti/fawe/config/Settings.java | 4 +- .../fawe/object/BufferedRandomAccessFile.java | 373 ++++++++++++++++++ .../com/boydti/fawe/object/FawePlayer.java | 68 +++- .../clipboard/DiskOptimizedClipboard.java | 148 ++++++- .../fawe/object/clipboard/FaweClipboard.java | 15 +- .../clipboard/MemoryOptimizedClipboard.java | 11 +- .../java/com/boydti/fawe/util/MainUtil.java | 18 + .../com/sk89q/worldedit/CuboidClipboard.java | 5 +- .../com/sk89q/worldedit/LocalSession.java | 25 +- .../worldedit/command/ClipboardCommands.java | 263 ++++++++++++ .../worldedit/command/SchematicCommands.java | 11 +- .../extent/clipboard/BlockArrayClipboard.java | 28 +- .../extent/clipboard/io/SchematicReader.java | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 6 + 18 files changed, 952 insertions(+), 64 deletions(-) create mode 100644 core/src/main/java/com/boydti/fawe/object/BufferedRandomAccessFile.java create mode 100644 core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/README.md b/README.md index d690f08c..43a40a81 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,23 @@ -# Overview -Optimize worldedit -# Main resource page -https://www.spigotmc.org/resources/13932/ \ No newline at end of file +# About +FAWE is an addon for WorldEdit that has huge speed and memory improvements as well as a few extra features. + +# Spigot page +https://www.spigotmc.org/resources/13932/ + +# IRC +http://webchat.esper.net/?nick=&channels=IntellectualCrafters + +# Releases: +https://github.com/boy0001/FastAsyncWorldedit/releases + +# Building +> FAWE uses gradle to build + +gradlew setupDecompWorkspace +gradlew build + +# Contributing +Have an idea for an optimization, or a cool feature? + - I'll accept most PR's + - Let me know what you've tested / what may need further testing + - If you need any help, create a ticket or discuss on IRC \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/Fawe.java b/core/src/main/java/com/boydti/fawe/Fawe.java index 44b70f8f..7da5ce81 100644 --- a/core/src/main/java/com/boydti/fawe/Fawe.java +++ b/core/src/main/java/com/boydti/fawe/Fawe.java @@ -22,6 +22,7 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.command.ClipboardCommands; import com.sk89q.worldedit.command.SchematicCommands; import com.sk89q.worldedit.command.ScriptingCommands; import com.sk89q.worldedit.extension.platform.CommandManager; @@ -155,6 +156,7 @@ public class Fawe { */ this.setupConfigs(); MainUtil.deleteOlder(new File(IMP.getDirectory(), "history"), TimeUnit.DAYS.toMillis(Settings.DELETE_HISTORY_AFTER_DAYS)); + MainUtil.deleteOlder(new File(IMP.getDirectory(), "clipboard"), TimeUnit.DAYS.toMillis(Settings.DELETE_CLIPBOARD_AFTER_DAYS)); TaskManager.IMP = this.IMP.getTaskManager(); if (Settings.METRICS) { @@ -221,6 +223,7 @@ public class Fawe { private void setupInjector() { EditSession.inject(); Operations.inject(); + ClipboardCommands.inject(); SchematicCommands.inject(); ScriptingCommands.inject(); BreadthFirstSearch.inject(); diff --git a/core/src/main/java/com/boydti/fawe/config/BBC.java b/core/src/main/java/com/boydti/fawe/config/BBC.java index 48e4bce7..afe1eb5d 100644 --- a/core/src/main/java/com/boydti/fawe/config/BBC.java +++ b/core/src/main/java/com/boydti/fawe/config/BBC.java @@ -51,6 +51,10 @@ public enum BBC { WORLDEDIT_CANCEL_REASON_MAX_FAILS("Outside allowed region", "Cancel"), WORLDEDIT_FAILED_LOAD_CHUNK("&cSkipped loading chunk: &7%s0;%s1&c. Try increasing chunk-wait.", "Cancel"), + LOADING_CLIPBOARD("&dLoading clipboard from disk, please wait.", "History"), + INDEXING_HISTORY("&dIndexing %s history objects on disk, please wait.", "History"), + INDEXING_COMPLETE("&dIndexing complete. Took: %s seconds!", "History"), + WORLDEDIT_OOM_ADMIN("&cPossible options:\n&8 - &7//fast\n&8 - &7Do smaller edits\n&8 - &7Allocate more memory\n&8 - &7Disable this safeguard", "Info"), NOT_PLAYER("&cYou must be a player to perform this action!", "Error"), COMPRESSED("History compressed. Saved ~ %s0b (%s1x smaller)", "Info"), diff --git a/core/src/main/java/com/boydti/fawe/config/Settings.java b/core/src/main/java/com/boydti/fawe/config/Settings.java index 4d5d88cc..0dca8739 100644 --- a/core/src/main/java/com/boydti/fawe/config/Settings.java +++ b/core/src/main/java/com/boydti/fawe/config/Settings.java @@ -24,6 +24,7 @@ public class Settings { public static boolean STORE_CLIPBOARD_ON_DISK = false; public static int DELETE_HISTORY_AFTER_DAYS = 7; + public static int DELETE_CLIPBOARD_AFTER_DAYS = 0; public static int COMPRESSION_LEVEL = 0; public static int BUFFER_SIZE = 531441; public static boolean METRICS = true; @@ -86,6 +87,7 @@ public class Settings { options.put("lighting.fix-all", FIX_ALL_LIGHTING); options.put("lighting.async", ASYNC_LIGHTING); options.put("clipboard.use-disk", STORE_CLIPBOARD_ON_DISK); + options.put("clipboard.delete-after-days", DELETE_CLIPBOARD_AFTER_DAYS); options.put("history.use-disk", STORE_HISTORY_ON_DISK); options.put("history.compress", false); options.put("history.chunk-wait-ms", CHUNK_WAIT); @@ -136,7 +138,7 @@ public class Settings { ALLOWED_3RDPARTY_EXTENTS = config.getStringList("extent.allowed-plugins"); EXTENT_DEBUG = config.getBoolean("extent.debug"); STORE_CLIPBOARD_ON_DISK = config.getBoolean("clipboard.use-disk"); - + DELETE_CLIPBOARD_AFTER_DAYS = config.getInt("clipboard.delete-after-days"); if (STORE_HISTORY_ON_DISK = config.getBoolean("history.use-disk")) { LocalSession.MAX_HISTORY_SIZE = Integer.MAX_VALUE; } diff --git a/core/src/main/java/com/boydti/fawe/object/BufferedRandomAccessFile.java b/core/src/main/java/com/boydti/fawe/object/BufferedRandomAccessFile.java new file mode 100644 index 00000000..97414c5a --- /dev/null +++ b/core/src/main/java/com/boydti/fawe/object/BufferedRandomAccessFile.java @@ -0,0 +1,373 @@ +/** + * 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.boydti.fawe.object; + +import java.io.*; +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 final 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); + } + + 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; + } + + 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. + */ + 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; + } + + public long getFilePointer() + { + return this.curr_; + } + + public long length() throws IOException + { + return Math.max(this.curr_, super.length()); + } + + 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 int read(byte[] b) throws IOException + { + return this.read(b, 0, b.length); + } + + 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 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; + } + + public void write(byte[] b) throws IOException + { + this.write(b, 0, b.length); + } + + 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; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/boydti/fawe/object/FawePlayer.java b/core/src/main/java/com/boydti/fawe/object/FawePlayer.java index fa678dea..c7d5b31b 100644 --- a/core/src/main/java/com/boydti/fawe/object/FawePlayer.java +++ b/core/src/main/java/com/boydti/fawe/object/FawePlayer.java @@ -5,6 +5,7 @@ import com.boydti.fawe.FaweAPI; import com.boydti.fawe.config.BBC; import com.boydti.fawe.config.Settings; import com.boydti.fawe.object.changeset.DiskStorageHistory; +import com.boydti.fawe.object.clipboard.DiskOptimizedClipboard; import com.boydti.fawe.util.TaskManager; import com.boydti.fawe.util.WEManager; import com.boydti.fawe.wrappers.PlayerWrapper; @@ -13,10 +14,13 @@ import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.regions.RegionSelector; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; +import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.world.World; +import com.sk89q.worldedit.world.registry.WorldData; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; @@ -95,12 +99,33 @@ public abstract class FawePlayer { loadSessionsFromDisk(world); } } + loadClipboardFromDisk(); } catch (Exception e) { e.printStackTrace(); Fawe.debug("Failed to load history for: " + getName()); } } + public void loadClipboardFromDisk() { + try { + File file = new File(Fawe.imp().getDirectory(), "clipboard" + File.separator + getUUID()); + if (file.exists()) { + DiskOptimizedClipboard doc = new DiskOptimizedClipboard(file); + Player player = getPlayer(); + LocalSession session = getSession(); + if (player != null && session != null) { + sendMessage("&d" + BBC.PREFIX.s() + " " + BBC.LOADING_CLIPBOARD.s()); + WorldData worldData = player.getWorld().getWorldData(); + Clipboard clip = doc.toClipboard(); + ClipboardHolder holder = new ClipboardHolder(clip, worldData); + getSession().setClipboard(holder); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + /** * Get the current World * @return @@ -118,35 +143,38 @@ public abstract class FawePlayer { if (world == null) { return; } - TaskManager.IMP.async(new Runnable() { - @Override - public void run() { - UUID uuid = getUUID(); - List editIds = new ArrayList<>(); - File folder = new File(Fawe.imp().getDirectory(), "history" + File.separator + world.getName() + File.separator + uuid); - if (folder.isDirectory()) { - for (File file : folder.listFiles()) { - if (file.getName().endsWith(".bd")) { - int index = Integer.parseInt(file.getName().split("\\.")[0]); - editIds.add(index); - } - } + final long start = System.currentTimeMillis(); + final UUID uuid = getUUID(); + final List editIds = new ArrayList<>(); + final File folder = new File(Fawe.imp().getDirectory(), "history" + File.separator + world.getName() + File.separator + uuid); + if (folder.isDirectory()) { + for (File file : folder.listFiles()) { + if (file.getName().endsWith(".bd")) { + int index = Integer.parseInt(file.getName().split("\\.")[0]); + editIds.add(index); } - Collections.sort(editIds); - if (editIds.size() > 0) { - Fawe.debug(BBC.PREFIX.s() + " Indexing " + editIds.size() + " history objects for " + getName()); - for (int index : editIds) { + } + } + if (editIds.size() > 0) { + sendMessage("&d" + BBC.PREFIX.s() + " " + BBC.INDEXING_HISTORY.format(editIds.size())); + TaskManager.IMP.async(new Runnable() { + @Override + public void run() { + Collections.sort(editIds); + for (int i = editIds.size() - 1; i >= 0; i--) { + int index = editIds.get(i); DiskStorageHistory set = new DiskStorageHistory(world, uuid, index); EditSession edit = set.toEditSession(getPlayer()); if (world.equals(getWorld())) { - session.remember(edit); + session.remember(edit, 0); } else { return; } } + sendMessage("&d" + BBC.PREFIX.s() + " " + BBC.INDEXING_COMPLETE.format((System.currentTimeMillis() - start) / 1000d)); } - } - }); + }); + } } /** diff --git a/core/src/main/java/com/boydti/fawe/object/clipboard/DiskOptimizedClipboard.java b/core/src/main/java/com/boydti/fawe/object/clipboard/DiskOptimizedClipboard.java index 27e93c0a..0d262a4b 100644 --- a/core/src/main/java/com/boydti/fawe/object/clipboard/DiskOptimizedClipboard.java +++ b/core/src/main/java/com/boydti/fawe/object/clipboard/DiskOptimizedClipboard.java @@ -2,14 +2,19 @@ package com.boydti.fawe.object.clipboard; import com.boydti.fawe.Fawe; import com.boydti.fawe.FaweCache; +import com.boydti.fawe.config.Settings; +import com.boydti.fawe.object.BufferedRandomAccessFile; import com.boydti.fawe.object.IntegerTrio; import com.boydti.fawe.util.TaskManager; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.regions.CuboidRegion; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; @@ -28,55 +33,167 @@ import java.util.UUID; */ public class DiskOptimizedClipboard extends FaweClipboard { + private static int HEADER_SIZE = 10; + + protected int length; + protected int height; + protected int width; + protected int area; + private final HashMap nbtMap; private final HashSet entities; private final File file; private final byte[] buffer; - private volatile RandomAccessFile raf; + private volatile BufferedRandomAccessFile raf; private long lastAccessed; private int last; + public DiskOptimizedClipboard(int width, int height, int length, UUID uuid) { + this(width, height, length, new File(Fawe.imp().getDirectory(), "clipboard" + File.separator + uuid)); + } + + public DiskOptimizedClipboard(File file) { + nbtMap = new HashMap<>(); + entities = new HashSet<>();this.buffer = new byte[2]; + this.file = file; + this.lastAccessed = System.currentTimeMillis(); + try { + this.raf = new BufferedRandomAccessFile(file, "rw", Settings.BUFFER_SIZE); + raf.setLength(file.length()); + long size = (raf.length() - HEADER_SIZE) / 2; + raf.seek(0); + last = -1; + raf.read(buffer); + width = (((buffer[1] & 0xFF) << 8) + ((buffer[0] & 0xFF))); + raf.read(buffer); + length = (((buffer[1] & 0xFF) << 8) + ((buffer[0] & 0xFF))); + height = (int) (size / (width * length)); + area = width * length; + } catch (IOException e) { + e.printStackTrace(); + } + autoCloseTask(); + } + + public BlockArrayClipboard toClipboard() { + try { + CuboidRegion region = new CuboidRegion(new Vector(0, 0, 0), new Vector(width - 1, height - 1, length - 1)) { + @Override + public boolean contains(Vector position) { + return true; + } + }; + if (raf == null) { + open(); + } + raf.seek(4); + last = -1; + int ox = (((byte) raf.read() << 8) | ((byte) raf.read()) & 0xFF); + int oy = (((byte) raf.read() << 8) | ((byte) raf.read()) & 0xFF); + int oz = (((byte) raf.read() << 8) | ((byte) raf.read()) & 0xFF); + BlockArrayClipboard clipboard = new BlockArrayClipboard(region, this); + clipboard.setOrigin(new Vector(ox, oy, oz)); + return clipboard; + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void setOrigin(Vector offset) { + try { + if (raf == null) { + open(); + } + raf.seek(4); + last = -1; + raf.write((byte) (offset.getBlockX() >> 8)); + raf.write((byte) (offset.getBlockX())); + + raf.write((byte) (offset.getBlockY() >> 8)); + raf.write((byte) (offset.getBlockY())); + + raf.write((byte) (offset.getBlockZ() >> 8)); + raf.write((byte) (offset.getBlockZ())); + } catch (IOException e) { + e.printStackTrace(); + } + } + public DiskOptimizedClipboard(int width, int height, int length, File file) { - super(width, height, length); nbtMap = new HashMap<>(); entities = new HashSet<>(); this.file = file; this.buffer = new byte[2]; this.lastAccessed = System.currentTimeMillis(); + this.width = width; + this.height = height; + this.length = length; + this.area = width * length; try { if (!file.exists()) { file.getParentFile().mkdirs(); - file.createNewFile(); } + file.createNewFile(); } catch (Exception e) { e.printStackTrace(); } } + public void flush() { + try { + raf.close(); + raf = null; + file.setWritable(true); + System.gc(); + } catch (IOException e) { + e.printStackTrace(); + } + } + public DiskOptimizedClipboard(int width, int height, int length) { this(width, height, length, new File(Fawe.imp().getDirectory(), "clipboard" + File.separator + UUID.randomUUID())); } - + public void close() { + try { + RandomAccessFile tmp = raf; + raf = null; + tmp.close(); + tmp = null; + System.gc(); + } catch (IOException e) { + e.printStackTrace(); + } + } public void open() throws IOException { - this.raf = new RandomAccessFile(file, "rw"); + if (raf != null) { + close(); + } + this.raf = new BufferedRandomAccessFile(file, "rw", Settings.BUFFER_SIZE); long size = width * height * length * 2l; if (raf.length() != size) { - raf.setLength(size); + raf.setLength(size + HEADER_SIZE); + // write length etc + raf.seek(0); + last = 0; + raf.write((width) & 0xff); + raf.write(((width) >> 8) & 0xff); + raf.write((length) & 0xff); + raf.write(((length) >> 8) & 0xff); } + autoCloseTask(); + } + + private void autoCloseTask() { TaskManager.IMP.laterAsync(new Runnable() { @Override public void run() { if (raf != null && System.currentTimeMillis() - lastAccessed > 10000) { - try { - RandomAccessFile tmp = raf; - raf = null; - tmp.close(); - } catch (IOException e) { - e.printStackTrace(); - } + close(); } else if (raf == null) { return; } else { @@ -95,7 +212,7 @@ public class DiskOptimizedClipboard extends FaweClipboard { lastAccessed = System.currentTimeMillis(); int i = x + z * width + y * area; if (i != last + 1) { - raf.seek(i << 1); + raf.seek((HEADER_SIZE) + (i << 1)); } raf.read(buffer); last = i; @@ -129,8 +246,9 @@ public class DiskOptimizedClipboard extends FaweClipboard { lastAccessed = System.currentTimeMillis(); int i = x + z * width + y * area; if (i != last + 1) { - raf.seek(i << 1); + raf.seek((HEADER_SIZE) + (i << 1)); } + last = i; final int id = block.getId(); final int data = block.getData(); int combined = (id << 4) + data; diff --git a/core/src/main/java/com/boydti/fawe/object/clipboard/FaweClipboard.java b/core/src/main/java/com/boydti/fawe/object/clipboard/FaweClipboard.java index 0a84fe00..959889e5 100644 --- a/core/src/main/java/com/boydti/fawe/object/clipboard/FaweClipboard.java +++ b/core/src/main/java/com/boydti/fawe/object/clipboard/FaweClipboard.java @@ -1,5 +1,6 @@ package com.boydti.fawe.object.clipboard; +import com.sk89q.worldedit.Vector; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; @@ -12,18 +13,6 @@ import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; public abstract class FaweClipboard { - public final int length; - public final int height; - public final int width; - public final int area; - - public FaweClipboard(int width, int height, int length) { - this.width = width; - this.height = height; - this.length = length; - this.area = width * length; - } - public abstract BaseBlock getBlock(int x, int y, int z); public abstract boolean setBlock(int x, int y, int z, BaseBlock block); @@ -34,6 +23,8 @@ public abstract class FaweClipboard { public abstract boolean remove(ClipboardEntity clipboardEntity); + public void setOrigin(Vector offset) {} // Do nothing + /** * Stores entity data. */ diff --git a/core/src/main/java/com/boydti/fawe/object/clipboard/MemoryOptimizedClipboard.java b/core/src/main/java/com/boydti/fawe/object/clipboard/MemoryOptimizedClipboard.java index 9e8cddb5..6d4d5f62 100644 --- a/core/src/main/java/com/boydti/fawe/object/clipboard/MemoryOptimizedClipboard.java +++ b/core/src/main/java/com/boydti/fawe/object/clipboard/MemoryOptimizedClipboard.java @@ -13,6 +13,10 @@ import java.util.HashSet; import java.util.List; public class MemoryOptimizedClipboard extends FaweClipboard { + protected int length; + protected int height; + protected int width; + protected int area; // x,z,y+15>>4 | y&15 private final byte[][] ids; @@ -21,10 +25,13 @@ public class MemoryOptimizedClipboard extends FaweClipboard { private final HashSet entities; public MemoryOptimizedClipboard(int width, int height, int length) { - super(width, height, length); + this.width = width; + this.height = height; + this.length = length; + this.area = width * length; ids = new byte[width * length * ((height + 15) >> 4)][]; nbtMap = new HashMap<>(); - entities = new HashSet(); + entities = new HashSet<>(); } @Override diff --git a/core/src/main/java/com/boydti/fawe/util/MainUtil.java b/core/src/main/java/com/boydti/fawe/util/MainUtil.java index e3e2a36a..2a047c1c 100644 --- a/core/src/main/java/com/boydti/fawe/util/MainUtil.java +++ b/core/src/main/java/com/boydti/fawe/util/MainUtil.java @@ -35,6 +35,24 @@ public class MainUtil { Fawe.debug(s); } + public static void warnDeprecated(Class... alternatives) { + StackTraceElement[] stack = new RuntimeException().getStackTrace(); + if (stack.length > 1) { + try { + StackTraceElement creatorElement = stack[stack.length - 2]; + String className = creatorElement.getClassName(); + Class clazz = Class.forName(className); + String creator = clazz.getSimpleName(); + String packageName = clazz.getPackage().getName(); + + StackTraceElement deprecatedElement = stack[stack.length - 1]; + String myName = Class.forName(deprecatedElement.getFileName()).getSimpleName(); + Fawe.debug("@" + creator + " from " + packageName +": " + myName + " is deprecated."); + Fawe.debug(" - Alternatives: " + StringMan.getString(alternatives)); + } catch (Throwable ignore) {} + } + } + public static void iterateFiles(File directory, RunnableVal task) { if (directory.exists()) { File[] files = directory.listFiles(); diff --git a/core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java b/core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java index d60d8a2a..d0713371 100644 --- a/core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java +++ b/core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java @@ -21,12 +21,14 @@ package com.sk89q.worldedit; import com.boydti.fawe.FaweCache; import com.boydti.fawe.object.IntegerTrio; +import com.boydti.fawe.util.MainUtil; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.blocks.BlockID; import com.sk89q.worldedit.command.ClipboardCommands; import com.sk89q.worldedit.command.SchematicCommands; import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.function.operation.ForwardExtentCopy; import com.sk89q.worldedit.regions.CuboidRegion; @@ -76,7 +78,7 @@ public class CuboidClipboard { public byte[][] ids; public byte[][] datas; public HashMap nbtMap; - public List entities = new ArrayList(); + public List entities = new ArrayList<>(); public Vector size; private int dx; @@ -91,6 +93,7 @@ public class CuboidClipboard { */ public CuboidClipboard(Vector size) { checkNotNull(size); + MainUtil.warnDeprecated(BlockArrayClipboard.class); origin = new Vector(); offset = new Vector(); this.size = size; diff --git a/core/src/main/java/com/sk89q/worldedit/LocalSession.java b/core/src/main/java/com/sk89q/worldedit/LocalSession.java index 3904f908..92e19b92 100644 --- a/core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -20,6 +20,8 @@ package com.sk89q.worldedit; import com.boydti.fawe.object.changeset.FaweChangeSet; +import com.boydti.fawe.object.clipboard.DiskOptimizedClipboard; +import com.boydti.fawe.util.FaweQueue; import com.boydti.fawe.util.SetQueue; import com.sk89q.jchronic.Chronic; import com.sk89q.jchronic.Options; @@ -32,6 +34,8 @@ import com.sk89q.worldedit.command.tool.SinglePickaxe; import com.sk89q.worldedit.command.tool.Tool; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.inventory.BlockBag; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.mask.Masks; @@ -190,10 +194,16 @@ public class LocalSession { * @param editSession the edit session */ public void remember(EditSession editSession) { - checkNotNull(editSession); + remember(editSession, history.size()); + } + public void remember(EditSession editSession, int index) { + // Enqueue it if (editSession.getQueue() != null) { - SetQueue.IMP.enqueue(editSession.getQueue()); + FaweQueue queue = editSession.getQueue(); + if (queue.size() > 0) { + SetQueue.IMP.enqueue(editSession.getQueue()); + } } // Don't store anything if no changes were made @@ -207,7 +217,7 @@ public class LocalSession { if (set instanceof FaweChangeSet) { ((FaweChangeSet) set).flush(); } - history.add(editSession); + history.add(Math.max(0, Math.min(index, history.size())), editSession); while (history.size() > MAX_HISTORY_SIZE) { history.remove(0); } @@ -456,6 +466,15 @@ public class LocalSession { * @param clipboard the clipboard, or null if the clipboard is to be cleared */ public void setClipboard(@Nullable ClipboardHolder clipboard) { + if (this.clipboard != null && clipboard != null) { + Clipboard clip = clipboard.getClipboard(); + if (clip instanceof BlockArrayClipboard) { + BlockArrayClipboard bac = (BlockArrayClipboard) clip; + if (bac.IMP instanceof DiskOptimizedClipboard) { + ((DiskOptimizedClipboard) bac.IMP).flush(); + } + } + } this.clipboard = clipboard; } diff --git a/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java b/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java new file mode 100644 index 00000000..943546e3 --- /dev/null +++ b/core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java @@ -0,0 +1,263 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.command; + +import com.sk89q.minecraft.util.commands.Command; +import com.sk89q.minecraft.util.commands.CommandPermissions; +import com.sk89q.minecraft.util.commands.Logging; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.Vector; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.function.block.BlockReplace; +import com.sk89q.worldedit.function.mask.Mask; +import com.sk89q.worldedit.function.operation.ForwardExtentCopy; +import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.function.operation.Operations; +import com.sk89q.worldedit.function.pattern.Pattern; +import com.sk89q.worldedit.internal.annotation.Direction; +import com.sk89q.worldedit.internal.annotation.Selection; +import com.sk89q.worldedit.math.transform.AffineTransform; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.regions.RegionSelector; +import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; +import com.sk89q.worldedit.session.ClipboardHolder; +import com.sk89q.worldedit.util.command.binding.Switch; +import com.sk89q.worldedit.util.command.parametric.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.sk89q.minecraft.util.commands.Logging.LogMode.PLACEMENT; +import static com.sk89q.minecraft.util.commands.Logging.LogMode.REGION; + +/** + * Clipboard commands. + */ +public class ClipboardCommands { + + private final WorldEdit worldEdit; + + /** + * Create a new instance. + * + * @param worldEdit reference to WorldEdit + */ + public ClipboardCommands(WorldEdit worldEdit) { + checkNotNull(worldEdit); + this.worldEdit = worldEdit; + } + + @Command( + aliases = { "/copy" }, + flags = "em", + desc = "Copy the selection to the clipboard", + help = "Copy the selection to the clipboard\n" + + "Flags:\n" + + " -e controls whether entities are copied\n" + + " -m sets a source mask so that excluded blocks become air\n" + + "WARNING: Pasting entities cannot yet be undone!", + min = 0, + max = 0 + ) + @CommandPermissions("worldedit.clipboard.copy") + public void copy(Player player, LocalSession session, EditSession editSession, + @Selection Region region, @Switch('e') boolean copyEntities, + @Switch('m') Mask mask) throws WorldEditException { + + BlockArrayClipboard clipboard = new BlockArrayClipboard(region, player.getUniqueId()); + + + clipboard.setOrigin(session.getPlacementPosition(player)); + ForwardExtentCopy copy = new ForwardExtentCopy(editSession, region, clipboard, region.getMinimumPoint()); + if (mask != null) { + copy.setSourceMask(mask); + } + Operations.completeLegacy(copy); + session.setClipboard(new ClipboardHolder(clipboard, editSession.getWorld().getWorldData())); + + player.print(region.getArea() + " block(s) were copied."); + } + + @Command( + aliases = { "/cut" }, + flags = "em", + usage = "[leave-id]", + desc = "Cut the selection to the clipboard", + help = "Copy the selection to the clipboard\n" + + "Flags:\n" + + " -e controls whether entities are copied\n" + + " -m sets a source mask so that excluded blocks become air\n" + + "WARNING: Cutting and pasting entities cannot yet be undone!", + min = 0, + max = 1 + ) + @CommandPermissions("worldedit.clipboard.cut") + @Logging(REGION) + public void cut(Player player, LocalSession session, EditSession editSession, + @Selection Region region, @Optional("air") Pattern leavePattern, @Switch('e') boolean copyEntities, + @Switch('m') Mask mask) throws WorldEditException { + + BlockArrayClipboard clipboard = new BlockArrayClipboard(region, player.getUniqueId()); + clipboard.setOrigin(session.getPlacementPosition(player)); + ForwardExtentCopy copy = new ForwardExtentCopy(editSession, region, clipboard, region.getMinimumPoint()); + copy.setSourceFunction(new BlockReplace(editSession, leavePattern)); + if (mask != null) { + copy.setSourceMask(mask); + } + Operations.completeLegacy(copy); + session.setClipboard(new ClipboardHolder(clipboard, editSession.getWorld().getWorldData())); + + player.print(region.getArea() + " block(s) were copied."); + } + + @Command( + aliases = { "/paste" }, + usage = "", + flags = "sao", + desc = "Paste the clipboard's contents", + help = + "Pastes the clipboard's contents.\n" + + "Flags:\n" + + " -a skips air blocks\n" + + " -o pastes at the original position\n" + + " -s selects the region after pasting", + min = 0, + max = 0 + ) + @CommandPermissions("worldedit.clipboard.paste") + @Logging(PLACEMENT) + public void paste(Player player, LocalSession session, EditSession editSession, + @Switch('a') boolean ignoreAirBlocks, @Switch('o') boolean atOrigin, + @Switch('s') boolean selectPasted) throws WorldEditException { + + ClipboardHolder holder = session.getClipboard(); + Clipboard clipboard = holder.getClipboard(); + Region region = clipboard.getRegion(); + + Vector to = atOrigin ? clipboard.getOrigin() : session.getPlacementPosition(player); + Operation operation = holder + .createPaste(editSession, editSession.getWorld().getWorldData()) + .to(to) + .ignoreAirBlocks(ignoreAirBlocks) + .build(); + Operations.completeLegacy(operation); + + if (selectPasted) { + Vector max = to.add(region.getMaximumPoint().subtract(region.getMinimumPoint())); + RegionSelector selector = new CuboidRegionSelector(player.getWorld(), to, max); + session.setRegionSelector(player.getWorld(), selector); + selector.learnChanges(); + selector.explainRegionAdjust(player, session); + } + + player.print("The clipboard has been pasted at " + to); + } + + @Command( + aliases = { "/rotate" }, + usage = " [] []", + desc = "Rotate the contents of the clipboard", + help = "Non-destructively rotate the contents of the clipboard.\n" + + "Angles are provided in degrees and a positive angle will result in a clockwise rotation. " + + "Multiple rotations can be stacked. Interpolation is not performed so angles should be a multiple of 90 degrees.\n" + ) + @CommandPermissions("worldedit.clipboard.rotate") + public void rotate(Player player, LocalSession session, Double yRotate, @Optional Double xRotate, @Optional Double zRotate) throws WorldEditException { + if ((yRotate != null && Math.abs(yRotate % 90) > 0.001) || + xRotate != null && Math.abs(xRotate % 90) > 0.001 || + zRotate != null && Math.abs(zRotate % 90) > 0.001) { + player.printDebug("Note: Interpolation is not yet supported, so angles that are multiples of 90 is recommended."); + } + + ClipboardHolder holder = session.getClipboard(); + AffineTransform transform = new AffineTransform(); + transform = transform.rotateY(-(yRotate != null ? yRotate : 0)); + transform = transform.rotateX(-(xRotate != null ? xRotate : 0)); + transform = transform.rotateZ(-(zRotate != null ? zRotate : 0)); + holder.setTransform(holder.getTransform().combine(transform)); + player.print("The clipboard copy has been rotated."); + } + + @Command( + aliases = { "/flip" }, + usage = "[]", + desc = "Flip the contents of the clipboard", + help = + "Flips the contents of the clipboard across the point from which the copy was made.\n", + min = 0, + max = 1 + ) + @CommandPermissions("worldedit.clipboard.flip") + public void flip(Player player, LocalSession session, EditSession editSession, + @Optional(Direction.AIM) @Direction Vector direction) throws WorldEditException { + ClipboardHolder holder = session.getClipboard(); + Clipboard clipboard = holder.getClipboard(); + AffineTransform transform = new AffineTransform(); + transform = transform.scale(direction.positive().multiply(-2).add(1, 1, 1)); + holder.setTransform(holder.getTransform().combine(transform)); + player.print("The clipboard copy has been flipped."); + } + + @Command( + aliases = { "/load" }, + usage = "", + desc = "Load a schematic into your clipboard", + min = 0, + max = 1 + ) + @Deprecated + @CommandPermissions("worldedit.clipboard.load") + public void load(Actor actor) { + actor.printError("This command is no longer used. See //schematic load."); + } + + @Command( + aliases = { "/save" }, + usage = "", + desc = "Save a schematic into your clipboard", + min = 0, + max = 1 + ) + @Deprecated + @CommandPermissions("worldedit.clipboard.save") + public void save(Actor actor) { + actor.printError("This command is no longer used. See //schematic save."); + } + + @Command( + aliases = { "clearclipboard" }, + usage = "", + desc = "Clear your clipboard", + min = 0, + max = 0 + ) + @CommandPermissions("worldedit.clipboard.clear") + public void clearClipboard(Player player, LocalSession session, EditSession editSession) throws WorldEditException { + session.setClipboard(null); + player.print("Clipboard cleared."); + } + public static Class inject() { + return ClipboardCommands.class; + } +} diff --git a/core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 96f206d7..0ddde6e1 100644 --- a/core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -37,6 +37,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; +import com.sk89q.worldedit.extent.clipboard.io.SchematicReader; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.session.ClipboardHolder; @@ -117,9 +118,13 @@ public class SchematicCommands { final ClipboardReader reader = format.getReader(bis); final WorldData worldData = player.getWorld().getWorldData(); - final Clipboard clipboard = reader.read(player.getWorld().getWorldData()); + final Clipboard clipboard; + if (reader instanceof SchematicReader) { + clipboard = ((SchematicReader) reader).read(player.getWorld().getWorldData(), player.getUniqueId()); + } else { + clipboard = reader.read(player.getWorld().getWorldData()); + } session.setClipboard(new ClipboardHolder(clipboard, worldData)); - log.info(player.getName() + " loaded " + filePath); player.print(filename + " loaded. Paste it with //paste"); } @@ -160,7 +165,7 @@ public class SchematicCommands { // If we have a transform, bake it into the copy if (!transform.isIdentity()) { final FlattenedClipboardTransform result = FlattenedClipboardTransform.transform(clipboard, transform, holder.getWorldData()); - target = new BlockArrayClipboard(result.getTransformedRegion()); + target = new BlockArrayClipboard(result.getTransformedRegion(), player.getUniqueId()); target.setOrigin(clipboard.getOrigin()); Operations.completeLegacy(result.copyTo(target)); } else { diff --git a/core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java b/core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java index 147fad17..84f8a1f8 100644 --- a/core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java +++ b/core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java @@ -37,6 +37,7 @@ import com.sk89q.worldedit.world.biome.BaseBiome; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import javax.annotation.Nullable; @@ -64,6 +65,17 @@ public class BlockArrayClipboard implements Clipboard { private Vector origin; + public BlockArrayClipboard(Region region) { + checkNotNull(region); + this.region = region.clone(); + this.size = getDimensions(); + this.IMP = Settings.STORE_CLIPBOARD_ON_DISK ? new DiskOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ()) : new MemoryOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ()); + this.origin = region.getMinimumPoint(); + this.mx = origin.getBlockX(); + this.my = origin.getBlockY(); + this.mz = origin.getBlockZ(); + } + /** * Create a new instance. * @@ -71,11 +83,22 @@ public class BlockArrayClipboard implements Clipboard { * * @param region the bounding region */ - public BlockArrayClipboard(Region region) { + public BlockArrayClipboard(Region region, UUID clipboardId) { checkNotNull(region); this.region = region.clone(); this.size = getDimensions(); - this.IMP = Settings.STORE_CLIPBOARD_ON_DISK ? new DiskOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ()) : new MemoryOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ()); + this.IMP = Settings.STORE_CLIPBOARD_ON_DISK ? new DiskOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ(), clipboardId) : new MemoryOptimizedClipboard(size.getBlockX(), size.getBlockY(), size.getBlockZ()); + this.origin = region.getMinimumPoint(); + this.mx = origin.getBlockX(); + this.my = origin.getBlockY(); + this.mz = origin.getBlockZ(); + } + + public BlockArrayClipboard(Region region, DiskOptimizedClipboard clipboard) { + checkNotNull(region); + this.region = region.clone(); + this.size = getDimensions(); + this.IMP = clipboard; this.origin = region.getMinimumPoint(); this.mx = origin.getBlockX(); this.my = origin.getBlockY(); @@ -95,6 +118,7 @@ public class BlockArrayClipboard implements Clipboard { @Override public void setOrigin(Vector origin) { this.origin = origin; + IMP.setOrigin(origin.subtract(region.getMinimumPoint())); } @Override diff --git a/core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java b/core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java index ed4a7014..0a8130d6 100644 --- a/core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java +++ b/core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java @@ -44,6 +44,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -71,6 +72,10 @@ public class SchematicReader implements ClipboardReader { @Override public Clipboard read(WorldData data) throws IOException { + return read(data, UUID.randomUUID()); + } + + public Clipboard read(WorldData data, UUID clipboardId) throws IOException { // Schematic tag NamedTag rootTag = inputStream.readNamedTag(); if (!rootTag.getName().equals("Schematic")) { @@ -171,7 +176,7 @@ public class SchematicReader implements ClipboardReader { tileEntitiesMap.put(vec, values); } - BlockArrayClipboard clipboard = new BlockArrayClipboard(region); + BlockArrayClipboard clipboard = new BlockArrayClipboard(region, clipboardId); clipboard.setOrigin(origin); // Don't log a torrent of errors diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5ccda13e9cb94678ba179b32452cf3d60dc36353 GIT binary patch literal 53638 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

i1O>I_eGKr^&y;^hm-V94&lC5U~@}s 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 GfBiq