Feature/v6/arkitektonika (#2916)

* Start working on Arkitektonika support

* Progress on Arkitektonika

* Add license headers

* Some QoL and javadocs

* Fix maximum calculation

* Fix minor formatting

* Reimplement legacy webinterface support

* Add documentation and fix deletion link

* Resolve conflicts/gradle stuff

* Fix links

* Make message readable

* Do not allow download of merged plots
This commit is contained in:
Hannes Greule 2020-12-05 19:50:41 +01:00 committed by GitHub
parent c8ad936d26
commit ff70d5db14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 569 additions and 256 deletions

View File

@ -63,6 +63,10 @@ dependencies {
// Other libraries // Other libraries
implementation("com.sk89q:squirrelid:1.0.0-SNAPSHOT") { isTransitive = false } 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 // Adventure
implementation("net.kyori:adventure-platform-bukkit:4.0.0-SNAPSHOT") implementation("net.kyori:adventure-platform-bukkit:4.0.0-SNAPSHOT")
} }
@ -91,6 +95,8 @@ tasks.named<ShadowJar>("shadowJar") {
relocate("javax.inject", "com.plotsquared.core.inject.javax") relocate("javax.inject", "com.plotsquared.core.inject.javax")
relocate("org.aopalliance", "com.plotsquared.core.aopalliance") relocate("org.aopalliance", "com.plotsquared.core.aopalliance")
relocate("com.intellectualsites.services", "com.plotsquared.core.services") 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. // Get rid of all the libs which are 100% unused.
minimize() minimize()

View File

@ -45,6 +45,7 @@ dependencies {
api("com.intellectualsites:Pipeline:1.4.0-SNAPSHOT") { api("com.intellectualsites:Pipeline:1.4.0-SNAPSHOT") {
exclude(group = "com.google.guava") exclude(group = "com.google.guava")
} }
api("com.intellectualsites.arkitektonika:Arkitektonika-Client:2.0-SNAPSHOT")
} }
tasks.processResources { tasks.processResources {

View File

@ -26,21 +26,21 @@
package com.plotsquared.core.command; package com.plotsquared.core.command;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.plotsquared.core.permissions.Permission;
import com.plotsquared.core.configuration.Settings; import com.plotsquared.core.configuration.Settings;
import com.plotsquared.core.configuration.caption.StaticCaption; import com.plotsquared.core.configuration.caption.StaticCaption;
import com.plotsquared.core.configuration.caption.TranslatableCaption; import com.plotsquared.core.configuration.caption.TranslatableCaption;
import com.plotsquared.core.permissions.Permission;
import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.player.PlotPlayer;
import com.plotsquared.core.plot.Plot; import com.plotsquared.core.plot.Plot;
import com.plotsquared.core.plot.flag.implementations.DoneFlag; import com.plotsquared.core.plot.flag.implementations.DoneFlag;
import com.plotsquared.core.plot.world.PlotAreaManager; import com.plotsquared.core.plot.world.PlotAreaManager;
import com.plotsquared.core.util.Permissions; import com.plotsquared.core.util.Permissions;
import com.plotsquared.core.util.PlotUploader;
import com.plotsquared.core.util.SchematicHandler; import com.plotsquared.core.util.SchematicHandler;
import com.plotsquared.core.util.StringMan; import com.plotsquared.core.util.StringMan;
import com.plotsquared.core.util.TabCompletions; import com.plotsquared.core.util.TabCompletions;
import com.plotsquared.core.util.WorldUtil; import com.plotsquared.core.util.WorldUtil;
import com.plotsquared.core.util.task.RunnableVal; import com.plotsquared.core.util.task.RunnableVal;
import com.sk89q.jnbt.CompoundTag;
import net.kyori.adventure.text.minimessage.Template; import net.kyori.adventure.text.minimessage.Template;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -60,13 +60,16 @@ import java.util.stream.Collectors;
public class Download extends SubCommand { public class Download extends SubCommand {
private final PlotAreaManager plotAreaManager; private final PlotAreaManager plotAreaManager;
private final SchematicHandler schematicHandler; private final PlotUploader plotUploader;
@Nonnull private final SchematicHandler schematicHandler;
private final WorldUtil worldUtil; private final WorldUtil worldUtil;
@Inject public Download(@Nonnull final PlotAreaManager plotAreaManager, @Inject public Download(@Nonnull final PlotAreaManager plotAreaManager,
@Nonnull final PlotUploader plotUploader,
@Nonnull final SchematicHandler schematicHandler, @Nonnull final SchematicHandler schematicHandler,
@Nonnull final WorldUtil worldUtil) { @Nonnull final WorldUtil worldUtil) {
this.plotAreaManager = plotAreaManager; this.plotAreaManager = plotAreaManager;
this.plotUploader = plotUploader;
this.schematicHandler = schematicHandler; this.schematicHandler = schematicHandler;
this.worldUtil = worldUtil; this.worldUtil = worldUtil;
} }
@ -96,6 +99,10 @@ public class Download extends SubCommand {
player.sendMessage(TranslatableCaption.of("permission.no_plot_perms")); player.sendMessage(TranslatableCaption.of("permission.no_plot_perms"));
return false; return false;
} }
if (plot.isMerged()) {
player.sendMessage(TranslatableCaption.of("web.plot_merged"));
return false;
}
if (plot.getRunning() > 0) { if (plot.getRunning() > 0) {
player.sendMessage(TranslatableCaption.of("errors.wait_for_timer")); player.sendMessage(TranslatableCaption.of("errors.wait_for_timer"));
return false; return false;
@ -103,27 +110,11 @@ public class Download extends SubCommand {
if (args.length == 0 || (args.length == 1 && StringMan if (args.length == 0 || (args.length == 1 && StringMan
.isEqualIgnoreCaseToAny(args[0], "sch", "schem", "schematic"))) { .isEqualIgnoreCaseToAny(args[0], "sch", "schem", "schematic"))) {
if (plot.getVolume() > Integer.MAX_VALUE) { if (plot.getVolume() > Integer.MAX_VALUE) {
player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large")); player.sendMessage(TranslatableCaption.of("schematics.schematic_too_large"));
return false; return false;
} }
plot.addRunning(); plot.addRunning();
this.schematicHandler.getCompoundTag(plot, new RunnableVal<CompoundTag>() { upload(player, plot);
@Override public void run(CompoundTag value) {
plot.removeRunning();
schematicHandler.upload(value, null, null, new RunnableVal<URL>() {
@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())
);
}
});
}
});
} else if (args.length == 1 && StringMan } else if (args.length == 1 && StringMan
.isEqualIgnoreCaseToAny(args[0], "mcr", "world", "mca")) { .isEqualIgnoreCaseToAny(args[0], "mcr", "world", "mca")) {
if (!Permissions.hasPermission(player, Permission.PERMISSION_DOWNLOAD_WORLD)) { if (!Permissions.hasPermission(player, Permission.PERMISSION_DOWNLOAD_WORLD)) {
@ -153,6 +144,7 @@ public class Download extends SubCommand {
player.sendMessage(TranslatableCaption.of("web.generating_link")); player.sendMessage(TranslatableCaption.of("web.generating_link"));
return true; return true;
} }
@Override @Override
public Collection<Command> tab(final PlotPlayer<?> player, final String[] args, final boolean space) { public Collection<Command> tab(final PlotPlayer<?> player, final String[] args, final boolean space) {
if (args.length == 1) { if (args.length == 1) {
@ -173,4 +165,36 @@ public class Download extends SubCommand {
} }
return TabCompletions.completePlayers(String.join(",", args).trim(), Collections.emptyList()); 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<URL>() {
@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()));
}
});
}
} }

View File

@ -76,7 +76,10 @@ public class MainCommand extends Command {
final List<Class<? extends Command>> commands = new LinkedList<>(); final List<Class<? extends Command>> commands = new LinkedList<>();
commands.add(Caps.class); commands.add(Caps.class);
commands.add(Buy.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(Load.class);
commands.add(Confirm.class); commands.add(Confirm.class);
commands.add(Template.class); commands.add(Template.class);

View File

@ -86,36 +86,36 @@ public class Save extends SubCommand {
return false; return false;
} }
plot.addRunning(); plot.addRunning();
this.schematicHandler.getCompoundTag(plot, new RunnableVal<CompoundTag>() { this.schematicHandler.getCompoundTag(plot)
@Override public void run(final CompoundTag value) { .whenComplete((compoundTag, throwable) -> {
TaskManager.runTaskAsync(() -> { TaskManager.runTaskAsync(() -> {
String time = (System.currentTimeMillis() / 1000) + ""; String time = (System.currentTimeMillis() / 1000) + "";
Location[] corners = plot.getCorners(); Location[] corners = plot.getCorners();
corners[0] = corners[0].withY(0); corners[0] = corners[0].withY(0);
corners[1] = corners[1].withY(255); corners[1] = corners[1].withY(255);
int size = (corners[1].getX() - corners[0].getX()) + 1; int size = (corners[1].getX() - corners[0].getX()) + 1;
PlotId id = plot.getId(); PlotId id = plot.getId();
String world1 = plot.getArea().toString().replaceAll(";", "-") String world1 = plot.getArea().toString().replaceAll(";", "-")
.replaceAll("[^A-Za-z0-9]", ""); .replaceAll("[^A-Za-z0-9]", "");
final String file = time + '_' + world1 + '_' + id.getX() + '_' + id.getY() + '_' + size; final String file = time + '_' + world1 + '_' + id.getX() + '_' + id.getY() + '_' + size;
UUID uuid = player.getUUID(); UUID uuid = player.getUUID();
schematicHandler.upload(value, uuid, file, new RunnableVal<URL>() { schematicHandler.upload(compoundTag, uuid, file, new RunnableVal<URL>() {
@Override public void run(URL url) { @Override
plot.removeRunning(); public void run(URL url) {
if (url == null) { plot.removeRunning();
player.sendMessage(TranslatableCaption.of("backups.backup_save_failed")); if (url == null) {
return; player.sendMessage(TranslatableCaption.of("backups.backup_save_failed"));
return;
}
player.sendMessage(TranslatableCaption.of("web.save_success"));
try (final MetaDataAccess<List<String>> schematicAccess =
player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_SCHEMATICS)) {
schematicAccess.get().ifPresent(schematics -> schematics.add(file + ".schem"));
}
} }
player.sendMessage(TranslatableCaption.of("web.save_success")); });
try (final MetaDataAccess<List<String>> schematicAccess =
player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_SCHEMATICS)) {
schematicAccess.get().ifPresent(schematics -> schematics.add(file + ".schem"));
}
}
}); });
}); });
}
});
return true; return true;
} }
} }

