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