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