diff --git a/worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java b/worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java index 5c3c91203..7edd24c78 100644 --- a/worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java +++ b/worldedit-core/src/main/java/com/boydti/fawe/config/Settings.java @@ -394,6 +394,18 @@ public class Settings extends Config { " - Requires combine_stages = true" }) public boolean SEND_BEFORE_HISTORY = false; + + @Comment({ + "Sets a maximum limit (in kb) for the size of a player's schematics directory (per-player mode only)", + "Set to -1 to disable" + }) + public int PER_PLAYER_FILE_SIZE_LIMIT = -1; + + @Comment({ + "Sets a maximum limit for the amount of schematics in a player's schematics directory (per-player mode only)", + "Set to -1 to disable" + }) + public int PER_PLAYER_FILE_NUM_LIMIT = -1; } public static class PLOTSQUARED_INTEGRATION { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 9f1b4638f..ab247e2b0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -80,6 +80,7 @@ import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -94,7 +95,6 @@ import static com.boydti.fawe.util.ReflectionUtils.as; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -//import com.boydti.fawe.object.schematic.visualizer.SchemVis; /** * Commands that work with schematic files. @@ -115,6 +115,31 @@ public class SchematicCommands { this.worldEdit = worldEdit; } + //TODO filtering for directories, global, and private scheamtics needs to be reimplemented here + private static List getFiles(File root, String filter, ClipboardFormat format) { + File[] files = root.listFiles(); + if (files == null) { + return null; + } + //Only get the files that match the format parameter + if (format != null) { + files = Arrays.stream(files).filter(format::isFormat).toArray(File[]::new); + } + List fileList = new ArrayList<>(); + for (File f : files) { + if (f.isDirectory()) { + List subFiles = getFiles(f, filter, format); + if (subFiles == null) { + continue; // empty subdir + } + fileList.addAll(subFiles); + } else { + fileList.add(f); + } + } + return fileList; + } + @Command( name = "loadall", desc = "Load multiple clipboards (paste will randomly choose one)" @@ -122,12 +147,12 @@ public class SchematicCommands { @Deprecated @CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load", "worldedit.schematic.load.web", "worldedit.schematic.load.asset"}) public void loadall(Player player, LocalSession session, - @Arg(desc = "Format name.", def = "schematic") - String formatName, - @Arg(desc = "File name.") - String filename, - @Switch(name = 'r', desc = "Apply random rotation") - boolean randomRotate) throws FilenameException { + @Arg(desc = "Format name.", def = "schematic") + String formatName, + @Arg(desc = "File name.") + String filename, + @Switch(name = 'r', desc = "Apply random rotation") + boolean randomRotate) throws FilenameException { final ClipboardFormat format = ClipboardFormats.findByAlias(formatName); if (format == null) { player.print(Caption.of("fawe.worldedit.clipboard.clipboard.invalid.format", formatName)); @@ -355,19 +380,19 @@ public class SchematicCommands { if (parent != null && !parent.exists()) { if (!parent.mkdirs()) { throw new StopExecutionException(TranslatableComponent.of( - "worldedit.schematic.save.failed-directory")); + "worldedit.schematic.save.failed-directory")); } } ClipboardHolder holder = session.getClipboard(); - SchematicSaveTask task = new SchematicSaveTask(actor, f, format, holder, overwrite); + SchematicSaveTask task = new SchematicSaveTask(actor, f, dir, format, holder, overwrite); AsyncCommandBuilder.wrap(task, actor) - .registerWithSupervisor(worldEdit.getSupervisor(), "Saving schematic " + filename) - .sendMessageAfterDelay(TranslatableComponent.of("worldedit.schematic.save.saving")) - .onSuccess(filename + " saved" + (overwrite ? " (overwriting previous file)." : "."), null) - .onFailure("Failed to save schematic", worldEdit.getPlatformManager().getPlatformCommandManager().getExceptionConverter()) - .buildAndExec(worldEdit.getExecutorService()); + .registerWithSupervisor(worldEdit.getSupervisor(), "Saving schematic " + filename) + .sendMessageAfterDelay(TranslatableComponent.of("worldedit.schematic.save.saving")) + .onSuccess(filename + " saved" + (overwrite ? " (overwriting previous file)." : "."), null) + .onFailure("Failed to save schematic", worldEdit.getPlatformManager().getPlatformCommandManager().getExceptionConverter()) + .buildAndExec(worldEdit.getExecutorService()); } @Command( @@ -531,22 +556,22 @@ public class SchematicCommands { if (loaded) { msg.append(TextComponent.of("[-]", TextColor.RED) - .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, unload + " " + path)) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Unload")))); + .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, unload + " " + path)) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Unload")))); } else { msg.append(TextComponent.of("[+]", TextColor.GREEN) - .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, loadMulti + " " + path)) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Add to clipboard")))); + .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, loadMulti + " " + path)) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Add to clipboard")))); } if (type != UtilityCommands.URIType.DIRECTORY) { msg.append(TextComponent.of("[X]", TextColor.DARK_RED) - .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, delete + " " + path)) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("delete"))) + .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, delete + " " + path)) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("delete"))) ); } else if (hasShow) { msg.append(TextComponent.of("[O]", TextColor.DARK_AQUA) - .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, showCmd + " " + path)) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("visualize"))) + .clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, showCmd + " " + path)) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TextComponent.of("visualize"))) ); } TextComponent msgElem = TextComponent.of(name, color); @@ -559,173 +584,44 @@ public class SchematicCommands { } msg.append(msgElem); + if (type == UtilityCommands.URIType.FILE) { + long filesize = 0; + try { + filesize = Files.size(Paths.get(dir.getAbsolutePath() + File.separator + + (playerFolder ? (uuid.toString() + File.separator) : "") + path)); + } catch (IOException e) { + e.printStackTrace(); + } + TextComponent sizeElem = TextComponent.of(String.format(" (%.1f kb)", filesize / 1000.0), TextColor.GRAY); + msg.append(sizeElem); + } return msg.create(); }); - PaginationBox paginationBox = PaginationBox.fromComponents("Available schematics", pageCommand, components); + + long totalBytes = 0; + File parentDir = new File(dir.getAbsolutePath() + (playerFolder ? File.separator + uuid.toString() : "")); + try { + for (File schem : getFiles(parentDir, null, null)) { + if (schem.getName().endsWith(".schem") || schem.getName().endsWith(".schematic")) { + totalBytes += Files.size(Paths.get(schem.getAbsolutePath())); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + String headerBytesElem = String.format("%.1fkb", totalBytes / 1000.0); + + if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT > -1) { + headerBytesElem += String.format(" / %dkb", + Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT ); + } + + String fullHeader = "| Schematics: " + headerBytesElem + " |"; + PaginationBox paginationBox = PaginationBox.fromComponents(fullHeader, pageCommand, components); actor.print(paginationBox.create(page)); } - private static class SchematicLoadTask implements Callable { - private final Actor actor; - private final File file; - private final ClipboardFormat format; - - SchematicLoadTask(Actor actor, File file, ClipboardFormat format) { - this.actor = actor; - this.file = file; - this.format = format; - } - - @Override - public ClipboardHolder call() throws Exception { - try (Closer closer = Closer.create()) { - FileInputStream fis = closer.register(new FileInputStream(file)); - BufferedInputStream bis = closer.register(new BufferedInputStream(fis)); - ClipboardReader reader = closer.register(format.getReader(bis)); - - Clipboard clipboard = reader.read(); - log.info(actor.getName() + " loaded " + file.getCanonicalPath()); - return new ClipboardHolder(clipboard); - } - } - } - - private static class SchematicSaveTask implements Callable { - private final Actor actor; - private final File file; - private final ClipboardFormat format; - private final ClipboardHolder holder; - private final boolean overwrite; - - SchematicSaveTask(Actor actor, File file, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) { - this.actor = actor; - this.file = file; - this.format = format; - this.holder = holder; - this.overwrite = overwrite; - } - - @Override - public Void call() throws Exception { - Clipboard clipboard = holder.getClipboard(); - Transform transform = holder.getTransform(); - Clipboard target; - - // If we have a transform, bake it into the copy - if (transform.isIdentity()) { - target = clipboard; - } else { - FlattenedClipboardTransform result = FlattenedClipboardTransform.transform(clipboard, transform); - target = new BlockArrayClipboard(result.getTransformedRegion()); - target.setOrigin(clipboard.getOrigin()); - Operations.completeLegacy(result.copyTo(target)); - } - - try (Closer closer = Closer.create()) { - FileOutputStream fos = closer.register(new FileOutputStream(file)); - BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos)); - ClipboardWriter writer = closer.register(format.getWriter(bos)); - URI uri = null; - if (holder instanceof URIClipboardHolder) { - uri = ((URIClipboardHolder) holder).getURI(clipboard); - } - if (new ActorSaveClipboardEvent(actor, clipboard, uri, file.toURI()).call()) { - if (writer instanceof MinecraftStructure) { - ((MinecraftStructure) writer).write(target, actor.getName()); - } else { - writer.write(target); - } - log.info(actor.getName() + " saved " + file.getCanonicalPath()); - actor.print(Caption.of("fawe.worldedit.schematic.schematic.saved", file.getName())); - } else { - actor.printError(TranslatableComponent.of("fawe.cancel.worldedit.cancel.reason.manual")); - } - } - return null; - } - } - - private static class SchematicListTask implements Callable { - private final String prefix; - private final int sortType; - private final int page; - private final File rootDir; - private final String pageCommand; - private final String filter; - private String formatName; - - SchematicListTask(String prefix, int sortType, int page, String pageCommand, - String filter, String formatName) { - this.prefix = prefix; - this.sortType = sortType; - this.page = page; - this.rootDir = WorldEdit.getInstance().getWorkingDirectoryFile(prefix); - this.pageCommand = pageCommand; - this.filter = filter; - this.formatName = formatName; - } - - @Override - public Component call() throws Exception { - ClipboardFormat format = ClipboardFormats.findByAlias(formatName); - List fileList = getFiles(rootDir,filter,format); - - if (fileList == null || fileList.isEmpty()) { - return ErrorFormat.wrap("No schematics found."); - } - - File[] files = new File[fileList.size()]; - fileList.toArray(files); - // cleanup file list - Arrays.sort(files, (f1, f2) -> { - // http://stackoverflow.com/questions/203030/best-way-to-list-files-in-java-sorted-by-date-modified - int res; - if (sortType == 0) { // use name by default - int p = f1.getParent().compareTo(f2.getParent()); - if (p == 0) { // same parent, compare names - res = f1.getName().compareTo(f2.getName()); - } else { // different parent, sort by that - res = p; - } - } else { - res = Long.compare(f1.lastModified(), f2.lastModified()); // use date if there is a flag - if (sortType == 1) { - res = -res; // flip date for newest first instead of oldest first - } - } - return res; - }); - - PaginationBox paginationBox = new SchematicPaginationBox(prefix, files, pageCommand); - return paginationBox.create(page); - } - } - - //TODO filtering for directories, global, and private scheamtics needs to be reimplemented here - private static List getFiles(File root, String filter, ClipboardFormat format) { - File[] files = root.listFiles(); - if (files == null) { - return null; - } - //Only get the files that match the format parameter - if (format != null) { - files = Arrays.stream(files).filter(format::isFormat).toArray(File[]::new); - } - List fileList = new ArrayList<>(); - for (File f : files) { - if (f.isDirectory()) { - List subFiles = getFiles(f, filter, format); - if (subFiles == null) { - continue; // empty subdir - } - fileList.addAll(subFiles); - } else { - fileList.add(f); - } - } - return fileList; - } - @Command( name = "delete", aliases = {"d"}, @@ -776,6 +672,246 @@ public class SchematicCommands { return false; } + private static class SchematicLoadTask implements Callable { + private final Actor actor; + private final ClipboardFormat format; + private final File file; + + SchematicLoadTask(Actor actor, File file, ClipboardFormat format) { + this.actor = actor; + this.file = file; + this.format = format; + } + + @Override + public ClipboardHolder call() throws Exception { + try (Closer closer = Closer.create()) { + FileInputStream fis = closer.register(new FileInputStream(file)); + BufferedInputStream bis = closer.register(new BufferedInputStream(fis)); + ClipboardReader reader = closer.register(format.getReader(bis)); + + Clipboard clipboard = reader.read(); + log.info(actor.getName() + " loaded " + file.getCanonicalPath()); + return new ClipboardHolder(clipboard); + } + } + } + + private static class SchematicSaveTask implements Callable { + private final Actor actor; + private final ClipboardFormat format; + private final ClipboardHolder holder; + private final boolean overwrite; + private final File rootDir; + private File file; + + SchematicSaveTask(Actor actor, File file, File rootDir, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) { + this.actor = actor; + this.file = file; + this.rootDir = rootDir; + this.format = format; + this.holder = holder; + this.overwrite = overwrite; + } + + @Override + public Void call() throws Exception { + Clipboard clipboard = holder.getClipboard(); + Transform transform = holder.getTransform(); + Clipboard target; + + boolean checkFilesize = false; + + if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS + && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT > -1) { + checkFilesize = true; + } + + double directorysizeKb = 0; + String curFilepath = file.getAbsolutePath(); + final String SCHEMATIC_NAME = file.getName(); + + double oldKbOverwritten = 0; + String overwrittenPath = curFilepath; + + int numFiles = -1; + if (checkFilesize) { + File parentDir = new File(file.getParent()); + + for (File child : getFiles(rootDir, null, null)) { + if (child.getName().endsWith(".schem") || child.getName().endsWith(".schematic")) { + directorysizeKb += Files.size(Paths.get(child.getAbsolutePath())) / 1000.0; + numFiles++; + } + } + + if (overwrite) { + oldKbOverwritten = Files.size(Paths.get(file.getAbsolutePath())) / 1000.0; + int iter = 1; + while (new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension()).exists()) { + iter++; + } + file = new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension()); + } + } + + + if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT > -1) { + + if (numFiles == -1) { + numFiles = 0; + for (File child : getFiles(rootDir, null, null)) { + if (child.getName().endsWith(".schem") || child.getName().endsWith(".schematic")) { + numFiles++; + } + } + } + int limit = Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT; + + if (numFiles >= limit) { + TextComponent noSlotsErr = TextComponent.of( //TODO - to be moved into captions/translatablecomponents + String.format("You have " + numFiles + "/" + limit + " saved schematics. Delete some to save this one!", + TextColor.RED)); + log.info(actor.getName() + " failed to save " + file.getCanonicalPath() + " - too many schematics!"); + throw new WorldEditException(noSlotsErr) { + }; + } + } + + // If we have a transform, bake it into the copy + if (transform.isIdentity()) { + target = clipboard; + } else { + FlattenedClipboardTransform result = FlattenedClipboardTransform.transform(clipboard, transform); + target = new BlockArrayClipboard(result.getTransformedRegion()); + target.setOrigin(clipboard.getOrigin()); + Operations.completeLegacy(result.copyTo(target)); + } + + try (Closer closer = Closer.create()) { + FileOutputStream fos = closer.register(new FileOutputStream(file)); + BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos)); + ClipboardWriter writer = closer.register(format.getWriter(bos)); + URI uri = null; + if (holder instanceof URIClipboardHolder) { + uri = ((URIClipboardHolder) holder).getURI(clipboard); + } + if (new ActorSaveClipboardEvent(actor, clipboard, uri, file.toURI()).call()) { + if (writer instanceof MinecraftStructure) { + ((MinecraftStructure) writer).write(target, actor.getName()); + } else { + writer.write(target); + } + + closer.close(); // release the new .schem file so that its size can be measured + double filesizeKb = Files.size(Paths.get(file.getAbsolutePath())) / 1000.0; + + TextComponent filesizeNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents + SCHEMATIC_NAME + " size: " + String.format("%.1f", filesizeKb) + "kb", TextColor.GRAY); + actor.print(filesizeNotif); + + if (checkFilesize) { + + double curKb = filesizeKb + directorysizeKb; + int allocatedKb = Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_SIZE_LIMIT; + + if (overwrite) { + curKb -= oldKbOverwritten; + } + + if ((curKb) > allocatedKb) { + file.delete(); + TextComponent notEnoughKbErr = TextComponent.of( //TODO - to be moved into captions/translatablecomponents + "You're about to be at " + String.format("%.1f", curKb) + "kb of schematics. (" + + String.format("%dkb", allocatedKb) + " available) Delete some first to save this one!", + TextColor.RED); + log.info(actor.getName() + " failed to save " + SCHEMATIC_NAME + " - not enough space!"); + throw new WorldEditException(notEnoughKbErr) { + }; + } + if (overwrite) { + new File(curFilepath).delete(); + file.renameTo(new File(curFilepath)); + } else { + numFiles++; + } + TextComponent kbRemainingNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents + "You have " + String.format("%.1f", (allocatedKb - curKb)) + "kb left for schematics.", TextColor.GRAY); + actor.print(kbRemainingNotif); + } + + if (Settings.IMP.PATHS.PER_PLAYER_SCHEMATICS && Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT > -1) { + + TextComponent slotsRemainingNotif = TextComponent.of( //TODO - to be moved into captions/translatablecomponents + "You have " + (Settings.IMP.EXPERIMENTAL.PER_PLAYER_FILE_NUM_LIMIT - numFiles) + + " schematic file slots left.", TextColor.GRAY); + actor.print(slotsRemainingNotif); + } + log.info(actor.getName() + " saved " + file.getCanonicalPath()); + } else { + actor.printError(TranslatableComponent.of("fawe.cancel.worldedit.cancel.reason.manual")); + } + } + return null; + } + } + + private static class SchematicListTask implements Callable { + private final String prefix; + private final int sortType; + private final int page; + private final File rootDir; + private final String pageCommand; + private final String filter; + private final String formatName; + + SchematicListTask(String prefix, int sortType, int page, String pageCommand, + String filter, String formatName) { + this.prefix = prefix; + this.sortType = sortType; + this.page = page; + this.rootDir = WorldEdit.getInstance().getWorkingDirectoryFile(prefix); + this.pageCommand = pageCommand; + this.filter = filter; + this.formatName = formatName; + } + + @Override + public Component call() throws Exception { + ClipboardFormat format = ClipboardFormats.findByAlias(formatName); + List fileList = getFiles(rootDir, filter, format); + + if (fileList == null || fileList.isEmpty()) { + return ErrorFormat.wrap("No schematics found."); + } + + File[] files = new File[fileList.size()]; + fileList.toArray(files); + // cleanup file list + Arrays.sort(files, (f1, f2) -> { + // http://stackoverflow.com/questions/203030/best-way-to-list-files-in-java-sorted-by-date-modified + int res; + if (sortType == 0) { // use name by default + int p = f1.getParent().compareTo(f2.getParent()); + if (p == 0) { // same parent, compare names + res = f1.getName().compareTo(f2.getName()); + } else { // different parent, sort by that + res = p; + } + } else { + res = Long.compare(f1.lastModified(), f2.lastModified()); // use date if there is a flag + if (sortType == 1) { + res = -res; // flip date for newest first instead of oldest first + } + } + return res; + }); + + PaginationBox paginationBox = new SchematicPaginationBox(prefix, files, pageCommand); + return paginationBox.create(page); + } + } + private static class SchematicPaginationBox extends PaginationBox { private final String prefix; private final File[] files;