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