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