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