001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.util;
020
021import com.google.gson.Gson;
022import com.google.gson.JsonArray;
023import com.google.gson.JsonParseException;
024import com.google.inject.Inject;
025import com.plotsquared.core.PlotSquared;
026import com.plotsquared.core.configuration.Settings;
027import com.plotsquared.core.configuration.caption.TranslatableCaption;
028import com.plotsquared.core.generator.ClassicPlotWorld;
029import com.plotsquared.core.inject.factory.ProgressSubscriberFactory;
030import com.plotsquared.core.location.Location;
031import com.plotsquared.core.player.PlotPlayer;
032import com.plotsquared.core.plot.Plot;
033import com.plotsquared.core.plot.PlotArea;
034import com.plotsquared.core.plot.schematic.Schematic;
035import com.plotsquared.core.queue.QueueCoordinator;
036import com.plotsquared.core.util.net.AbstractDelegateOutputStream;
037import com.plotsquared.core.util.task.RunnableVal;
038import com.plotsquared.core.util.task.TaskManager;
039import com.plotsquared.core.util.task.YieldRunnable;
040import com.sk89q.jnbt.ByteArrayTag;
041import com.sk89q.jnbt.CompoundTag;
042import com.sk89q.jnbt.IntArrayTag;
043import com.sk89q.jnbt.IntTag;
044import com.sk89q.jnbt.ListTag;
045import com.sk89q.jnbt.NBTInputStream;
046import com.sk89q.jnbt.NBTOutputStream;
047import com.sk89q.jnbt.ShortTag;
048import com.sk89q.jnbt.StringTag;
049import com.sk89q.jnbt.Tag;
050import com.sk89q.worldedit.WorldEdit;
051import com.sk89q.worldedit.extension.platform.Capability;
052import com.sk89q.worldedit.extent.clipboard.Clipboard;
053import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
054import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats;
055import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
056import com.sk89q.worldedit.extent.clipboard.io.MCEditSchematicReader;
057import com.sk89q.worldedit.extent.clipboard.io.SpongeSchematicReader;
058import com.sk89q.worldedit.math.BlockVector2;
059import com.sk89q.worldedit.math.BlockVector3;
060import com.sk89q.worldedit.regions.CuboidRegion;
061import com.sk89q.worldedit.regions.Region;
062import com.sk89q.worldedit.regions.RegionIntersection;
063import com.sk89q.worldedit.world.World;
064import com.sk89q.worldedit.world.biome.BiomeType;
065import com.sk89q.worldedit.world.block.BaseBlock;
066import com.sk89q.worldedit.world.block.BlockTypes;
067import org.apache.logging.log4j.LogManager;
068import org.apache.logging.log4j.Logger;
069import org.checkerframework.checker.nullness.qual.NonNull;
070import org.checkerframework.checker.nullness.qual.Nullable;
071
072import java.io.BufferedReader;
073import java.io.ByteArrayOutputStream;
074import java.io.File;
075import java.io.FileInputStream;
076import java.io.FileNotFoundException;
077import java.io.FileOutputStream;
078import java.io.IOException;
079import java.io.InputStream;
080import java.io.InputStreamReader;
081import java.io.OutputStream;
082import java.io.OutputStreamWriter;
083import java.io.PrintWriter;
084import java.net.HttpURLConnection;
085import java.net.MalformedURLException;
086import java.net.URI;
087import java.net.URL;
088import java.net.URLConnection;
089import java.nio.channels.Channels;
090import java.nio.channels.ReadableByteChannel;
091import java.nio.charset.StandardCharsets;
092import java.util.ArrayList;
093import java.util.Arrays;
094import java.util.Collection;
095import java.util.Collections;
096import java.util.HashMap;
097import java.util.Iterator;
098import java.util.List;
099import java.util.Map;
100import java.util.Objects;
101import java.util.Scanner;
102import java.util.Set;
103import java.util.UUID;
104import java.util.concurrent.CompletableFuture;
105import java.util.stream.Collectors;
106import java.util.zip.GZIPInputStream;
107import java.util.zip.GZIPOutputStream;
108
109public abstract class SchematicHandler {
110
111    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + SchematicHandler.class.getSimpleName());
112    private static final Gson GSON = new Gson();
113    public static SchematicHandler manager;
114    private final WorldUtil worldUtil;
115    private final ProgressSubscriberFactory subscriberFactory;
116    private boolean exportAll = false;
117
118    @Inject
119    public SchematicHandler(final @NonNull WorldUtil worldUtil, @NonNull ProgressSubscriberFactory subscriberFactory) {
120        this.worldUtil = worldUtil;
121        this.subscriberFactory = subscriberFactory;
122    }
123
124    @Deprecated(forRemoval = true, since = "6.0.0")
125    public static void upload(
126            @Nullable UUID uuid,
127            final @Nullable String file,
128            final @NonNull String extension,
129            final @Nullable RunnableVal<OutputStream> writeTask,
130            final @NonNull RunnableVal<URL> whenDone
131    ) {
132        if (writeTask == null) {
133            TaskManager.runTask(whenDone);
134            return;
135        }
136        final String filename;
137        final String website;
138        if (uuid == null) {
139            uuid = UUID.randomUUID();
140            website = Settings.Web.URL + "upload.php?" + uuid;
141            filename = "plot." + extension;
142        } else {
143            website = Settings.Web.URL + "save.php?" + uuid;
144            filename = file + '.' + extension;
145        }
146        final URL url;
147        try {
148            url = URI.create(Settings.Web.URL + "?key=" + uuid + "&type=" + extension).toURL();
149        } catch (MalformedURLException e) {
150            e.printStackTrace();
151            whenDone.run();
152            return;
153        }
154        TaskManager.runTaskAsync(() -> {
155            try {
156                String boundary = Long.toHexString(System.currentTimeMillis());
157                URLConnection con = URI.create(website).toURL().openConnection();
158                con.setDoOutput(true);
159                con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
160                try (OutputStream output = con.getOutputStream();
161                     PrintWriter writer = new PrintWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8), true)) {
162                    String CRLF = "\r\n";
163                    writer.append("--").append(boundary).append(CRLF);
164                    writer.append("Content-Disposition: form-data; name=\"param\"").append(CRLF);
165                    writer.append("Content-Type: text/plain; charset=").append(StandardCharsets.UTF_8.displayName()).append(CRLF);
166                    String param = "value";
167                    writer.append(CRLF).append(param).append(CRLF).flush();
168                    writer.append("--").append(boundary).append(CRLF);
169                    writer.append("Content-Disposition: form-data; name=\"schematicFile\"; filename=\"").append(filename)
170                            .append(String.valueOf('"')).append(CRLF);
171                    writer.append("Content-Type: ").append(URLConnection.guessContentTypeFromName(filename)).append(CRLF);
172                    writer.append("Content-Transfer-Encoding: binary").append(CRLF);
173                    writer.append(CRLF).flush();
174                    writeTask.value = new AbstractDelegateOutputStream(output) {
175                        @Override
176                        public void close() {
177                        } // Don't close
178                    };
179                    writeTask.run();
180                    output.flush();
181                    writer.append(CRLF).flush();
182                    writer.append("--").append(boundary).append("--").append(CRLF).flush();
183                }
184                String content;
185                try (Scanner scanner = new Scanner(con.getInputStream()).useDelimiter("\\A")) {
186                    content = scanner.next().trim();
187                }
188                if (!content.startsWith("<")) {
189                }
190                int responseCode = ((HttpURLConnection) con).getResponseCode();
191                if (responseCode == 200) {
192                    whenDone.value = url;
193                }
194                TaskManager.runTask(whenDone);
195            } catch (IOException e) {
196                e.printStackTrace();
197                TaskManager.runTask(whenDone);
198            }
199        });
200    }
201
202    public boolean exportAll(
203            Collection<Plot> collection,
204            final File outputDir,
205            final String namingScheme,
206            final Runnable ifSuccess
207    ) {
208        if (this.exportAll) {
209            return false;
210        }
211        if (collection.isEmpty()) {
212            return false;
213        }
214        this.exportAll = true;
215        final ArrayList<Plot> plots = new ArrayList<>(collection);
216        TaskManager.runTaskAsync(new Runnable() {
217            @Override
218            public void run() {
219                if (plots.isEmpty()) {
220                    SchematicHandler.this.exportAll = false;
221                    TaskManager.runTask(ifSuccess);
222                    return;
223                }
224                Iterator<Plot> i = plots.iterator();
225                final Plot plot = i.next();
226                i.remove();
227
228                final String owner;
229                if (plot.hasOwner()) {
230                    owner = plot.getOwnerAbs().toString();
231                } else {
232                    owner = "unknown";
233                }
234
235                final String name;
236                if (namingScheme == null) {
237                    name = plot.getId().getX() + ";" + plot.getId().getY() + ',' + plot.getArea() + ',' + owner;
238                } else {
239                    name = namingScheme.replaceAll("%id%", plot.getId().toString()).replaceAll("%idx%", plot.getId().getX() + "")
240                            .replaceAll("%idy%", plot.getId().getY() + "").replaceAll("%world%", plot.getArea().toString());
241                }
242
243                final String directory;
244                if (outputDir == null) {
245                    directory = Settings.Paths.SCHEMATICS;
246                } else {
247                    directory = outputDir.getAbsolutePath();
248                }
249
250                final Runnable THIS = this;
251                getCompoundTag(plot)
252                        .whenComplete((compoundTag, throwable) -> {
253                            if (compoundTag != null) {
254                                TaskManager.runTaskAsync(() -> {
255                                    boolean result = save(compoundTag, directory + File.separator + name + ".schem");
256                                    if (!result) {
257                                        LOGGER.error("Failed to save {}", plot.getId());
258                                    }
259                                    TaskManager.runTask(THIS);
260                                });
261                            }
262                        });
263            }
264        });
265        return true;
266    }
267
268    /**
269     * Paste a schematic.
270     *
271     * @param schematic  the schematic object to paste
272     * @param plot       plot to paste in
273     * @param xOffset    offset x to paste it from plot origin
274     * @param yOffset    offset y to paste it from plot origin
275     * @param zOffset    offset z to paste it from plot origin
276     * @param autoHeight if to automatically choose height to paste from
277     * @param actor      the actor pasting the schematic
278     * @param whenDone   task to run when schematic is pasted
279     */
280    public void paste(
281            final Schematic schematic,
282            final Plot plot,
283            final int xOffset,
284            final int yOffset,
285            final int zOffset,
286            final boolean autoHeight,
287            final PlotPlayer<?> actor,
288            final RunnableVal<Boolean> whenDone
289    ) {
290        if (whenDone != null) {
291            whenDone.value = false;
292        }
293        if (schematic == null) {
294            TaskManager.runTask(whenDone);
295            return;
296        }
297        try {
298            BlockVector3 dimension = schematic.getClipboard().getDimensions();
299            final int WIDTH = dimension.getX();
300            final int LENGTH = dimension.getZ();
301            final int HEIGHT = dimension.getY();
302            final int worldHeight = plot.getArea().getMaxGenHeight() - plot.getArea().getMinGenHeight() + 1;
303            // Validate dimensions
304            CuboidRegion region = plot.getLargestRegion();
305            boolean sizeMismatch =
306                    ((region.getMaximumPoint().getX() - region.getMinimumPoint().getX() + xOffset + 1) < WIDTH) || (
307                            (region.getMaximumPoint().getZ() - region.getMinimumPoint().getZ() + zOffset + 1) < LENGTH) || (HEIGHT
308                            > worldHeight);
309            if (!Settings.Schematics.PASTE_MISMATCHES && sizeMismatch) {
310                actor.sendMessage(TranslatableCaption.of("schematics.schematic_size_mismatch"));
311                TaskManager.runTask(whenDone);
312                return;
313            }
314            // block type and data arrays
315            final Clipboard blockArrayClipboard = schematic.getClipboard();
316            // Calculate the optimal height to paste the schematic at
317            final int y_offset_actual;
318            if (autoHeight) {
319                if (HEIGHT >= worldHeight) {
320                    y_offset_actual = yOffset;
321                } else {
322                    PlotArea pw = plot.getArea();
323                    if (pw instanceof ClassicPlotWorld) {
324                        y_offset_actual = yOffset + pw.getMinBuildHeight() + ((ClassicPlotWorld) pw).PLOT_HEIGHT;
325                    } else {
326                        y_offset_actual = yOffset + pw.getMinBuildHeight() + this.worldUtil
327                                .getHighestBlockSynchronous(plot.getWorldName(), region.getMinimumPoint().getX() + 1,
328                                        region.getMinimumPoint().getZ() + 1
329                                );
330                    }
331                }
332            } else {
333                y_offset_actual = yOffset;
334            }
335
336            final int p1x;
337            final int p1z;
338            final int p2x;
339            final int p2z;
340            final Region allRegion;
341            if (!sizeMismatch || plot.getRegions().size() == 1) {
342                p1x = region.getMinimumPoint().getX() + xOffset;
343                p1z = region.getMinimumPoint().getZ() + zOffset;
344                p2x = region.getMaximumPoint().getX() + xOffset;
345                p2z = region.getMaximumPoint().getZ() + zOffset;
346                allRegion = region;
347            } else {
348                Location[] corners = plot.getCorners();
349                p1x = corners[0].getX() + xOffset;
350                p1z = corners[0].getZ() + zOffset;
351                p2x = corners[1].getX() + xOffset;
352                p2z = corners[1].getZ() + zOffset;
353                allRegion = new RegionIntersection(null, plot.getRegions().toArray(new CuboidRegion[]{}));
354            }
355            // Paste schematic here
356            final QueueCoordinator queue = plot.getArea().getQueue();
357
358            for (int ry = 0; ry < Math.min(worldHeight, HEIGHT); ry++) {
359                int yy = y_offset_actual + ry;
360                if (yy > plot.getArea().getMaxGenHeight() || yy < plot.getArea().getMinGenHeight()) {
361                    continue;
362                }
363                for (int rz = 0; rz < blockArrayClipboard.getDimensions().getZ(); rz++) {
364                    for (int rx = 0; rx < blockArrayClipboard.getDimensions().getX(); rx++) {
365                        int xx = p1x + rx;
366                        int zz = p1z + rz;
367                        if (sizeMismatch && (xx < p1x || xx > p2x || zz < p1z || zz > p2z || !allRegion.contains(BlockVector3.at(
368                                xx,
369                                ry,
370                                zz
371                        )))) {
372                            continue;
373                        }
374                        BlockVector3 loc = BlockVector3.at(rx, ry, rz);
375                        BaseBlock id = blockArrayClipboard.getFullBlock(loc);
376                        queue.setBlock(xx, yy, zz, id);
377                        BiomeType biome = blockArrayClipboard.getBiome(loc);
378                        queue.setBiome(xx, yy, zz, biome);
379                    }
380                }
381            }
382            if (actor != null && Settings.QUEUE.NOTIFY_PROGRESS) {
383                queue.addProgressSubscriber(subscriberFactory.createWithActor(actor));
384            }
385            if (whenDone != null) {
386                whenDone.value = true;
387                queue.setCompleteTask(whenDone);
388            }
389            queue.enqueue();
390        } catch (Exception e) {
391            e.printStackTrace();
392            TaskManager.runTask(whenDone);
393        }
394    }
395
396    public abstract boolean restoreTile(QueueCoordinator queue, CompoundTag tag, int x, int y, int z);
397
398    /**
399     * Get a schematic
400     *
401     * @param name to check
402     * @return schematic if found, else null
403     * @throws UnsupportedFormatException thrown if schematic format is unsupported
404     */
405    public Schematic getSchematic(String name) throws UnsupportedFormatException {
406        File parent = FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.SCHEMATICS);
407        if (!parent.exists()) {
408            if (!parent.mkdir()) {
409                throw new RuntimeException("Could not create schematic parent directory");
410            }
411        }
412        if (!name.endsWith(".schem") && !name.endsWith(".schematic")) {
413            name = name + ".schem";
414        }
415        File file = FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.SCHEMATICS + File.separator + name);
416        if (!file.exists()) {
417            file = FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.SCHEMATICS + File.separator + name);
418        }
419        return getSchematic(file);
420    }
421
422    /**
423     * Get an immutable collection containing all schematic names
424     *
425     * @return Immutable collection with schematic names
426     */
427    public Collection<String> getSchematicNames() {
428        final File parent = FileUtils.getFile(PlotSquared.platform().getDirectory(), Settings.Paths.SCHEMATICS);
429        final List<String> names = new ArrayList<>();
430        if (parent.exists()) {
431            final String[] rawNames = parent.list((dir, name) -> name.endsWith(".schematic") || name.endsWith(".schem"));
432            if (rawNames != null) {
433                final List<String> transformed = Arrays.stream(rawNames)
434                        //.map(rawName -> rawName.substring(0, rawName.length() - 10))
435                        .collect(Collectors.toList());
436                names.addAll(transformed);
437            }
438        }
439        return Collections.unmodifiableList(names);
440    }
441
442    /**
443     * Get a schematic
444     *
445     * @param file to check
446     * @return schematic if found, else null
447     * @throws UnsupportedFormatException thrown if schematic format is unsupported
448     */
449    public Schematic getSchematic(File file) throws UnsupportedFormatException {
450        if (!file.exists()) {
451            return null;
452        }
453        ClipboardFormat format = ClipboardFormats.findByFile(file);
454        if (format != null) {
455            try (ClipboardReader reader = format.getReader(new FileInputStream(file))) {
456                Clipboard clip = reader.read();
457                return new Schematic(clip);
458            } catch (IOException e) {
459                e.printStackTrace();
460            }
461        } else {
462            throw new UnsupportedFormatException("This schematic format is not recognised or supported.");
463        }
464        return null;
465    }
466
467    public Schematic getSchematic(@NonNull URL url) {
468        try {
469            ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
470            InputStream inputStream = Channels.newInputStream(readableByteChannel);
471            return getSchematic(inputStream);
472        } catch (IOException e) {
473            e.printStackTrace();
474        }
475        return null;
476    }
477
478    public Schematic getSchematic(@NonNull InputStream is) {
479        try {
480            SpongeSchematicReader schematicReader = new SpongeSchematicReader(new NBTInputStream(new GZIPInputStream(is)));
481            Clipboard clip = schematicReader.read();
482            return new Schematic(clip);
483        } catch (IOException ignored) {
484            try {
485                MCEditSchematicReader schematicReader = new MCEditSchematicReader(new NBTInputStream(new GZIPInputStream(is)));
486                Clipboard clip = schematicReader.read();
487                return new Schematic(clip);
488            } catch (IOException e) {
489                e.printStackTrace();
490            }
491        }
492        return null;
493    }
494
495    /**
496     * The legacy web interface is deprecated for removal in favor of Arkitektonika.
497     */
498    @Deprecated(forRemoval = true, since = "6.11.0")
499    public List<String> getSaves(UUID uuid) {
500        String rawJSON;
501        try {
502            URLConnection connection = URI.create(
503                    Settings.Web.URL + "list.php?" + uuid.toString())
504                    .toURL()
505                    .openConnection();
506            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
507            try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
508                rawJSON = reader.lines().collect(Collectors.joining());
509            }
510            JsonArray array = GSON.fromJson(rawJSON, JsonArray.class);
511            List<String> schematics = new ArrayList<>();
512            for (int i = 0; i < array.size(); i++) {
513                String schematic = array.get(i).getAsString();
514                schematics.add(schematic);
515            }
516            return schematics;
517        } catch (JsonParseException | IOException e) {
518            e.printStackTrace();
519        }
520        return null;
521    }
522
523    @Deprecated(forRemoval = true, since = "6.0.0")
524    public void upload(final CompoundTag tag, UUID uuid, String file, RunnableVal<URL> whenDone) {
525        if (tag == null) {
526            TaskManager.runTask(whenDone);
527            return;
528        }
529        upload(uuid, file, "schem", new RunnableVal<>() {
530            @Override
531            public void run(OutputStream output) {
532                try (NBTOutputStream nos = new NBTOutputStream(new GZIPOutputStream(output, true))) {
533                    nos.writeNamedTag("Schematic", tag);
534                } catch (IOException e1) {
535                    e1.printStackTrace();
536                }
537            }
538        }, whenDone);
539    }
540
541    /**
542     * Saves a schematic to a file path.
543     *
544     * @param tag  to save
545     * @param path to save in
546     * @return {@code true} if succeeded
547     */
548    public boolean save(CompoundTag tag, String path) {
549        if (tag == null) {
550            return false;
551        }
552        try {
553            File tmp = FileUtils.getFile(PlotSquared.platform().getDirectory(), path);
554            tmp.getParentFile().mkdirs();
555            try (NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(new FileOutputStream(tmp)))) {
556                nbtStream.writeNamedTag("Schematic", tag);
557            }
558        } catch (FileNotFoundException e) {
559            e.printStackTrace();
560        } catch (IOException e) {
561            e.printStackTrace();
562            return false;
563        }
564        return true;
565    }
566
567    private void writeSchematicData(
568            final @NonNull Map<String, Tag> schematic,
569            final @NonNull Map<String, Integer> palette,
570            final @NonNull Map<String, Integer> biomePalette,
571            final @NonNull List<CompoundTag> tileEntities,
572            final @NonNull ByteArrayOutputStream buffer,
573            final @NonNull ByteArrayOutputStream biomeBuffer
574    ) {
575        schematic.put("PaletteMax", new IntTag(palette.size()));
576
577        Map<String, Tag> paletteTag = new HashMap<>();
578        palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value)));
579
580        schematic.put("Palette", new CompoundTag(paletteTag));
581        schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray()));
582        schematic.put("BlockEntities", new ListTag(CompoundTag.class, tileEntities));
583
584        if (biomeBuffer.size() == 0 || biomePalette.size() == 0) {
585            return;
586        }
587
588        schematic.put("BiomePaletteMax", new IntTag(biomePalette.size()));
589
590        Map<String, Tag> biomePaletteTag = new HashMap<>();
591        biomePalette.forEach((key, value) -> biomePaletteTag.put(key, new IntTag(value)));
592
593        schematic.put("BiomePalette", new CompoundTag(biomePaletteTag));
594        schematic.put("BiomeData", new ByteArrayTag(biomeBuffer.toByteArray()));
595    }
596
597    @NonNull
598    private Map<String, Tag> initSchematic(short width, short height, short length) {
599        Map<String, Tag> schematic = new HashMap<>();
600        schematic.put("Version", new IntTag(2));
601        schematic.put(
602                "DataVersion",
603                new IntTag(WorldEdit
604                        .getInstance()
605                        .getPlatformManager()
606                        .queryCapability(Capability.WORLD_EDITING)
607                        .getDataVersion())
608        );
609
610        Map<String, Tag> metadata = new HashMap<>();
611        metadata.put("WEOffsetX", new IntTag(0));
612        metadata.put("WEOffsetY", new IntTag(0));
613        metadata.put("WEOffsetZ", new IntTag(0));
614
615        schematic.put("Metadata", new CompoundTag(metadata));
616
617        schematic.put("Width", new ShortTag(width));
618        schematic.put("Height", new ShortTag(height));
619        schematic.put("Length", new ShortTag(length));
620
621        // The Sponge format Offset refers to the 'min' points location in the world. That's our 'Origin'
622        schematic.put("Offset", new IntArrayTag(new int[]{0, 0, 0,}));
623        return schematic;
624    }
625
626    /**
627     * Get the given plot as {@link CompoundTag} matching the Sponge schematic format.
628     *
629     * @param plot The plot to get the contents from.
630     * @return a {@link CompletableFuture} that provides the created {@link CompoundTag}.
631     */
632    public CompletableFuture<CompoundTag> getCompoundTag(final @NonNull Plot plot) {
633        return getCompoundTag(Objects.requireNonNull(plot.getWorldName()), plot.getRegions());
634    }
635
636    /**
637     * Get the contents of the given regions in the given world as {@link CompoundTag}
638     * matching the Sponge schematic format.
639     *
640     * @param worldName The world to get the contents from.
641     * @param regions   The regions to get the contents from.
642     * @return a {@link CompletableFuture} that provides the created {@link CompoundTag}.
643     */
644    public @NonNull CompletableFuture<CompoundTag> getCompoundTag(
645            final @NonNull String worldName,
646            final @NonNull Set<CuboidRegion> regions
647    ) {
648        CompletableFuture<CompoundTag> completableFuture = new CompletableFuture<>();
649        TaskManager.runTaskAsync(() -> {
650            World world = this.worldUtil.getWeWorld(worldName);
651            // All positions
652            CuboidRegion aabb = RegionUtil.getAxisAlignedBoundingBox(regions);
653            aabb.setWorld(world);
654
655            RegionIntersection intersection = new RegionIntersection(new ArrayList<>(regions));
656
657            final int width = aabb.getWidth();
658            int height = aabb.getHeight();
659            final int length = aabb.getLength();
660            final boolean multipleRegions = regions.size() > 1;
661
662            Map<String, Tag> schematic = initSchematic((short) width, (short) height, (short) length);
663
664            Map<String, Integer> palette = new HashMap<>();
665            Map<String, Integer> biomePalette = new HashMap<>();
666
667            List<CompoundTag> tileEntities = new ArrayList<>();
668            ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * height * length);
669            ByteArrayOutputStream biomeBuffer = new ByteArrayOutputStream(width * length);
670            // Queue
671            TaskManager.runTaskAsync(() -> {
672                final BlockVector3 minimum = aabb.getMinimumPoint();
673                final BlockVector3 maximum = aabb.getMaximumPoint();
674
675                final int minX = minimum.getX();
676                final int minZ = minimum.getZ();
677                final int minY = minimum.getY();
678
679                final int maxX = maximum.getX();
680                final int maxZ = maximum.getZ();
681                final int maxY = maximum.getY();
682
683                final Runnable yTask = new YieldRunnable() {
684                    int currentY = minY;
685                    int currentX = minX;
686                    int currentZ = minZ;
687
688                    @Override
689                    public void run() {
690                        long start = System.currentTimeMillis();
691                        int lastBiome = 0;
692                        for (; currentY <= maxY; currentY++) {
693                            int relativeY = currentY - minY;
694                            for (; currentZ <= maxZ; currentZ++) {
695                                int relativeZ = currentZ - minZ;
696                                for (; currentX <= maxX; currentX++) {
697                                    // if too much time was spent here, we yield this task
698                                    // note that current(X/Y/Z) aren't incremented, so the same position
699                                    // as *right now* will be visited again
700                                    if (System.currentTimeMillis() - start > 40) {
701                                        this.yield();
702                                        return;
703                                    }
704                                    int relativeX = currentX - minX;
705                                    BlockVector3 point = BlockVector3.at(currentX, currentY, currentZ);
706                                    if (multipleRegions && !intersection.contains(point)) {
707                                        String blockKey = BlockTypes.AIR.getDefaultState().getAsString();
708                                        int blockId;
709                                        if (palette.containsKey(blockKey)) {
710                                            blockId = palette.get(blockKey);
711                                        } else {
712                                            blockId = palette.size();
713                                            palette.put(blockKey, palette.size());
714                                        }
715                                        while ((blockId & -128) != 0) {
716                                            buffer.write(blockId & 127 | 128);
717                                            blockId >>>= 7;
718                                        }
719                                        buffer.write(blockId);
720
721                                        if (relativeY > 0) {
722                                            continue;
723                                        }
724
725                                        // Write the last biome if we're not getting it from the plot;
726                                        int biomeId = lastBiome;
727                                        while ((biomeId & -128) != 0) {
728                                            biomeBuffer.write(biomeId & 127 | 128);
729                                            biomeId >>>= 7;
730                                        }
731                                        biomeBuffer.write(biomeId);
732                                        continue;
733                                    }
734                                    BaseBlock block = aabb.getWorld().getFullBlock(point);
735                                    if (block.getNbtData() != null) {
736                                        Map<String, Tag> values = new HashMap<>();
737                                        for (Map.Entry<String, Tag> entry : block.getNbtData().getValue().entrySet()) {
738                                            values.put(entry.getKey(), entry.getValue());
739                                        }
740
741                                        // Positions are kept in NBT, we don't want that.
742                                        values.remove("x");
743                                        values.remove("y");
744                                        values.remove("z");
745
746                                        values.put("Id", new StringTag(block.getNbtId()));
747
748                                        // Remove 'id' if it exists. We want 'Id'.
749                                        // Do this after we get "getNbtId" cos otherwise "getNbtId" doesn't work.
750                                        // Dum.
751                                        values.remove("id");
752                                        values.put("Pos", new IntArrayTag(new int[]{relativeX, relativeY, relativeZ}));
753
754                                        tileEntities.add(new CompoundTag(values));
755                                    }
756                                    String blockKey = block.toImmutableState().getAsString();
757                                    int blockId;
758                                    if (palette.containsKey(blockKey)) {
759                                        blockId = palette.get(blockKey);
760                                    } else {
761                                        blockId = palette.size();
762                                        palette.put(blockKey, palette.size());
763                                    }
764
765                                    while ((blockId & -128) != 0) {
766                                        buffer.write(blockId & 127 | 128);
767                                        blockId >>>= 7;
768                                    }
769                                    buffer.write(blockId);
770
771                                    if (relativeY > 0) {
772                                        continue;
773                                    }
774                                    BlockVector2 pt = BlockVector2.at(currentX, currentZ);
775                                    BiomeType biome = aabb.getWorld().getBiome(pt);
776                                    String biomeStr = biome.getId();
777                                    int biomeId;
778                                    if (biomePalette.containsKey(biomeStr)) {
779                                        biomeId = lastBiome = biomePalette.get(biomeStr);
780                                    } else {
781                                        biomeId = lastBiome = biomePalette.size();
782                                        biomePalette.put(biomeStr, biomeId);
783                                    }
784                                    while ((biomeId & -128) != 0) {
785                                        biomeBuffer.write(biomeId & 127 | 128);
786                                        biomeId >>>= 7;
787                                    }
788                                    biomeBuffer.write(biomeId);
789                                }
790                                currentX = minX; // reset manually as not using local variable
791                            }
792                            currentZ = minZ; // reset manually as not using local variable
793                        }
794                        TaskManager.runTaskAsync(() -> {
795                            writeSchematicData(schematic, palette, biomePalette, tileEntities, buffer, biomeBuffer);
796                            completableFuture.complete(new CompoundTag(schematic));
797                        });
798                    }
799                };
800                yTask.run();
801            });
802        });
803        return completableFuture;
804    }
805
806
807    public static class UnsupportedFormatException extends Exception {
808
809        /**
810         * Throw with a message.
811         *
812         * @param message the message
813         */
814        public UnsupportedFormatException(String message) {
815            super(message);
816        }
817
818        /**
819         * Throw with a message and a cause.
820         *
821         * @param message the message
822         * @param cause   the cause
823         */
824        public UnsupportedFormatException(String message, Throwable cause) {
825            super(message, cause);
826        }
827
828    }
829
830}