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
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>("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()

View File

@ -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 {

View File

@ -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<CompoundTag>() {
@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())
);
}
});
}
});
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<Command> 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<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<>();
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);

View File

@ -86,36 +86,36 @@ public class Save extends SubCommand {
return false;
}
plot.addRunning();
this.schematicHandler.getCompoundTag(plot, new RunnableVal<CompoundTag>() {
@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<URL>() {
@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<URL>() {
@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<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;
}
}

View File

@ -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}";
}

View File

@ -504,27 +504,25 @@ public class HybridUtils {
int tz = sz - 1;
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> intersection = new HashSet<>(Collections.singletonList(RegionUtil.createRegion(sx, ex, sy, ty, bz, tz)));
final Set<CuboidRegion> sideRoad = Collections.singleton(RegionUtil.createRegion(sx, ex, sy, ey, sz, ez));
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;
this.schematicHandler.getCompoundTag(world, sideRoad, new RunnableVal<CompoundTag>() {
@Override public void run(CompoundTag value) {
schematicHandler.save(value, dir + "sideroad.schem");
schematicHandler.getCompoundTag(world, intersection, new RunnableVal<CompoundTag>() {
@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;
}

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 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<CuboidRegion> 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<CuboidRegion> regions) {
Iterator<CuboidRegion> 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) {

View File

@ -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<CompoundTag>() {
@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<URL> 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<CuboidRegion> regions, final RunnableVal<CompoundTag> whenDone) {
// async
private void writeSchematicData(@Nonnull final Map<String, Tag> schematic,
@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(() -> {
// 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<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, Tag> schematic = initSchematic((short) width, (short) height, (short) length);
Map<String, Integer> palette = new HashMap<>();
Map<String, Integer> 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<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);
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<Integer> 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<Integer> 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<Integer> 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<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();
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<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);
}
};
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<CompoundTag> whenDone) {
getCompoundTag(plot.getWorldName(), plot.getRegions(), new RunnableVal<CompoundTag>() {
@Override public void run(CompoundTag value) {
whenDone.run(value);
}
});
return completableFuture;
}

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.",
"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.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.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>",

View File

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