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.common.collect.ImmutableSet;
022import com.google.common.collect.Lists;
023import com.google.common.collect.Sets;
024import com.google.inject.Inject;
025import com.plotsquared.core.PlotSquared;
026import com.plotsquared.core.command.Like;
027import com.plotsquared.core.configuration.Settings;
028import com.plotsquared.core.configuration.caption.Caption;
029import com.plotsquared.core.configuration.caption.CaptionUtility;
030import com.plotsquared.core.configuration.caption.StaticCaption;
031import com.plotsquared.core.configuration.caption.TranslatableCaption;
032import com.plotsquared.core.database.DBFunc;
033import com.plotsquared.core.events.Result;
034import com.plotsquared.core.events.TeleportCause;
035import com.plotsquared.core.generator.ClassicPlotWorld;
036import com.plotsquared.core.listener.PlotListener;
037import com.plotsquared.core.location.BlockLoc;
038import com.plotsquared.core.location.Direction;
039import com.plotsquared.core.location.Location;
040import com.plotsquared.core.permissions.Permission;
041import com.plotsquared.core.player.ConsolePlayer;
042import com.plotsquared.core.player.PlotPlayer;
043import com.plotsquared.core.plot.expiration.PlotAnalysis;
044import com.plotsquared.core.plot.flag.FlagContainer;
045import com.plotsquared.core.plot.flag.GlobalFlagContainer;
046import com.plotsquared.core.plot.flag.InternalFlag;
047import com.plotsquared.core.plot.flag.PlotFlag;
048import com.plotsquared.core.plot.flag.implementations.DescriptionFlag;
049import com.plotsquared.core.plot.flag.implementations.KeepFlag;
050import com.plotsquared.core.plot.flag.implementations.ServerPlotFlag;
051import com.plotsquared.core.plot.flag.types.DoubleFlag;
052import com.plotsquared.core.plot.schematic.Schematic;
053import com.plotsquared.core.plot.world.SinglePlotArea;
054import com.plotsquared.core.queue.QueueCoordinator;
055import com.plotsquared.core.util.EventDispatcher;
056import com.plotsquared.core.util.MathMan;
057import com.plotsquared.core.util.PlayerManager;
058import com.plotsquared.core.util.RegionManager;
059import com.plotsquared.core.util.RegionUtil;
060import com.plotsquared.core.util.SchematicHandler;
061import com.plotsquared.core.util.TimeUtil;
062import com.plotsquared.core.util.WorldUtil;
063import com.plotsquared.core.util.query.PlotQuery;
064import com.plotsquared.core.util.task.RunnableVal;
065import com.plotsquared.core.util.task.TaskManager;
066import com.plotsquared.core.util.task.TaskTime;
067import com.sk89q.worldedit.math.BlockVector3;
068import com.sk89q.worldedit.regions.CuboidRegion;
069import com.sk89q.worldedit.world.biome.BiomeType;
070import net.kyori.adventure.text.Component;
071import net.kyori.adventure.text.TextComponent;
072import net.kyori.adventure.text.minimessage.MiniMessage;
073import net.kyori.adventure.text.minimessage.Template;
074import org.apache.logging.log4j.LogManager;
075import org.apache.logging.log4j.Logger;
076import org.checkerframework.checker.nullness.qual.NonNull;
077import org.checkerframework.checker.nullness.qual.Nullable;
078
079import java.lang.ref.Cleaner;
080import java.text.DecimalFormat;
081import java.text.SimpleDateFormat;
082import java.util.ArrayDeque;
083import java.util.ArrayList;
084import java.util.Collection;
085import java.util.Collections;
086import java.util.HashMap;
087import java.util.HashSet;
088import java.util.List;
089import java.util.Map;
090import java.util.Map.Entry;
091import java.util.Objects;
092import java.util.Set;
093import java.util.TimeZone;
094import java.util.UUID;
095import java.util.concurrent.CompletableFuture;
096import java.util.concurrent.ConcurrentHashMap;
097import java.util.function.Consumer;
098
099import static com.plotsquared.core.util.entity.EntityCategories.CAP_ANIMAL;
100import static com.plotsquared.core.util.entity.EntityCategories.CAP_ENTITY;
101import static com.plotsquared.core.util.entity.EntityCategories.CAP_MISC;
102import static com.plotsquared.core.util.entity.EntityCategories.CAP_MOB;
103import static com.plotsquared.core.util.entity.EntityCategories.CAP_MONSTER;
104import static com.plotsquared.core.util.entity.EntityCategories.CAP_VEHICLE;
105
106/**
107 * The plot class<br>
108 * [IMPORTANT]
109 * - Unclaimed plots will not have persistent information.
110 * - Any information set/modified in an unclaimed object may not be reflected in other instances
111 * - Using the `new` operator will create an unclaimed plot instance
112 * - Use the methods from the PlotArea/PS/Location etc to get existing plots
113 */
114public class Plot {
115
116    @Deprecated(forRemoval = true, since = "6.6.0")
117    public static final int MAX_HEIGHT = 256;
118
119    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + Plot.class.getSimpleName());
120    private static final DecimalFormat FLAG_DECIMAL_FORMAT = new DecimalFormat("0");
121    private static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build();
122    private static final Cleaner CLEANER = Cleaner.create();
123
124    static {
125        FLAG_DECIMAL_FORMAT.setMaximumFractionDigits(340);
126    }
127
128    /**
129     * Plot flag container
130     */
131    private final FlagContainer flagContainer = new FlagContainer(null);
132    /**
133     * Utility used to manage plot comments
134     */
135    private final PlotCommentContainer plotCommentContainer = new PlotCommentContainer(this);
136    /**
137     * Utility used to modify the plot
138     */
139    private final PlotModificationManager plotModificationManager = new PlotModificationManager(this);
140    /**
141     * Represents whatever the database manager needs it to: <br>
142     * - A value of -1 usually indicates the plot will not be stored in the DB<br>
143     * - A value of 0 usually indicates that the DB manager hasn't set a value<br>
144     *
145     * @deprecated magical
146     */
147    @Deprecated
148    public int temp;
149    /**
150     * List of trusted (with plot permissions).
151     */
152    HashSet<UUID> trusted;
153    /**
154     * List of members users (with plot permissions).
155     */
156    HashSet<UUID> members;
157    /**
158     * List of denied players.
159     */
160    HashSet<UUID> denied;
161    /**
162     * External settings class.
163     * - Please favor the methods over direct access to this class<br>
164     * - The methods are more likely to be left unchanged from version changes<br>
165     */
166    PlotSettings settings;
167    @NonNull
168    private PlotId id;
169    // These will be injected
170    @Inject
171    private EventDispatcher eventDispatcher;
172    @Inject
173    private PlotListener plotListener;
174    @Inject
175    private RegionManager regionManager;
176    @Inject
177    private WorldUtil worldUtil;
178    @Inject
179    private SchematicHandler schematicHandler;
180    /**
181     * plot owner
182     * (Merged plots can have multiple owners)
183     * Direct access is Deprecated: use getOwners()
184     *
185     * @deprecated
186     */
187    private UUID owner;
188    /**
189     * Plot creation timestamp (not accurate if the plot was created before this was implemented)<br>
190     * - Milliseconds since the epoch<br>
191     */
192    private long timestamp;
193    private PlotArea area;
194    /**
195     * Session only plot metadata (session is until the server stops)<br>
196     * <br>
197     * For persistent metadata use the flag system
198     */
199    private ConcurrentHashMap<String, Object> meta;
200    /**
201     * The cached origin plot.
202     * - The origin plot is used for plot grouping and relational data
203     */
204    private Plot origin;
205
206    private Set<Plot> connectedCache;
207
208    /**
209     * Constructor for a new plot.
210     * (Only changes after plot.create() will be properly set in the database)
211     *
212     * <p>
213     * See {@link Plot#getPlot(Location)} for existing plots
214     * </p>
215     *
216     * @param area  the PlotArea where the plot is located
217     * @param id    the plot id
218     * @param owner the plot owner
219     */
220    public Plot(final PlotArea area, final @NonNull PlotId id, final UUID owner) {
221        this(area, id, owner, 0);
222    }
223
224    /**
225     * Constructor for an unowned plot.
226     * (Only changes after plot.create() will be properly set in the database)
227     *
228     * <p>
229     * See {@link Plot#getPlot(Location)} for existing plots
230     * </p>
231     *
232     * @param area the PlotArea where the plot is located
233     * @param id   the plot id
234     */
235    public Plot(final @NonNull PlotArea area, final @NonNull PlotId id) {
236        this(area, id, null, 0);
237    }
238
239    /**
240     * Constructor for a temporary plot (use -1 for temp)<br>
241     * The database will ignore any queries regarding temporary plots.
242     * Please note that some bulk plot management functions may still affect temporary plots (TODO: fix this)
243     *
244     * <p>
245     * See {@link Plot#getPlot(Location)} for existing plots
246     * </p>
247     *
248     * @param area  the PlotArea where the plot is located
249     * @param id    the plot id
250     * @param owner the owner of the plot
251     * @param temp  Represents whatever the database manager needs it to
252     */
253    public Plot(final PlotArea area, final @NonNull PlotId id, final UUID owner, final int temp) {
254        this.area = area;
255        this.id = id;
256        this.owner = owner;
257        this.temp = temp;
258        this.flagContainer.setParentContainer(area.getFlagContainer());
259        PlotSquared.platform().injector().injectMembers(this);
260        // This is needed, because otherwise the Plot, the FlagContainer and its
261        // `this::handleUnknown` PlotFlagUpdateHandler won't get cleaned up ever
262        CLEANER.register(this, this.flagContainer.createCleanupHook());
263    }
264
265    /**
266     * Constructor for a saved plots (Used by the database manager when plots are fetched)
267     *
268     * <p>
269     * See {@link Plot#getPlot(Location)} for existing plots
270     * </p>
271     *
272     * @param id        the plot id
273     * @param owner     the plot owner
274     * @param trusted   the plot trusted players
275     * @param members   the plot added players
276     * @param denied    the plot denied players
277     * @param alias     the plot's alias
278     * @param position  plot home position
279     * @param flags     the plot's flags
280     * @param area      the plot's PlotArea
281     * @param merged    an array giving merged plots
282     * @param timestamp when the plot was created
283     * @param temp      value representing whatever DBManager needs to to. Do not touch tbh.
284     */
285    public Plot(
286            @NonNull PlotId id,
287            UUID owner,
288            HashSet<UUID> trusted,
289            HashSet<UUID> members,
290            HashSet<UUID> denied,
291            String alias,
292            BlockLoc position,
293            Collection<PlotFlag<?, ?>> flags,
294            PlotArea area,
295            boolean[] merged,
296            long timestamp,
297            int temp
298    ) {
299        this.id = id;
300        this.area = area;
301        this.owner = owner;
302        this.settings = new PlotSettings();
303        this.members = members;
304        this.trusted = trusted;
305        this.denied = denied;
306        this.settings.setAlias(alias);
307        this.settings.setPosition(position);
308        this.settings.setMerged(merged);
309        this.timestamp = timestamp;
310        this.temp = temp;
311        if (area != null) {
312            this.flagContainer.setParentContainer(area.getFlagContainer());
313            if (flags != null) {
314                for (PlotFlag<?, ?> flag : flags) {
315                    this.flagContainer.addFlag(flag);
316                }
317            }
318        }
319        PlotSquared.platform().injector().injectMembers(this);
320    }
321
322    /**
323     * Get the plot from a string.
324     *
325     * @param player  Provides a context for what world to search in. Prefixing the term with 'world_name;' will override this context.
326     * @param arg     The search term
327     * @param message If a message should be sent to the player if a plot cannot be found
328     * @return The plot if only 1 result is found, or null
329     */
330    public static @Nullable Plot getPlotFromString(
331            final @Nullable PlotPlayer<?> player,
332            final @Nullable String arg,
333            final boolean message
334    ) {
335        if (arg == null) {
336            if (player == null) {
337                if (message) {
338                    LOGGER.info("No plot area string was supplied");
339                }
340                return null;
341            }
342            return player.getCurrentPlot();
343        }
344        PlotArea area;
345        if (player != null) {
346            area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(arg);
347            if (area == null) {
348                area = player.getApplicablePlotArea();
349            }
350        } else {
351            area = ConsolePlayer.getConsole().getApplicablePlotArea();
352        }
353        String[] split = arg.split("[;,]");
354        PlotId id;
355        if (split.length == 4) {
356            area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(split[0] + ';' + split[1]);
357            id = PlotId.fromString(split[2] + ';' + split[3]);
358        } else if (split.length == 3) {
359            area = PlotSquared.get().getPlotAreaManager().getPlotAreaByString(split[0]);
360            id = PlotId.fromString(split[1] + ';' + split[2]);
361        } else if (split.length == 2) {
362            id = PlotId.fromString(arg);
363        } else {
364            Collection<Plot> plots;
365            if (area == null) {
366                plots = PlotQuery.newQuery().allPlots().asList();
367            } else {
368                plots = area.getPlots();
369            }
370            for (Plot p : plots) {
371                String name = p.getAlias();
372                if (!name.isEmpty() && name.equalsIgnoreCase(arg)) {
373                    return p.getBasePlot(false);
374                }
375            }
376            if (message && player != null) {
377                player.sendMessage(TranslatableCaption.of("invalid.not_valid_plot_id"));
378            }
379            return null;
380        }
381        if (area == null) {
382            if (message && player != null) {
383                player.sendMessage(TranslatableCaption.of("errors.invalid_plot_world"));
384            }
385            return null;
386        }
387        return area.getPlotAbs(id);
388    }
389
390    /**
391     * Gets a plot from a string e.g. [area];[id]
392     *
393     * @param defaultArea if no area is specified
394     * @param string      plot id/area + id
395     * @return New or existing plot object
396     */
397    public static @Nullable Plot fromString(final @Nullable PlotArea defaultArea, final @NonNull String string) {
398        final String[] split = string.split("[;,]");
399        if (split.length == 2) {
400            if (defaultArea != null) {
401                PlotId id = PlotId.fromString(split[0] + ';' + split[1]);
402                return defaultArea.getPlotAbs(id);
403            }
404        } else if (split.length == 3) {
405            PlotArea pa = PlotSquared.get().getPlotAreaManager().getPlotArea(split[0], null);
406            if (pa != null) {
407                PlotId id = PlotId.fromString(split[1] + ';' + split[2]);
408                return pa.getPlotAbs(id);
409            }
410        } else if (split.length == 4) {
411            PlotArea pa = PlotSquared.get().getPlotAreaManager().getPlotArea(split[0], split[1]);
412            if (pa != null) {
413                PlotId id = PlotId.fromString(split[1] + ';' + split[2]);
414                return pa.getPlotAbs(id);
415            }
416        }
417        return null;
418    }
419
420    /**
421     * Return a new/cached plot object at a given location.
422     *
423     * <p>
424     * Use {@link PlotPlayer#getCurrentPlot()} if a player is expected here.
425     * </p>
426     *
427     * @param location the location of the plot
428     * @return plot at location or null
429     */
430    public static @Nullable Plot getPlot(final @NonNull Location location) {
431        final PlotArea pa = location.getPlotArea();
432        if (pa != null) {
433            return pa.getPlot(location);
434        }
435        return null;
436    }
437
438    @NonNull
439    static Location[] getCorners(final @NonNull String world, final @NonNull CuboidRegion region) {
440        final BlockVector3 min = region.getMinimumPoint();
441        final BlockVector3 max = region.getMaximumPoint();
442        return new Location[]{Location.at(world, min), Location.at(world, max)};
443    }
444
445    /**
446     * Get the owner of this exact plot, as it is
447     * stored in the database.
448     * <p>
449     * If the plot is a mega-plot, then the method returns
450     * the owner of this particular subplot.
451     * <p>
452     * Unlike {@link #getOwner()} this method does not
453     * consider factors such as {@link com.plotsquared.core.plot.flag.implementations.ServerPlotFlag}
454     * that could alter the de facto owner of the plot.
455     *
456     * @return The plot owner of this particular (sub-)plot
457     *         as stored in the database, if one exists. Else, null.
458     */
459    public @Nullable UUID getOwnerAbs() {
460        return this.owner;
461    }
462
463    /**
464     * Set the owner of this exact sub-plot. This does
465     * not update the database.
466     *
467     * @param owner The new owner of this particular sub-plot.
468     */
469    public void setOwnerAbs(final @Nullable UUID owner) {
470        this.owner = owner;
471    }
472
473    /**
474     * Get the name of the world that the plot is in
475     *
476     * @return World name
477     */
478    public @Nullable String getWorldName() {
479        return area.getWorldName();
480    }
481
482    /**
483     * Session only plot metadata (session is until the server stops)<br>
484     * <br>
485     * For persistent metadata use the flag system
486     *
487     * @param key   metadata key
488     * @param value metadata value
489     */
490    public void setMeta(final @NonNull String key, final @NonNull Object value) {
491        if (this.meta == null) {
492            this.meta = new ConcurrentHashMap<>();
493        }
494        this.meta.put(key, value);
495    }
496
497    /**
498     * Gets the metadata for a key<br>
499     * <br>
500     * For persistent metadata use the flag system
501     *
502     * @param key metadata key to get value for
503     * @return Object value
504     */
505    public @Nullable Object getMeta(final @NonNull String key) {
506        if (this.meta != null) {
507            return this.meta.get(key);
508        }
509        return null;
510    }
511
512    /**
513     * Delete the metadata for a key<br>
514     * - metadata is session only
515     * - deleting other plugin's metadata may cause issues
516     *
517     * @param key key to delete
518     */
519    public void deleteMeta(final @NonNull String key) {
520        if (this.meta != null) {
521            this.meta.remove(key);
522        }
523    }
524
525    /**
526     * Gets the cluster this plot is associated with
527     *
528     * @return the PlotCluster object, or null
529     */
530    public @Nullable PlotCluster getCluster() {
531        if (this.getArea() == null) {
532            return null;
533        }
534        return this.getArea().getCluster(this.id);
535    }
536
537    /**
538     * Efficiently get the players currently inside this plot<br>
539     * - Will return an empty list if no players are in the plot<br>
540     * - Remember, you can cast a PlotPlayer to its respective implementation (BukkitPlayer, SpongePlayer) to obtain the player object
541     *
542     * @return list of PlotPlayer(s) or an empty list
543     */
544    public @NonNull List<PlotPlayer<?>> getPlayersInPlot() {
545        final List<PlotPlayer<?>> players = new ArrayList<>();
546        for (final PlotPlayer<?> player : PlotSquared.platform().playerManager().getPlayers()) {
547            if (this.equals(player.getCurrentPlot())) {
548                players.add(player);
549            }
550        }
551        return players;
552    }
553
554    /**
555     * Checks if the plot has an owner.
556     *
557     * @return {@code true} if there is an owner, else {@code false}
558     */
559    public boolean hasOwner() {
560        return this.getOwnerAbs() != null;
561    }
562
563    /**
564     * Checks if a UUID is a plot owner (merged plots may have multiple owners)
565     *
566     * @param uuid Player UUID
567     * @return {@code true} if the provided uuid is the owner of the plot, else {@code false}
568     */
569    public boolean isOwner(final @NonNull UUID uuid) {
570        if (uuid.equals(this.getOwner())) {
571            return true;
572        }
573        if (!isMerged()) {
574            return false;
575        }
576        final Set<Plot> connected = getConnectedPlots();
577        for (Plot current : connected) {
578            // can skip ServerPlotFlag check in getOwner()
579            // as flags are synchronized between plots
580            if (uuid.equals(current.getOwnerAbs())) {
581                return true;
582            }
583        }
584        return false;
585    }
586
587    /**
588     * Checks if the given UUID is the owner of this specific plot
589     *
590     * @param uuid Player UUID
591     * @return {@code true} if the provided uuid is the owner of the plot, else {@code false}
592     */
593    public boolean isOwnerAbs(final @Nullable UUID uuid) {
594        if (uuid == null) {
595            return false;
596        }
597        return uuid.equals(this.getOwner());
598    }
599
600    /**
601     * Get the plot owner of this particular sub-plot.
602     * (Merged plots can have multiple owners)
603     * Direct access is discouraged: use {@link #getOwners()}
604     *
605     * <p>
606     * Use {@link #getOwnerAbs()} to get the owner as stored in the database
607     * </p>
608     *
609     * @return Server if ServerPlot flag set, else {@link #getOwnerAbs()}
610     */
611    public @Nullable UUID getOwner() {
612        if (this.getFlag(ServerPlotFlag.class)) {
613            return DBFunc.SERVER;
614        }
615        return this.getOwnerAbs();
616    }
617
618    /**
619     * Sets the plot owner (and update the database)
620     *
621     * @param owner uuid to set as owner
622     */
623    public void setOwner(final @NonNull UUID owner) {
624        if (!hasOwner()) {
625            this.setOwnerAbs(owner);
626            this.getPlotModificationManager().create();
627            return;
628        }
629        if (!isMerged()) {
630            if (!owner.equals(this.getOwnerAbs())) {
631                this.setOwnerAbs(owner);
632                DBFunc.setOwner(this, owner);
633            }
634            return;
635        }
636        for (final Plot current : getConnectedPlots()) {
637            if (!owner.equals(current.getOwnerAbs())) {
638                current.setOwnerAbs(owner);
639                DBFunc.setOwner(current, owner);
640            }
641        }
642    }
643
644    /**
645     * Gets a immutable set of owner UUIDs for a plot (supports multi-owner mega-plots).
646     * <p>
647     * This method cannot be used to add or remove owners from a plot.
648     * </p>
649     *
650     * @return Immutable view of plot owners
651     */
652    public @NonNull Set<UUID> getOwners() {
653        if (this.getOwner() == null) {
654            return ImmutableSet.of();
655        }
656        if (isMerged()) {
657            Set<Plot> plots = getConnectedPlots();
658            Plot[] array = plots.toArray(new Plot[0]);
659            ImmutableSet.Builder<UUID> owners = ImmutableSet.builder();
660            UUID last = this.getOwner();
661            owners.add(this.getOwner());
662            for (final Plot current : array) {
663                if (current.getOwner() == null) {
664                    continue;
665                }
666                if (last == null || current.getOwner().getMostSignificantBits() != last.getMostSignificantBits()) {
667                    owners.add(current.getOwner());
668                    last = current.getOwner();
669                }
670            }
671            return owners.build();
672        }
673        return ImmutableSet.of(this.getOwner());
674    }
675
676    /**
677     * Checks if the player is either the owner or on the trusted/added list.
678     *
679     * @param uuid uuid to check
680     * @return {@code true} if the player is added/trusted or is the owner, else {@code false}
681     */
682    public boolean isAdded(final @NonNull UUID uuid) {
683        if (!this.hasOwner() || getDenied().contains(uuid)) {
684            return false;
685        }
686        if (isOwner(uuid)) {
687            return true;
688        }
689        if (getMembers().contains(uuid)) {
690            return isOnline();
691        }
692        if (getTrusted().contains(uuid) || getTrusted().contains(DBFunc.EVERYONE)) {
693            return true;
694        }
695        if (getMembers().contains(DBFunc.EVERYONE)) {
696            return isOnline();
697        }
698        return false;
699    }
700
701    /**
702     * Checks if the player is not permitted on this plot.
703     *
704     * @param uuid uuid to check
705     * @return {@code false} if the player is allowed to enter the plot, else {@code true}
706     */
707    public boolean isDenied(final @NonNull UUID uuid) {
708        return this.denied != null && (this.denied.contains(DBFunc.EVERYONE) && !this.isAdded(uuid) || !this.isAdded(uuid) && this.denied
709                .contains(uuid));
710    }
711
712    /**
713     * Gets the {@link PlotId} of this plot.
714     *
715     * @return the PlotId for this plot
716     */
717    public @NonNull PlotId getId() {
718        return this.id;
719    }
720
721    /**
722     * Change the plot ID
723     *
724     * @param id new plot ID
725     */
726    public void setId(final @NonNull PlotId id) {
727        this.id = id;
728    }
729
730    /**
731     * Gets the plot world object for this plot<br>
732     * - The generic PlotArea object can be casted to its respective class for more control (e.g. HybridPlotWorld)
733     *
734     * @return PlotArea
735     */
736    public @Nullable PlotArea getArea() {
737        return this.area;
738    }
739
740    /**
741     * Assigns this plot to a plot area.<br>
742     * (Mostly used during startup when worlds are being created)<br>
743     * <p>
744     * Do not use this unless you absolutely know what you are doing.
745     * </p>
746     *
747     * @param area area to assign to
748     */
749    public void setArea(final @NonNull PlotArea area) {
750        if (this.getArea() == area) {
751            return;
752        }
753        if (this.getArea() != null) {
754            this.area.removePlot(this.id);
755        }
756        this.area = area;
757        area.addPlot(this);
758        this.flagContainer.setParentContainer(area.getFlagContainer());
759    }
760
761    /**
762     * Gets the plot manager object for this plot<br>
763     * - The generic PlotManager object can be casted to its respective class for more control (e.g. HybridPlotManager)
764     *
765     * @return PlotManager
766     */
767    public @NonNull PlotManager getManager() {
768        return this.area.getPlotManager();
769    }
770
771    /**
772     * Gets or create plot settings.
773     *
774     * @return PlotSettings
775     */
776    public @NonNull PlotSettings getSettings() {
777        if (this.settings == null) {
778            this.settings = new PlotSettings();
779        }
780        return this.settings;
781    }
782
783    /**
784     * Returns true if the plot is not merged, or it is the base
785     * plot of multiple merged plots.
786     *
787     * @return Boolean
788     */
789    public boolean isBasePlot() {
790        return !this.isMerged() || this.equals(this.getBasePlot(false));
791    }
792
793    /**
794     * The base plot is an arbitrary but specific connected plot. It is useful for the following:<br>
795     * - Merged plots need to be treated as a single plot for most purposes<br>
796     * - Some data such as home location needs to be associated with the group rather than each plot<br>
797     * - If the plot is not merged it will return itself.<br>
798     * - The result is cached locally
799     *
800     * @param recalculate whether to recalculate the merged plots to find the origin
801     * @return base Plot
802     */
803    public Plot getBasePlot(final boolean recalculate) {
804        if (this.origin != null && !recalculate) {
805            if (this.equals(this.origin)) {
806                return this;
807            }
808            return this.origin.getBasePlot(false);
809        }
810        if (!this.isMerged()) {
811            this.origin = this;
812            return this.origin;
813        }
814        this.origin = this;
815        PlotId min = this.id;
816        for (Plot plot : this.getConnectedPlots()) {
817            if (plot.id.getY() < min.getY() || plot.id.getY() == min.getY() && plot.id.getX() < min.getX()) {
818                this.origin = plot;
819                min = plot.id;
820            }
821        }
822        for (Plot plot : this.getConnectedPlots()) {
823            plot.origin = this.origin;
824        }
825        return this.origin;
826    }
827
828    /**
829     * Checks if this plot is merged in any direction.
830     *
831     * @return {@code true} if this plot is merged, otherwise {@code false}
832     */
833    public boolean isMerged() {
834        return getSettings().getMerged(0) || getSettings().getMerged(2) || getSettings().getMerged(1) || getSettings().getMerged(3);
835    }
836
837    /**
838     * Gets the timestamp of when the plot was created (unreliable)<br>
839     * - not accurate if the plot was created before this was implemented<br>
840     * - Milliseconds since the epoch<br>
841     *
842     * @return the creation date of the plot
843     */
844    public long getTimestamp() {
845        if (this.timestamp == 0) {
846            this.timestamp = System.currentTimeMillis();
847        }
848        return this.timestamp;
849    }
850
851    /**
852     * Gets if the plot is merged in a direction<br>
853     * ------- Actual -------<br>
854     * 0 = north<br>
855     * 1 = east<br>
856     * 2 = south<br>
857     * 3 = west<br>
858     * ----- Artificial -----<br>
859     * 4 = north-east<br>
860     * 5 = south-east<br>
861     * 6 = south-west<br>
862     * 7 = north-west<br>
863     * ----------<br>
864     * <p>
865     * Note: A plot that is merged north and east will not be merged northeast if the northeast plot is not part of the same group<br>
866     *
867     * @param dir direction to check for merged plot
868     * @return {@code true} if merged in that direction, else {@code false}
869     */
870    public boolean isMerged(final int dir) {
871        if (this.settings == null) {
872            return false;
873        }
874        switch (dir) {
875            case 0:
876            case 1:
877            case 2:
878            case 3:
879                return this.getSettings().getMerged(dir);
880            case 7:
881                int i = dir - 4;
882                int i2 = 0;
883                if (this.getSettings().getMerged(i2)) {
884                    if (this.getSettings().getMerged(i)) {
885                        if (Objects.requireNonNull(
886                                this.area.getPlotAbs(this.id.getRelative(Direction.getFromIndex(i)))).isMerged(i2)) {
887                            return Objects.requireNonNull(this.area
888                                    .getPlotAbs(this.id.getRelative(Direction.getFromIndex(i2)))).isMerged(i);
889                        }
890                    }
891                }
892                return false;
893            case 4:
894            case 5:
895            case 6:
896                i = dir - 4;
897                i2 = dir - 3;
898                return this.getSettings().getMerged(i2) && this.getSettings().getMerged(i) && Objects
899                        .requireNonNull(
900                                this.area.getPlotAbs(this.id.getRelative(Direction.getFromIndex(i)))).isMerged(i2) && Objects
901                        .requireNonNull(
902                                this.area.getPlotAbs(this.id.getRelative(Direction.getFromIndex(i2)))).isMerged(i);
903
904        }
905        return false;
906    }
907
908    /**
909     * Gets the denied users.
910     *
911     * @return a set of denied users
912     */
913    public @NonNull HashSet<UUID> getDenied() {
914        if (this.denied == null) {
915            this.denied = new HashSet<>();
916        }
917        return this.denied;
918    }
919
920    /**
921     * Sets the denied users for this plot.
922     *
923     * @param uuids uuids to deny
924     */
925    public void setDenied(final @NonNull Set<UUID> uuids) {
926        boolean larger = uuids.size() > getDenied().size();
927        HashSet<UUID> intersection;
928        if (larger) {
929            intersection = new HashSet<>(getDenied());
930        } else {
931            intersection = new HashSet<>(uuids);
932        }
933        if (larger) {
934            intersection.retainAll(uuids);
935        } else {
936            intersection.retainAll(getDenied());
937        }
938        uuids.removeAll(intersection);
939        HashSet<UUID> toRemove = new HashSet<>(getDenied());
940        toRemove.removeAll(intersection);
941        for (UUID uuid : toRemove) {
942            removeDenied(uuid);
943        }
944        for (UUID uuid : uuids) {
945            addDenied(uuid);
946        }
947    }
948
949    /**
950     * Gets the trusted users.
951     *
952     * @return a set of trusted users
953     */
954    public @NonNull HashSet<UUID> getTrusted() {
955        if (this.trusted == null) {
956            this.trusted = new HashSet<>();
957        }
958        return this.trusted;
959    }
960
961    /**
962     * Sets the trusted users for this plot.
963     *
964     * @param uuids uuids to trust
965     */
966    public void setTrusted(final @NonNull Set<UUID> uuids) {
967        boolean larger = uuids.size() > getTrusted().size();
968        HashSet<UUID> intersection = new HashSet<>(larger ? getTrusted() : uuids);
969        intersection.retainAll(larger ? uuids : getTrusted());
970        uuids.removeAll(intersection);
971        HashSet<UUID> toRemove = new HashSet<>(getTrusted());
972        toRemove.removeAll(intersection);
973        for (UUID uuid : toRemove) {
974            removeTrusted(uuid);
975        }
976        for (UUID uuid : uuids) {
977            addTrusted(uuid);
978        }
979    }
980
981    /**
982     * Gets the members
983     *
984     * @return a set of members
985     */
986    public @NonNull HashSet<UUID> getMembers() {
987        if (this.members == null) {
988            this.members = new HashSet<>();
989        }
990        return this.members;
991    }
992
993    /**
994     * Sets the members for this plot.
995     *
996     * @param uuids uuids to set member status for
997     */
998    public void setMembers(final @NonNull Set<UUID> uuids) {
999        boolean larger = uuids.size() > getMembers().size();
1000        HashSet<UUID> intersection = new HashSet<>(larger ? getMembers() : uuids);
1001        intersection.retainAll(larger ? uuids : getMembers());
1002        uuids.removeAll(intersection);
1003        HashSet<UUID> toRemove = new HashSet<>(getMembers());
1004        toRemove.removeAll(intersection);
1005        for (UUID uuid : toRemove) {
1006            removeMember(uuid);
1007        }
1008        for (UUID uuid : uuids) {
1009            addMember(uuid);
1010        }
1011    }
1012
1013    /**
1014     * Denies a player from this plot. (updates database as well)
1015     *
1016     * @param uuid the uuid of the player to deny.
1017     */
1018    public void addDenied(final @NonNull UUID uuid) {
1019        for (final Plot current : getConnectedPlots()) {
1020            if (current.getDenied().add(uuid)) {
1021                DBFunc.setDenied(current, uuid);
1022            }
1023        }
1024    }
1025
1026    /**
1027     * Add someone as a helper (updates database as well)
1028     *
1029     * @param uuid the uuid of the player to trust
1030     */
1031    public void addTrusted(final @NonNull UUID uuid) {
1032        for (final Plot current : getConnectedPlots()) {
1033            if (current.getTrusted().add(uuid)) {
1034                DBFunc.setTrusted(current, uuid);
1035            }
1036        }
1037    }
1038
1039    /**
1040     * Add someone as a trusted user (updates database as well)
1041     *
1042     * @param uuid the uuid of the player to add as a member
1043     */
1044    public void addMember(final @NonNull UUID uuid) {
1045        for (final Plot current : getConnectedPlots()) {
1046            if (current.getMembers().add(uuid)) {
1047                DBFunc.setMember(current, uuid);
1048            }
1049        }
1050    }
1051
1052    /**
1053     * Sets the plot owner (and update the database)
1054     *
1055     * @param owner     uuid to set as owner
1056     * @param initiator player initiating set owner
1057     * @return boolean
1058     */
1059    public boolean setOwner(UUID owner, PlotPlayer<?> initiator) {
1060        if (!hasOwner()) {
1061            this.setOwnerAbs(owner);
1062            this.getPlotModificationManager().create();
1063            return true;
1064        }
1065        if (!isMerged()) {
1066            if (!owner.equals(this.getOwnerAbs())) {
1067                this.setOwnerAbs(owner);
1068                DBFunc.setOwner(this, owner);
1069            }
1070            return true;
1071        }
1072        for (final Plot current : getConnectedPlots()) {
1073            if (!owner.equals(current.getOwnerAbs())) {
1074                current.setOwnerAbs(owner);
1075                DBFunc.setOwner(current, owner);
1076            }
1077        }
1078        return true;
1079    }
1080
1081    public boolean isLoaded() {
1082        return this.worldUtil.isWorld(getWorldName());
1083    }
1084
1085    /**
1086     * This will return null if the plot hasn't been analyzed
1087     *
1088     * @param settings The set of settings to obtain the analysis of
1089     * @return analysis of plot
1090     */
1091    public PlotAnalysis getComplexity(Settings.Auto_Clear settings) {
1092        return PlotAnalysis.getAnalysis(this, settings);
1093    }
1094
1095    /**
1096     * Get an immutable view of all the flags associated with the plot.
1097     *
1098     * @return Immutable set containing the flags associated with the plot
1099     */
1100    public Set<PlotFlag<?, ?>> getFlags() {
1101        return ImmutableSet.copyOf(flagContainer.getFlagMap().values());
1102    }
1103
1104    /**
1105     * Sets a flag for the plot and stores it in the database.
1106     *
1107     * @param flag Flag to set
1108     * @param <V>  flag value type
1109     * @return A boolean indicating whether or not the operation succeeded
1110     */
1111    public <V> boolean setFlag(final @NonNull PlotFlag<V, ?> flag) {
1112        if (flag instanceof KeepFlag && PlotSquared.platform().expireManager() != null) {
1113            PlotSquared.platform().expireManager().updateExpired(this);
1114        }
1115        for (final Plot plot : this.getConnectedPlots()) {
1116            plot.getFlagContainer().addFlag(flag);
1117            plot.reEnter();
1118            DBFunc.setFlag(plot, flag);
1119        }
1120        return true;
1121    }
1122
1123    /**
1124     * Parse the flag value into a flag instance based on the provided
1125     * flag class, and store it in the database.
1126     *
1127     * @param flag  Flag type
1128     * @param value Flag value
1129     * @return A boolean indicating whether or not the operation succeeded
1130     */
1131    public boolean setFlag(final @NonNull Class<?> flag, final @NonNull String value) {
1132        try {
1133            this.setFlag(GlobalFlagContainer.getInstance().getFlagErased(flag).parse(value));
1134        } catch (final Exception e) {
1135            return false;
1136        }
1137        return true;
1138    }
1139
1140    /**
1141     * Remove a flag from this plot
1142     *
1143     * @param flag the flag to remove
1144     * @return success
1145     */
1146    public boolean removeFlag(final @NonNull Class<? extends PlotFlag<?, ?>> flag) {
1147        return this.removeFlag(getFlagContainer().queryLocal(flag));
1148    }
1149
1150    /**
1151     * Get flags associated with the plot.
1152     *
1153     * @param plotOnly          Whether or not to only consider the plot. If this parameter is set to
1154     *                          true, the default values of the owning plot area will not be considered
1155     * @param ignorePluginFlags Whether or not to ignore {@link InternalFlag internal flags}
1156     * @return Collection containing all the flags that matched the given criteria
1157     */
1158    public Collection<PlotFlag<?, ?>> getApplicableFlags(final boolean plotOnly, final boolean ignorePluginFlags) {
1159        if (!hasOwner()) {
1160            return Collections.emptyList();
1161        }
1162        final Map<Class<?>, PlotFlag<?, ?>> flags = new HashMap<>();
1163        if (!plotOnly && getArea() != null && !getArea().getFlagContainer().getFlagMap().isEmpty()) {
1164            final Map<Class<?>, PlotFlag<?, ?>> flagMap = getArea().getFlagContainer().getFlagMap();
1165            flags.putAll(flagMap);
1166        }
1167        final Map<Class<?>, PlotFlag<?, ?>> flagMap = getFlagContainer().getFlagMap();
1168        if (ignorePluginFlags) {
1169            for (final PlotFlag<?, ?> flag : flagMap.values()) {
1170                if (flag instanceof InternalFlag) {
1171                    continue;
1172                }
1173                flags.put(flag.getClass(), flag);
1174            }
1175        } else {
1176            flags.putAll(flagMap);
1177        }
1178        return flags.values();
1179    }
1180
1181    /**
1182     * Get flags associated with the plot and the plot area that contains it.
1183     *
1184     * @param ignorePluginFlags Whether or not to ignore {@link InternalFlag internal flags}
1185     * @return Collection containing all the flags that matched the given criteria
1186     */
1187    public Collection<PlotFlag<?, ?>> getApplicableFlags(final boolean ignorePluginFlags) {
1188        return getApplicableFlags(false, ignorePluginFlags);
1189    }
1190
1191    /**
1192     * Remove a flag from this plot
1193     *
1194     * @param flag the flag to remove
1195     * @return success
1196     */
1197    public boolean removeFlag(final @NonNull PlotFlag<?, ?> flag) {
1198        if (flag == null || origin == null) {
1199            return false;
1200        }
1201        boolean removed = false;
1202        for (final Plot plot : origin.getConnectedPlots()) {
1203            final Object value = plot.getFlagContainer().removeFlag(flag);
1204            if (value == null) {
1205                continue;
1206            }
1207            plot.reEnter();
1208            DBFunc.removeFlag(plot, flag);
1209            removed = true;
1210        }
1211        return removed;
1212    }
1213
1214    /**
1215     * Count the entities in a plot
1216     *
1217     * @return array of entity counts
1218     * @see RegionManager#countEntities(Plot)
1219     */
1220    public int[] countEntities() {
1221        int[] count = new int[6];
1222        for (Plot current : this.getConnectedPlots()) {
1223            int[] result = this.regionManager.countEntities(current);
1224            count[CAP_ENTITY] += result[CAP_ENTITY];
1225            count[CAP_ANIMAL] += result[CAP_ANIMAL];
1226            count[CAP_MONSTER] += result[CAP_MONSTER];
1227            count[CAP_MOB] += result[CAP_MOB];
1228            count[CAP_VEHICLE] += result[CAP_VEHICLE];
1229            count[CAP_MISC] += result[CAP_MISC];
1230        }
1231        return count;
1232    }
1233
1234    /**
1235     * Returns true if a previous task was running
1236     *
1237     * @return {@code true} if a previous task is running
1238     */
1239    public int addRunning() {
1240        int value = this.getRunning();
1241        for (Plot plot : this.getConnectedPlots()) {
1242            plot.setMeta("running", value + 1);
1243        }
1244        return value;
1245    }
1246
1247    /**
1248     * Decrement the number of tracked tasks this plot is running<br>
1249     * - Used to track/limit the number of things a player can do on the plot at once
1250     *
1251     * @return previous number of tasks (int)
1252     */
1253    public int removeRunning() {
1254        int value = this.getRunning();
1255        if (value < 2) {
1256            for (Plot plot : this.getConnectedPlots()) {
1257                plot.deleteMeta("running");
1258            }
1259        } else {
1260            for (Plot plot : this.getConnectedPlots()) {
1261                plot.setMeta("running", value - 1);
1262            }
1263        }
1264        return value;
1265    }
1266
1267    /**
1268     * Gets the number of tracked running tasks for this plot<br>
1269     * - Used to track/limit the number of things a player can do on the plot at once
1270     *
1271     * @return number of tasks (int)
1272     */
1273    public int getRunning() {
1274        Integer value = (Integer) this.getMeta("running");
1275        return value == null ? 0 : value;
1276    }
1277
1278    /**
1279     * Unclaim the plot (does not modify terrain). Changes made to this plot will not be reflected in unclaimed plot objects.
1280     *
1281     * @return {@code false} if the Plot has no owner, otherwise {@code true}.
1282     */
1283    public boolean unclaim() {
1284        if (!this.hasOwner()) {
1285            return false;
1286        }
1287        for (Plot current : getConnectedPlots()) {
1288            List<PlotPlayer<?>> players = current.getPlayersInPlot();
1289            for (PlotPlayer<?> pp : players) {
1290                this.plotListener.plotExit(pp, current);
1291            }
1292
1293            if (Settings.Backup.DELETE_ON_UNCLAIM) {
1294                // Destroy all backups when the plot is unclaimed
1295                Objects.requireNonNull(PlotSquared.platform()).backupManager().getProfile(current).destroy();
1296            }
1297
1298            getArea().removePlot(getId());
1299            DBFunc.delete(current);
1300            current.setOwnerAbs(null);
1301            current.settings = null;
1302            current.clearCache();
1303            for (final PlotPlayer<?> pp : players) {
1304                this.plotListener.plotEntry(pp, current);
1305            }
1306        }
1307        return true;
1308    }
1309
1310    public void getCenter(final Consumer<Location> result) {
1311        Location[] corners = getCorners();
1312        Location top = corners[0];
1313        Location bot = corners[1];
1314        Location location = Location.at(
1315                this.getWorldName(),
1316                MathMan.average(bot.getX(), top.getX()),
1317                MathMan.average(bot.getY(), top.getY()),
1318                MathMan.average(bot.getZ(), top.getZ())
1319        );
1320        this.worldUtil.getHighestBlock(getWorldName(), location.getX(), location.getZ(), y -> {
1321            int height = y;
1322            if (area.allowSigns()) {
1323                height = Math.max(y, getManager().getSignLoc(this).getY());
1324            }
1325            result.accept(location.withY(1 + height));
1326        });
1327    }
1328
1329    /**
1330     * @return Location of center
1331     * @deprecated May cause synchronous chunk loads
1332     */
1333    @Deprecated
1334    public Location getCenterSynchronous() {
1335        Location[] corners = getCorners();
1336        Location top = corners[0];
1337        Location bot = corners[1];
1338        if (!isLoaded()) {
1339            return Location.at(
1340                    "",
1341                    0,
1342                    this.getArea() instanceof ClassicPlotWorld ? ((ClassicPlotWorld) this.getArea()).PLOT_HEIGHT + 1 : 4,
1343                    0
1344            );
1345        }
1346        Location location = Location.at(
1347                this.getWorldName(),
1348                MathMan.average(bot.getX(), top.getX()),
1349                MathMan.average(bot.getY(), top.getY()),
1350                MathMan.average(bot.getZ(), top.getZ())
1351        );
1352        int y = this.worldUtil.getHighestBlockSynchronous(getWorldName(), location.getX(), location.getZ());
1353        if (area.allowSigns()) {
1354            y = Math.max(y, getManager().getSignLoc(this).getY());
1355        }
1356        return location.withY(1 + y);
1357    }
1358
1359    /**
1360     * @return side where players should teleport to
1361     * @deprecated May cause synchronous chunk loads
1362     */
1363    @Deprecated
1364    public Location getSideSynchronous() {
1365        CuboidRegion largest = getLargestRegion();
1366        int x = (largest.getMaximumPoint().getX() >> 1) - (largest.getMinimumPoint().getX() >> 1) + largest
1367                .getMinimumPoint()
1368                .getX();
1369        int z = largest.getMinimumPoint().getZ() - 1;
1370        PlotManager manager = getManager();
1371        int y = isLoaded() ? this.worldUtil.getHighestBlockSynchronous(getWorldName(), x, z) : 62;
1372        if (area.allowSigns() && (y <= area.getMinGenHeight() || y >= area.getMaxGenHeight())) {
1373            y = Math.max(y, manager.getSignLoc(this).getY() - 1);
1374        }
1375        return Location.at(getWorldName(), x, y + 1, z);
1376    }
1377
1378    public void getSide(Consumer<Location> result) {
1379        CuboidRegion largest = getLargestRegion();
1380        int x = (largest.getMaximumPoint().getX() >> 1) - (largest.getMinimumPoint().getX() >> 1) + largest
1381                .getMinimumPoint()
1382                .getX();
1383        int z = largest.getMinimumPoint().getZ() - 1;
1384        PlotManager manager = getManager();
1385        if (isLoaded()) {
1386            this.worldUtil.getHighestBlock(getWorldName(), x, z, y -> {
1387                int height = y;
1388                if (area.allowSigns() && (y <= area.getMinGenHeight() || y >= area.getMaxGenHeight())) {
1389                    height = Math.max(y, manager.getSignLoc(this).getY() - 1);
1390                }
1391                result.accept(Location.at(getWorldName(), x, height + 1, z));
1392            });
1393        } else {
1394            int y = 62;
1395            if (area.allowSigns()) {
1396                y = Math.max(y, manager.getSignLoc(this).getY() - 1);
1397            }
1398            result.accept(Location.at(getWorldName(), x, y + 1, z));
1399        }
1400    }
1401
1402    /**
1403     * @return the plot home location
1404     * @deprecated May cause synchronous chunk loading
1405     */
1406    @Deprecated
1407    public Location getHomeSynchronous() {
1408        BlockLoc home = this.getPosition();
1409        if (home == null || home.getX() == 0 && home.getZ() == 0) {
1410            return this.getDefaultHomeSynchronous(true);
1411        } else {
1412            Location bottom = this.getBottomAbs();
1413            if (!isLoaded()) {
1414                return Location.at(
1415                        "",
1416                        0,
1417                        this.getArea() instanceof ClassicPlotWorld ? ((ClassicPlotWorld) this.getArea()).PLOT_HEIGHT + 1 : 4,
1418                        0
1419                );
1420            }
1421            Location location = toHomeLocation(bottom, home);
1422            if (!this.worldUtil.getBlockSynchronous(location).getBlockType().getMaterial().isAir()) {
1423                location = location.withY(
1424                        Math.max(1 + this.worldUtil.getHighestBlockSynchronous(
1425                                this.getWorldName(),
1426                                location.getX(),
1427                                location.getZ()
1428                        ), bottom.getY()));
1429            }
1430            return location;
1431        }
1432    }
1433
1434    /**
1435     * Return the home location for the plot
1436     *
1437     * @param result consumer to pass location to when found
1438     */
1439    public void getHome(final Consumer<Location> result) {
1440        BlockLoc home = this.getPosition();
1441        if (home == null || home.getX() == 0 && home.getZ() == 0) {
1442            this.getDefaultHome(result);
1443        } else {
1444            if (!isLoaded()) {
1445                result.accept(Location.at(
1446                        "",
1447                        0,
1448                        this.getArea() instanceof ClassicPlotWorld ? ((ClassicPlotWorld) this.getArea()).PLOT_HEIGHT + 1 : 4,
1449                        0
1450                ));
1451                return;
1452            }
1453            Location bottom = this.getBottomAbs();
1454            Location location = toHomeLocation(bottom, home);
1455            this.worldUtil.getBlock(location, block -> {
1456                if (!block.getBlockType().getMaterial().isAir()) {
1457                    this.worldUtil.getHighestBlock(this.getWorldName(), location.getX(), location.getZ(),
1458                            y -> result.accept(location.withY(Math.max(1 + y, bottom.getY())))
1459                    );
1460                } else {
1461                    result.accept(location);
1462                }
1463            });
1464        }
1465    }
1466
1467    private Location toHomeLocation(Location bottom, BlockLoc relativeHome) {
1468        return Location.at(
1469                bottom.getWorldName(),
1470                bottom.getX() + relativeHome.getX(),
1471                relativeHome.getY(), // y is absolute
1472                bottom.getZ() + relativeHome.getZ(),
1473                relativeHome.getYaw(),
1474                relativeHome.getPitch()
1475        );
1476    }
1477
1478    /**
1479     * Sets the home location
1480     *
1481     * @param location location to set as home
1482     */
1483    public void setHome(BlockLoc location) {
1484        Plot plot = this.getBasePlot(false);
1485        if (BlockLoc.ZERO.equals(location) || BlockLoc.MINY.equals(location)) {
1486            return;
1487        }
1488        plot.getSettings().setPosition(location);
1489        if (location != null) {
1490            DBFunc.setPosition(plot, plot.getSettings().getPosition().toString());
1491            return;
1492        }
1493        DBFunc.setPosition(plot, null);
1494    }
1495
1496    /**
1497     * Gets the default home location for a plot<br>
1498     * - Ignores any home location set for that specific plot
1499     *
1500     * @param result consumer to pass location to when found
1501     */
1502    public void getDefaultHome(Consumer<Location> result) {
1503        getDefaultHome(false, result);
1504    }
1505
1506    /**
1507     * @param member if to get the home for plot members
1508     * @return location of home for members or visitors
1509     * @deprecated May cause synchronous chunk loads
1510     */
1511    @Deprecated
1512    public Location getDefaultHomeSynchronous(final boolean member) {
1513        Plot plot = this.getBasePlot(false);
1514        BlockLoc loc = member ? area.defaultHome() : area.nonmemberHome();
1515        if (loc != null) {
1516            int x;
1517            int z;
1518            if (loc.getX() == Integer.MAX_VALUE && loc.getZ() == Integer.MAX_VALUE) {
1519                // center
1520                if (getArea() instanceof SinglePlotArea) {
1521                    int y = loc.getY() == Integer.MIN_VALUE
1522                            ? (isLoaded() ? this.worldUtil.getHighestBlockSynchronous(plot.getWorldName(), 0, 0) + 1 : 63)
1523                            : loc.getY();
1524                    return Location.at(plot.getWorldName(), 0, y, 0, 0, 0);
1525                }
1526                CuboidRegion largest = plot.getLargestRegion();
1527                x = (largest.getMaximumPoint().getX() >> 1) - (largest.getMinimumPoint().getX() >> 1) + largest
1528                        .getMinimumPoint()
1529                        .getX();
1530                z = (largest.getMaximumPoint().getZ() >> 1) - (largest.getMinimumPoint().getZ() >> 1) + largest
1531                        .getMinimumPoint()
1532                        .getZ();
1533            } else {
1534                // specific
1535                Location bot = plot.getBottomAbs();
1536                x = bot.getX() + loc.getX();
1537                z = bot.getZ() + loc.getZ();
1538            }
1539            int y = loc.getY() == Integer.MIN_VALUE
1540                    ? (isLoaded() ? this.worldUtil.getHighestBlockSynchronous(plot.getWorldName(), x, z) + 1 : 63)
1541                    : loc.getY();
1542            return Location.at(plot.getWorldName(), x, y, z, loc.getYaw(), loc.getPitch());
1543        }
1544        if (getArea() instanceof SinglePlotArea) {
1545            int y = isLoaded() ? this.worldUtil.getHighestBlockSynchronous(plot.getWorldName(), 0, 0) + 1 : 63;
1546            return Location.at(plot.getWorldName(), 0, y, 0, 0, 0);
1547        }
1548        // Side
1549        return plot.getSideSynchronous();
1550    }
1551
1552    public void getDefaultHome(boolean member, Consumer<Location> result) {
1553        Plot plot = this.getBasePlot(false);
1554        if (!isLoaded()) {
1555            result.accept(Location.at(
1556                    "",
1557                    0,
1558                    this.getArea() instanceof ClassicPlotWorld ? ((ClassicPlotWorld) this.getArea()).PLOT_HEIGHT + 1 : 4,
1559                    0
1560            ));
1561            return;
1562        }
1563        BlockLoc loc = member ? area.defaultHome() : area.nonmemberHome();
1564        if (loc != null) {
1565            int x;
1566            int z;
1567            if (loc.getX() == Integer.MAX_VALUE && loc.getZ() == Integer.MAX_VALUE) {
1568                // center
1569                if (getArea() instanceof SinglePlotArea) {
1570                    x = 0;
1571                    z = 0;
1572                } else {
1573                    CuboidRegion largest = plot.getLargestRegion();
1574                    x = (largest.getMaximumPoint().getX() >> 1) - (largest.getMinimumPoint().getX() >> 1) + largest
1575                            .getMinimumPoint()
1576                            .getX();
1577                    z = (largest.getMaximumPoint().getZ() >> 1) - (largest.getMinimumPoint().getZ() >> 1) + largest
1578                            .getMinimumPoint()
1579                            .getZ();
1580                }
1581            } else {
1582                // specific
1583                Location bot = plot.getBottomAbs();
1584                x = bot.getX() + loc.getX();
1585                z = bot.getZ() + loc.getZ();
1586            }
1587            if (loc.getY() == Integer.MIN_VALUE) {
1588                if (isLoaded()) {
1589                    this.worldUtil.getHighestBlock(
1590                            plot.getWorldName(),
1591                            x,
1592                            z,
1593                            y -> result.accept(Location.at(plot.getWorldName(), x, y + 1, z))
1594                    );
1595                } else {
1596                    int y = this.getArea() instanceof ClassicPlotWorld ? ((ClassicPlotWorld) this.getArea()).PLOT_HEIGHT + 1 : 63;
1597                    result.accept(Location.at(plot.getWorldName(), x, y, z, loc.getYaw(), loc.getPitch()));
1598                }
1599            } else {
1600                result.accept(Location.at(plot.getWorldName(), x, loc.getY(), z, loc.getYaw(), loc.getPitch()));
1601            }
1602            return;
1603        }
1604        // Side
1605        if (getArea() instanceof SinglePlotArea) {
1606            int y = isLoaded() ? this.worldUtil.getHighestBlockSynchronous(plot.getWorldName(), 0, 0) + 1 : 63;
1607            result.accept(Location.at(plot.getWorldName(), 0, y, 0, 0, 0));
1608        }
1609        plot.getSide(result);
1610    }
1611
1612    public double getVolume() {
1613        double count = 0;
1614        for (CuboidRegion region : getRegions()) {
1615            // CuboidRegion#getArea is deprecated and we want to ensure use of correct height
1616            count += region.getLength() * region.getWidth() * (area.getMaxGenHeight() - area.getMinGenHeight() + 1);
1617        }
1618        return count;
1619    }
1620
1621    /**
1622     * Gets the average rating of the plot. This is the value displayed in /plot info
1623     *
1624     * @return average rating as double, {@link Double#NaN} of no ratings exist
1625     */
1626    public double getAverageRating() {
1627        Collection<Rating> ratings = this.getRatings().values();
1628        double sum = ratings.stream().mapToDouble(Rating::getAverageRating).sum();
1629        return sum / ratings.size();
1630    }
1631
1632    /**
1633     * Sets a rating for a user<br>
1634     * - If the user has already rated, the following will return false
1635     *
1636     * @param uuid   uuid of rater
1637     * @param rating rating
1638     * @return success
1639     */
1640    public boolean addRating(UUID uuid, Rating rating) {
1641        Plot base = this.getBasePlot(false);
1642        PlotSettings baseSettings = base.getSettings();
1643        if (baseSettings.getRatings().containsKey(uuid)) {
1644            return false;
1645        }
1646        int aggregate = rating.getAggregate();
1647        baseSettings.getRatings().put(uuid, aggregate);
1648        DBFunc.setRating(base, uuid, aggregate);
1649        return true;
1650    }
1651
1652    /**
1653     * Clear the ratings/likes for this plot
1654     */
1655    public void clearRatings() {
1656        Plot base = this.getBasePlot(false);
1657        PlotSettings baseSettings = base.getSettings();
1658        if (baseSettings.getRatings() != null && !baseSettings.getRatings().isEmpty()) {
1659            DBFunc.deleteRatings(base);
1660            baseSettings.setRatings(null);
1661        }
1662    }
1663
1664    public Map<UUID, Boolean> getLikes() {
1665        final Map<UUID, Boolean> map = new HashMap<>();
1666        final Map<UUID, Rating> ratings = this.getRatings();
1667        ratings.forEach((uuid, rating) -> map.put(uuid, rating.getLike()));
1668        return map;
1669    }
1670
1671    /**
1672     * Gets the ratings associated with a plot<br>
1673     * - The rating object may contain multiple categories
1674     *
1675     * @return Map of user who rated to the rating
1676     */
1677    public HashMap<UUID, Rating> getRatings() {
1678        Plot base = this.getBasePlot(false);
1679        HashMap<UUID, Rating> map = new HashMap<>();
1680        if (!base.hasRatings()) {
1681            return map;
1682        }
1683        for (Entry<UUID, Integer> entry : base.getSettings().getRatings().entrySet()) {
1684            map.put(entry.getKey(), new Rating(entry.getValue()));
1685        }
1686        return map;
1687    }
1688
1689    public boolean hasRatings() {
1690        Plot base = this.getBasePlot(false);
1691        return base.settings != null && base.settings.getRatings() != null;
1692    }
1693
1694    @Deprecated(forRemoval = true, since = "6.1.0")
1695    public boolean claim(final @NonNull PlotPlayer<?> player, boolean teleport, String schematic) {
1696        if (!canClaim(player)) {
1697            return false;
1698        }
1699        return claim(player, teleport, schematic, true);
1700    }
1701
1702    @Deprecated(forRemoval = true, since = "6.1.0")
1703    public boolean claim(final @NonNull PlotPlayer<?> player, boolean teleport, String schematic, boolean updateDB) {
1704        return claim(player, teleport, schematic, updateDB, false);
1705    }
1706
1707    /**
1708     * Claim the plot
1709     *
1710     * @param player    The player to set the owner to
1711     * @param teleport  If the player should be teleported
1712     * @param schematic The schematic name to paste on the plot
1713     * @param updateDB  If the database should be updated
1714     * @param auto      If the plot is being claimed by a /plot auto
1715     * @return success
1716     * @since 6.1.0
1717     */
1718    public boolean claim(
1719            final @NonNull PlotPlayer<?> player, boolean teleport, String schematic, boolean updateDB,
1720            boolean auto
1721    ) {
1722        this.eventDispatcher.callPlotClaimedNotify(this, auto);
1723        if (updateDB) {
1724            if (!this.getPlotModificationManager().create(player.getUUID(), true)) {
1725                LOGGER.error("Player {} attempted to claim plot {}, but the database failed to update", player.getName(),
1726                        this.getId().toCommaSeparatedString()
1727                );
1728                return false;
1729            }
1730        } else {
1731            area.addPlot(this);
1732            updateWorldBorder();
1733        }
1734        player.sendMessage(TranslatableCaption.of("working.claimed"), Template.of("plot", this.getId().toString()));
1735        if (teleport) {
1736            if (!auto && Settings.Teleport.ON_CLAIM) {
1737                teleportPlayer(player, TeleportCause.COMMAND_CLAIM, result -> {
1738                });
1739            } else if (auto && Settings.Teleport.ON_AUTO) {
1740                teleportPlayer(player, TeleportCause.COMMAND_AUTO, result -> {
1741                });
1742            }
1743        }
1744        PlotArea plotworld = getArea();
1745        if (plotworld.isSchematicOnClaim()) {
1746            Schematic sch;
1747            try {
1748                if (schematic == null || schematic.isEmpty()) {
1749                    sch = schematicHandler.getSchematic(plotworld.getSchematicFile());
1750                } else {
1751                    sch = schematicHandler.getSchematic(schematic);
1752                    if (sch == null) {
1753                        sch = schematicHandler.getSchematic(plotworld.getSchematicFile());
1754                    }
1755                }
1756            } catch (SchematicHandler.UnsupportedFormatException e) {
1757                e.printStackTrace();
1758                return true;
1759            }
1760            schematicHandler.paste(
1761                    sch,
1762                    this,
1763                    0,
1764                    getArea().getMinBuildHeight(),
1765                    0,
1766                    Settings.Schematics.PASTE_ON_TOP,
1767                    player,
1768                    new RunnableVal<>() {
1769                        @Override
1770                        public void run(Boolean value) {
1771                            if (value) {
1772                                player.sendMessage(TranslatableCaption.of("schematics.schematic_paste_success"));
1773                            } else {
1774                                player.sendMessage(TranslatableCaption.of("schematics.schematic_paste_failed"));
1775                            }
1776                        }
1777                    }
1778            );
1779        }
1780        plotworld.getPlotManager().claimPlot(this, null);
1781        this.getPlotModificationManager().setSign(player.getName());
1782        return true;
1783    }
1784
1785    /**
1786     * Retrieve the biome of the plot.
1787     *
1788     * @param result consumer to pass biome to when found
1789     */
1790    public void getBiome(Consumer<BiomeType> result) {
1791        this.getCenter(location -> this.worldUtil.getBiome(location.getWorldName(), location.getX(), location.getZ(), result));
1792    }
1793
1794    //TODO Better documentation needed.
1795
1796    /**
1797     * @return biome at center of plot
1798     * @deprecated May cause synchronous chunk loads
1799     */
1800    @Deprecated
1801    public BiomeType getBiomeSynchronous() {
1802        final Location location = this.getCenterSynchronous();
1803        return this.worldUtil.getBiomeSynchronous(location.getWorldName(), location.getX(), location.getZ());
1804    }
1805
1806    /**
1807     * Returns the top location for the plot.
1808     *
1809     * @return location of Absolute Top
1810     */
1811    public Location getTopAbs() {
1812        return this.getManager().getPlotTopLocAbs(this.id).withWorld(this.getWorldName());
1813    }
1814
1815    /**
1816     * Returns the bottom location for the plot.
1817     *
1818     * @return location of absolute bottom of plot
1819     */
1820    public Location getBottomAbs() {
1821        return this.getManager().getPlotBottomLocAbs(this.id).withWorld(this.getWorldName());
1822    }
1823
1824    /**
1825     * Swaps the settings for two plots.
1826     *
1827     * @param plot the plot to swap data with
1828     * @return Future containing the result
1829     */
1830    public CompletableFuture<Boolean> swapData(Plot plot) {
1831        if (!this.hasOwner()) {
1832            if (plot != null && plot.hasOwner()) {
1833                plot.moveData(this, null);
1834                return CompletableFuture.completedFuture(true);
1835            }
1836            return CompletableFuture.completedFuture(false);
1837        }
1838        if (plot == null || plot.getOwner() == null) {
1839            this.moveData(plot, null);
1840            return CompletableFuture.completedFuture(true);
1841        }
1842        // Swap cached
1843        final PlotId temp = PlotId.of(this.getId().getX(), this.getId().getY());
1844        this.id = plot.getId();
1845        plot.id = temp;
1846        this.area.removePlot(this.getId());
1847        plot.area.removePlot(plot.getId());
1848        this.area.addPlotAbs(this);
1849        plot.area.addPlotAbs(plot);
1850        // Swap database
1851        return DBFunc.swapPlots(plot, this);
1852    }
1853
1854    /**
1855     * Moves the settings for a plot.
1856     *
1857     * @param plot     the plot to move
1858     * @param whenDone task to run when settings have been moved
1859     * @return success or not
1860     */
1861    public boolean moveData(Plot plot, Runnable whenDone) {
1862        if (!this.hasOwner()) {
1863            TaskManager.runTask(whenDone);
1864            return false;
1865        }
1866        if (plot.hasOwner()) {
1867            TaskManager.runTask(whenDone);
1868            return false;
1869        }
1870        this.area.removePlot(this.id);
1871        this.id = plot.getId();
1872        this.area.addPlotAbs(this);
1873        clearCache();
1874        DBFunc.movePlot(this, plot);
1875        TaskManager.runTaskLater(whenDone, TaskTime.ticks(1L));
1876        return true;
1877    }
1878
1879    /**
1880     * Gets the top loc of a plot (if mega, returns top loc of that mega plot) - If you would like each plot treated as
1881     * a small plot use {@link #getTopAbs()}
1882     *
1883     * @return Location top of mega plot
1884     */
1885    public Location getExtendedTopAbs() {
1886        Location top = this.getTopAbs();
1887        if (!this.isMerged()) {
1888            return top;
1889        }
1890        if (this.isMerged(Direction.SOUTH)) {
1891            top = top.withZ(this.getRelative(Direction.SOUTH).getBottomAbs().getZ() - 1);
1892        }
1893        if (this.isMerged(Direction.EAST)) {
1894            top = top.withX(this.getRelative(Direction.EAST).getBottomAbs().getX() - 1);
1895        }
1896        return top;
1897    }
1898
1899    /**
1900     * Gets the bot loc of a plot (if mega, returns bot loc of that mega plot) - If you would like each plot treated as
1901     * a small plot use {@link #getBottomAbs()}
1902     *
1903     * @return Location bottom of mega plot
1904     */
1905    public Location getExtendedBottomAbs() {
1906        Location bot = this.getBottomAbs();
1907        if (!this.isMerged()) {
1908            return bot;
1909        }
1910        if (this.isMerged(Direction.NORTH)) {
1911            bot = bot.withZ(this.getRelative(Direction.NORTH).getTopAbs().getZ() + 1);
1912        }
1913        if (this.isMerged(Direction.WEST)) {
1914            bot = bot.withX(this.getRelative(Direction.WEST).getTopAbs().getX() + 1);
1915        }
1916        return bot;
1917    }
1918
1919    /**
1920     * Returns the top and bottom location.<br>
1921     * - If the plot is not connected, it will return its own corners<br>
1922     * - the returned locations will not necessarily correspond to claimed plots if the connected plots do not form a rectangular shape
1923     *
1924     * @return new Location[] { bottom, top }
1925     * @deprecated as merged plots no longer need to be rectangular
1926     */
1927    @Deprecated
1928    public Location[] getCorners() {
1929        if (!this.isMerged()) {
1930            return new Location[]{this.getBottomAbs(), this.getTopAbs()};
1931        }
1932        return RegionUtil.getCorners(this.getWorldName(), this.getRegions());
1933    }
1934
1935    /**
1936     * @return bottom corner location
1937     * @deprecated in favor of getCorners()[0];<br>
1938     */
1939    // Won't remove as suggestion also points to deprecated method
1940    @Deprecated
1941    public Location getBottom() {
1942        return this.getCorners()[0];
1943    }
1944
1945    /**
1946     * @return the top corner of the plot
1947     * @deprecated in favor of getCorners()[1];
1948     */
1949    // Won't remove as suggestion also points to deprecated method
1950    @Deprecated
1951    public Location getTop() {
1952        return this.getCorners()[1];
1953    }
1954
1955    /**
1956     * Gets plot display name.
1957     *
1958     * @return alias if set, else id
1959     */
1960    @Override
1961    public String toString() {
1962        if (this.settings != null && this.settings.getAlias().length() > 1) {
1963            return this.settings.getAlias();
1964        }
1965        return this.area + ";" + this.id;
1966    }
1967
1968    /**
1969     * Remove a denied player (use DBFunc as well)<br>
1970     * Using the * uuid will remove all users
1971     *
1972     * @param uuid uuid of player to remove from denied list
1973     * @return success or not
1974     */
1975    public boolean removeDenied(UUID uuid) {
1976        if (uuid == DBFunc.EVERYONE && !denied.contains(uuid)) {
1977            boolean result = false;
1978            for (UUID other : new HashSet<>(getDenied())) {
1979                result = rmvDenied(other) || result;
1980            }
1981            return result;
1982        }
1983        return rmvDenied(uuid);
1984    }
1985
1986    private boolean rmvDenied(UUID uuid) {
1987        for (Plot current : this.getConnectedPlots()) {
1988            if (current.getDenied().remove(uuid)) {
1989                DBFunc.removeDenied(current, uuid);
1990            } else {
1991                return false;
1992            }
1993        }
1994        return true;
1995    }
1996
1997    /**
1998     * Remove a helper (use DBFunc as well)<br>
1999     * Using the * uuid will remove all users
2000     *
2001     * @param uuid uuid of trusted player to remove
2002     * @return success or not
2003     */
2004    public boolean removeTrusted(UUID uuid) {
2005        if (uuid == DBFunc.EVERYONE && !trusted.contains(uuid)) {
2006            boolean result = false;
2007            for (UUID other : new HashSet<>(getTrusted())) {
2008                result = rmvTrusted(other) || result;
2009            }
2010            return result;
2011        }
2012        return rmvTrusted(uuid);
2013    }
2014
2015    private boolean rmvTrusted(UUID uuid) {
2016        for (Plot plot : this.getConnectedPlots()) {
2017            if (plot.getTrusted().remove(uuid)) {
2018                DBFunc.removeTrusted(plot, uuid);
2019            } else {
2020                return false;
2021            }
2022        }
2023        return true;
2024    }
2025
2026    /**
2027     * Remove a trusted user (use DBFunc as well)<br>
2028     * Using the * uuid will remove all users
2029     *
2030     * @param uuid uuid of player to remove
2031     * @return success or not
2032     */
2033    public boolean removeMember(UUID uuid) {
2034        if (this.members == null) {
2035            return false;
2036        }
2037        if (uuid == DBFunc.EVERYONE && !members.contains(uuid)) {
2038            boolean result = false;
2039            for (UUID other : new HashSet<>(this.members)) {
2040                result = rmvMember(other) || result;
2041            }
2042            return result;
2043        }
2044        return rmvMember(uuid);
2045    }
2046
2047    private boolean rmvMember(UUID uuid) {
2048        for (Plot current : this.getConnectedPlots()) {
2049            if (current.getMembers().remove(uuid)) {
2050                DBFunc.removeMember(current, uuid);
2051            } else {
2052                return false;
2053            }
2054        }
2055        return true;
2056    }
2057
2058    @Override
2059    public boolean equals(Object obj) {
2060        if (this == obj) {
2061            return true;
2062        }
2063        if (obj == null) {
2064            return false;
2065        }
2066        if (this.getClass() != obj.getClass()) {
2067            return false;
2068        }
2069        Plot other = (Plot) obj;
2070        return this.hashCode() == other.hashCode() && this.id.equals(other.id) && this.area == other.area;
2071    }
2072
2073    /**
2074     * Gets the plot hashcode<br>
2075     * Note: The hashcode is unique if:<br>
2076     * - Plots are in the same world<br>
2077     * - The x,z coordinates are between Short.MIN_VALUE and Short.MAX_VALUE<br>
2078     *
2079     * @return integer.
2080     */
2081    @Override
2082    public int hashCode() {
2083        return this.id.hashCode();
2084    }
2085
2086    /**
2087     * Gets the plot alias.
2088     * - Returns an empty string if no alias is set
2089     *
2090     * @return The plot alias
2091     */
2092    public @NonNull String getAlias() {
2093        if (this.settings == null) {
2094            return "";
2095        }
2096        return this.settings.getAlias();
2097    }
2098
2099    /**
2100     * Sets the plot alias.
2101     *
2102     * @param alias The alias
2103     */
2104    public void setAlias(String alias) {
2105        for (Plot current : this.getConnectedPlots()) {
2106            String name = this.getSettings().getAlias();
2107            if (alias == null) {
2108                alias = "";
2109            }
2110            if (name.equals(alias)) {
2111                return;
2112            }
2113            current.getSettings().setAlias(alias);
2114            DBFunc.setAlias(current, alias);
2115        }
2116    }
2117
2118    /**
2119     * Sets the raw merge data<br>
2120     * - Updates DB<br>
2121     * - Does not modify terrain<br>
2122     *
2123     * @param direction direction to merge the plot in
2124     * @param value     if the plot is merged or not
2125     */
2126    public void setMerged(Direction direction, boolean value) {
2127        if (this.getSettings().setMerged(direction, value)) {
2128            if (value) {
2129                Plot other = this.getRelative(direction).getBasePlot(false);
2130                if (!other.equals(this.getBasePlot(false))) {
2131                    Plot base = other.id.getY() < this.id.getY() || other.id.getY() == this.id.getY() && other.id.getX() < this.id
2132                            .getX() ?
2133                            other :
2134                            this.origin;
2135                    this.origin.origin = base;
2136                    other.origin = base;
2137                    this.origin = base;
2138                    this.connectedCache = null;
2139                }
2140            } else {
2141                if (this.origin != null) {
2142                    this.origin.origin = null;
2143                    this.origin = null;
2144                }
2145                this.connectedCache = null;
2146            }
2147            DBFunc.setMerged(this, this.getSettings().getMerged());
2148        }
2149    }
2150
2151    /**
2152     * Gets the merged array.
2153     *
2154     * @return boolean [ north, east, south, west ]
2155     */
2156    public boolean[] getMerged() {
2157        return this.getSettings().getMerged();
2158    }
2159
2160    /**
2161     * Sets the raw merge data<br>
2162     * - Updates DB<br>
2163     * - Does not modify terrain<br>
2164     * Gets if the plot is merged in a direction<br>
2165     * ----------<br>
2166     * 0 = north<br>
2167     * 1 = east<br>
2168     * 2 = south<br>
2169     * 3 = west<br>
2170     * ----------<br>
2171     * Note: Diagonal merging (4-7) must be done by merging the corresponding plots.
2172     *
2173     * @param merged set the plot's merged plots
2174     */
2175    public void setMerged(boolean[] merged) {
2176        this.getSettings().setMerged(merged);
2177        DBFunc.setMerged(this, merged);
2178        clearCache();
2179    }
2180
2181    public void clearCache() {
2182        this.connectedCache = null;
2183        if (this.origin != null) {
2184            this.origin.origin = null;
2185            this.origin = null;
2186        }
2187    }
2188
2189    /**
2190     * Gets the set home location or 0,Integer#MIN_VALUE,0 if no location is set<br>
2191     * - Does not take the default home location into account
2192     * - PlotSquared will internally find the correct place to teleport to if y = Integer#MIN_VALUE when teleporting to the plot.
2193     *
2194     * @return home location
2195     */
2196    public BlockLoc getPosition() {
2197        return this.getSettings().getPosition();
2198    }
2199
2200    /**
2201     * Check if a plot can be claimed by the provided player.
2202     *
2203     * @param player the claiming player
2204     * @return if the given player can claim the plot
2205     */
2206    public boolean canClaim(@NonNull PlotPlayer<?> player) {
2207        PlotCluster cluster = this.getCluster();
2208        if (cluster != null) {
2209            if (!cluster.isAdded(player.getUUID()) && !player.hasPermission("plots.admin.command.claim")) {
2210                return false;
2211            }
2212        }
2213        final UUID owner = this.getOwnerAbs();
2214        if (owner != null) {
2215            return false;
2216        }
2217        return !isMerged();
2218    }
2219
2220    /**
2221     * Merge the plot settings<br>
2222     * - Used when a plot is merged<br>
2223     *
2224     * @param plot plot to merge the data from
2225     */
2226    public void mergeData(Plot plot) {
2227        final FlagContainer flagContainer1 = this.getFlagContainer();
2228        final FlagContainer flagContainer2 = plot.getFlagContainer();
2229        if (!flagContainer1.equals(flagContainer2)) {
2230            boolean greater = flagContainer1.getFlagMap().size() > flagContainer2.getFlagMap().size();
2231            if (greater) {
2232                flagContainer1.addAll(flagContainer2.getFlagMap().values());
2233            } else {
2234                flagContainer2.addAll(flagContainer1.getFlagMap().values());
2235            }
2236            if (!greater) {
2237                this.flagContainer.clearLocal();
2238                this.flagContainer.addAll(flagContainer2.getFlagMap().values());
2239            }
2240            plot.flagContainer.clearLocal();
2241            plot.flagContainer.addAll(this.flagContainer.getFlagMap().values());
2242        }
2243        if (!this.getAlias().isEmpty()) {
2244            plot.setAlias(this.getAlias());
2245        } else if (!plot.getAlias().isEmpty()) {
2246            this.setAlias(plot.getAlias());
2247        }
2248        for (UUID uuid : this.getTrusted()) {
2249            plot.addTrusted(uuid);
2250        }
2251        for (UUID uuid : plot.getTrusted()) {
2252            this.addTrusted(uuid);
2253        }
2254        for (UUID uuid : this.getMembers()) {
2255            plot.addMember(uuid);
2256        }
2257        for (UUID uuid : plot.getMembers()) {
2258            this.addMember(uuid);
2259        }
2260
2261        for (UUID uuid : this.getDenied()) {
2262            plot.addDenied(uuid);
2263        }
2264        for (UUID uuid : plot.getDenied()) {
2265            this.addDenied(uuid);
2266        }
2267    }
2268
2269    /**
2270     * Gets the plot in a relative location<br>
2271     * Note: May be null if the partial plot area does not include the relative location
2272     *
2273     * @param x relative id X
2274     * @param y relative id Y
2275     * @return Plot
2276     */
2277    public Plot getRelative(int x, int y) {
2278        return this.area.getPlotAbs(PlotId.of(this.id.getX() + x, this.id.getY() + y));
2279    }
2280
2281    public Plot getRelative(PlotArea area, int x, int y) {
2282        return area.getPlotAbs(PlotId.of(this.id.getX() + x, this.id.getY() + y));
2283    }
2284
2285    /**
2286     * Gets the plot in a relative direction
2287     * Note: May be null if the partial plot area does not include the relative location
2288     *
2289     * @param direction Direction
2290     * @return the plot relative to this one
2291     */
2292    public @Nullable Plot getRelative(@NonNull Direction direction) {
2293        return this.area.getPlotAbs(this.id.getRelative(direction));
2294    }
2295
2296    /**
2297     * Gets a set of plots connected (and including) this plot<br>
2298     * - This result is cached globally
2299     *
2300     * @return a Set of Plots connected to this Plot
2301     */
2302    public Set<Plot> getConnectedPlots() {
2303        if (this.settings == null) {
2304            return Collections.singleton(this);
2305        }
2306        if (!this.isMerged()) {
2307            return Collections.singleton(this);
2308        }
2309        if (this.connectedCache != null && this.connectedCache.contains(this)) {
2310            return this.connectedCache;
2311        }
2312
2313        HashSet<Plot> tmpSet = new HashSet<>();
2314        tmpSet.add(this);
2315        Plot tmp;
2316        HashSet<Object> queuecache = new HashSet<>();
2317        ArrayDeque<Plot> frontier = new ArrayDeque<>();
2318        if (this.isMerged(Direction.NORTH)) {
2319            tmp = this.area.getPlotAbs(this.id.getRelative(Direction.NORTH));
2320            if (!tmp.isMerged(Direction.SOUTH)) {
2321                // invalid merge
2322                if (tmp.isOwnerAbs(this.getOwnerAbs())) {
2323                    tmp.getSettings().setMerged(Direction.SOUTH, true);
2324                    DBFunc.setMerged(tmp, tmp.getSettings().getMerged());
2325                } else {
2326                    this.getSettings().setMerged(Direction.NORTH, false);
2327                    DBFunc.setMerged(this, this.getSettings().getMerged());
2328                }
2329            }
2330            queuecache.add(tmp);
2331            frontier.add(tmp);
2332        }
2333        if (this.isMerged(Direction.EAST)) {
2334            tmp = this.area.getPlotAbs(this.id.getRelative(Direction.EAST));
2335            assert tmp != null;
2336            if (!tmp.isMerged(Direction.WEST)) {
2337                // invalid merge
2338                if (tmp.isOwnerAbs(this.getOwnerAbs())) {
2339                    tmp.getSettings().setMerged(Direction.WEST, true);
2340                    DBFunc.setMerged(tmp, tmp.getSettings().getMerged());
2341                } else {
2342                    this.getSettings().setMerged(Direction.EAST, false);
2343                    DBFunc.setMerged(this, this.getSettings().getMerged());
2344                }
2345            }
2346            queuecache.add(tmp);
2347            frontier.add(tmp);
2348        }
2349        if (this.isMerged(Direction.SOUTH)) {
2350            tmp = this.area.getPlotAbs(this.id.getRelative(Direction.SOUTH));
2351            assert tmp != null;
2352            if (!tmp.isMerged(Direction.NORTH)) {
2353                // invalid merge
2354                if (tmp.isOwnerAbs(this.getOwnerAbs())) {
2355                    tmp.getSettings().setMerged(Direction.NORTH, true);
2356                    DBFunc.setMerged(tmp, tmp.getSettings().getMerged());
2357                } else {
2358                    this.getSettings().setMerged(Direction.SOUTH, false);
2359                    DBFunc.setMerged(this, this.getSettings().getMerged());
2360                }
2361            }
2362            queuecache.add(tmp);
2363            frontier.add(tmp);
2364        }
2365        if (this.isMerged(Direction.WEST)) {
2366            tmp = this.area.getPlotAbs(this.id.getRelative(Direction.WEST));
2367            if (!tmp.isMerged(Direction.EAST)) {
2368                // invalid merge
2369                if (tmp.isOwnerAbs(this.getOwnerAbs())) {
2370                    tmp.getSettings().setMerged(Direction.EAST, true);
2371                    DBFunc.setMerged(tmp, tmp.getSettings().getMerged());
2372                } else {
2373                    this.getSettings().setMerged(Direction.WEST, false);
2374                    DBFunc.setMerged(this, this.getSettings().getMerged());
2375                }
2376            }
2377            queuecache.add(tmp);
2378            frontier.add(tmp);
2379        }
2380        Plot current;
2381        while ((current = frontier.poll()) != null) {
2382            if (!current.hasOwner() || current.settings == null) {
2383                continue;
2384            }
2385            tmpSet.add(current);
2386            queuecache.remove(current);
2387            if (current.isMerged(Direction.NORTH)) {
2388                tmp = current.area.getPlotAbs(current.id.getRelative(Direction.NORTH));
2389                if (tmp != null && !queuecache.contains(tmp) && !tmpSet.contains(tmp)) {
2390                    queuecache.add(tmp);
2391                    frontier.add(tmp);
2392                }
2393            }
2394            if (current.isMerged(Direction.EAST)) {
2395                tmp = current.area.getPlotAbs(current.id.getRelative(Direction.EAST));
2396                if (tmp != null && !queuecache.contains(tmp) && !tmpSet.contains(tmp)) {
2397                    queuecache.add(tmp);
2398                    frontier.add(tmp);
2399                }
2400            }
2401            if (current.isMerged(Direction.SOUTH)) {
2402                tmp = current.area.getPlotAbs(current.id.getRelative(Direction.SOUTH));
2403                if (tmp != null && !queuecache.contains(tmp) && !tmpSet.contains(tmp)) {
2404                    queuecache.add(tmp);
2405                    frontier.add(tmp);
2406                }
2407            }
2408            if (current.isMerged(Direction.WEST)) {
2409                tmp = current.area.getPlotAbs(current.id.getRelative(Direction.WEST));
2410                if (tmp != null && !queuecache.contains(tmp) && !tmpSet.contains(tmp)) {
2411                    queuecache.add(tmp);
2412                    frontier.add(tmp);
2413                }
2414            }
2415        }
2416        this.connectedCache = tmpSet;
2417        return tmpSet;
2418    }
2419
2420    /**
2421     * This will combine each plot into effective rectangular regions<br>
2422     * - This result is cached globally<br>
2423     * - Useful for handling non rectangular shapes
2424     *
2425     * @return all regions within the plot
2426     */
2427    public @NonNull Set<CuboidRegion> getRegions() {
2428        if (!this.isMerged()) {
2429            Location pos1 = this.getBottomAbs().withY(getArea().getMinBuildHeight());
2430            Location pos2 = this.getTopAbs().withY(getArea().getMaxBuildHeight());
2431            this.connectedCache = Sets.newHashSet(this);
2432            CuboidRegion rg = new CuboidRegion(pos1.getBlockVector3(), pos2.getBlockVector3());
2433            return Collections.singleton(rg);
2434        }
2435        Set<Plot> plots = this.getConnectedPlots();
2436        Set<CuboidRegion> regions = new HashSet<>();
2437        Set<PlotId> visited = new HashSet<>();
2438        for (Plot current : plots) {
2439            if (visited.contains(current.getId())) {
2440                continue;
2441            }
2442            boolean merge = true;
2443            PlotId bot = current.getId();
2444            PlotId top = current.getId();
2445            while (merge) {
2446                merge = false;
2447                Iterable<PlotId> ids = PlotId.PlotRangeIterator.range(
2448                        PlotId.of(bot.getX(), bot.getY() - 1),
2449                        PlotId.of(top.getX(), bot.getY() - 1)
2450                );
2451                boolean tmp = true;
2452                for (PlotId id : ids) {
2453                    Plot plot = this.area.getPlotAbs(id);
2454                    if (plot == null || !plot.isMerged(Direction.SOUTH) || visited.contains(plot.getId())) {
2455                        tmp = false;
2456                    }
2457                }
2458                if (tmp) {
2459                    merge = true;
2460                    bot = PlotId.of(bot.getX(), bot.getY() - 1);
2461                }
2462                ids = PlotId.PlotRangeIterator.range(
2463                        PlotId.of(top.getX() + 1, bot.getY()),
2464                        PlotId.of(top.getX() + 1, top.getY())
2465                );
2466                tmp = true;
2467                for (PlotId id : ids) {
2468                    Plot plot = this.area.getPlotAbs(id);
2469                    if (plot == null || !plot.isMerged(Direction.WEST) || visited.contains(plot.getId())) {
2470                        tmp = false;
2471                    }
2472                }
2473                if (tmp) {
2474                    merge = true;
2475                    top = PlotId.of(top.getX() + 1, top.getY());
2476                }
2477                ids = PlotId.PlotRangeIterator.range(
2478                        PlotId.of(bot.getX(), top.getY() + 1),
2479                        PlotId.of(top.getX(), top.getY() + 1)
2480                );
2481                tmp = true;
2482                for (PlotId id : ids) {
2483                    Plot plot = this.area.getPlotAbs(id);
2484                    if (plot == null || !plot.isMerged(Direction.NORTH) || visited.contains(plot.getId())) {
2485                        tmp = false;
2486                    }
2487                }
2488                if (tmp) {
2489                    merge = true;
2490                    top = PlotId.of(top.getX(), top.getY() + 1);
2491                }
2492                ids = PlotId.PlotRangeIterator.range(
2493                        PlotId.of(bot.getX() - 1, bot.getY()),
2494                        PlotId.of(bot.getX() - 1, top.getY())
2495                );
2496                tmp = true;
2497                for (PlotId id : ids) {
2498                    Plot plot = this.area.getPlotAbs(id);
2499                    if (plot == null || !plot.isMerged(Direction.EAST) || visited.contains(plot.getId())) {
2500                        tmp = false;
2501                    }
2502                }
2503                if (tmp) {
2504                    merge = true;
2505                    bot = PlotId.of(bot.getX() - 1, bot.getY());
2506                }
2507            }
2508            int minHeight = getArea().getMinBuildHeight();
2509            int maxHeight = getArea().getMaxBuildHeight() - 1;
2510            Location gtopabs = this.area.getPlotAbs(top).getTopAbs();
2511            Location gbotabs = this.area.getPlotAbs(bot).getBottomAbs();
2512            visited.addAll(Lists.newArrayList((Iterable<? extends PlotId>) PlotId.PlotRangeIterator.range(bot, top)));
2513            for (int x = bot.getX(); x <= top.getX(); x++) {
2514                Plot plot = this.area.getPlotAbs(PlotId.of(x, top.getY()));
2515                if (plot.isMerged(Direction.SOUTH)) {
2516                    // south wedge
2517                    Location toploc = plot.getExtendedTopAbs();
2518                    Location botabs = plot.getBottomAbs();
2519                    Location topabs = plot.getTopAbs();
2520                    BlockVector3 pos1 = BlockVector3.at(botabs.getX(), minHeight, topabs.getZ() + 1);
2521                    BlockVector3 pos2 = BlockVector3.at(topabs.getX(), maxHeight, toploc.getZ());
2522                    regions.add(new CuboidRegion(pos1, pos2));
2523                    if (plot.isMerged(Direction.SOUTHEAST)) {
2524                        pos1 = BlockVector3.at(topabs.getX() + 1, minHeight, topabs.getZ() + 1);
2525                        pos2 = BlockVector3.at(toploc.getX(), maxHeight, toploc.getZ());
2526                        regions.add(new CuboidRegion(pos1, pos2));
2527                        // intersection
2528                    }
2529                }
2530            }
2531
2532            for (int y = bot.getY(); y <= top.getY(); y++) {
2533                Plot plot = this.area.getPlotAbs(PlotId.of(top.getX(), y));
2534                if (plot.isMerged(Direction.EAST)) {
2535                    // east wedge
2536                    Location toploc = plot.getExtendedTopAbs();
2537                    Location botabs = plot.getBottomAbs();
2538                    Location topabs = plot.getTopAbs();
2539                    BlockVector3 pos1 = BlockVector3.at(topabs.getX() + 1, minHeight, botabs.getZ());
2540                    BlockVector3 pos2 = BlockVector3.at(toploc.getX(), maxHeight, topabs.getZ());
2541                    regions.add(new CuboidRegion(pos1, pos2));
2542                    if (plot.isMerged(Direction.SOUTHEAST)) {
2543                        pos1 = BlockVector3.at(topabs.getX() + 1, minHeight, topabs.getZ() + 1);
2544                        pos2 = BlockVector3.at(toploc.getX(), maxHeight, toploc.getZ());
2545                        regions.add(new CuboidRegion(pos1, pos2));
2546                        // intersection
2547                    }
2548                }
2549            }
2550            BlockVector3 pos1 = BlockVector3.at(gbotabs.getX(), minHeight, gbotabs.getZ());
2551            BlockVector3 pos2 = BlockVector3.at(gtopabs.getX(), maxHeight, gtopabs.getZ());
2552            regions.add(new CuboidRegion(pos1, pos2));
2553        }
2554        return regions;
2555    }
2556
2557    /**
2558     * Attempt to find the largest rectangular region in a plot (as plots can form non rectangular shapes)
2559     *
2560     * @return the plot's largest CuboidRegion
2561     */
2562    public CuboidRegion getLargestRegion() {
2563        Set<CuboidRegion> regions = this.getRegions();
2564        CuboidRegion max = null;
2565        double area = Double.NEGATIVE_INFINITY;
2566        for (CuboidRegion region : regions) {
2567            double current = (region.getMaximumPoint().getX() - (double) region.getMinimumPoint().getX() + 1) * (
2568                    region.getMaximumPoint().getZ() - (double) region.getMinimumPoint().getZ() + 1);
2569            if (current > area) {
2570                max = region;
2571                area = current;
2572            }
2573        }
2574        return max;
2575    }
2576
2577    /**
2578     * Do the plot entry tasks for each player in the plot<br>
2579     * - Usually called when the plot state changes (unclaimed/claimed/flag change etc)
2580     */
2581    public void reEnter() {
2582        TaskManager.runTaskLater(() -> {
2583            for (PlotPlayer<?> pp : Plot.this.getPlayersInPlot()) {
2584                this.plotListener.plotExit(pp, Plot.this);
2585                this.plotListener.plotEntry(pp, Plot.this);
2586            }
2587        }, TaskTime.ticks(1L));
2588    }
2589
2590    public void debug(final @NonNull String message) {
2591        try {
2592            final Collection<PlotPlayer<?>> players = PlotPlayer.getDebugModePlayersInPlot(this);
2593            if (players.isEmpty()) {
2594                return;
2595            }
2596            Caption caption = TranslatableCaption.of("debug.plot_debug");
2597            Template plotTemplate = Template.of("plot", this.toString());
2598            Template messageTemplate = Template.of("message", message);
2599            for (final PlotPlayer<?> player : players) {
2600                if (isOwner(player.getUUID()) || player.hasPermission(Permission.PERMISSION_ADMIN_DEBUG_OTHER)) {
2601                    player.sendMessage(caption, plotTemplate, messageTemplate);
2602                }
2603            }
2604        } catch (final Exception ignored) {
2605        }
2606    }
2607
2608    /**
2609     * Teleport a player to a plot and send them the teleport message.
2610     *
2611     * @param player the player
2612     * @param result Called with the result of the teleportation
2613     */
2614    public void teleportPlayer(final PlotPlayer<?> player, Consumer<Boolean> result) {
2615        teleportPlayer(player, TeleportCause.PLUGIN, result);
2616    }
2617
2618    /**
2619     * Teleport a player to a plot and send them the teleport message.
2620     *
2621     * @param player         the player
2622     * @param cause          the cause of the teleport
2623     * @param resultConsumer Called with the result of the teleportation
2624     */
2625    public void teleportPlayer(final PlotPlayer<?> player, TeleportCause cause, Consumer<Boolean> resultConsumer) {
2626        Plot plot = this.getBasePlot(false);
2627        Result result = this.eventDispatcher.callTeleport(player, player.getLocation(), plot, cause).getEventResult();
2628        if (result == Result.DENY) {
2629            player.sendMessage(
2630                    TranslatableCaption.of("events.event_denied"),
2631                    Template.of("value", "Teleport")
2632            );
2633            resultConsumer.accept(false);
2634            return;
2635        }
2636        final Consumer<Location> locationConsumer = location -> {
2637            if (Settings.Teleport.DELAY == 0 || player.hasPermission("plots.teleport.delay.bypass")) {
2638                player.sendMessage(TranslatableCaption.of("teleport.teleported_to_plot"));
2639                player.teleport(location, cause);
2640                resultConsumer.accept(true);
2641                return;
2642            }
2643            player.sendMessage(
2644                    TranslatableCaption.of("teleport.teleport_in_seconds"),
2645                    Template.of("amount", String.valueOf(Settings.Teleport.DELAY))
2646            );
2647            final String name = player.getName();
2648            TaskManager.addToTeleportQueue(name);
2649            TaskManager.runTaskLater(() -> {
2650                if (!TaskManager.removeFromTeleportQueue(name)) {
2651                    return;
2652                }
2653                try {
2654                    player.sendMessage(TranslatableCaption.of("teleport.teleported_to_plot"));
2655                    player.teleport(location, cause);
2656                } catch (final Exception ignored) {
2657                }
2658            }, TaskTime.seconds(Settings.Teleport.DELAY));
2659            resultConsumer.accept(true);
2660        };
2661        if (this.area.isHomeAllowNonmember() || plot.isAdded(player.getUUID())) {
2662            this.getHome(locationConsumer);
2663        } else {
2664            this.getDefaultHome(false, locationConsumer);
2665        }
2666    }
2667
2668    /**
2669     * Checks if the owner of this Plot is online.
2670     *
2671     * @return {@code true} if the owner of the Plot is online
2672     */
2673    public boolean isOnline() {
2674        if (!this.hasOwner()) {
2675            return false;
2676        }
2677        if (!isMerged()) {
2678            return PlotSquared.platform().playerManager().getPlayerIfExists(Objects.requireNonNull(this.getOwnerAbs())) != null;
2679        }
2680        for (final Plot current : getConnectedPlots()) {
2681            if (current.hasOwner()
2682                    && PlotSquared
2683                    .platform()
2684                    .playerManager()
2685                    .getPlayerIfExists(Objects.requireNonNull(current.getOwnerAbs())) != null) {
2686                return true;
2687            }
2688        }
2689        return false;
2690    }
2691
2692    public int getDistanceFromOrigin() {
2693        Location bot = getManager().getPlotBottomLocAbs(id);
2694        Location top = getManager().getPlotTopLocAbs(id);
2695        return Math.max(
2696                Math.max(Math.abs(bot.getX()), Math.abs(bot.getZ())),
2697                Math.max(Math.abs(top.getX()), Math.abs(top.getZ()))
2698        );
2699    }
2700
2701    /**
2702     * Expands the world border to include this plot if it is beyond the current border.
2703     */
2704    public void updateWorldBorder() {
2705        int border = this.area.getBorder();
2706        if (border == Integer.MAX_VALUE) {
2707            return;
2708        }
2709        int max = getDistanceFromOrigin();
2710        if (max > border) {
2711            this.area.setMeta("worldBorder", max);
2712        }
2713    }
2714
2715    /**
2716     * Merges two plots. <br>- Assumes plots are directly next to each other <br> - saves to DB
2717     *
2718     * @param lesserPlot  the plot to merge into this plot instance
2719     * @param removeRoads if roads should be removed during the merge
2720     * @param queue       Nullable {@link QueueCoordinator}. If null, creates own queue and enqueues,
2721     *                    otherwise writes to the queue but does not enqueue.
2722     */
2723    public void mergePlot(Plot lesserPlot, boolean removeRoads, @Nullable QueueCoordinator queue) {
2724        Plot greaterPlot = this;
2725        lesserPlot.getPlotModificationManager().removeSign();
2726        if (lesserPlot.getId().getX() == greaterPlot.getId().getX()) {
2727            if (lesserPlot.getId().getY() > greaterPlot.getId().getY()) {
2728                Plot tmp = lesserPlot;
2729                lesserPlot = greaterPlot;
2730                greaterPlot = tmp;
2731            }
2732            if (!lesserPlot.isMerged(Direction.SOUTH)) {
2733                lesserPlot.clearRatings();
2734                greaterPlot.clearRatings();
2735                lesserPlot.setMerged(Direction.SOUTH, true);
2736                greaterPlot.setMerged(Direction.NORTH, true);
2737                lesserPlot.mergeData(greaterPlot);
2738                if (removeRoads) {
2739                    //lesserPlot.removeSign();
2740                    lesserPlot.getPlotModificationManager().removeRoadSouth(queue);
2741                    Plot diagonal = greaterPlot.getRelative(Direction.EAST);
2742                    if (diagonal.isMerged(Direction.NORTHWEST)) {
2743                        lesserPlot.plotModificationManager.removeRoadSouthEast(queue);
2744                    }
2745                    Plot below = greaterPlot.getRelative(Direction.WEST);
2746                    if (below.isMerged(Direction.NORTHEAST)) {
2747                        below.getRelative(Direction.NORTH).plotModificationManager.removeRoadSouthEast(queue);
2748                    }
2749                }
2750            }
2751        } else {
2752            if (lesserPlot.getId().getX() > greaterPlot.getId().getX()) {
2753                Plot tmp = lesserPlot;
2754                lesserPlot = greaterPlot;
2755                greaterPlot = tmp;
2756            }
2757            if (!lesserPlot.isMerged(Direction.EAST)) {
2758                lesserPlot.clearRatings();
2759                greaterPlot.clearRatings();
2760                lesserPlot.setMerged(Direction.EAST, true);
2761                greaterPlot.setMerged(Direction.WEST, true);
2762                lesserPlot.mergeData(greaterPlot);
2763                if (removeRoads) {
2764                    //lesserPlot.removeSign();
2765                    Plot diagonal = greaterPlot.getRelative(Direction.SOUTH);
2766                    if (diagonal.isMerged(Direction.NORTHWEST)) {
2767                        lesserPlot.plotModificationManager.removeRoadSouthEast(queue);
2768                    }
2769                    lesserPlot.plotModificationManager.removeRoadEast(queue);
2770                }
2771                Plot below = greaterPlot.getRelative(Direction.NORTH);
2772                if (below.isMerged(Direction.SOUTHWEST)) {
2773                    below.getRelative(Direction.WEST).getPlotModificationManager().removeRoadSouthEast(queue);
2774                }
2775            }
2776        }
2777    }
2778
2779    /**
2780     * Check if the plot is merged in a given direction
2781     *
2782     * @param direction Direction
2783     * @return {@code true} if the plot is merged in the given direction
2784     */
2785    public boolean isMerged(final @NonNull Direction direction) {
2786        return isMerged(direction.getIndex());
2787    }
2788
2789    /**
2790     * Get the value associated with the specified flag. This will first look at plot
2791     * specific flag values, then at the containing plot area and its default values
2792     * and at last, it will look at the default values stored in {@link GlobalFlagContainer}.
2793     *
2794     * @param flagClass The flag type (Class)
2795     * @param <T>       the flag value type
2796     * @return The flag value
2797     */
2798    public @NonNull <T> T getFlag(final @NonNull Class<? extends PlotFlag<T, ?>> flagClass) {
2799        return this.flagContainer.getFlag(flagClass).getValue();
2800    }
2801
2802    /**
2803     * Get the value associated with the specified flag. This will first look at plot
2804     * specific flag values, then at the containing plot area and its default values
2805     * and at last, it will look at the default values stored in {@link GlobalFlagContainer}.
2806     *
2807     * @param flag The flag type (Any instance of the flag)
2808     * @param <V>  the flag type (Any instance of the flag)
2809     * @param <T>  the flag's value type
2810     * @return The flag value
2811     */
2812    public @NonNull <T, V extends PlotFlag<T, ?>> T getFlag(final @NonNull V flag) {
2813        final Class<?> flagClass = flag.getClass();
2814        final PlotFlag<?, ?> flagInstance = this.flagContainer.getFlagErased(flagClass);
2815        return FlagContainer.<T, V>castUnsafe(flagInstance).getValue();
2816    }
2817
2818    public CompletableFuture<Caption> format(final Caption iInfo, PlotPlayer<?> player, final boolean full) {
2819        final CompletableFuture<Caption> future = new CompletableFuture<>();
2820        int num = this.getConnectedPlots().size();
2821        String alias = !this.getAlias().isEmpty() ? this.getAlias() : TranslatableCaption.of("info.none").getComponent(player);
2822        Location bot = this.getCorners()[0];
2823        PlotSquared.platform().worldUtil().getBiome(
2824                Objects.requireNonNull(this.getWorldName()),
2825                bot.getX(),
2826                bot.getZ(),
2827                biome -> {
2828                    Component trusted = PlayerManager.getPlayerList(this.getTrusted(), player);
2829                    Component members = PlayerManager.getPlayerList(this.getMembers(), player);
2830                    Component denied = PlayerManager.getPlayerList(this.getDenied(), player);
2831                    String seen;
2832                    if (Settings.Enabled_Components.PLOT_EXPIRY && PlotSquared.platform().expireManager() != null) {
2833                        if (this.isOnline()) {
2834                            seen = TranslatableCaption.of("info.now").getComponent(player);
2835                        } else {
2836                            int time = (int) (PlotSquared.platform().expireManager().getAge(this, false) / 1000);
2837                            if (time != 0) {
2838                                seen = TimeUtil.secToTime(time);
2839                            } else {
2840                                seen = TranslatableCaption.of("info.unknown").getComponent(player);
2841                            }
2842                        }
2843                    } else {
2844                        seen = TranslatableCaption.of("info.never").getComponent(player);
2845                    }
2846
2847                    String description = this.getFlag(DescriptionFlag.class);
2848                    if (description.isEmpty()) {
2849                        description = TranslatableCaption.of("info.plot_no_description").getComponent(player);
2850                    }
2851
2852                    Component flags;
2853                    Collection<PlotFlag<?, ?>> flagCollection = this.getApplicableFlags(true);
2854                    if (flagCollection.isEmpty()) {
2855                        flags = MINI_MESSAGE.parse(TranslatableCaption.of("info.none").getComponent(player));
2856                    } else {
2857                        TextComponent.Builder flagBuilder = Component.text();
2858                        String prefix = "";
2859                        for (final PlotFlag<?, ?> flag : flagCollection) {
2860                            Object value;
2861                            if (flag instanceof DoubleFlag && !Settings.General.SCIENTIFIC) {
2862                                value = FLAG_DECIMAL_FORMAT.format(flag.getValue());
2863                            } else {
2864                                value = flag.toString();
2865                            }
2866                            Component snip = MINI_MESSAGE.parse(
2867                                    prefix + CaptionUtility.format(
2868                                            player,
2869                                            TranslatableCaption.of("info.plot_flag_list").getComponent(player)
2870                                    ),
2871                                    Template.of("flag", flag.getName()),
2872                                    Template.of("value", CaptionUtility.formatRaw(player, value.toString()))
2873                            );
2874                            flagBuilder.append(snip);
2875                            prefix = ", ";
2876                        }
2877                        flags = flagBuilder.build();
2878                    }
2879                    boolean build = this.isAdded(player.getUUID());
2880                    Component owner;
2881                    if (this.getOwner() == null) {
2882                        owner = Component.text("unowned");
2883                    } else if (this.getOwner().equals(DBFunc.SERVER)) {
2884                        owner = Component.text(MINI_MESSAGE.stripTokens(TranslatableCaption
2885                                .of("info.server")
2886                                .getComponent(player)));
2887                    } else {
2888                        owner = PlayerManager.getPlayerList(this.getOwners(), player);
2889                    }
2890                    Template headerTemplate = Template.of(
2891                            "header",
2892                            TranslatableCaption.of("info.plot_info_header").getComponent(player)
2893                    );
2894                    Template footerTemplate = Template.of(
2895                            "footer",
2896                            TranslatableCaption.of("info.plot_info_footer").getComponent(player)
2897                    );
2898                    Template areaTemplate;
2899                    if (this.getArea() != null) {
2900                        areaTemplate =
2901                                Template.of(
2902                                        "area",
2903                                        this.getArea().getWorldName() + (this.getArea().getId() == null
2904                                                ? ""
2905                                                : "(" + this.getArea().getId() + ")")
2906                                );
2907                    } else {
2908                        areaTemplate = Template.of("area", TranslatableCaption.of("info.none").getComponent(player));
2909                    }
2910                    long creationDate = Long.parseLong(String.valueOf(timestamp));
2911                    SimpleDateFormat sdf = new SimpleDateFormat(Settings.Timeformat.DATE_FORMAT);
2912                    sdf.setTimeZone(TimeZone.getTimeZone(Settings.Timeformat.TIME_ZONE));
2913                    String newDate = sdf.format(creationDate);
2914
2915                    Template idTemplate = Template.of("id", this.getId().toString());
2916                    Template aliasTemplate = Template.of("alias", alias);
2917                    Template numTemplate = Template.of("num", String.valueOf(num));
2918                    Template descTemplate = Template.of("desc", description);
2919                    Template biomeTemplate = Template.of("biome", biome.toString().toLowerCase());
2920                    Template ownerTemplate = Template.of("owner", owner);
2921                    Template membersTemplate = Template.of("members", members);
2922                    Template playerTemplate = Template.of("player", player.getName());
2923                    Template trustedTemplate = Template.of("trusted", trusted);
2924                    Template helpersTemplate = Template.of("helpers", members);
2925                    Template deniedTemplate = Template.of("denied", denied);
2926                    Template seenTemplate = Template.of("seen", seen);
2927                    Template flagsTemplate = Template.of("flags", flags);
2928                    Template creationTemplate = Template.of("creationdate", newDate);
2929                    Template buildTemplate = Template.of("build", String.valueOf(build));
2930                    Template sizeTemplate = Template.of("size", String.valueOf(getConnectedPlots().size()));
2931                    String component = iInfo.getComponent(player);
2932                    if (component.contains("<rating>") || component.contains("<likes>")) {
2933                        TaskManager.runTaskAsync(() -> {
2934                            Template ratingTemplate;
2935                            Template likesTemplate;
2936                            if (Settings.Ratings.USE_LIKES) {
2937                                ratingTemplate = Template.of(
2938                                        "rating",
2939                                        String.format("%.0f%%", Like.getLikesPercentage(this) * 100D)
2940                                );
2941                                likesTemplate = Template.of(
2942                                        "likes",
2943                                        String.format("%.0f%%", Like.getLikesPercentage(this) * 100D)
2944                                );
2945                            } else {
2946                                int max = 10;
2947                                if (Settings.Ratings.CATEGORIES != null && !Settings.Ratings.CATEGORIES.isEmpty()) {
2948                                    max = 8;
2949                                }
2950                                if (full && Settings.Ratings.CATEGORIES != null && Settings.Ratings.CATEGORIES.size() > 1) {
2951                                    double[] ratings = this.getAverageRatings();
2952                                    StringBuilder rating = new StringBuilder();
2953                                    String prefix = "";
2954                                    for (int i = 0; i < ratings.length; i++) {
2955                                        rating.append(prefix).append(Settings.Ratings.CATEGORIES.get(i)).append('=')
2956                                                .append(String.format("%.1f", ratings[i]));
2957                                        prefix = ",";
2958                                    }
2959                                    ratingTemplate = Template.of("rating", rating.toString());
2960                                } else {
2961                                    double rating = this.getAverageRating();
2962                                    if (Double.isFinite(rating)) {
2963                                        ratingTemplate = Template.of("rating", String.format("%.1f", rating) + '/' + max);
2964                                    } else {
2965                                        ratingTemplate = Template.of(
2966                                                "rating",
2967                                                TranslatableCaption.of("info.none").getComponent(player)
2968                                        );
2969                                    }
2970                                }
2971                                likesTemplate = Template.of("likes", "N/A");
2972                            }
2973                            future.complete(StaticCaption.of(MINI_MESSAGE.serialize(MINI_MESSAGE
2974                                    .parse(
2975                                            iInfo.getComponent(player),
2976                                            headerTemplate,
2977                                            areaTemplate,
2978                                            idTemplate,
2979                                            aliasTemplate,
2980                                            numTemplate,
2981                                            descTemplate,
2982                                            biomeTemplate,
2983                                            ownerTemplate,
2984                                            membersTemplate,
2985                                            playerTemplate,
2986                                            trustedTemplate,
2987                                            helpersTemplate,
2988                                            deniedTemplate,
2989                                            seenTemplate,
2990                                            flagsTemplate,
2991                                            buildTemplate,
2992                                            ratingTemplate,
2993                                            creationTemplate,
2994                                            sizeTemplate,
2995                                            likesTemplate,
2996                                            footerTemplate
2997                                    ))));
2998                        });
2999                        return;
3000                    }
3001                    future.complete(StaticCaption.of(MINI_MESSAGE.serialize(MINI_MESSAGE
3002                            .parse(
3003                                    iInfo.getComponent(player),
3004                                    headerTemplate,
3005                                    areaTemplate,
3006                                    idTemplate,
3007                                    aliasTemplate,
3008                                    numTemplate,
3009                                    descTemplate,
3010                                    biomeTemplate,
3011                                    ownerTemplate,
3012                                    membersTemplate,
3013                                    playerTemplate,
3014                                    trustedTemplate,
3015                                    helpersTemplate,
3016                                    deniedTemplate,
3017                                    seenTemplate,
3018                                    flagsTemplate,
3019                                    buildTemplate,
3020                                    creationTemplate,
3021                                    sizeTemplate,
3022                                    footerTemplate
3023                            ))));
3024                }
3025        );
3026        return future;
3027    }
3028
3029    /**
3030     * If rating categories are enabled, get the average rating by category.<br>
3031     * - The index corresponds to the index of the category in the config
3032     *
3033     * <p>
3034     * See {@link Settings.Ratings#CATEGORIES} for rating categories
3035     * </p>
3036     *
3037     * @return Average ratings in each category
3038     */
3039    public @NonNull double[] getAverageRatings() {
3040        Map<UUID, Integer> rating;
3041        if (this.getSettings().getRatings() != null) {
3042            rating = this.getSettings().getRatings();
3043        } else if (Settings.Enabled_Components.RATING_CACHE) {
3044            rating = new HashMap<>();
3045        } else {
3046            rating = DBFunc.getRatings(this);
3047        }
3048        int size = 1;
3049        if (!Settings.Ratings.CATEGORIES.isEmpty()) {
3050            size = Math.max(1, Settings.Ratings.CATEGORIES.size());
3051        }
3052        double[] ratings = new double[size];
3053        if (rating == null || rating.isEmpty()) {
3054            return ratings;
3055        }
3056        for (Entry<UUID, Integer> entry : rating.entrySet()) {
3057            int current = entry.getValue();
3058            if (Settings.Ratings.CATEGORIES.isEmpty()) {
3059                ratings[0] += current;
3060            } else {
3061                for (int i = 0; i < Settings.Ratings.CATEGORIES.size(); i++) {
3062                    ratings[i] += current % 10 - 1;
3063                    current /= 10;
3064                }
3065            }
3066        }
3067        for (int i = 0; i < size; i++) {
3068            ratings[i] /= rating.size();
3069        }
3070        return ratings;
3071    }
3072
3073    /**
3074     * Get the plot flag container
3075     *
3076     * @return Flag container
3077     */
3078    public @NonNull FlagContainer getFlagContainer() {
3079        return this.flagContainer;
3080    }
3081
3082    /**
3083     * Get the plot comment container. This can be used to manage
3084     * and access plot comments
3085     *
3086     * @return Plot comment container
3087     */
3088    public @NonNull PlotCommentContainer getPlotCommentContainer() {
3089        return this.plotCommentContainer;
3090    }
3091
3092    /**
3093     * Get the plot modification manager
3094     *
3095     * @return Plot modification manager
3096     */
3097    public @NonNull PlotModificationManager getPlotModificationManager() {
3098        return this.plotModificationManager;
3099    }
3100
3101}