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.plot; 020 021import com.google.inject.Inject; 022import com.plotsquared.core.PlotSquared; 023import com.plotsquared.core.configuration.ConfigurationUtil; 024import com.plotsquared.core.configuration.Settings; 025import com.plotsquared.core.configuration.caption.Caption; 026import com.plotsquared.core.configuration.caption.LocaleHolder; 027import com.plotsquared.core.configuration.caption.TranslatableCaption; 028import com.plotsquared.core.database.DBFunc; 029import com.plotsquared.core.events.PlotComponentSetEvent; 030import com.plotsquared.core.events.PlotMergeEvent; 031import com.plotsquared.core.events.PlotUnlinkEvent; 032import com.plotsquared.core.events.Result; 033import com.plotsquared.core.generator.ClassicPlotWorld; 034import com.plotsquared.core.generator.SquarePlotWorld; 035import com.plotsquared.core.inject.factory.ProgressSubscriberFactory; 036import com.plotsquared.core.location.Direction; 037import com.plotsquared.core.location.Location; 038import com.plotsquared.core.player.MetaDataAccess; 039import com.plotsquared.core.player.PlayerMetaDataKeys; 040import com.plotsquared.core.player.PlotPlayer; 041import com.plotsquared.core.plot.flag.PlotFlag; 042import com.plotsquared.core.plot.world.SinglePlotManager; 043import com.plotsquared.core.queue.QueueCoordinator; 044import com.plotsquared.core.util.task.TaskManager; 045import com.plotsquared.core.util.task.TaskTime; 046import com.sk89q.worldedit.function.pattern.Pattern; 047import com.sk89q.worldedit.math.BlockVector2; 048import com.sk89q.worldedit.regions.CuboidRegion; 049import com.sk89q.worldedit.world.biome.BiomeType; 050import com.sk89q.worldedit.world.block.BlockTypes; 051import net.kyori.adventure.text.Component; 052import net.kyori.adventure.text.minimessage.tag.Tag; 053import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 054import org.apache.logging.log4j.LogManager; 055import org.apache.logging.log4j.Logger; 056import org.checkerframework.checker.nullness.qual.NonNull; 057import org.checkerframework.checker.nullness.qual.Nullable; 058 059import java.util.ArrayDeque; 060import java.util.ArrayList; 061import java.util.Collection; 062import java.util.HashSet; 063import java.util.Iterator; 064import java.util.List; 065import java.util.Set; 066import java.util.UUID; 067import java.util.concurrent.CompletableFuture; 068import java.util.concurrent.atomic.AtomicBoolean; 069import java.util.stream.Collectors; 070 071/** 072 * Manager that handles {@link Plot} modifications 073 */ 074public final class PlotModificationManager { 075 076 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotModificationManager.class.getSimpleName()); 077 078 private final Plot plot; 079 private final ProgressSubscriberFactory subscriberFactory; 080 081 @Inject 082 PlotModificationManager(final @NonNull Plot plot) { 083 this.plot = plot; 084 this.subscriberFactory = PlotSquared.platform().injector().getInstance(ProgressSubscriberFactory.class); 085 } 086 087 /** 088 * Copy a plot to a location, both physically and the settings 089 * 090 * @param destination destination plot 091 * @param actor the actor associated with the copy 092 * @return Future that completes with {@code true} if the copy was successful, else {@code false} 093 */ 094 public CompletableFuture<Boolean> copy(final @NonNull Plot destination, @Nullable PlotPlayer<?> actor) { 095 final CompletableFuture<Boolean> future = new CompletableFuture<>(); 096 final PlotId offset = PlotId.of( 097 destination.getId().getX() - this.plot.getId().getX(), 098 destination.getId().getY() - this.plot.getId().getY() 099 ); 100 final Location db = destination.getBottomAbs(); 101 final Location ob = this.plot.getBottomAbs(); 102 final int offsetX = db.getX() - ob.getX(); 103 final int offsetZ = db.getZ() - ob.getZ(); 104 if (!this.plot.hasOwner()) { 105 TaskManager.runTaskLater(() -> future.complete(false), TaskTime.ticks(1L)); 106 return future; 107 } 108 final Set<Plot> plots = this.plot.getConnectedPlots(); 109 for (final Plot plot : plots) { 110 final Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 111 if (other.hasOwner()) { 112 TaskManager.runTaskLater(() -> future.complete(false), TaskTime.ticks(1L)); 113 return future; 114 } 115 } 116 // world border 117 destination.updateWorldBorder(); 118 // copy data 119 for (final Plot plot : plots) { 120 final Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 121 other.getPlotModificationManager().create(plot.getOwner(), false); 122 if (!plot.getFlagContainer().getFlagMap().isEmpty()) { 123 final Collection<PlotFlag<?, ?>> existingFlags = other.getFlags(); 124 other.getFlagContainer().clearLocal(); 125 other.getFlagContainer().addAll(plot.getFlagContainer().getFlagMap().values()); 126 // Update the database 127 for (final PlotFlag<?, ?> flag : existingFlags) { 128 final PlotFlag<?, ?> newFlag = other.getFlagContainer().queryLocal(flag.getClass()); 129 if (other.getFlagContainer().queryLocal(flag.getClass()) == null) { 130 DBFunc.removeFlag(other, flag); 131 } else { 132 DBFunc.setFlag(other, newFlag); 133 } 134 } 135 } 136 if (plot.isMerged()) { 137 other.setMerged(plot.getMerged()); 138 } 139 if (plot.members != null && !plot.members.isEmpty()) { 140 other.members = plot.members; 141 for (UUID member : plot.members) { 142 DBFunc.setMember(other, member); 143 } 144 } 145 if (plot.trusted != null && !plot.trusted.isEmpty()) { 146 other.trusted = plot.trusted; 147 for (UUID trusted : plot.trusted) { 148 DBFunc.setTrusted(other, trusted); 149 } 150 } 151 if (plot.denied != null && !plot.denied.isEmpty()) { 152 other.denied = plot.denied; 153 for (UUID denied : plot.denied) { 154 DBFunc.setDenied(other, denied); 155 } 156 } 157 } 158 // copy terrain 159 final ArrayDeque<CuboidRegion> regions = new ArrayDeque<>(this.plot.getRegions()); 160 final Runnable run = new Runnable() { 161 @Override 162 public void run() { 163 if (regions.isEmpty()) { 164 final QueueCoordinator queue = plot.getArea().getQueue(); 165 for (final Plot current : plot.getConnectedPlots()) { 166 destination.getManager().claimPlot(current, queue); 167 } 168 if (queue.size() > 0) { 169 queue.enqueue(); 170 } 171 destination.getPlotModificationManager().setSign(); 172 future.complete(true); 173 return; 174 } 175 CuboidRegion region = regions.poll(); 176 Location[] corners = Plot.getCorners(plot.getWorldName(), region); 177 Location pos1 = corners[0]; 178 Location pos2 = corners[1]; 179 Location newPos = pos1.add(offsetX, 0, offsetZ).withWorld(destination.getWorldName()); 180 PlotSquared.platform().regionManager().copyRegion(pos1, pos2, newPos, actor, this); 181 } 182 }; 183 run.run(); 184 return future; 185 } 186 187 /** 188 * Clear the plot 189 * 190 * <p> 191 * Use {@link #deletePlot(PlotPlayer, Runnable)} to clear and delete a plot 192 * </p> 193 * 194 * @param whenDone A runnable to execute when clearing finishes, or null 195 * @see #clear(boolean, boolean, PlotPlayer, Runnable) 196 */ 197 public void clear(final @Nullable Runnable whenDone) { 198 this.clear(false, false, null, whenDone); 199 } 200 201 /** 202 * Clear the plot 203 * 204 * <p> 205 * Use {@link #deletePlot(PlotPlayer, Runnable)} to clear and delete a plot 206 * </p> 207 * 208 * @param checkRunning Whether or not already executing tasks should be checked 209 * @param isDelete Whether or not the plot is being deleted 210 * @param actor The actor clearing the plot 211 * @param whenDone A runnable to execute when clearing finishes, or null 212 */ 213 public boolean clear( 214 final boolean checkRunning, 215 final boolean isDelete, 216 final @Nullable PlotPlayer<?> actor, 217 final @Nullable Runnable whenDone 218 ) { 219 if (checkRunning && this.plot.getRunning() != 0) { 220 return false; 221 } 222 final Set<CuboidRegion> regions = this.plot.getRegions(); 223 final Set<Plot> plots = this.plot.getConnectedPlots(); 224 final ArrayDeque<Plot> queue = new ArrayDeque<>(plots); 225 if (isDelete) { 226 this.removeSign(); 227 } 228 final PlotManager manager = this.plot.getArea().getPlotManager(); 229 Runnable run = new Runnable() { 230 @Override 231 public void run() { 232 if (queue.isEmpty()) { 233 // don't touch world for single plot areas on deletion (un-fuck this in the future) 234 Runnable run = isDelete && manager instanceof SinglePlotManager ? whenDone : () -> { 235 for (CuboidRegion region : regions) { 236 Location[] corners = Plot.getCorners(plot.getWorldName(), region); 237 PlotSquared.platform().regionManager().clearAllEntities(corners[0], corners[1]); 238 } 239 TaskManager.runTask(whenDone); 240 }; 241 QueueCoordinator queue = plot.getArea().getQueue(); 242 for (Plot current : plots) { 243 if (isDelete || !current.hasOwner()) { 244 manager.unClaimPlot(current, null, queue); 245 } else { 246 manager.claimPlot(current, queue); 247 if (plot.getArea() instanceof ClassicPlotWorld cpw) { 248 manager.setComponent(current.getId(), "wall", cpw.WALL_FILLING.toPattern(), actor, queue); 249 } 250 } 251 } 252 if (queue.size() > 0) { 253 queue.setCompleteTask(run); 254 queue.enqueue(); 255 return; 256 } 257 if (run != null) { 258 run.run(); 259 } 260 return; 261 } 262 Plot current = queue.poll(); 263 current.clearCache(); 264 if (plot.getArea().getTerrain() != PlotAreaTerrainType.NONE) { 265 try { 266 PlotSquared.platform().regionManager().regenerateRegion( 267 current.getBottomAbs(), 268 current.getTopAbs(), 269 false, 270 this 271 ); 272 } catch (UnsupportedOperationException exception) { 273 exception.printStackTrace(); 274 return; 275 } 276 return; 277 } 278 manager.clearPlot(current, this, actor, null); 279 } 280 }; 281 PlotUnlinkEvent event = PlotSquared.get().getEventDispatcher() 282 .callUnlink( 283 this.plot.getArea(), 284 this.plot, 285 true, 286 !isDelete, 287 isDelete ? PlotUnlinkEvent.REASON.DELETE : PlotUnlinkEvent.REASON.CLEAR 288 ); 289 if (event.getEventResult() != Result.DENY) { 290 if (this.unlinkPlot(event.isCreateRoad(), event.isCreateSign(), run)) { 291 PlotSquared.get().getEventDispatcher().callPostUnlink(plot, event.getReason()); 292 } 293 } else { 294 run.run(); 295 } 296 return true; 297 } 298 299 /** 300 * Sets the biome for a plot asynchronously. 301 * 302 * @param biome The biome e.g. "forest" 303 * @param whenDone The task to run when finished, or null 304 */ 305 public void setBiome(final @Nullable BiomeType biome, final @NonNull Runnable whenDone) { 306 final ArrayDeque<CuboidRegion> regions = new ArrayDeque<>(this.plot.getRegions()); 307 final int extendBiome; 308 if (this.plot.getArea() instanceof SquarePlotWorld) { 309 extendBiome = (((SquarePlotWorld) this.plot.getArea()).ROAD_WIDTH > 0) ? 1 : 0; 310 } else { 311 extendBiome = 0; 312 } 313 Runnable run = new Runnable() { 314 @Override 315 public void run() { 316 if (regions.isEmpty()) { 317 TaskManager.runTask(whenDone); 318 return; 319 } 320 CuboidRegion region = regions.poll(); 321 PlotSquared.platform().regionManager().setBiome(region, extendBiome, biome, plot.getArea(), this); 322 } 323 }; 324 run.run(); 325 } 326 327 /** 328 * Unlink the plot and all connected plots. 329 * 330 * @param createRoad whether to recreate road 331 * @param createSign whether to recreate signs 332 * @return success/!cancelled 333 */ 334 public boolean unlinkPlot(final boolean createRoad, final boolean createSign) { 335 return unlinkPlot(createRoad, createSign, null); 336 } 337 338 /** 339 * Unlink the plot and all connected plots. 340 * 341 * @param createRoad whether to recreate road 342 * @param createSign whether to recreate signs 343 * @param whenDone Task to run when unlink is complete 344 * @return success/!cancelled 345 * @since 6.10.9 346 */ 347 public boolean unlinkPlot(final boolean createRoad, final boolean createSign, final Runnable whenDone) { 348 if (!this.plot.isMerged()) { 349 if (whenDone != null) { 350 whenDone.run(); 351 } 352 return false; 353 } 354 final Set<Plot> plots = this.plot.getConnectedPlots(); 355 ArrayList<PlotId> ids = new ArrayList<>(plots.size()); 356 for (Plot current : plots) { 357 current.setHome(null); 358 current.clearCache(); 359 ids.add(current.getId()); 360 } 361 this.plot.clearRatings(); 362 QueueCoordinator queue = this.plot.getArea().getQueue(); 363 if (createSign) { 364 this.removeSign(); 365 } 366 PlotManager manager = this.plot.getArea().getPlotManager(); 367 if (createRoad) { 368 manager.startPlotUnlink(ids, queue); 369 } 370 if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL && createRoad) { 371 for (Plot current : plots) { 372 if (current.isMerged(Direction.EAST)) { 373 manager.createRoadEast(current, queue); 374 if (current.isMerged(Direction.SOUTH)) { 375 manager.createRoadSouth(current, queue); 376 if (current.isMerged(Direction.SOUTHEAST)) { 377 manager.createRoadSouthEast(current, queue); 378 } 379 } 380 } else if (current.isMerged(Direction.SOUTH)) { 381 manager.createRoadSouth(current, queue); 382 } 383 } 384 } 385 for (Plot current : plots) { 386 boolean[] merged = new boolean[]{false, false, false, false}; 387 current.setMerged(merged); 388 } 389 // Update TEMPORARY_LAST_PLOT metadata for all players that were in the merged plot 390 // so that getCurrentPlot() returns the correct individual plot based on their location 391 for (PlotPlayer<?> player : PlotSquared.platform().playerManager().getPlayers()) { 392 try (MetaDataAccess<Plot> lastPlotAccess = player.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) { 393 Plot lastPlot = lastPlotAccess.get().orElse(null); 394 if (lastPlot != null && plots.contains(lastPlot)) { 395 // Player was in the merged plot, update to their actual current plot 396 Plot actualPlot = player.getLocation().getPlot(); 397 if (actualPlot != null) { 398 lastPlotAccess.set(actualPlot); 399 } else { 400 lastPlotAccess.remove(); 401 } 402 } 403 } 404 } 405 if (createSign) { 406 queue.setCompleteTask(() -> TaskManager.runTaskAsync(() -> { 407 List<CompletableFuture<Void>> tasks = plots.stream().map(current -> PlotSquared.platform().playerManager() 408 .getUsernameCaption(current.getOwnerAbs()) 409 .thenAccept(caption -> current 410 .getPlotModificationManager() 411 .setSign(caption.getComponent(LocaleHolder.console())))) 412 .toList(); 413 CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).whenComplete((unused, throwable) -> { 414 if (whenDone != null) { 415 TaskManager.runTask(whenDone); 416 } 417 }); 418 })); 419 } else if (whenDone != null) { 420 queue.setCompleteTask(whenDone); 421 } 422 if (createRoad) { 423 manager.finishPlotUnlink(ids, queue); 424 } 425 queue.enqueue(); 426 return true; 427 } 428 429 /** 430 * Sets the sign for a plot to a specific name 431 * 432 * @param name name 433 */ 434 public void setSign(final @NonNull String name) { 435 if (!this.plot.isLoaded()) { 436 return; 437 } 438 PlotManager manager = this.plot.getArea().getPlotManager(); 439 if (this.plot.getArea().allowSigns()) { 440 Location location = manager.getSignLoc(this.plot); 441 String id = this.plot.getId().toString(); 442 Caption[] lines = new Caption[]{TranslatableCaption.of("signs.owner_sign_line_1"), TranslatableCaption.of( 443 "signs.owner_sign_line_2"), 444 TranslatableCaption.of("signs.owner_sign_line_3"), TranslatableCaption.of("signs.owner_sign_line_4")}; 445 PlotSquared.platform().worldUtil().setSign(location, lines, TagResolver.builder() 446 .tag("id", Tag.inserting(Component.text(id))) 447 .tag("owner", Tag.inserting(Component.text(name))) 448 .build()); 449 } 450 } 451 452 /** 453 * Resend all chunks inside the plot to nearby players<br> 454 * This should not need to be called 455 */ 456 public void refreshChunks() { 457 final HashSet<BlockVector2> chunks = new HashSet<>(); 458 for (final CuboidRegion region : this.plot.getRegions()) { 459 for (int x = region.getMinimumPoint().getX() >> 4; x <= region.getMaximumPoint().getX() >> 4; x++) { 460 for (int z = region.getMinimumPoint().getZ() >> 4; z <= region.getMaximumPoint().getZ() >> 4; z++) { 461 if (chunks.add(BlockVector2.at(x, z))) { 462 PlotSquared.platform().worldUtil().refreshChunk(x, z, this.plot.getWorldName()); 463 } 464 } 465 } 466 } 467 } 468 469 /** 470 * Remove the plot sign if it is set. 471 */ 472 public void removeSign() { 473 PlotManager manager = this.plot.getArea().getPlotManager(); 474 if (!this.plot.getArea().allowSigns()) { 475 return; 476 } 477 Location location = manager.getSignLoc(this.plot); 478 QueueCoordinator queue = 479 PlotSquared.platform().globalBlockQueue().getNewQueue(PlotSquared 480 .platform() 481 .worldUtil() 482 .getWeWorld(this.plot.getWorldName())); 483 queue.setBlock(location.getX(), location.getY(), location.getZ(), BlockTypes.AIR.getDefaultState()); 484 queue.enqueue(); 485 } 486 487 /** 488 * Sets the plot sign if plot signs are enabled. 489 */ 490 public void setSign() { 491 if (!this.plot.hasOwner()) { 492 this.setSign("unknown"); 493 return; 494 } 495 PlotSquared.get().getImpromptuUUIDPipeline().getSingle( 496 this.plot.getOwnerAbs(), 497 (username, sign) -> this.setSign(username) 498 ); 499 } 500 501 /** 502 * Register a plot and create it in the database<br> 503 * - The plot will not be created if the owner is null<br> 504 * - Any setting from before plot creation will not be saved until the server is stopped properly. i.e. Set any values/options after plot 505 * creation. 506 * 507 * @return {@code true} if plot was created successfully 508 */ 509 public boolean create() { 510 return this.create(this.plot.getOwnerAbs(), true); 511 } 512 513 /** 514 * Register a plot and create it in the database<br> 515 * - The plot will not be created if the owner is null<br> 516 * - Any setting from before plot creation will not be saved until the server is stopped properly. i.e. Set any values/options after plot 517 * creation. 518 * 519 * @param uuid the uuid of the plot owner 520 * @param notify notify 521 * @return {@code true} if plot was created successfully, else {@code false} 522 */ 523 public boolean create(final @NonNull UUID uuid, final boolean notify) { 524 this.plot.setOwnerAbs(uuid); 525 Plot existing = this.plot.getArea().getOwnedPlotAbs(this.plot.getId()); 526 if (existing != null) { 527 throw new IllegalStateException("Plot already exists!"); 528 } 529 if (notify) { 530 Integer meta = (Integer) this.plot.getArea().getMeta("worldBorder"); 531 if (meta != null) { 532 this.plot.updateWorldBorder(); 533 } 534 } 535 this.plot.clearCache(); 536 this.plot.getTrusted().clear(); 537 this.plot.getMembers().clear(); 538 this.plot.getDenied().clear(); 539 this.plot.settings = new PlotSettings(); 540 if (this.plot.getArea().addPlot(this.plot)) { 541 DBFunc.createPlotAndSettings(this.plot, () -> { 542 PlotArea plotworld = plot.getArea(); 543 if (notify && plotworld.isAutoMerge()) { 544 final PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(uuid); 545 546 PlotMergeEvent event = PlotSquared.get().getEventDispatcher().callMerge( 547 this.plot, 548 Direction.ALL, 549 Integer.MAX_VALUE, 550 player 551 ); 552 553 if (event.getEventResult() == Result.DENY) { 554 if (player != null) { 555 player.sendMessage( 556 TranslatableCaption.of("events.event_denied"), 557 TagResolver.resolver("value", Tag.inserting(Component.text("Auto merge on claim"))) 558 ); 559 } 560 return; 561 } 562 if (plot.getPlotModificationManager().autoMerge(event.getDir(), event.getMax(), uuid, player, true)) { 563 PlotSquared.get().getEventDispatcher().callPostMerge(player, plot); 564 } 565 } 566 }); 567 return true; 568 } 569 LOGGER.info( 570 "Failed to add plot {} to plot area {}", 571 this.plot.getId().toCommaSeparatedString(), 572 this.plot.getArea().toString() 573 ); 574 return false; 575 } 576 577 /** 578 * Auto merge a plot in a specific direction. 579 * 580 * @param dir the direction to merge 581 * @param max the max number of merges to do 582 * @param uuid the UUID it is allowed to merge with 583 * @param actor The actor executing the task 584 * @param removeRoads whether to remove roads 585 * @return {@code true} if a merge takes place, else {@code false} 586 */ 587 public boolean autoMerge( 588 final @NonNull Direction dir, 589 int max, 590 final @NonNull UUID uuid, 591 @Nullable PlotPlayer<?> actor, 592 final boolean removeRoads 593 ) { 594 //Ignore merging if there is no owner for the plot 595 if (!this.plot.hasOwner()) { 596 return false; 597 } 598 Set<Plot> connected = this.plot.getConnectedPlots(); 599 HashSet<PlotId> merged = connected.stream().map(Plot::getId).collect(Collectors.toCollection(HashSet::new)); 600 ArrayDeque<Plot> frontier = new ArrayDeque<>(connected); 601 Plot current; 602 boolean toReturn = false; 603 HashSet<Plot> visited = new HashSet<>(); 604 QueueCoordinator queue = this.plot.getArea().getQueue(); 605 while ((current = frontier.poll()) != null && max >= 0) { 606 if (visited.contains(current)) { 607 continue; 608 } 609 visited.add(current); 610 Set<Plot> plots; 611 if ((dir == Direction.ALL || dir == Direction.NORTH) && !current.isMerged(Direction.NORTH)) { 612 Plot other = current.getRelative(Direction.NORTH); 613 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 614 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 615 current.mergePlot(other, removeRoads, queue); 616 merged.add(current.getId()); 617 merged.add(other.getId()); 618 toReturn = true; 619 620 if (removeRoads) { 621 ArrayList<PlotId> ids = new ArrayList<>(); 622 ids.add(current.getId()); 623 ids.add(other.getId()); 624 this.plot.getManager().finishPlotMerge(ids, queue); 625 } 626 } 627 } 628 if (max >= 0 && (dir == Direction.ALL || dir == Direction.EAST) && !current.isMerged(Direction.EAST)) { 629 Plot other = current.getRelative(Direction.EAST); 630 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 631 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 632 current.mergePlot(other, removeRoads, queue); 633 merged.add(current.getId()); 634 merged.add(other.getId()); 635 toReturn = true; 636 637 if (removeRoads) { 638 ArrayList<PlotId> ids = new ArrayList<>(); 639 ids.add(current.getId()); 640 ids.add(other.getId()); 641 this.plot.getManager().finishPlotMerge(ids, queue); 642 } 643 } 644 } 645 if (max >= 0 && (dir == Direction.ALL || dir == Direction.SOUTH) && !current.isMerged(Direction.SOUTH)) { 646 Plot other = current.getRelative(Direction.SOUTH); 647 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 648 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 649 current.mergePlot(other, removeRoads, queue); 650 merged.add(current.getId()); 651 merged.add(other.getId()); 652 toReturn = true; 653 654 if (removeRoads) { 655 ArrayList<PlotId> ids = new ArrayList<>(); 656 ids.add(current.getId()); 657 ids.add(other.getId()); 658 this.plot.getManager().finishPlotMerge(ids, queue); 659 } 660 } 661 } 662 if (max >= 0 && (dir == Direction.ALL || dir == Direction.WEST) && !current.isMerged(Direction.WEST)) { 663 Plot other = current.getRelative(Direction.WEST); 664 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 665 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 666 current.mergePlot(other, removeRoads, queue); 667 merged.add(current.getId()); 668 merged.add(other.getId()); 669 toReturn = true; 670 671 if (removeRoads) { 672 ArrayList<PlotId> ids = new ArrayList<>(); 673 ids.add(current.getId()); 674 ids.add(other.getId()); 675 this.plot.getManager().finishPlotMerge(ids, queue); 676 } 677 } 678 } 679 } 680 if (actor != null && Settings.QUEUE.NOTIFY_PROGRESS) { 681 queue.addProgressSubscriber(subscriberFactory.createWithActor(actor)); 682 } 683 if (queue.size() > 0) { 684 queue.enqueue(); 685 } 686 visited.forEach(Plot::clearCache); 687 return toReturn; 688 } 689 690 /** 691 * Moves a plot physically, as well as the corresponding settings. 692 * 693 * @param destination Plot moved to 694 * @param actor The actor executing the task 695 * @param whenDone task when done 696 * @param allowSwap whether to swap plots 697 * @return {@code true} if the move was successful, else {@code false} 698 */ 699 public @NonNull CompletableFuture<Boolean> move( 700 final @NonNull Plot destination, 701 final @Nullable PlotPlayer<?> actor, 702 final @NonNull Runnable whenDone, 703 final boolean allowSwap 704 ) { 705 final PlotId offset = PlotId.of( 706 destination.getId().getX() - this.plot.getId().getX(), 707 destination.getId().getY() - this.plot.getId().getY() 708 ); 709 Location db = destination.getBottomAbs(); 710 Location ob = this.plot.getBottomAbs(); 711 final int offsetX = db.getX() - ob.getX(); 712 final int offsetZ = db.getZ() - ob.getZ(); 713 if (!this.plot.hasOwner()) { 714 TaskManager.runTaskLater(whenDone, TaskTime.ticks(1L)); 715 return CompletableFuture.completedFuture(false); 716 } 717 AtomicBoolean occupied = new AtomicBoolean(false); 718 Set<Plot> plots = this.plot.getConnectedPlots(); 719 for (Plot plot : plots) { 720 Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 721 if (other.hasOwner()) { 722 if (!allowSwap) { 723 TaskManager.runTaskLater(whenDone, TaskTime.ticks(1L)); 724 return CompletableFuture.completedFuture(false); 725 } 726 occupied.set(true); 727 } else { 728 plot.getPlotModificationManager().removeSign(); 729 } 730 } 731 // world border 732 destination.updateWorldBorder(); 733 final ArrayDeque<CuboidRegion> regions = new ArrayDeque<>(this.plot.getRegions()); 734 // move / swap data 735 final PlotArea originArea = this.plot.getArea(); 736 737 final Iterator<Plot> plotIterator = plots.iterator(); 738 739 CompletableFuture<Boolean> future = null; 740 if (plotIterator.hasNext()) { 741 while (plotIterator.hasNext()) { 742 final Plot plot = plotIterator.next(); 743 final Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 744 final CompletableFuture<Boolean> swapResult = plot.swapData(other); 745 if (future == null) { 746 future = swapResult; 747 } else { 748 future = future.thenCombine(swapResult, (fn, th) -> fn); 749 } 750 } 751 } else { 752 future = CompletableFuture.completedFuture(true); 753 } 754 755 return future.thenApply(result -> { 756 if (!result) { 757 return false; 758 } 759 // copy terrain 760 if (occupied.get()) { 761 new Runnable() { 762 @Override 763 public void run() { 764 if (regions.isEmpty()) { 765 // Update signs 766 destination.getPlotModificationManager().setSign(); 767 setSign(); 768 // Run final tasks 769 TaskManager.runTask(whenDone); 770 } else { 771 CuboidRegion region = regions.poll(); 772 Location[] corners = Plot.getCorners(plot.getWorldName(), region); 773 Location pos1 = corners[0]; 774 Location pos2 = corners[1]; 775 Location pos3 = pos1.add(offsetX, 0, offsetZ).withWorld(destination.getWorldName()); 776 PlotSquared.platform().regionManager().swap(pos1, pos2, pos3, actor, this); 777 } 778 } 779 }.run(); 780 } else { 781 new Runnable() { 782 @Override 783 public void run() { 784 if (regions.isEmpty()) { 785 Plot plot = destination.getRelative(0, 0); 786 Plot originPlot = 787 originArea.getPlotAbs(PlotId.of( 788 plot.getId().getX() - offset.getX(), 789 plot.getId().getY() - offset.getY() 790 )); 791 final Runnable clearDone = () -> { 792 QueueCoordinator queue = PlotModificationManager.this.plot.getArea().getQueue(); 793 for (final Plot current : plot.getConnectedPlots()) { 794 PlotModificationManager.this.plot.getManager().claimPlot(current, queue); 795 } 796 if (queue.size() > 0) { 797 queue.enqueue(); 798 } 799 plot.getPlotModificationManager().setSign(); 800 TaskManager.runTask(whenDone); 801 }; 802 if (originPlot != null) { 803 originPlot.getPlotModificationManager().clear(false, true, actor, clearDone); 804 } else { 805 clearDone.run(); 806 } 807 return; 808 } 809 final Runnable task = this; 810 CuboidRegion region = regions.poll(); 811 Location[] corners = Plot.getCorners( 812 PlotModificationManager.this.plot.getWorldName(), 813 region 814 ); 815 final Location pos1 = corners[0]; 816 final Location pos2 = corners[1]; 817 Location newPos = pos1.add(offsetX, 0, offsetZ).withWorld(destination.getWorldName()); 818 PlotSquared.platform().regionManager().copyRegion(pos1, pos2, newPos, actor, task); 819 } 820 }.run(); 821 } 822 return true; 823 }); 824 } 825 826 /** 827 * Unlink a plot and remove the roads 828 * 829 * @return {@code true} if plot was linked 830 * @see #unlinkPlot(boolean, boolean) 831 */ 832 public boolean unlink() { 833 return this.unlinkPlot(true, true); 834 } 835 836 /** 837 * Swap the plot contents and settings with another location<br> 838 * - The destination must correspond to a valid plot of equal dimensions 839 * 840 * @param destination The other plot to swap with 841 * @param actor The actor executing the task 842 * @param whenDone A task to run when finished, or null 843 * @return Future that completes with {@code true} if the swap was successful, else {@code false} 844 */ 845 public @NonNull CompletableFuture<Boolean> swap( 846 final @NonNull Plot destination, 847 @Nullable PlotPlayer<?> actor, 848 final @NonNull Runnable whenDone 849 ) { 850 return this.move(destination, actor, whenDone, true); 851 } 852 853 /** 854 * Moves the plot to an empty location<br> 855 * - The location must be empty 856 * 857 * @param destination Where to move the plot 858 * @param actor The actor executing the task 859 * @param whenDone A task to run when done, or null 860 * @return Future that completes with {@code true} if the move was successful, else {@code false} 861 */ 862 public @NonNull CompletableFuture<Boolean> move( 863 final @NonNull Plot destination, 864 @Nullable PlotPlayer<?> actor, 865 final @NonNull Runnable whenDone 866 ) { 867 return this.move(destination, actor, whenDone, false); 868 } 869 870 /** 871 * Sets a component for a plot to the provided blocks<br> 872 * - E.g. floor, wall, border etc.<br> 873 * - The available components depend on the generator being used<br> 874 * 875 * @param component Component to set 876 * @param blocks Pattern to use the generation 877 * @param actor The actor executing the task 878 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 879 * otherwise writes to the queue but does not enqueue. 880 * @return {@code true} if the component was set successfully, else {@code false} 881 */ 882 public boolean setComponent( 883 final @NonNull String component, 884 final @NonNull Pattern blocks, 885 @Nullable PlotPlayer<?> actor, 886 final @Nullable QueueCoordinator queue 887 ) { 888 final PlotComponentSetEvent event = PlotSquared.get().getEventDispatcher().callComponentSet(this.plot, component, blocks); 889 return this.plot.getManager().setComponent(this.plot.getId(), event.getComponent(), event.getPattern(), actor, queue); 890 } 891 892 /** 893 * Delete a plot (use null for the runnable if you don't need to be notified on completion) 894 * 895 * <p> 896 * Use {@link PlotModificationManager#clear(boolean, boolean, PlotPlayer, Runnable)} to simply clear a plot 897 * </p> 898 * 899 * @param actor The actor executing the task 900 * @param whenDone task to run when plot has been deleted. Nullable 901 * @return {@code true} if the deletion was successful, {@code false} if not 902 * @see PlotSquared#removePlot(Plot, boolean) 903 */ 904 public boolean deletePlot(@Nullable PlotPlayer<?> actor, final Runnable whenDone) { 905 if (!this.plot.hasOwner()) { 906 return false; 907 } 908 final Set<Plot> plots = this.plot.getConnectedPlots(); 909 this.clear(false, true, actor, () -> { 910 for (Plot current : plots) { 911 current.unclaim(); 912 } 913 TaskManager.runTask(whenDone); 914 }); 915 return true; 916 } 917 918 /** 919 * Sets components such as border, wall, floor. 920 * (components are generator specific) 921 * 922 * @param component component to set 923 * @param blocks string of block(s) to set component to 924 * @param actor The player executing the task 925 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 926 * otherwise writes to the queue but does not enqueue. 927 * @return {@code true} if the update was successful, {@code false} if not 928 */ 929 @Deprecated 930 public boolean setComponent( 931 String component, 932 String blocks, 933 @Nullable PlotPlayer<?> actor, 934 @Nullable QueueCoordinator queue 935 ) { 936 final BlockBucket parsed = ConfigurationUtil.BLOCK_BUCKET.parseString(blocks); 937 if (parsed != null && parsed.isEmpty()) { 938 return false; 939 } 940 return this.setComponent(component, parsed.toPattern(), actor, queue); 941 } 942 943 /** 944 * Remove the south road section of a plot<br> 945 * - Used when a plot is merged<br> 946 * 947 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 948 * otherwise writes to the queue but does not enqueue. 949 */ 950 public void removeRoadSouth(final @Nullable QueueCoordinator queue) { 951 if (this.plot.getArea().getType() != PlotAreaType.NORMAL && this.plot 952 .getArea() 953 .getTerrain() == PlotAreaTerrainType.ROAD) { 954 Plot other = this.plot.getRelative(Direction.SOUTH); 955 Location bot = other.getBottomAbs(); 956 Location top = this.plot.getTopAbs(); 957 Location pos1 = Location.at(this.plot.getWorldName(), bot.getX(), plot.getArea().getMinGenHeight(), top.getZ()); 958 Location pos2 = Location.at(this.plot.getWorldName(), top.getX(), plot.getArea().getMaxGenHeight(), bot.getZ()); 959 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 960 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 961 this.plot.getManager().removeRoadSouth(this.plot, queue); 962 } 963 } 964 965 /** 966 * Remove the east road section of a plot<br> 967 * - Used when a plot is merged<br> 968 * 969 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 970 * otherwise writes to the queue but does not enqueue. 971 */ 972 public void removeRoadEast(@Nullable QueueCoordinator queue) { 973 if (this.plot.getArea().getType() != PlotAreaType.NORMAL && this.plot 974 .getArea() 975 .getTerrain() == PlotAreaTerrainType.ROAD) { 976 Plot other = this.plot.getRelative(Direction.EAST); 977 Location bot = other.getBottomAbs(); 978 Location top = this.plot.getTopAbs(); 979 Location pos1 = Location.at(this.plot.getWorldName(), top.getX(), plot.getArea().getMinGenHeight(), bot.getZ()); 980 Location pos2 = Location.at(this.plot.getWorldName(), bot.getX(), plot.getArea().getMaxGenHeight(), top.getZ()); 981 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 982 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 983 this.plot.getArea().getPlotManager().removeRoadEast(this.plot, queue); 984 } 985 } 986 987 /** 988 * Remove the SE road (only effects terrain) 989 * 990 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 991 * otherwise writes to the queue but does not enqueue. 992 */ 993 public void removeRoadSouthEast(@Nullable QueueCoordinator queue) { 994 if (this.plot.getArea().getType() != PlotAreaType.NORMAL && this.plot 995 .getArea() 996 .getTerrain() == PlotAreaTerrainType.ROAD) { 997 Plot other = this.plot.getRelative(1, 1); 998 Location pos1 = this.plot.getTopAbs().add(1, 0, 1); 999 Location pos2 = other.getBottomAbs().subtract(1, 0, 1); 1000 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 1001 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 1002 this.plot.getArea().getPlotManager().removeRoadSouthEast(this.plot, queue); 1003 } 1004 } 1005 1006}