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