diff --git a/Bukkit/build.gradle.kts b/Bukkit/build.gradle.kts index fc301e0d5..885fe5da3 100644 --- a/Bukkit/build.gradle.kts +++ b/Bukkit/build.gradle.kts @@ -63,6 +63,10 @@ dependencies { // Other libraries implementation("com.sk89q:squirrelid:1.0.0-SNAPSHOT") { isTransitive = false } + // Our libraries + implementation("com.intellectualsites.arkitektonika:Arkitektonika-Client:2.0-SNAPSHOT") + implementation("com.intellectualsites.http:HTTP4J:1.1-SNAPSHOT") + // Adventure implementation("net.kyori:adventure-platform-bukkit:4.0.0-SNAPSHOT") } @@ -91,6 +95,8 @@ tasks.named("shadowJar") { relocate("javax.inject", "com.plotsquared.core.inject.javax") relocate("org.aopalliance", "com.plotsquared.core.aopalliance") relocate("com.intellectualsites.services", "com.plotsquared.core.services") + relocate("com.intellectualsites.arkitektonika", "com.plotsquared.core.arkitektonika") + relocate("com.intellectualsites.http", "com.plotsquared.core.http") // Get rid of all the libs which are 100% unused. minimize() diff --git a/Core/build.gradle.kts b/Core/build.gradle.kts index b7b290c22..85f530c94 100644 --- a/Core/build.gradle.kts +++ b/Core/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { api("com.intellectualsites:Pipeline:1.4.0-SNAPSHOT") { exclude(group = "com.google.guava") } + api("com.intellectualsites.arkitektonika:Arkitektonika-Client:2.0-SNAPSHOT") } tasks.processResources { diff --git a/Core/src/main/java/com/plotsquared/core/command/Download.java b/Core/src/main/java/com/plotsquared/core/command/Download.java index ea5586c4a..4cc47aa3e 100644 --- a/Core/src/main/java/com/plotsquared/core/command/Download.java +++ b/Core/src/main/java/com/plotsquared/core/command/Download.java @@ -26,21 +26,21 @@ package com.plotsquared.core.command; import com.google.inject.Inject; -import com.plotsquared.core.permissions.Permission; import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.configuration.caption.StaticCaption; import com.plotsquared.core.configuration.caption.TranslatableCaption; +import com.plotsquared.core.permissions.Permission; import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.plot.Plot; import com.plotsquared.core.plot.flag.implementations.DoneFlag; import com.plotsquared.core.plot.world.PlotAreaManager; import com.plotsquared.core.util.Permissions; +import com.plotsquared.core.util.PlotUploader; import com.plotsquared.core.util.SchematicHandler; import com.plotsquared.core.util.StringMan; import com.plotsquared.core.util.TabCompletions; import com.plotsquared.core.util.WorldUtil; import com.plotsquared.core.util.task.RunnableVal; -import com.sk89q.jnbt.CompoundTag; import net.kyori.adventure.text.minimessage.Template; import javax.annotation.Nonnull; @@ -60,13 +60,16 @@ import java.util.stream.Collectors; public class Download extends SubCommand { private final PlotAreaManager plotAreaManager; - private final SchematicHandler schematicHandler; + private final PlotUploader plotUploader; + @Nonnull private final SchematicHandler schematicHandler; private final WorldUtil worldUtil; @Inject public Download(@Nonnull final PlotAreaManager plotAreaManager, + @Nonnull final PlotUploader plotUploader, @Nonnull final SchematicHandler schematicHandler, @Nonnull final WorldUtil worldUtil) { this.plotAreaManager = plotAreaManager; + this.plotUploader = plotUploader; this.schematicHandler = schematicHandler; this.worldUtil = worldUtil; } @@ -96,6 +99,10 @@ public class Download extends SubCommand { player.sendMessage(TranslatableCaption.of("permission.no_plot_perms")); return false; } + if (plot.isMerged()) { + player.sendMessage(TranslatableCaption.of("web.plot_merged")); + return false; + } if (plot.getRunning() > 0) { player.sendMessage(TranslatableCaption.of("errors.wait_for_timer")); return false; @@ -103,27 +110,11 @@ public class Download extends SubCommand { if (args.length == 0 || (args.length == 1 && StringMan .isEqualIgnoreCaseToAny(args[0], "sch", "schem", "schematic"))) { if (plot.getVolume() > Integer.MAX_VALUE) { - player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large")); + player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large")); return false; } plot.addRunning(); - this.schematicHandler.getCompoundTag(plot, new RunnableVal() { - @Override public void run(CompoundTag value) { - plot.removeRunning(); - schematicHandler.upload(value, null, null, new RunnableVal() { - @Override public void run(URL url) { - if (url == null) { - player.sendMessage(TranslatableCaption.of("web.generating_link_failed")); - return; - } - player.sendMessage( - TranslatableCaption.of("web.generation_link_success"), - Template.of("url", url.toString()) - ); - } - }); - } - }); + upload(player, plot); } else if (args.length == 1 && StringMan .isEqualIgnoreCaseToAny(args[0], "mcr", "world", "mca")) { if (!Permissions.hasPermission(player, Permission.PERMISSION_DOWNLOAD_WORLD)) { @@ -153,6 +144,7 @@ public class Download extends SubCommand { player.sendMessage(TranslatableCaption.of("web.generating_link")); return true; } + @Override public Collection tab(final PlotPlayer player, final String[] args, final boolean space) { if (args.length == 1) { @@ -173,4 +165,36 @@ public class Download extends SubCommand { } return TabCompletions.completePlayers(String.join(",", args).trim(), Collections.emptyList()); } + + private void upload(PlotPlayer player, Plot plot) { + if (Settings.Web.LEGACY_WEBINTERFACE) { + schematicHandler + .getCompoundTag(plot) + .whenComplete((compoundTag, throwable) -> { + schematicHandler.upload(compoundTag, null, null, new RunnableVal() { + @Override + public void run(URL value) { + player.sendMessage( + TranslatableCaption.of("web.generation_link_success"), + Template.of("download", value.toString()), + Template.of("delete", "Not available")); + player.sendMessage(StaticCaption.of(value.toString())); + } + }); + }); + return; + } + // TODO legacy support + this.plotUploader.upload(plot) + .whenComplete((result, throwable) -> { + if (throwable != null || !result.isSuccess()) { + player.sendMessage(TranslatableCaption.of("web.generating_link_failed")); + } else { + player.sendMessage( + TranslatableCaption.of("web.generation_link_success"), + Template.of("download", result.getDownloadUrl()), + Template.of("delete", result.getDeletionUrl())); + } + }); + } } diff --git a/Core/src/main/java/com/plotsquared/core/command/MainCommand.java b/Core/src/main/java/com/plotsquared/core/command/MainCommand.java index 46107291b..be39562ae 100644 --- a/Core/src/main/java/com/plotsquared/core/command/MainCommand.java +++ b/Core/src/main/java/com/plotsquared/core/command/MainCommand.java @@ -76,7 +76,10 @@ public class MainCommand extends Command { final List> commands = new LinkedList<>(); commands.add(Caps.class); commands.add(Buy.class); - commands.add(Save.class); + if (Settings.Web.LEGACY_WEBINTERFACE) { + logger.warn("Legacy webinterface is used. Please note that it will be removed in future."); + commands.add(Save.class); + } commands.add(Load.class); commands.add(Confirm.class); commands.add(Template.class); diff --git a/Core/src/main/java/com/plotsquared/core/command/Save.java b/Core/src/main/java/com/plotsquared/core/command/Save.java index 636096943..ccf23cf04 100644 --- a/Core/src/main/java/com/plotsquared/core/command/Save.java +++ b/Core/src/main/java/com/plotsquared/core/command/Save.java @@ -86,36 +86,36 @@ public class Save extends SubCommand { return false; } plot.addRunning(); - this.schematicHandler.getCompoundTag(plot, new RunnableVal() { - @Override public void run(final CompoundTag value) { - TaskManager.runTaskAsync(() -> { - String time = (System.currentTimeMillis() / 1000) + ""; - Location[] corners = plot.getCorners(); - corners[0] = corners[0].withY(0); - corners[1] = corners[1].withY(255); - int size = (corners[1].getX() - corners[0].getX()) + 1; - PlotId id = plot.getId(); - String world1 = plot.getArea().toString().replaceAll(";", "-") - .replaceAll("[^A-Za-z0-9]", ""); - final String file = time + '_' + world1 + '_' + id.getX() + '_' + id.getY() + '_' + size; - UUID uuid = player.getUUID(); - schematicHandler.upload(value, uuid, file, new RunnableVal() { - @Override public void run(URL url) { - plot.removeRunning(); - if (url == null) { - player.sendMessage(TranslatableCaption.of("backups.backup_save_failed")); - return; + this.schematicHandler.getCompoundTag(plot) + .whenComplete((compoundTag, throwable) -> { + TaskManager.runTaskAsync(() -> { + String time = (System.currentTimeMillis() / 1000) + ""; + Location[] corners = plot.getCorners(); + corners[0] = corners[0].withY(0); + corners[1] = corners[1].withY(255); + int size = (corners[1].getX() - corners[0].getX()) + 1; + PlotId id = plot.getId(); + String world1 = plot.getArea().toString().replaceAll(";", "-") + .replaceAll("[^A-Za-z0-9]", ""); + final String file = time + '_' + world1 + '_' + id.getX() + '_' + id.getY() + '_' + size; + UUID uuid = player.getUUID(); + schematicHandler.upload(compoundTag, uuid, file, new RunnableVal() { + @Override + public void run(URL url) { + plot.removeRunning(); + if (url == null) { + player.sendMessage(TranslatableCaption.of("backups.backup_save_failed")); + return; + } + player.sendMessage(TranslatableCaption.of("web.save_success")); + try (final MetaDataAccess> schematicAccess = + player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_SCHEMATICS)) { + schematicAccess.get().ifPresent(schematics -> schematics.add(file + ".schem")); + } } - player.sendMessage(TranslatableCaption.of("web.save_success")); - try (final MetaDataAccess> schematicAccess = - player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_SCHEMATICS)) { - schematicAccess.get().ifPresent(schematics -> schematics.add(file + ".schem")); - } - } + }); }); }); - } - }); return true; } } diff --git a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java index bfa462404..98e3d3f9a 100644 --- a/Core/src/main/java/com/plotsquared/core/configuration/Settings.java +++ b/Core/src/main/java/com/plotsquared/core/configuration/Settings.java @@ -409,12 +409,31 @@ public class Settings extends Config { } + @Deprecated @Comment("Schematic interface related settings") public static class Web { @Comment({"The web interface for schematics", " - All schematics are anonymous and private", " - Downloads can be deleted by the user", " - Supports plot uploads, downloads and saves",}) public static String URL = "https://schem.intellectualsites.com/plots/"; + @Comment({"Whether or not the legacy web interface will be used for /plot download and /plot save", + "Note that this will be removed in future versions. Updating to Arkitektonika is highly suggested"}) + public static boolean LEGACY_WEBINTERFACE = false; + } + + @Comment("Schematic web interface related settings") + public static class Arkitektonika { + + @Comment("The url of the backend server (Arkitektonika)") + public static String BACKEND_URL = "https://ark.jacobandersen.dev/"; + + @Comment({"The url used to generate a download link from.", + "{key} will be replaced with the generated key"}) + public static String DOWNLOAD_URL = "https://sw.jacobandersen.dev/download/{key}"; + + @Comment({"The url used to generate a deletion link from.", + "{key} will be replaced with the generated key"}) + public static String DELETE_URL = "https://sw.jacobandersen.dev/delete/{key}"; } diff --git a/Core/src/main/java/com/plotsquared/core/generator/HybridUtils.java b/Core/src/main/java/com/plotsquared/core/generator/HybridUtils.java index 0384f12ae..057e83edb 100644 --- a/Core/src/main/java/com/plotsquared/core/generator/HybridUtils.java +++ b/Core/src/main/java/com/plotsquared/core/generator/HybridUtils.java @@ -504,27 +504,25 @@ public class HybridUtils { int tz = sz - 1; int ty = get_ey(plotManager, queue, sx, ex, bz, tz, sy); - Set sideRoad = new HashSet<>(Collections.singletonList(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez))); - final Set intersection = new HashSet<>(Collections.singletonList(RegionUtil.createRegion(sx, ex, sy, ty, bz, tz))); + final Set sideRoad = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez)); + final Set intersection = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ty, bz, tz)); final String dir = "schematics" + File.separator + "GEN_ROAD_SCHEMATIC" + File.separator + plot.getArea().toString() + File.separator; - this.schematicHandler.getCompoundTag(world, sideRoad, new RunnableVal() { - @Override public void run(CompoundTag value) { - schematicHandler.save(value, dir + "sideroad.schem"); - schematicHandler.getCompoundTag(world, intersection, new RunnableVal() { - @Override public void run(CompoundTag value) { - schematicHandler.save(value, dir + "intersection.schem"); - plotworld.ROAD_SCHEMATIC_ENABLED = true; - try { - plotworld.setupSchematics(); - } catch (SchematicHandler.UnsupportedFormatException e) { - e.printStackTrace(); - } - } + this.schematicHandler.getCompoundTag(world, sideRoad) + .whenComplete((compoundTag, throwable) -> { + schematicHandler.save(compoundTag, dir + "sideroad.schem"); + schematicHandler.getCompoundTag(world, intersection) + .whenComplete((c, t) -> { + schematicHandler.save(c, dir + "intersection.schem"); + plotworld.ROAD_SCHEMATIC_ENABLED = true; + try { + plotworld.setupSchematics(); + } catch (SchematicHandler.UnsupportedFormatException e) { + e.printStackTrace(); + } + }); }); - } - }); return true; } diff --git a/Core/src/main/java/com/plotsquared/core/util/PlotUploader.java b/Core/src/main/java/com/plotsquared/core/util/PlotUploader.java new file mode 100644 index 000000000..184fc17ec --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/util/PlotUploader.java @@ -0,0 +1,196 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.util; + +import com.google.inject.Inject; +import com.intellectualsites.arkitektonika.Arkitektonika; +import com.intellectualsites.arkitektonika.SchematicKeys; +import com.plotsquared.core.PlotSquared; +import com.plotsquared.core.configuration.Settings; +import com.plotsquared.core.plot.Plot; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.NBTOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.zip.GZIPOutputStream; + +/** + * This class handles communication with the Arkitektonika REST service. + */ +public class PlotUploader { + private static final Logger logger = LoggerFactory.getLogger("P2/" + PlotUploader.class.getSimpleName()); + private static final Path TEMP_DIR = Paths.get(PlotSquared.platform().getDirectory().getPath()); + private final SchematicHandler schematicHandler; + private final Arkitektonika arkitektonika; + + /** + * Create a new PlotUploader instance that uses the given schematic handler to create + * schematics of plots. + * + * @param schematicHandler the handler to create schematics of plots. + */ + @Inject + public PlotUploader(@Nonnull final SchematicHandler schematicHandler) { + this.schematicHandler = schematicHandler; + this.arkitektonika = Arkitektonika.builder().withUrl(Settings.Arkitektonika.BACKEND_URL).build(); + } + + /** + * Upload a plot and retrieve a result. The plot will be saved into a temporary + * schematic file and uploaded to the REST service + * specified by {@link Settings.Arkitektonika#BACKEND_URL}. + * + * @param plot The plot to upload + * @return a {@link CompletableFuture} that provides a {@link PlotUploadResult} if finished. + */ + public CompletableFuture upload(@Nonnull final Plot plot) { + return this.schematicHandler.getCompoundTag(plot) + .handle((tag, t) -> { + plot.removeRunning(); + return tag; + }) + .thenApply(this::writeToTempFile) + .thenApply(this::uploadAndDelete) + .thenApply(this::wrapIntoResult); + } + + @Nonnull + private PlotUploadResult wrapIntoResult(@Nullable final SchematicKeys schematicKeys) { + if (schematicKeys == null) { + return PlotUploadResult.failed(); + } + String download = Settings.Arkitektonika.DOWNLOAD_URL.replace("{key}", schematicKeys.getAccessKey()); + String delete = Settings.Arkitektonika.DELETE_URL.replace("{key}", schematicKeys.getDeletionKey()); + return PlotUploadResult.success(download, delete); + } + + @Nullable + private SchematicKeys uploadAndDelete(@Nonnull final Path file) { + try { + final CompletableFuture upload = this.arkitektonika.upload(file.toFile()); + return upload.join(); + } catch (CompletionException e) { + logger.error("Failed to upload schematic", e); + return null; + } finally { + try { + Files.delete(file); + } catch (IOException e) { + logger.error("Failed to delete temporary file {}", file, e); + } + } + } + + @Nonnull + private Path writeToTempFile(@Nonnull final CompoundTag schematic) { + try { + final Path tempFile = Files.createTempFile(TEMP_DIR, null, null); + try (final OutputStream stream = Files.newOutputStream(tempFile)) { + writeSchematic(schematic, stream); + } + return tempFile; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Writes a schematic provided as CompoundTag to an OutputStream. + * + * @param schematic The schematic to write to the stream + * @param stream The stream to write the schematic to + * @throws IOException if an I/O error occurred + */ + private void writeSchematic(@Nonnull final CompoundTag schematic, @Nonnull final OutputStream stream) + throws IOException { + try (final NBTOutputStream nbtOutputStream = new NBTOutputStream(new GZIPOutputStream(stream))) { + nbtOutputStream.writeNamedTag("Schematic", schematic); + } + } + + /** + * A result of a plot upload process. + */ + public static class PlotUploadResult { + private final boolean success; + private final String downloadUrl; + private final String deletionUrl; + + private PlotUploadResult(boolean success, @Nullable final String downloadUrl, + @Nullable final String deletionUrl) { + this.success = success; + this.downloadUrl = downloadUrl; + this.deletionUrl = deletionUrl; + } + + @Nonnull + private static PlotUploadResult success(@Nonnull final String downloadUrl, @Nullable final String deletionUrl) { + return new PlotUploadResult(true, downloadUrl, deletionUrl); + } + + @Nonnull + private static PlotUploadResult failed() { + return new PlotUploadResult(false, null, null); + } + + /** + * Get whether this result is a success. + * + * @return {@code true} if this is a sucessful result, {@code false} otherwise. + */ + public boolean isSuccess() { + return success; + } + + /** + * Get the url that can be used to download the uploaded plot schematic. + * + * @return The url to download the schematic. + */ + public String getDownloadUrl() { + return downloadUrl; + } + + /** + * Get the url that can be used to delete the uploaded plot schematic. + * + * @return The url to delete the schematic. + */ + public String getDeletionUrl() { + return deletionUrl; + } + } +} diff --git a/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java b/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java index 6dc419f6b..42bd66fe2 100644 --- a/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java +++ b/Core/src/main/java/com/plotsquared/core/util/RegionUtil.java @@ -34,6 +34,7 @@ import com.sk89q.worldedit.regions.CuboidRegion; import javax.annotation.Nonnull; import java.awt.geom.Rectangle2D; import java.util.Collection; +import java.util.Iterator; public class RegionUtil { @@ -45,31 +46,33 @@ public class RegionUtil { } @Nonnull public static Location[] getCorners(String world, Collection regions) { - Location min = null; - Location max = null; - for (CuboidRegion region : regions) { - Location[] corners = getCorners(world, region); - if (min == null) { - min = corners[0]; - max = corners[1]; - continue; - } - Location pos1 = corners[0]; - Location pos2 = corners[1]; - if (pos2.getX() > max.getX()) { - max = max.withX(pos2.getX()); - } - if (pos1.getX() < min.getX()) { - min = min.withX(pos1.getX()); - } - if (pos2.getZ() > max.getZ()) { - max = max.withZ(pos2.getZ()); - } - if (pos1.getZ() < min.getZ()) { - min = min.withZ(pos1.getZ()); - } + CuboidRegion aabb = getAxisAlignedBoundingBox(regions); + return getCorners(world, aabb); + } + + /** + * Create a minimum {@link CuboidRegion} containing all given regions. + * + * @param regions The regions the bounding box should contain. + * @return a CuboidRegion that contains all given regions. + */ + @Nonnull + public static CuboidRegion getAxisAlignedBoundingBox(Iterable regions) { + Iterator iterator = regions.iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException("No regions given"); } - return new Location[] {min, max}; + CuboidRegion next = iterator.next(); + BlockVector3 min = next.getMinimumPoint(); + BlockVector3 max = next.getMaximumPoint(); + + while (iterator.hasNext()) { + next = iterator.next(); + // as max >= min, this is enough to check + min = min.getMinimum(next.getMinimumPoint()); + max = max.getMaximum(next.getMaximumPoint()); + } + return new CuboidRegion(min, max); } public static CuboidRegion createRegion(int pos1x, int pos2x, int pos1z, int pos2z) { diff --git a/Core/src/main/java/com/plotsquared/core/util/SchematicHandler.java b/Core/src/main/java/com/plotsquared/core/util/SchematicHandler.java index bc9581ea4..7a614f1f4 100644 --- a/Core/src/main/java/com/plotsquared/core/util/SchematicHandler.java +++ b/Core/src/main/java/com/plotsquared/core/util/SchematicHandler.java @@ -42,7 +42,7 @@ import com.plotsquared.core.queue.QueueCoordinator; import com.plotsquared.core.util.net.AbstractDelegateOutputStream; import com.plotsquared.core.util.task.RunnableVal; import com.plotsquared.core.util.task.TaskManager; -import com.plotsquared.core.util.task.TaskTime; +import com.plotsquared.core.util.task.YieldRunnable; import com.sk89q.jnbt.ByteArrayTag; import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.IntArrayTag; @@ -90,6 +90,8 @@ import java.net.URLConnection; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -99,11 +101,12 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Scanner; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -111,6 +114,7 @@ public abstract class SchematicHandler { private static final Logger logger = LoggerFactory.getLogger("P2/" + SchematicHandler.class.getSimpleName()); private static final Gson GSON = new Gson(); + private static final Path TEMP_DIR = Paths.get("TODO-PATH"); public static SchematicHandler manager; private final WorldUtil worldUtil; private boolean exportAll = false; @@ -121,6 +125,7 @@ public abstract class SchematicHandler { this.subscriberFactory = subscriberFactory; } + @Deprecated public static void upload(@Nullable UUID uuid, @Nullable final String file, @Nonnull final String extension, @@ -237,19 +242,18 @@ public abstract class SchematicHandler { } final Runnable THIS = this; - getCompoundTag(plot, new RunnableVal() { - @Override public void run(final CompoundTag value) { - if (value != null) { - TaskManager.runTaskAsync(() -> { - boolean result = save(value, directory + File.separator + name + ".schem"); - if (!result) { - logger.error("Failed to save {}", plot.getId()); - } - TaskManager.runTask(THIS); - }); - } - } - }); + getCompoundTag(plot) + .whenComplete((compoundTag, throwable) -> { + if (compoundTag != null) { + TaskManager.runTaskAsync(() -> { + boolean result = save(compoundTag, directory + File.separator + name + ".schem"); + if (!result) { + logger.error("Failed to save {}", plot.getId()); + } + TaskManager.runTask(THIS); + }); + } + }); } }); return true; @@ -487,6 +491,7 @@ public abstract class SchematicHandler { return null; } + @Deprecated public void upload(final CompoundTag tag, UUID uuid, String file, RunnableVal whenDone) { if (tag == null) { TaskManager.runTask(whenDone); @@ -529,37 +534,85 @@ public abstract class SchematicHandler { return true; } - public void getCompoundTag(final String world, final Set regions, final RunnableVal whenDone) { - // async + private void writeSchematicData(@Nonnull final Map schematic, + @Nonnull final Map palette, + @Nonnull final Map biomePalette, + @Nonnull final List tileEntities, + @Nonnull final ByteArrayOutputStream buffer, + @Nonnull final ByteArrayOutputStream biomeBuffer) { + schematic.put("PaletteMax", new IntTag(palette.size())); + + Map paletteTag = new HashMap<>(); + palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value))); + + schematic.put("Palette", new CompoundTag(paletteTag)); + schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray())); + schematic.put("TileEntities", new ListTag(CompoundTag.class, tileEntities)); + + schematic.put("BiomePaletteMax", new IntTag(biomePalette.size())); + + Map biomePaletteTag = new HashMap<>(); + biomePalette.forEach((key, value) -> biomePaletteTag.put(key, new IntTag(value))); + + schematic.put("BiomePalette", new CompoundTag(biomePaletteTag)); + schematic.put("BiomeData", new ByteArrayTag(biomeBuffer.toByteArray())); + } + + @Nonnull + private Map initSchematic(short width, short height, short length) { + Map schematic = new HashMap<>(); + schematic.put("Version", new IntTag(2)); + schematic.put("DataVersion", + new IntTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion())); + + Map metadata = new HashMap<>(); + metadata.put("WEOffsetX", new IntTag(0)); + metadata.put("WEOffsetY", new IntTag(0)); + metadata.put("WEOffsetZ", new IntTag(0)); + + schematic.put("Metadata", new CompoundTag(metadata)); + + schematic.put("Width", new ShortTag(width)); + schematic.put("Height", new ShortTag(height)); + schematic.put("Length", new ShortTag(length)); + + // The Sponge format Offset refers to the 'min' points location in the world. That's our 'Origin' + schematic.put("Offset", new IntArrayTag(new int[] {0, 0, 0,})); + return schematic; + } + + /** + * Get the given plot as {@link CompoundTag} matching the Sponge schematic format. + * + * @param plot The plot to get the contents from. + * @return a {@link CompletableFuture} that provides the created {@link CompoundTag}. + */ + public CompletableFuture getCompoundTag(@Nonnull final Plot plot) { + return getCompoundTag(Objects.requireNonNull(plot.getWorldName()), plot.getRegions()); + } + + /** + * Get the contents of the given regions in the given world as {@link CompoundTag} + * matching the Sponge schematic format. + * + * @param worldName The world to get the contents from. + * @param regions The regions to get the contents from. + * @return a {@link CompletableFuture} that provides the created {@link CompoundTag}. + */ + @Nonnull + public CompletableFuture getCompoundTag(@Nonnull final String worldName, + @Nonnull final Set regions) { + CompletableFuture completableFuture = new CompletableFuture<>(); TaskManager.runTaskAsync(() -> { // Main positions - Location[] corners = RegionUtil.getCorners(world, regions); - final Location bot = corners[0]; - final Location top = corners[1]; + CuboidRegion aabb = RegionUtil.getAxisAlignedBoundingBox(regions); + aabb.setWorld(this.worldUtil.getWeWorld(worldName)); - CuboidRegion cuboidRegion = new CuboidRegion(this.worldUtil.getWeWorld(world), bot.getBlockVector3(), top.getBlockVector3()); + final int width = aabb.getWidth(); + int height = aabb.getHeight(); + final int length = aabb.getLength(); - final int width = cuboidRegion.getWidth(); - int height = cuboidRegion.getHeight(); - final int length = cuboidRegion.getLength(); - Map schematic = new HashMap<>(); - schematic.put("Version", new IntTag(2)); - schematic.put("DataVersion", - new IntTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion())); - - Map metadata = new HashMap<>(); - metadata.put("WEOffsetX", new IntTag(0)); - metadata.put("WEOffsetY", new IntTag(0)); - metadata.put("WEOffsetZ", new IntTag(0)); - - schematic.put("Metadata", new CompoundTag(metadata)); - - schematic.put("Width", new ShortTag((short) width)); - schematic.put("Height", new ShortTag((short) height)); - schematic.put("Length", new ShortTag((short) length)); - - // The Sponge format Offset refers to the 'min' points location in the world. That's our 'Origin' - schematic.put("Offset", new IntArrayTag(new int[] {0, 0, 0,})); + Map schematic = initSchematic((short) width, (short) height, (short) length); Map palette = new HashMap<>(); Map biomePalette = new HashMap<>(); @@ -573,147 +626,110 @@ public abstract class SchematicHandler { @Override public void run() { if (queue.isEmpty()) { TaskManager.runTaskAsync(() -> { - schematic.put("PaletteMax", new IntTag(palette.size())); - - Map paletteTag = new HashMap<>(); - palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value))); - - schematic.put("Palette", new CompoundTag(paletteTag)); - schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray())); - schematic.put("TileEntities", new ListTag(CompoundTag.class, tileEntities)); - - schematic.put("BiomePaletteMax", new IntTag(biomePalette.size())); - - Map biomePaletteTag = new HashMap<>(); - biomePalette.forEach((key, value) -> biomePaletteTag.put(key, new IntTag(value))); - - schematic.put("BiomePalette", new CompoundTag(biomePaletteTag)); - schematic.put("BiomeData", new ByteArrayTag(biomeBuffer.toByteArray())); - whenDone.value = new CompoundTag(schematic); - TaskManager.runTask(whenDone); + writeSchematicData(schematic, palette, biomePalette, tileEntities, buffer, biomeBuffer); + completableFuture.complete(new CompoundTag(schematic)); }); return; } final Runnable regionTask = this; CuboidRegion region = queue.poll(); - final Location pos1 = Location.at(world, region.getMinimumPoint()); - final Location pos2 = Location.at(world, region.getMaximumPoint()); + final BlockVector3 minimum = region.getMinimumPoint(); + final BlockVector3 maximum = region.getMaximumPoint(); - final int p1x = pos1.getX(); - final int sy = pos1.getY(); - final int p1z = pos1.getZ(); - final int p2x = pos2.getX(); - final int p2z = pos2.getZ(); - final int ey = pos2.getY(); - Iterator yiter = IntStream.range(sy, ey + 1).iterator(); - final Runnable yTask = new Runnable() { + final int minX = minimum.getX(); + final int minZ = minimum.getZ(); + final int minY = minimum.getY(); + + final int maxX = maximum.getX(); + final int maxZ = maximum.getZ(); + final int maxY = maximum.getY(); + + final Runnable yTask = new YieldRunnable() { + int currentY = minY; + int currentX = minX; + int currentZ = minZ; @Override public void run() { - long ystart = System.currentTimeMillis(); - while (yiter.hasNext() && System.currentTimeMillis() - ystart < 20) { - final int y = yiter.next(); - Iterator ziter = IntStream.range(p1z, p2z + 1).iterator(); - final Runnable zTask = new Runnable() { - @Override public void run() { - long zstart = System.currentTimeMillis(); - while (ziter.hasNext() && System.currentTimeMillis() - zstart < 20) { - final int z = ziter.next(); - Iterator xiter = IntStream.range(p1x, p2x + 1).iterator(); - final Runnable xTask = new Runnable() { - @Override public void run() { - long xstart = System.currentTimeMillis(); - final int ry = y - sy; - final int rz = z - p1z; - while (xiter.hasNext() && System.currentTimeMillis() - xstart < 20) { - final int x = xiter.next(); - final int rx = x - p1x; - BlockVector3 point = BlockVector3.at(x, y, z); - BaseBlock block = cuboidRegion.getWorld().getFullBlock(point); - if (block.getNbtData() != null) { - Map values = new HashMap<>(); - for (Map.Entry entry : block.getNbtData().getValue().entrySet()) { - values.put(entry.getKey(), entry.getValue()); - } - // Remove 'id' if it exists. We want 'Id' - values.remove("id"); - - // Positions are kept in NBT, we don't want that. - values.remove("x"); - values.remove("y"); - values.remove("z"); - - values.put("Id", new StringTag(block.getNbtId())); - values.put("Pos", new IntArrayTag(new int[] {rx, ry, rz})); - - tileEntities.add(new CompoundTag(values)); - } - String blockKey = block.toImmutableState().getAsString(); - int blockId; - if (palette.containsKey(blockKey)) { - blockId = palette.get(blockKey); - } else { - blockId = palette.size(); - palette.put(blockKey, palette.size()); - } - - while ((blockId & -128) != 0) { - buffer.write(blockId & 127 | 128); - blockId >>>= 7; - } - buffer.write(blockId); - - if (ry > 0) { - continue; - } - BlockVector2 pt = BlockVector2.at(x, z); - BiomeType biome = cuboidRegion.getWorld().getBiome(pt); - String biomeStr = biome.getId(); - int biomeId; - if (biomePalette.containsKey(biomeStr)) { - biomeId = biomePalette.get(biomeStr); - } else { - biomeId = biomePalette.size(); - biomePalette.put(biomeStr, biomeId); - } - while ((biomeId & -128) != 0) { - biomeBuffer.write(biomeId & 127 | 128); - biomeId >>>= 7; - } - biomeBuffer.write(biomeId); - } - if (xiter.hasNext()) { - this.run(); - } - } - }; - xTask.run(); + long start = System.currentTimeMillis(); + for (; currentY <= maxY; currentY++) { + int relativeY = currentY - minY; + for (; currentZ <= maxZ; currentZ++) { + int relativeZ = currentZ - minZ; + for (; currentX <= maxX; currentX++) { + // if too much time was spent here, we yield this task + // note that current(X/Y/Z) aren't incremented, so the same position + // as *right now* will be visited again + if (System.currentTimeMillis() - start > 40) { + this.yield(); + return; } - if (ziter.hasNext()) { - this.run(); + int relativeX = currentX - minX; + BlockVector3 point = BlockVector3.at(currentX, currentY, currentZ); + BaseBlock block = aabb.getWorld().getFullBlock(point); + if (block.getNbtData() != null) { + Map values = new HashMap<>(); + for (Map.Entry entry : block.getNbtData().getValue().entrySet()) { + values.put(entry.getKey(), entry.getValue()); + } + // Remove 'id' if it exists. We want 'Id' + values.remove("id"); + + // Positions are kept in NBT, we don't want that. + values.remove("x"); + values.remove("y"); + values.remove("z"); + + values.put("Id", new StringTag(block.getNbtId())); + values.put("Pos", new IntArrayTag(new int[] {relativeX, relativeY, relativeZ})); + + tileEntities.add(new CompoundTag(values)); } + String blockKey = block.toImmutableState().getAsString(); + int blockId; + if (palette.containsKey(blockKey)) { + blockId = palette.get(blockKey); + } else { + blockId = palette.size(); + palette.put(blockKey, palette.size()); + } + + while ((blockId & -128) != 0) { + buffer.write(blockId & 127 | 128); + blockId >>>= 7; + } + buffer.write(blockId); + + if (relativeY > 0) { + continue; + } + BlockVector2 pt = BlockVector2.at(currentX, currentZ); + BiomeType biome = aabb.getWorld().getBiome(pt); + String biomeStr = biome.getId(); + int biomeId; + if (biomePalette.containsKey(biomeStr)) { + biomeId = biomePalette.get(biomeStr); + } else { + biomeId = biomePalette.size(); + biomePalette.put(biomeStr, biomeId); + } + while ((biomeId & -128) != 0) { + biomeBuffer.write(biomeId & 127 | 128); + biomeId >>>= 7; + } + biomeBuffer.write(biomeId); } - }; - zTask.run(); - } - if (yiter.hasNext()) { - TaskManager.runTaskLater(this, TaskTime.ticks(1L)); - } else { - regionTask.run(); + currentX = minX; // reset manually as not using local variable + } + currentZ = minZ; // reset manually as not using local variable } + regionTask.run(); } }; yTask.run(); } }); }); - } - - public void getCompoundTag(final Plot plot, final RunnableVal whenDone) { - getCompoundTag(plot.getWorldName(), plot.getRegions(), new RunnableVal() { - @Override public void run(CompoundTag value) { - whenDone.run(value); - } - }); + return completableFuture; } diff --git a/Core/src/main/java/com/plotsquared/core/util/task/YieldRunnable.java b/Core/src/main/java/com/plotsquared/core/util/task/YieldRunnable.java new file mode 100644 index 000000000..3a1285048 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/util/task/YieldRunnable.java @@ -0,0 +1,42 @@ +/* + * _____ _ _ _____ _ + * | __ \| | | | / ____| | | + * | |__) | | ___ | |_| (___ __ _ _ _ __ _ _ __ ___ __| | + * | ___/| |/ _ \| __|\___ \ / _` | | | |/ _` | '__/ _ \/ _` | + * | | | | (_) | |_ ____) | (_| | |_| | (_| | | | __/ (_| | + * |_| |_|\___/ \__|_____/ \__, |\__,_|\__,_|_| \___|\__,_| + * | | + * |_| + * PlotSquared plot management system for Minecraft + * Copyright (C) 2020 IntellectualSites + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.plotsquared.core.util.task; + +/** + * A runnable that can be yielded. + * If {@link #yield()} is invoked, {@link #run()} will be called + * on the next tick again. Implementations need to save their state + * correctly. + */ +public interface YieldRunnable extends Runnable { + + /** + * Runs the {@link #run()} method again on the next tick. + */ + default void yield() { + TaskManager.runTaskLater(this, TaskTime.ticks(1L)); + } +} diff --git a/Core/src/main/resources/lang/messages_en.json b/Core/src/main/resources/lang/messages_en.json index 346c23743..1e401fa77 100644 --- a/Core/src/main/resources/lang/messages_en.json +++ b/Core/src/main/resources/lang/messages_en.json @@ -24,8 +24,9 @@ "area.set_pos2": "You will now set pos2: . Note: The chosen plot size may result in the created area not exactly matching your second position.", "web.generating_link": "Processing plot...", + "web.plot_merged": "This plot is merged and therefore cannot be downloaded", "web.generating_link_failed": "Failed to generate download link!", - "web.generation_link_success": ">", + "web.generation_link_success": "Download: > \n Deletion: >\nAttention: Opening the deletion link will delete the file immediately.", "web.save_failed": "Failed to save.", "web.load_null": "Please use to get a list of schematics.", "web.load_failed": "Failed to load schematic.", diff --git a/build.gradle.kts b/build.gradle.kts index 723417229..b5b89e5ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -124,6 +124,10 @@ allprojects { id.set("N0tMyFaultOG") name.set("NotMyFault") } + developer { + id.set("SirYwell") + name.set("Hannes Greule") + } } scm {