View File

@ -409,12 +409,31 @@ public class Settings extends Config {
} }
@Deprecated
@Comment("Schematic interface related settings") @Comment("Schematic interface related settings")
public static class Web { public static class Web {
@Comment({"The web interface for schematics", " - All schematics are anonymous and private", @Comment({"The web interface for schematics", " - All schematics are anonymous and private",
" - Downloads can be deleted by the user", " - Downloads can be deleted by the user",
" - Supports plot uploads, downloads and saves",}) public static String URL = " - Supports plot uploads, downloads and saves",}) public static String URL =
"https://schem.intellectualsites.com/plots/"; "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}";
} }

View File

@ -504,27 +504,25 @@ public class HybridUtils {
int tz = sz - 1; int tz = sz - 1;
int ty = get_ey(plotManager, queue, sx, ex, bz, tz, sy); int ty = get_ey(plotManager, queue, sx, ex, bz, tz, sy);
Set<CuboidRegion> sideRoad = new HashSet<>(Collections.singletonList(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez))); final Set<CuboidRegion> sideRoad = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez));
final Set<CuboidRegion> intersection = new HashSet<>(Collections.singletonList(RegionUtil.createRegion(sx, ex, sy, ty, bz, tz))); final Set<CuboidRegion> 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; final String dir = "schematics" + File.separator + "GEN_ROAD_SCHEMATIC" + File.separator + plot.getArea().toString() + File.separator;
this.schematicHandler.getCompoundTag(world, sideRoad, new RunnableVal<CompoundTag>() { this.schematicHandler.getCompoundTag(world, sideRoad)
@Override public void run(CompoundTag value) { .whenComplete((compoundTag, throwable) -> {
schematicHandler.save(value, dir + "sideroad.schem"); schematicHandler.save(compoundTag, dir + "sideroad.schem");
schematicHandler.getCompoundTag(world, intersection, new RunnableVal<CompoundTag>() { schematicHandler.getCompoundTag(world, intersection)
@Override public void run(CompoundTag value) { .whenComplete((c, t) -> {
schematicHandler.save(value, dir + "intersection.schem"); schematicHandler.save(c, dir + "intersection.schem");
plotworld.ROAD_SCHEMATIC_ENABLED = true; plotworld.ROAD_SCHEMATIC_ENABLED = true;
try { try {
plotworld.setupSchematics(); plotworld.setupSchematics();
} catch (SchematicHandler.UnsupportedFormatException e) { } catch (SchematicHandler.UnsupportedFormatException e) {
e.printStackTrace(); e.printStackTrace();
} }
} });
}); });
}
});
return true; return true;
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<PlotUploadResult> 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<SchematicKeys> 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;
}
}
}

