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}