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