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 } 375 if (current.isMerged(Direction.SOUTH)) { 376 manager.createRoadSouth(current, queue); 377 } 378 } 379 } 380 for (Plot current : plots) { 381 boolean[] merged = new boolean[]{false, false, false, false}; 382 current.setMerged(merged); 383 } 384 if (createSign) { 385 queue.setCompleteTask(() -> TaskManager.runTaskAsync(() -> { 386 List<CompletableFuture<Void>> tasks = plots.stream().map(current -> PlotSquared.platform().playerManager() 387 .getUsernameCaption(current.getOwnerAbs()) 388 .thenAccept(caption -> current 389 .getPlotModificationManager() 390 .setSign(caption.getComponent(LocaleHolder.console())))) 391 .toList(); 392 CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).whenComplete((unused, throwable) -> { 393 if (whenDone != null) { 394 TaskManager.runTask(whenDone); 395 } 396 }); 397 })); 398 } else if (whenDone != null) { 399 queue.setCompleteTask(whenDone); 400 } 401 if (createRoad) { 402 manager.finishPlotUnlink(ids, queue); 403 } 404 queue.enqueue(); 405 return true; 406 } 407 408 /** 409 * Sets the sign for a plot to a specific name 410 * 411 * @param name name 412 */ 413 public void setSign(final @NonNull String name) { 414 if (!this.plot.isLoaded()) { 415 return; 416 } 417 PlotManager manager = this.plot.getArea().getPlotManager(); 418 if (this.plot.getArea().allowSigns()) { 419 Location location = manager.getSignLoc(this.plot); 420 String id = this.plot.getId().toString(); 421 Caption[] lines = new Caption[]{TranslatableCaption.of("signs.owner_sign_line_1"), TranslatableCaption.of( 422 "signs.owner_sign_line_2"), 423 TranslatableCaption.of("signs.owner_sign_line_3"), TranslatableCaption.of("signs.owner_sign_line_4")}; 424 PlotSquared.platform().worldUtil().setSign(location, lines, TagResolver.builder() 425 .tag("id", Tag.inserting(Component.text(id))) 426 .tag("owner", Tag.inserting(Component.text(name))) 427 .build()); 428 } 429 } 430 431 /** 432 * Resend all chunks inside the plot to nearby players<br> 433 * This should not need to be called 434 */ 435 public void refreshChunks() { 436 final HashSet<BlockVector2> chunks = new HashSet<>(); 437 for (final CuboidRegion region : this.plot.getRegions()) { 438 for (int x = region.getMinimumPoint().getX() >> 4; x <= region.getMaximumPoint().getX() >> 4; x++) { 439 for (int z = region.getMinimumPoint().getZ() >> 4; z <= region.getMaximumPoint().getZ() >> 4; z++) { 440 if (chunks.add(BlockVector2.at(x, z))) { 441 PlotSquared.platform().worldUtil().refreshChunk(x, z, this.plot.getWorldName()); 442 } 443 } 444 } 445 } 446 } 447 448 /** 449 * Remove the plot sign if it is set. 450 */ 451 public void removeSign() { 452 PlotManager manager = this.plot.getArea().getPlotManager(); 453 if (!this.plot.getArea().allowSigns()) { 454 return; 455 } 456 Location location = manager.getSignLoc(this.plot); 457 QueueCoordinator queue = 458 PlotSquared.platform().globalBlockQueue().getNewQueue(PlotSquared 459 .platform() 460 .worldUtil() 461 .getWeWorld(this.plot.getWorldName())); 462 queue.setBlock(location.getX(), location.getY(), location.getZ(), BlockTypes.AIR.getDefaultState()); 463 queue.enqueue(); 464 } 465 466 /** 467 * Sets the plot sign if plot signs are enabled. 468 */ 469 public void setSign() { 470 if (!this.plot.hasOwner()) { 471 this.setSign("unknown"); 472 return; 473 } 474 PlotSquared.get().getImpromptuUUIDPipeline().getSingle( 475 this.plot.getOwnerAbs(), 476 (username, sign) -> this.setSign(username) 477 ); 478 } 479 480 /** 481 * Register a plot and create it in the database<br> 482 * - The plot will not be created if the owner is null<br> 483 * - Any setting from before plot creation will not be saved until the server is stopped properly. i.e. Set any values/options after plot 484 * creation. 485 * 486 * @return {@code true} if plot was created successfully 487 */ 488 public boolean create() { 489 return this.create(this.plot.getOwnerAbs(), true); 490 } 491 492 /** 493 * Register a plot and create it in the database<br> 494 * - The plot will not be created if the owner is null<br> 495 * - Any setting from before plot creation will not be saved until the server is stopped properly. i.e. Set any values/options after plot 496 * creation. 497 * 498 * @param uuid the uuid of the plot owner 499 * @param notify notify 500 * @return {@code true} if plot was created successfully, else {@code false} 501 */ 502 public boolean create(final @NonNull UUID uuid, final boolean notify) { 503 this.plot.setOwnerAbs(uuid); 504 Plot existing = this.plot.getArea().getOwnedPlotAbs(this.plot.getId()); 505 if (existing != null) { 506 throw new IllegalStateException("Plot already exists!"); 507 } 508 if (notify) { 509 Integer meta = (Integer) this.plot.getArea().getMeta("worldBorder"); 510 if (meta != null) { 511 this.plot.updateWorldBorder(); 512 } 513 } 514 this.plot.clearCache(); 515 this.plot.getTrusted().clear(); 516 this.plot.getMembers().clear(); 517 this.plot.getDenied().clear(); 518 this.plot.settings = new PlotSettings(); 519 if (this.plot.getArea().addPlot(this.plot)) { 520 DBFunc.createPlotAndSettings(this.plot, () -> { 521 PlotArea plotworld = plot.getArea(); 522 if (notify && plotworld.isAutoMerge()) { 523 final PlotPlayer<?> player = PlotSquared.platform().playerManager().getPlayerIfExists(uuid); 524 525 PlotMergeEvent event = PlotSquared.get().getEventDispatcher().callMerge( 526 this.plot, 527 Direction.ALL, 528 Integer.MAX_VALUE, 529 player 530 ); 531 532 if (event.getEventResult() == Result.DENY) { 533 if (player != null) { 534 player.sendMessage( 535 TranslatableCaption.of("events.event_denied"), 536 TagResolver.resolver("value", Tag.inserting(Component.text("Auto merge on claim"))) 537 ); 538 } 539 return; 540 } 541 if (plot.getPlotModificationManager().autoMerge(event.getDir(), event.getMax(), uuid, player, true)) { 542 PlotSquared.get().getEventDispatcher().callPostMerge(player, plot); 543 } 544 } 545 }); 546 return true; 547 } 548 LOGGER.info( 549 "Failed to add plot {} to plot area {}", 550 this.plot.getId().toCommaSeparatedString(), 551 this.plot.getArea().toString() 552 ); 553 return false; 554 } 555 556 /** 557 * Auto merge a plot in a specific direction. 558 * 559 * @param dir the direction to merge 560 * @param max the max number of merges to do 561 * @param uuid the UUID it is allowed to merge with 562 * @param actor The actor executing the task 563 * @param removeRoads whether to remove roads 564 * @return {@code true} if a merge takes place, else {@code false} 565 */ 566 public boolean autoMerge( 567 final @NonNull Direction dir, 568 int max, 569 final @NonNull UUID uuid, 570 @Nullable PlotPlayer<?> actor, 571 final boolean removeRoads 572 ) { 573 //Ignore merging if there is no owner for the plot 574 if (!this.plot.hasOwner()) { 575 return false; 576 } 577 Set<Plot> connected = this.plot.getConnectedPlots(); 578 HashSet<PlotId> merged = connected.stream().map(Plot::getId).collect(Collectors.toCollection(HashSet::new)); 579 ArrayDeque<Plot> frontier = new ArrayDeque<>(connected); 580 Plot current; 581 boolean toReturn = false; 582 HashSet<Plot> visited = new HashSet<>(); 583 QueueCoordinator queue = this.plot.getArea().getQueue(); 584 while ((current = frontier.poll()) != null && max >= 0) { 585 if (visited.contains(current)) { 586 continue; 587 } 588 visited.add(current); 589 Set<Plot> plots; 590 if ((dir == Direction.ALL || dir == Direction.NORTH) && !current.isMerged(Direction.NORTH)) { 591 Plot other = current.getRelative(Direction.NORTH); 592 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 593 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 594 current.mergePlot(other, removeRoads, queue); 595 merged.add(current.getId()); 596 merged.add(other.getId()); 597 toReturn = true; 598 599 if (removeRoads) { 600 ArrayList<PlotId> ids = new ArrayList<>(); 601 ids.add(current.getId()); 602 ids.add(other.getId()); 603 this.plot.getManager().finishPlotMerge(ids, queue); 604 } 605 } 606 } 607 if (max >= 0 && (dir == Direction.ALL || dir == Direction.EAST) && !current.isMerged(Direction.EAST)) { 608 Plot other = current.getRelative(Direction.EAST); 609 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 610 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 611 current.mergePlot(other, removeRoads, queue); 612 merged.add(current.getId()); 613 merged.add(other.getId()); 614 toReturn = true; 615 616 if (removeRoads) { 617 ArrayList<PlotId> ids = new ArrayList<>(); 618 ids.add(current.getId()); 619 ids.add(other.getId()); 620 this.plot.getManager().finishPlotMerge(ids, queue); 621 } 622 } 623 } 624 if (max >= 0 && (dir == Direction.ALL || dir == Direction.SOUTH) && !current.isMerged(Direction.SOUTH)) { 625 Plot other = current.getRelative(Direction.SOUTH); 626 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 627 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 628 current.mergePlot(other, removeRoads, queue); 629 merged.add(current.getId()); 630 merged.add(other.getId()); 631 toReturn = true; 632 633 if (removeRoads) { 634 ArrayList<PlotId> ids = new ArrayList<>(); 635 ids.add(current.getId()); 636 ids.add(other.getId()); 637 this.plot.getManager().finishPlotMerge(ids, queue); 638 } 639 } 640 } 641 if (max >= 0 && (dir == Direction.ALL || dir == Direction.WEST) && !current.isMerged(Direction.WEST)) { 642 Plot other = current.getRelative(Direction.WEST); 643 if (other != null && other.isOwner(uuid) && (other.getBasePlot(false).equals(current.getBasePlot(false)) 644 || (plots = other.getConnectedPlots()).size() <= max && frontier.addAll(plots) && (max -= plots.size()) != -1)) { 645 current.mergePlot(other, removeRoads, queue); 646 merged.add(current.getId()); 647 merged.add(other.getId()); 648 toReturn = true; 649 650 if (removeRoads) { 651 ArrayList<PlotId> ids = new ArrayList<>(); 652 ids.add(current.getId()); 653 ids.add(other.getId()); 654 this.plot.getManager().finishPlotMerge(ids, queue); 655 } 656 } 657 } 658 } 659 if (actor != null && Settings.QUEUE.NOTIFY_PROGRESS) { 660 queue.addProgressSubscriber(subscriberFactory.createWithActor(actor)); 661 } 662 if (queue.size() > 0) { 663 queue.enqueue(); 664 } 665 visited.forEach(Plot::clearCache); 666 return toReturn; 667 } 668 669 /** 670 * Moves a plot physically, as well as the corresponding settings. 671 * 672 * @param destination Plot moved to 673 * @param actor The actor executing the task 674 * @param whenDone task when done 675 * @param allowSwap whether to swap plots 676 * @return {@code true} if the move was successful, else {@code false} 677 */ 678 public @NonNull CompletableFuture<Boolean> move( 679 final @NonNull Plot destination, 680 final @Nullable PlotPlayer<?> actor, 681 final @NonNull Runnable whenDone, 682 final boolean allowSwap 683 ) { 684 final PlotId offset = PlotId.of( 685 destination.getId().getX() - this.plot.getId().getX(), 686 destination.getId().getY() - this.plot.getId().getY() 687 ); 688 Location db = destination.getBottomAbs(); 689 Location ob = this.plot.getBottomAbs(); 690 final int offsetX = db.getX() - ob.getX(); 691 final int offsetZ = db.getZ() - ob.getZ(); 692 if (!this.plot.hasOwner()) { 693 TaskManager.runTaskLater(whenDone, TaskTime.ticks(1L)); 694 return CompletableFuture.completedFuture(false); 695 } 696 AtomicBoolean occupied = new AtomicBoolean(false); 697 Set<Plot> plots = this.plot.getConnectedPlots(); 698 for (Plot plot : plots) { 699 Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 700 if (other.hasOwner()) { 701 if (!allowSwap) { 702 TaskManager.runTaskLater(whenDone, TaskTime.ticks(1L)); 703 return CompletableFuture.completedFuture(false); 704 } 705 occupied.set(true); 706 } else { 707 plot.getPlotModificationManager().removeSign(); 708 } 709 } 710 // world border 711 destination.updateWorldBorder(); 712 final ArrayDeque<CuboidRegion> regions = new ArrayDeque<>(this.plot.getRegions()); 713 // move / swap data 714 final PlotArea originArea = this.plot.getArea(); 715 716 final Iterator<Plot> plotIterator = plots.iterator(); 717 718 CompletableFuture<Boolean> future = null; 719 if (plotIterator.hasNext()) { 720 while (plotIterator.hasNext()) { 721 final Plot plot = plotIterator.next(); 722 final Plot other = plot.getRelative(destination.getArea(), offset.getX(), offset.getY()); 723 final CompletableFuture<Boolean> swapResult = plot.swapData(other); 724 if (future == null) { 725 future = swapResult; 726 } else { 727 future = future.thenCombine(swapResult, (fn, th) -> fn); 728 } 729 } 730 } else { 731 future = CompletableFuture.completedFuture(true); 732 } 733 734 return future.thenApply(result -> { 735 if (!result) { 736 return false; 737 } 738 // copy terrain 739 if (occupied.get()) { 740 new Runnable() { 741 @Override 742 public void run() { 743 if (regions.isEmpty()) { 744 // Update signs 745 destination.getPlotModificationManager().setSign(); 746 setSign(); 747 // Run final tasks 748 TaskManager.runTask(whenDone); 749 } else { 750 CuboidRegion region = regions.poll(); 751 Location[] corners = Plot.getCorners(plot.getWorldName(), region); 752 Location pos1 = corners[0]; 753 Location pos2 = corners[1]; 754 Location pos3 = pos1.add(offsetX, 0, offsetZ).withWorld(destination.getWorldName()); 755 PlotSquared.platform().regionManager().swap(pos1, pos2, pos3, actor, this); 756 } 757 } 758 }.run(); 759 } else { 760 new Runnable() { 761 @Override 762 public void run() { 763 if (regions.isEmpty()) { 764 Plot plot = destination.getRelative(0, 0); 765 Plot originPlot = 766 originArea.getPlotAbs(PlotId.of( 767 plot.getId().getX() - offset.getX(), 768 plot.getId().getY() - offset.getY() 769 )); 770 final Runnable clearDone = () -> { 771 QueueCoordinator queue = PlotModificationManager.this.plot.getArea().getQueue(); 772 for (final Plot current : plot.getConnectedPlots()) { 773 PlotModificationManager.this.plot.getManager().claimPlot(current, queue); 774 } 775 if (queue.size() > 0) { 776 queue.enqueue(); 777 } 778 plot.getPlotModificationManager().setSign(); 779 TaskManager.runTask(whenDone); 780 }; 781 if (originPlot != null) { 782 originPlot.getPlotModificationManager().clear(false, true, actor, clearDone); 783 } else { 784 clearDone.run(); 785 } 786 return; 787 } 788 final Runnable task = this; 789 CuboidRegion region = regions.poll(); 790 Location[] corners = Plot.getCorners( 791 PlotModificationManager.this.plot.getWorldName(), 792 region 793 ); 794 final Location pos1 = corners[0]; 795 final Location pos2 = corners[1]; 796 Location newPos = pos1.add(offsetX, 0, offsetZ).withWorld(destination.getWorldName()); 797 PlotSquared.platform().regionManager().copyRegion(pos1, pos2, newPos, actor, task); 798 } 799 }.run(); 800 } 801 return true; 802 }); 803 } 804 805 /** 806 * Unlink a plot and remove the roads 807 * 808 * @return {@code true} if plot was linked 809 * @see #unlinkPlot(boolean, boolean) 810 */ 811 public boolean unlink() { 812 return this.unlinkPlot(true, true); 813 } 814 815 /** 816 * Swap the plot contents and settings with another location<br> 817 * - The destination must correspond to a valid plot of equal dimensions 818 * 819 * @param destination The other plot to swap with 820 * @param actor The actor executing the task 821 * @param whenDone A task to run when finished, or null 822 * @return Future that completes with {@code true} if the swap was successful, else {@code false} 823 */ 824 public @NonNull CompletableFuture<Boolean> swap( 825 final @NonNull Plot destination, 826 @Nullable PlotPlayer<?> actor, 827 final @NonNull Runnable whenDone 828 ) { 829 return this.move(destination, actor, whenDone, true); 830 } 831 832 /** 833 * Moves the plot to an empty location<br> 834 * - The location must be empty 835 * 836 * @param destination Where to move the plot 837 * @param actor The actor executing the task 838 * @param whenDone A task to run when done, or null 839 * @return Future that completes with {@code true} if the move was successful, else {@code false} 840 */ 841 public @NonNull CompletableFuture<Boolean> move( 842 final @NonNull Plot destination, 843 @Nullable PlotPlayer<?> actor, 844 final @NonNull Runnable whenDone 845 ) { 846 return this.move(destination, actor, whenDone, false); 847 } 848 849 /** 850 * Sets a component for a plot to the provided blocks<br> 851 * - E.g. floor, wall, border etc.<br> 852 * - The available components depend on the generator being used<br> 853 * 854 * @param component Component to set 855 * @param blocks Pattern to use the generation 856 * @param actor The actor executing the task 857 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 858 * otherwise writes to the queue but does not enqueue. 859 * @return {@code true} if the component was set successfully, else {@code false} 860 */ 861 public boolean setComponent( 862 final @NonNull String component, 863 final @NonNull Pattern blocks, 864 @Nullable PlotPlayer<?> actor, 865 final @Nullable QueueCoordinator queue 866 ) { 867 final PlotComponentSetEvent event = PlotSquared.get().getEventDispatcher().callComponentSet(this.plot, component, blocks); 868 return this.plot.getManager().setComponent(this.plot.getId(), event.getComponent(), event.getPattern(), actor, queue); 869 } 870 871 /** 872 * Delete a plot (use null for the runnable if you don't need to be notified on completion) 873 * 874 * <p> 875 * Use {@link PlotModificationManager#clear(boolean, boolean, PlotPlayer, Runnable)} to simply clear a plot 876 * </p> 877 * 878 * @param actor The actor executing the task 879 * @param whenDone task to run when plot has been deleted. Nullable 880 * @return {@code true} if the deletion was successful, {@code false} if not 881 * @see PlotSquared#removePlot(Plot, boolean) 882 */ 883 public boolean deletePlot(@Nullable PlotPlayer<?> actor, final Runnable whenDone) { 884 if (!this.plot.hasOwner()) { 885 return false; 886 } 887 final Set<Plot> plots = this.plot.getConnectedPlots(); 888 this.clear(false, true, actor, () -> { 889 for (Plot current : plots) { 890 current.unclaim(); 891 } 892 TaskManager.runTask(whenDone); 893 }); 894 return true; 895 } 896 897 /** 898 * Sets components such as border, wall, floor. 899 * (components are generator specific) 900 * 901 * @param component component to set 902 * @param blocks string of block(s) to set component to 903 * @param actor The player executing the task 904 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 905 * otherwise writes to the queue but does not enqueue. 906 * @return {@code true} if the update was successful, {@code false} if not 907 */ 908 @Deprecated 909 public boolean setComponent( 910 String component, 911 String blocks, 912 @Nullable PlotPlayer<?> actor, 913 @Nullable QueueCoordinator queue 914 ) { 915 final BlockBucket parsed = ConfigurationUtil.BLOCK_BUCKET.parseString(blocks); 916 if (parsed != null && parsed.isEmpty()) { 917 return false; 918 } 919 return this.setComponent(component, parsed.toPattern(), actor, queue); 920 } 921 922 /** 923 * Remove the south road section of a plot<br> 924 * - Used when a plot is merged<br> 925 * 926 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 927 * otherwise writes to the queue but does not enqueue. 928 */ 929 public void removeRoadSouth(final @Nullable QueueCoordinator queue) { 930 if (this.plot.getArea().getType() != PlotAreaType.NORMAL && this.plot 931 .getArea() 932 .getTerrain() == PlotAreaTerrainType.ROAD) { 933 Plot other = this.plot.getRelative(Direction.SOUTH); 934 Location bot = other.getBottomAbs(); 935 Location top = this.plot.getTopAbs(); 936 Location pos1 = Location.at(this.plot.getWorldName(), bot.getX(), plot.getArea().getMinGenHeight(), top.getZ()); 937 Location pos2 = Location.at(this.plot.getWorldName(), top.getX(), plot.getArea().getMaxGenHeight(), bot.getZ()); 938 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 939 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 940 this.plot.getManager().removeRoadSouth(this.plot, queue); 941 } 942 } 943 944 /** 945 * Remove the east road section of a plot<br> 946 * - Used when a plot is merged<br> 947 * 948 * @param queue Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues, 949 * otherwise writes to the queue but does not enqueue. 950 */ 951 public void removeRoadEast(@Nullable QueueCoordinator queue) { 952 if (this.plot.getArea().getType() != PlotAreaType.NORMAL && this.plot 953 .getArea() 954 .getTerrain() == PlotAreaTerrainType.ROAD) { 955 Plot other = this.plot.getRelative(Direction.EAST); 956 Location bot = other.getBottomAbs(); 957 Location top = this.plot.getTopAbs(); 958 Location pos1 = Location.at(this.plot.getWorldName(), top.getX(), plot.getArea().getMinGenHeight(), bot.getZ()); 959 Location pos2 = Location.at(this.plot.getWorldName(), bot.getX(), plot.getArea().getMaxGenHeight(), top.getZ()); 960 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 961 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 962 this.plot.getArea().getPlotManager().removeRoadEast(this.plot, queue); 963 } 964 } 965 966 /** 967 * Remove the SE road (only effects terrain) 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 removeRoadSouthEast(@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(1, 1); 977 Location pos1 = this.plot.getTopAbs().add(1, 0, 1); 978 Location pos2 = other.getBottomAbs().subtract(1, 0, 1); 979 PlotSquared.platform().regionManager().regenerateRegion(pos1, pos2, true, null); 980 } else if (this.plot.getArea().getTerrain() != PlotAreaTerrainType.ALL) { // no road generated => no road to remove 981 this.plot.getArea().getPlotManager().removeRoadSouthEast(this.plot, queue); 982 } 983 } 984 985}