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