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}