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