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}