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