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}