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