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