View File

@ -34,6 +34,7 @@ import com.sk89q.worldedit.regions.CuboidRegion;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
public class RegionUtil { public class RegionUtil {
@ -45,31 +46,33 @@ public class RegionUtil {
} }
@Nonnull public static Location[] getCorners(String world, Collection<CuboidRegion> regions) { @Nonnull public static Location[] getCorners(String world, Collection<CuboidRegion> regions) {
Location min = null; CuboidRegion aabb = getAxisAlignedBoundingBox(regions);
Location max = null; return getCorners(world, aabb);
for (CuboidRegion region : regions) { }
Location[] corners = getCorners(world, region);
if (min == null) { /**
min = corners[0]; * Create a minimum {@link CuboidRegion} containing all given regions.
max = corners[1]; *
continue; * @param regions The regions the bounding box should contain.
} * @return a CuboidRegion that contains all given regions.
Location pos1 = corners[0]; */
Location pos2 = corners[1]; @Nonnull
if (pos2.getX() > max.getX()) { public static CuboidRegion getAxisAlignedBoundingBox(Iterable<CuboidRegion> regions) {
max = max.withX(pos2.getX()); Iterator<CuboidRegion> iterator = regions.iterator();
} if (!iterator.hasNext()) {
if (pos1.getX() < min.getX()) { throw new IllegalArgumentException("No regions given");
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());
}
} }
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) { public static CuboidRegion createRegion(int pos1x, int pos2x, int pos1z, int pos2z) {

View File

@ -42,7 +42,7 @@ import com.plotsquared.core.queue.QueueCoordinator;
import com.plotsquared.core.util.net.AbstractDelegateOutputStream; import com.plotsquared.core.util.net.AbstractDelegateOutputStream;
import com.plotsquared.core.util.task.RunnableVal; import com.plotsquared.core.util.task.RunnableVal;
import com.plotsquared.core.util.task.TaskManager; 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.ByteArrayTag;
import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntArrayTag; import com.sk89q.jnbt.IntArrayTag;
@ -90,6 +90,8 @@ import java.net.URLConnection;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -99,11 +101,12 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Scanner; import java.util.Scanner;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; 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 Logger logger = LoggerFactory.getLogger("P2/" + SchematicHandler.class.getSimpleName());
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
private static final Path TEMP_DIR = Paths.get("TODO-PATH");
public static SchematicHandler manager; public static SchematicHandler manager;
private final WorldUtil worldUtil; private final WorldUtil worldUtil;
private boolean exportAll = false; private boolean exportAll = false;
@ -121,6 +125,7 @@ public abstract class SchematicHandler {
this.subscriberFactory = subscriberFactory; this.subscriberFactory = subscriberFactory;
} }
@Deprecated
public static void upload(@Nullable UUID uuid, public static void upload(@Nullable UUID uuid,
@Nullable final String file, @Nullable final String file,
@Nonnull final String extension, @Nonnull final String extension,
@ -237,19 +242,18 @@ public abstract class SchematicHandler {
} }
final Runnable THIS = this; final Runnable THIS = this;
getCompoundTag(plot, new RunnableVal<CompoundTag>() { getCompoundTag(plot)
@Override public void run(final CompoundTag value) { .whenComplete((compoundTag, throwable) -> {
if (value != null) { if (compoundTag != null) {
TaskManager.runTaskAsync(() -> { TaskManager.runTaskAsync(() -> {
boolean result = save(value, directory + File.separator + name + ".schem"); boolean result = save(compoundTag, directory + File.separator + name + ".schem");
if (!result) { if (!result) {
logger.error("Failed to save {}", plot.getId()); logger.error("Failed to save {}", plot.getId());
} }
TaskManager.runTask(THIS); TaskManager.runTask(THIS);
}); });
} }
} });
});
} }
}); });
return true; return true;
@ -487,6 +491,7 @@ public abstract class SchematicHandler {
return null; return null;
} }
@Deprecated
public void upload(final CompoundTag tag, UUID uuid, String file, RunnableVal<URL> whenDone) { public void upload(final CompoundTag tag, UUID uuid, String file, RunnableVal<URL> whenDone) {
if (tag == null) { if (tag == null) {
TaskManager.runTask(whenDone); TaskManager.runTask(whenDone);
@ -529,37 +534,85 @@ public abstract class SchematicHandler {
return true; return true;
} }
public void getCompoundTag(final String world, final Set<CuboidRegion> regions, final RunnableVal<CompoundTag> whenDone) { private void writeSchematicData(@Nonnull final Map<String, Tag> schematic,
// async @Nonnull final Map<String, Integer> palette,
@Nonnull final Map<String, Integer> biomePalette,
@Nonnull final List<CompoundTag> tileEntities,
@Nonnull final ByteArrayOutputStream buffer,
@Nonnull final ByteArrayOutputStream biomeBuffer) {
schematic.put("PaletteMax", new IntTag(palette.size()));
Map<String, Tag> 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<String, Tag> 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<String, Tag> initSchematic(short width, short height, short length) {
Map<String, Tag> schematic = new HashMap<>();
schematic.put("Version", new IntTag(2));
schematic.put("DataVersion",
new IntTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion()));
Map<String, Tag> 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<CompoundTag> 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<CompoundTag> getCompoundTag(@Nonnull final String worldName,
@Nonnull final Set<CuboidRegion> regions) {
CompletableFuture<CompoundTag> completableFuture = new CompletableFuture<>();
TaskManager.runTaskAsync(() -> { TaskManager.runTaskAsync(() -> {
// Main positions // Main positions
Location[] corners = RegionUtil.getCorners(world, regions); CuboidRegion aabb = RegionUtil.getAxisAlignedBoundingBox(regions);
final Location bot = corners[0]; aabb.setWorld(this.worldUtil.getWeWorld(worldName));
final Location top = corners[1];
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(); Map<String, Tag> schematic = initSchematic((short) width, (short) height, (short) length);
int height = cuboidRegion.getHeight();
final int length = cuboidRegion.getLength();
Map<String, Tag> schematic = new HashMap<>();
schematic.put("Version", new IntTag(2));
schematic.put("DataVersion",
new IntTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion()));
Map<String, Tag> 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<String, Integer> palette = new HashMap<>(); Map<String, Integer> palette = new HashMap<>();
Map<String, Integer> biomePalette = new HashMap<>(); Map<String, Integer> biomePalette = new HashMap<>();
@ -573,147 +626,110 @@ public abstract class SchematicHandler {
@Override public void run() { @Override public void run() {
if (queue.isEmpty()) { if (queue.isEmpty()) {
TaskManager.runTaskAsync(() -> { TaskManager.runTaskAsync(() -> {
schematic.put("PaletteMax", new IntTag(palette.size())); writeSchematicData(schematic, palette, biomePalette, tileEntities, buffer, biomeBuffer);
completableFuture.complete(new CompoundTag(schematic));
Map<String, Tag> 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<String, Tag> 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);
}); });
return; return;
} }
final Runnable regionTask = this; final Runnable regionTask = this;
CuboidRegion region = queue.poll(); CuboidRegion region = queue.poll();
final Location pos1 = Location.at(world, region.getMinimumPoint()); final BlockVector3 minimum = region.getMinimumPoint();
final Location pos2 = Location.at(world, region.getMaximumPoint()); final BlockVector3 maximum = region.getMaximumPoint();
final int p1x = pos1.getX(); final int minX = minimum.getX();
final int sy = pos1.getY(); final int minZ = minimum.getZ();
final int p1z = pos1.getZ(); final int minY = minimum.getY();
final int p2x = pos2.getX();
final int p2z = pos2.getZ(); final int maxX = maximum.getX();
final int ey = pos2.getY(); final int maxZ = maximum.getZ();
Iterator<Integer> yiter = IntStream.range(sy, ey + 1).iterator(); final int maxY = maximum.getY();
final Runnable yTask = new Runnable() {
final Runnable yTask = new YieldRunnable() {
int currentY = minY;
int currentX = minX;
int currentZ = minZ;
@Override public void run() { @Override public void run() {
long ystart = System.currentTimeMillis(); long start = System.currentTimeMillis();
while (yiter.hasNext() && System.currentTimeMillis() - ystart < 20) { for (; currentY <= maxY; currentY++) {
final int y = yiter.next(); int relativeY = currentY - minY;
Iterator<Integer> ziter = IntStream.range(p1z, p2z + 1).iterator(); for (; currentZ <= maxZ; currentZ++) {
final Runnable zTask = new Runnable() { int relativeZ = currentZ - minZ;
@Override public void run() { for (; currentX <= maxX; currentX++) {
long zstart = System.currentTimeMillis(); // if too much time was spent here, we yield this task
while (ziter.hasNext() && System.currentTimeMillis() - zstart < 20) { // note that current(X/Y/Z) aren't incremented, so the same position
final int z = ziter.next(); // as *right now* will be visited again
Iterator<Integer> xiter = IntStream.range(p1x, p2x + 1).iterator(); if (System.currentTimeMillis() - start > 40) {
final Runnable xTask = new Runnable() { this.yield();
@Override public void run() { return;
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<String, Tag> values = new HashMap<>();
for (Map.Entry<String, Tag> 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();
} }
if (ziter.hasNext()) { int relativeX = currentX - minX;
this.run(); BlockVector3 point = BlockVector3.at(currentX, currentY, currentZ);
BaseBlock block = aabb.getWorld().getFullBlock(point);
if (block.getNbtData() != null) {
Map<String, Tag> values = new HashMap<>();
for (Map.Entry<String, Tag> 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);
} }
}; currentX = minX; // reset manually as not using local variable
zTask.run(); }
} currentZ = minZ; // reset manually as not using local variable
if (yiter.hasNext()) {
TaskManager.runTaskLater(this, TaskTime.ticks(1L));
} else {
regionTask.run();
} }
regionTask.run();
} }
}; };
yTask.run(); yTask.run();
} }
}); });
}); });
} return completableFuture;
public void getCompoundTag(final Plot plot, final RunnableVal<CompoundTag> whenDone) {
getCompoundTag(plot.getWorldName(), plot.getRegions(), new RunnableVal<CompoundTag>() {
@Override public void run(CompoundTag value) {
whenDone.run(value);
}
});
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -24,8 +24,9 @@
"area.set_pos2": "You will now set pos2: <command>. Note: The chosen plot size may result in the created area not exactly matching your second position.", "area.set_pos2": "You will now set pos2: <command>. Note: The chosen plot size may result in the created area not exactly matching your second position.",
"web.generating_link": "<prefix><gold>Processing plot...</gold>", "web.generating_link": "<prefix><gold>Processing plot...</gold>",
"web.plot_merged": "<prefix><red>This plot is merged and therefore cannot be downloaded</red>",
"web.generating_link_failed": "<prefix><red>Failed to generate download link!</red>", "web.generating_link_failed": "<prefix><red>Failed to generate download link!</red>",
"web.generation_link_success": "<click:open_url:<url>><url></click>", "web.generation_link_success": "<prefix><gold>Download: <gray><click:open_url:<download>><download></click></gray> \n Deletion: <gray><click:open_url:<delete>><delete></click></gray>\n<red>Attention: Opening the deletion link will delete the file immediately.</red></gold>",
"web.save_failed": "<prefix><red>Failed to save.</red>", "web.save_failed": "<prefix><red>Failed to save.</red>",
"web.load_null": "<prefix><gray>Please use </gray><dark_aqua><command> </dark_aqua><gray>to get a list of schematics.</gray>", "web.load_null": "<prefix><gray>Please use </gray><dark_aqua><command> </dark_aqua><gray>to get a list of schematics.</gray>",
"web.load_failed": "<prefix><red>Failed to load schematic.</red>", "web.load_failed": "<prefix><red>Failed to load schematic.</red>",

View File

@ -124,6 +124,10 @@ allprojects {
id.set("N0tMyFaultOG") id.set("N0tMyFaultOG")
name.set("NotMyFault") name.set("NotMyFault")
} }
developer {
id.set("SirYwell")
name.set("Hannes Greule")
}
} }
scm { scm {