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.player;
020
021import com.google.common.base.Objects;
022import com.google.common.base.Preconditions;
023import com.google.common.primitives.Ints;
024import com.plotsquared.core.PlotSquared;
025import com.plotsquared.core.collection.ByteArrayUtilities;
026import com.plotsquared.core.command.CommandCaller;
027import com.plotsquared.core.command.RequiredType;
028import com.plotsquared.core.configuration.Settings;
029import com.plotsquared.core.configuration.caption.Caption;
030import com.plotsquared.core.configuration.caption.CaptionMap;
031import com.plotsquared.core.configuration.caption.CaptionUtility;
032import com.plotsquared.core.configuration.caption.LocaleHolder;
033import com.plotsquared.core.configuration.caption.TranslatableCaption;
034import com.plotsquared.core.database.DBFunc;
035import com.plotsquared.core.events.TeleportCause;
036import com.plotsquared.core.location.Location;
037import com.plotsquared.core.permissions.NullPermissionProfile;
038import com.plotsquared.core.permissions.PermissionHandler;
039import com.plotsquared.core.permissions.PermissionProfile;
040import com.plotsquared.core.plot.Plot;
041import com.plotsquared.core.plot.PlotArea;
042import com.plotsquared.core.plot.PlotCluster;
043import com.plotsquared.core.plot.PlotId;
044import com.plotsquared.core.plot.PlotWeather;
045import com.plotsquared.core.plot.flag.implementations.DoneFlag;
046import com.plotsquared.core.plot.world.PlotAreaManager;
047import com.plotsquared.core.plot.world.SinglePlotArea;
048import com.plotsquared.core.plot.world.SinglePlotAreaManager;
049import com.plotsquared.core.synchronization.LockRepository;
050import com.plotsquared.core.util.EventDispatcher;
051import com.plotsquared.core.util.query.PlotQuery;
052import com.plotsquared.core.util.task.RunnableVal;
053import com.plotsquared.core.util.task.TaskManager;
054import com.sk89q.worldedit.extension.platform.Actor;
055import com.sk89q.worldedit.world.gamemode.GameMode;
056import com.sk89q.worldedit.world.item.ItemType;
057import net.kyori.adventure.audience.Audience;
058import net.kyori.adventure.text.Component;
059import net.kyori.adventure.text.minimessage.MiniMessage;
060import net.kyori.adventure.text.minimessage.tag.Tag;
061import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
062import net.kyori.adventure.title.Title;
063import org.apache.logging.log4j.LogManager;
064import org.apache.logging.log4j.Logger;
065import org.checkerframework.checker.nullness.qual.NonNull;
066import org.checkerframework.checker.nullness.qual.Nullable;
067
068import java.nio.ByteBuffer;
069import java.time.Duration;
070import java.time.temporal.ChronoUnit;
071import java.util.ArrayDeque;
072import java.util.Arrays;
073import java.util.Collection;
074import java.util.Collections;
075import java.util.HashMap;
076import java.util.HashSet;
077import java.util.LinkedList;
078import java.util.Locale;
079import java.util.Map;
080import java.util.Queue;
081import java.util.Set;
082import java.util.UUID;
083import java.util.concurrent.CompletableFuture;
084import java.util.concurrent.ConcurrentHashMap;
085import java.util.concurrent.atomic.AtomicInteger;
086
087/**
088 * The abstract class supporting {@code BukkitPlayer} and {@code SpongePlayer}.
089 */
090public abstract class PlotPlayer<P> implements CommandCaller, OfflinePlotPlayer, LocaleHolder {
091
092    private static final String NON_EXISTENT_CAPTION = "<red>PlotSquared does not recognize the caption: ";
093
094    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotPlayer.class.getSimpleName());
095
096    // Used to track debug mode
097    private static final Set<PlotPlayer<?>> debugModeEnabled =
098            Collections.synchronizedSet(new HashSet<>());
099
100    @SuppressWarnings("rawtypes")
101    private static final Map<Class<?>, PlotPlayerConverter> converters = new HashMap<>();
102    private final LockRepository lockRepository = new LockRepository();
103    private final PlotAreaManager plotAreaManager;
104    private final EventDispatcher eventDispatcher;
105    private final PermissionHandler permissionHandler;
106    private Map<String, byte[]> metaMap = new HashMap<>();
107    /**
108     * The metadata map.
109     */
110    private ConcurrentHashMap<String, Object> meta;
111    private int hash;
112    private Locale locale;
113    // Delayed initialisation
114    private PermissionProfile permissionProfile;
115
116    public PlotPlayer(
117            final @NonNull PlotAreaManager plotAreaManager, final @NonNull EventDispatcher eventDispatcher,
118            final @NonNull PermissionHandler permissionHandler
119    ) {
120        this.plotAreaManager = plotAreaManager;
121        this.eventDispatcher = eventDispatcher;
122        this.permissionHandler = permissionHandler;
123    }
124
125    @SuppressWarnings({"rawtypes", "unchecked"})
126    public static <T> PlotPlayer<T> from(final @NonNull T object) {
127        // fast path
128        if (converters.containsKey(object.getClass())) {
129            return converters.get(object.getClass()).convert(object);
130        }
131        // slow path, meant to only run once per object#getClass instance
132        Queue<Class<?>> toVisit = new ArrayDeque<>();
133        toVisit.add(object.getClass());
134        Class<?> current;
135        while ((current = toVisit.poll()) != null) {
136            PlotPlayerConverter converter = converters.get(current);
137            if (converter != null) {
138                if (current != object.getClass()) {
139                    // register shortcut for this sub type to avoid further loops
140                    converters.put(object.getClass(), converter);
141                    LOGGER.info("Registered {} as with converter for {}", object.getClass(), current);
142                }
143                return converter.convert(object);
144            }
145            // no converter found yet
146            if (current.getSuperclass() != null) {
147                toVisit.add(current.getSuperclass()); // add super class if available
148            }
149            toVisit.addAll(Arrays.asList(current.getInterfaces())); // add interfaces
150        }
151        throw new IllegalArgumentException(String
152                .format(
153                        "There is no registered PlotPlayer converter for type %s",
154                        object.getClass().getSimpleName()
155                ));
156    }
157
158    public static <T> void registerConverter(
159            final @NonNull Class<T> clazz,
160            final PlotPlayerConverter<T> converter
161    ) {
162        converters.put(clazz, converter);
163    }
164
165    public static Collection<PlotPlayer<?>> getDebugModePlayers() {
166        return Collections.unmodifiableCollection(debugModeEnabled);
167    }
168
169    public static Collection<PlotPlayer<?>> getDebugModePlayersInPlot(final @NonNull Plot plot) {
170        if (debugModeEnabled.isEmpty()) {
171            return Collections.emptyList();
172        }
173        final Collection<PlotPlayer<?>> players = new LinkedList<>();
174        for (final PlotPlayer<?> player : debugModeEnabled) {
175            if (player.getCurrentPlot().equals(plot)) {
176                players.add(player);
177            }
178        }
179        return players;
180    }
181
182    protected void setupPermissionProfile() {
183        this.permissionProfile = permissionHandler.getPermissionProfile(this).orElse(
184                NullPermissionProfile.INSTANCE);
185    }
186
187    @Override
188    public final boolean hasPermission(
189            final @Nullable String world,
190            final @NonNull String permission
191    ) {
192        return this.permissionProfile.hasPermission(world, permission);
193    }
194
195    @Override
196    public final boolean hasKeyedPermission(
197            final @Nullable String world,
198            final @NonNull String permission,
199            final @NonNull String key
200    ) {
201        return this.permissionProfile.hasKeyedPermission(world, permission, key);
202    }
203
204    @Override
205    public final boolean hasPermission(@NonNull String permission, boolean notify) {
206        if (!hasPermission(permission)) {
207            if (notify) {
208                sendMessage(
209                        TranslatableCaption.of("permission.no_permission_event"),
210                        TagResolver.resolver("node", Tag.inserting(Component.text(permission)))
211                );
212            }
213            return false;
214        }
215        return true;
216    }
217
218    public abstract Actor toActor();
219
220    public abstract P getPlatformPlayer();
221
222    /**
223     * Set some session only metadata for this player.
224     *
225     * @param key
226     * @param value
227     */
228    void setMeta(String key, Object value) {
229        if (value == null) {
230            deleteMeta(key);
231        } else {
232            if (this.meta == null) {
233                this.meta = new ConcurrentHashMap<>();
234            }
235            this.meta.put(key, value);
236        }
237    }
238
239    /**
240     * Get the session metadata for a key.
241     *
242     * @param key the name of the metadata key
243     * @param <T> the object type to return
244     * @return the value assigned to the key or null if it does not exist
245     */
246    @SuppressWarnings("unchecked")
247    <T> T getMeta(String key) {
248        if (this.meta != null) {
249            return (T) this.meta.get(key);
250        }
251        return null;
252    }
253
254    <T> T getMeta(String key, T defaultValue) {
255        T meta = getMeta(key);
256        if (meta == null) {
257            return defaultValue;
258        }
259        return meta;
260    }
261
262    public ConcurrentHashMap<String, Object> getMeta() {
263        return meta;
264    }
265
266    /**
267     * Delete the metadata for a key.
268     * - metadata is session only
269     * - deleting other plugin's metadata may cause issues
270     *
271     * @param key
272     */
273    Object deleteMeta(String key) {
274        return this.meta == null ? null : this.meta.remove(key);
275    }
276
277
278    /**
279     * Returns the name of the player.
280     *
281     * @return the name of the player
282     */
283    @Override
284    public String toString() {
285        return getName();
286    }
287
288    /**
289     * Get this player's current plot.
290     *
291     * @return the plot the player is standing on or null if standing on a road or not in a {@link PlotArea}
292     */
293    public @Nullable Plot getCurrentPlot() {
294        try (final MetaDataAccess<Plot> lastPlotAccess =
295                     this.accessTemporaryMetaData(PlayerMetaDataKeys.TEMPORARY_LAST_PLOT)) {
296            if (lastPlotAccess.get().orElse(null) == null && !Settings.Enabled_Components.EVENTS) {
297                return this.getLocation().getPlot();
298            }
299            return lastPlotAccess.get().orElse(null);
300        }
301    }
302
303    /**
304     * Get the total number of allowed plots
305     *
306     * @return number of allowed plots within the scope (globally, or in the player's current world as defined in the settings.yml)
307     */
308    public int getAllowedPlots() {
309        final int calculatedLimit = hasPermissionRange("plots.plot", Settings.Limit.MAX_PLOTS);
310        return this.eventDispatcher.callPlayerPlotLimit(this, calculatedLimit).limit();
311    }
312
313    /**
314     * Get the number of plots this player owns.
315     *
316     * @return number of plots within the scope (globally, or in the player's current world as defined in the settings.yml)
317     * @see #getPlotCount(String)
318     * @see #getPlots()
319     */
320    public int getPlotCount() {
321        if (!Settings.Limit.GLOBAL) {
322            return getPlotCount(getContextualWorldName());
323        }
324        final AtomicInteger count = new AtomicInteger(0);
325        final UUID uuid = getUUID();
326        this.plotAreaManager.forEachPlotArea(value -> {
327            if (!Settings.Done.COUNTS_TOWARDS_LIMIT) {
328                for (Plot plot : value.getPlotsAbs(uuid)) {
329                    if (!DoneFlag.isDone(plot)) {
330                        count.incrementAndGet();
331                    }
332                }
333            } else {
334                count.addAndGet(value.getPlotsAbs(uuid).size());
335            }
336        });
337        return count.get();
338    }
339
340    public int getClusterCount() {
341        if (!Settings.Limit.GLOBAL) {
342            return getClusterCount(getContextualWorldName());
343        }
344        final AtomicInteger count = new AtomicInteger(0);
345        this.plotAreaManager.forEachPlotArea(value -> {
346            for (PlotCluster cluster : value.getClusters()) {
347                if (cluster.isOwner(getUUID())) {
348                    count.incrementAndGet();
349                }
350            }
351        });
352        return count.get();
353    }
354
355    /**
356     * {@return the world name at the player's contextual position}
357     * The contextual position can be affected when using a command with
358     * an explicit plot override, e.g., {@code /plot <id> info}.
359     */
360    private @NonNull String getContextualWorldName() {
361        Plot current = getCurrentPlot();
362        if (current != null) {
363            return current.getWorldName();
364        }
365        return getLocation().getWorldName();
366    }
367
368    /**
369     * {@return the plot area at the player's contextual position}
370     * The contextual position can be affected when using a command with
371     * an explicit plot override, e.g., {@code /plot <id> info}.
372     *
373     * @since 7.5.9
374     */
375    public @Nullable PlotArea getContextualPlotArea() {
376        Plot current = getCurrentPlot();
377        if (current != null) {
378            return current.getArea();
379        }
380        return getLocation().getPlotArea();
381    }
382
383    /**
384     * Get the number of plots this player owns in the world.
385     *
386     * @param world the name of the plotworld to check.
387     * @return plot count
388     */
389    public int getPlotCount(String world) {
390        UUID uuid = getUUID();
391        int count = 0;
392        for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) {
393            if (!Settings.Done.COUNTS_TOWARDS_LIMIT) {
394                count +=
395                        area.getPlotsAbs(uuid).stream().filter(plot -> !DoneFlag.isDone(plot)).count();
396            } else {
397                count += area.getPlotsAbs(uuid).size();
398            }
399        }
400        return count;
401    }
402
403    public int getClusterCount(String world) {
404        int count = 0;
405        for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) {
406            for (PlotCluster cluster : area.getClusters()) {
407                if (cluster.isOwner(getUUID())) {
408                    count++;
409                }
410            }
411        }
412        return count;
413    }
414
415    /**
416     * Get a {@link Set} of plots owned by this player.
417     *
418     * <p>
419     * Take a look at {@link PlotSquared} for more searching functions.
420     * See {@link #getPlotCount()} for the number of plots.
421     * </p>
422     *
423     * @return a {@link Set} of plots owned by the player
424     */
425    public Set<Plot> getPlots() {
426        return PlotQuery.newQuery().ownedBy(this).asSet();
427    }
428
429    /**
430     * Return the PlotArea this player is currently in, or null.
431     *
432     * @return Plot area the player is currently in, or {@code null}
433     */
434    public @Nullable PlotArea getPlotAreaAbs() {
435        return this.plotAreaManager.getPlotArea(getLocation());
436    }
437
438    public PlotArea getApplicablePlotArea() {
439        Plot plot = getCurrentPlot();
440        if (plot == null) {
441            return this.plotAreaManager.getApplicablePlotArea(getLocation());
442        }
443        return plot.getArea();
444    }
445
446    @Override
447    public @NonNull RequiredType getSuperCaller() {
448        return RequiredType.PLAYER;
449    }
450
451    /**
452     * Get this player's last recorded location or null if they don't any plot relevant location.
453     *
454     * @return The location
455     */
456    public @NonNull Location getLocation() {
457        Location location = getMeta("location");
458        if (location != null) {
459            return location;
460        }
461        return getLocationFull();
462    }
463
464    /////////////// PLAYER META ///////////////
465
466    ////////////// PARTIALLY IMPLEMENTED ///////////
467
468    /**
469     * Get this player's full location (including yaw/pitch)
470     *
471     * @return location
472     */
473    public abstract Location getLocationFull();
474
475    ////////////////////////////////////////////////
476
477    /**
478     * Get this player's UUID.
479     * <p>=== !IMPORTANT ===</p>
480     * The UUID is dependent on the mode chosen in the settings.yml and may not be the same as Bukkit has
481     * (especially if using an old version of Bukkit that does not support UUIDs)
482     *
483     * @return UUID
484     */
485    @Override
486    public @NonNull
487    abstract UUID getUUID();
488
489    public boolean canTeleport(final @NonNull Location location) {
490        Preconditions.checkNotNull(location, "Specified location cannot be null");
491        final Location current = getLocationFull();
492        teleport(location);
493        boolean result = getLocation().equals(location);
494        teleport(current);
495        return result;
496    }
497
498    /**
499     * Teleport this player to a location.
500     *
501     * @param location the target location
502     */
503    public void teleport(Location location) {
504        teleport(location, TeleportCause.PLUGIN);
505    }
506
507    /**
508     * Teleport this player to a location.
509     *
510     * @param location the target location
511     * @param cause    the cause of the teleport
512     */
513    public abstract void teleport(Location location, TeleportCause cause);
514
515    /**
516     * Kick this player to a location
517     *
518     * @param location the target location
519     */
520    public void plotkick(Location location) {
521        setMeta("kick", true);
522        teleport(location, TeleportCause.KICK);
523        deleteMeta("kick");
524    }
525
526    /**
527     * Set this compass target.
528     *
529     * @param location the target location
530     */
531    public abstract void setCompassTarget(Location location);
532
533    /**
534     * Set player data that will persist restarts.
535     * - Please note that this is not intended to store large values
536     * - For session only data use meta
537     *
538     * @param key metadata key
539     */
540    public void setAttribute(String key) {
541        setPersistentMeta("attrib_" + key, new byte[]{(byte) 1});
542    }
543
544    /**
545     * Retrieves the attribute of this player.
546     *
547     * @param key metadata key
548     * @return the attribute will be either {@code true} or {@code false}
549     */
550    public boolean getAttribute(String key) {
551        if (!hasPersistentMeta("attrib_" + key)) {
552            return false;
553        }
554        return getPersistentMeta("attrib_" + key)[0] == 1;
555    }
556
557    /**
558     * Remove an attribute from a player.
559     *
560     * @param key metadata key
561     */
562    public void removeAttribute(String key) {
563        removePersistentMeta("attrib_" + key);
564    }
565
566    /**
567     * Sets the local weather for this Player.
568     *
569     * @param weather the weather visible to the player
570     */
571    public abstract void setWeather(@NonNull PlotWeather weather);
572
573    /**
574     * Get this player's gamemode.
575     *
576     * @return the gamemode of the player.
577     */
578    public abstract @NonNull GameMode getGameMode();
579
580    /**
581     * Set this player's gameMode.
582     *
583     * @param gameMode the gamemode to set
584     */
585    public abstract void setGameMode(@NonNull GameMode gameMode);
586
587    /**
588     * Set this player's local time (ticks).
589     *
590     * @param time the time visible to the player
591     */
592    public abstract void setTime(long time);
593
594    /**
595     * Determines whether or not the player can fly.
596     *
597     * @return {@code true} if the player is allowed to fly
598     */
599    public abstract boolean getFlight();
600
601    /**
602     * Sets whether or not this player can fly.
603     *
604     * @param fly {@code true} if the player can fly, otherwise {@code false}
605     */
606    public abstract void setFlight(boolean fly);
607
608    /**
609     * Play music at a location for this player.
610     *
611     * @param location where to play the music
612     * @param id       the record item id
613     */
614    public abstract void playMusic(@NonNull Location location, @NonNull ItemType id);
615
616    /**
617     * Check if this player is banned.
618     *
619     * @return {@code true} if the player is banned, {@code false} otherwise.
620     */
621    public abstract boolean isBanned();
622
623    /**
624     * Kick this player from the game.
625     *
626     * @param message the reason for the kick
627     */
628    public abstract void kick(String message);
629
630    public void refreshDebug() {
631        final boolean debug = this.getAttribute("debug");
632        if (debug && !debugModeEnabled.contains(this)) {
633            debugModeEnabled.add(this);
634        } else if (!debug) {
635            debugModeEnabled.remove(this);
636        }
637    }
638
639    /**
640     * Called when this player quits.
641     */
642    public void unregister() {
643        Plot plot = getCurrentPlot();
644        if (plot != null && Settings.Enabled_Components.PERSISTENT_META && plot
645                .getArea() instanceof SinglePlotArea) {
646            PlotId id = plot.getId();
647            int x = id.getX();
648            int z = id.getY();
649            ByteBuffer buffer = ByteBuffer.allocate(14);
650            buffer.putShort((short) x);
651            buffer.putShort((short) z);
652            Location location = getLocation();
653            buffer.putInt(location.getX());
654            buffer.putShort((short) location.getY());
655            buffer.putInt(location.getZ());
656            setPersistentMeta("quitLocV2", buffer.array());
657        } else if (hasPersistentMeta("quitLocV2")) {
658            removePersistentMeta("quitLocV2");
659        }
660        if (plot != null) {
661            this.eventDispatcher.callLeave(this, plot);
662        }
663        if (Settings.Enabled_Components.BAN_DELETER && isBanned()) {
664            for (Plot owned : getPlots()) {
665                owned.getPlotModificationManager().deletePlot(null, null);
666                LOGGER.info("Plot {} was deleted + cleared due to {} getting banned", owned.getId(), getName());
667            }
668        }
669        if (PlotSquared.platform().expireManager() != null) {
670            PlotSquared.platform().expireManager().storeDate(getUUID(), System.currentTimeMillis());
671        }
672        PlotSquared.platform().playerManager().removePlayer(this);
673        PlotSquared.platform().unregister(this);
674
675        debugModeEnabled.remove(this);
676    }
677
678    /**
679     * Get the amount of clusters this player owns in the specific world.
680     *
681     * @param world world
682     * @return number of clusters owned
683     */
684    public int getPlayerClusterCount(String world) {
685        return PlotSquared.get().getClusters(world).stream()
686                .filter(cluster -> getUUID().equals(cluster.owner)).mapToInt(PlotCluster::getArea)
687                .sum();
688    }
689
690    /**
691     * Get the amount of clusters this player owns.
692     *
693     * @return the number of clusters this player owns
694     */
695    public int getPlayerClusterCount() {
696        final AtomicInteger count = new AtomicInteger();
697        this.plotAreaManager.forEachPlotArea(value -> count.addAndGet(value.getClusters().size()));
698        return count.get();
699    }
700
701    /**
702     * Return a {@code Set} of all plots this player owns in a certain world.
703     *
704     * @param world the world to retrieve plots from
705     * @return a {@code Set} of plots this player owns in the provided world
706     */
707    public Set<Plot> getPlots(String world) {
708        return PlotQuery.newQuery().inWorld(world).ownedBy(getUUID()).asSet();
709    }
710
711    public void populatePersistentMetaMap() {
712        if (Settings.Enabled_Components.PERSISTENT_META) {
713            DBFunc.getPersistentMeta(
714                    getUUID(), new RunnableVal<>() {
715                        @Override
716                        public void run(Map<String, byte[]> value) {
717                            try {
718                                PlotPlayer.this.metaMap = value;
719                                if (value.isEmpty()) {
720                                    return;
721                                }
722
723                                if (PlotPlayer.this.getAttribute("debug")) {
724                                    debugModeEnabled.add(PlotPlayer.this);
725                                }
726
727                                if (!Settings.Teleport.ON_LOGIN) {
728                                    return;
729                                }
730                                PlotAreaManager manager = PlotPlayer.this.plotAreaManager;
731
732                                if (!(manager instanceof SinglePlotAreaManager)) {
733                                    return;
734                                }
735                                PlotArea area = ((SinglePlotAreaManager) manager).getArea();
736                                boolean V2 = false;
737                                byte[] arr = PlotPlayer.this.getPersistentMeta("quitLoc");
738                                if (arr == null) {
739                                    arr = PlotPlayer.this.getPersistentMeta("quitLocV2");
740                                    if (arr == null) {
741                                        return;
742                                    }
743                                    V2 = true;
744                                    removePersistentMeta("quitLocV2");
745                                } else {
746                                    removePersistentMeta("quitLoc");
747                                }
748
749                                if (!getMeta("teleportOnLogin", true)) {
750                                    return;
751                                }
752                                ByteBuffer quitWorld = ByteBuffer.wrap(arr);
753                                final int plotX = quitWorld.getShort();
754                                final int plotZ = quitWorld.getShort();
755                                PlotId id = PlotId.of(plotX, plotZ);
756                                int x = quitWorld.getInt();
757                                int y = V2 ? quitWorld.getShort() : (quitWorld.get() & 0xFF);
758                                int z = quitWorld.getInt();
759                                Plot plot = area.getOwnedPlot(id);
760
761                                if (plot == null) {
762                                    return;
763                                }
764
765                                final Location location = Location.at(plot.getWorldName(), x, y, z);
766                                if (plot.isLoaded()) {
767                                    TaskManager.runTask(() -> {
768                                        if (getMeta("teleportOnLogin", true)) {
769                                            teleport(location, TeleportCause.LOGIN);
770                                            sendMessage(
771                                                    TranslatableCaption.of("teleport.teleported_to_plot"));
772                                        }
773                                    });
774                                } else if (!PlotSquared.get().isMainThread(Thread.currentThread())) {
775                                    if (getMeta("teleportOnLogin", true)) {
776                                        plot.teleportPlayer(
777                                                PlotPlayer.this,
778                                                result -> TaskManager.runTask(() -> {
779                                                    if (getMeta("teleportOnLogin", true)) {
780                                                        if (plot.isLoaded()) {
781                                                            teleport(location, TeleportCause.LOGIN);
782                                                            sendMessage(TranslatableCaption
783                                                                    .of("teleport.teleported_to_plot"));
784                                                        }
785                                                    }
786                                                })
787                                        );
788                                    }
789                                }
790                            } catch (Throwable e) {
791                                LOGGER.error("Error populating persistent meta for player {}", PlotPlayer.this.getName(), e);
792                            }
793                        }
794                    }
795            );
796        }
797    }
798
799    byte[] getPersistentMeta(String key) {
800        return this.metaMap.get(key);
801    }
802
803    Object removePersistentMeta(String key) {
804        final Object old = this.metaMap.remove(key);
805        if (Settings.Enabled_Components.PERSISTENT_META) {
806            DBFunc.removePersistentMeta(getUUID(), key);
807        }
808        return old;
809    }
810
811    /**
812     * Access keyed persistent meta data for this player. This returns a meta data
813     * access instance, that MUST be closed. It is meant to be used with try-with-resources,
814     * like such:
815     * <pre>{@code
816     * try (final MetaDataAccess<Integer> access = player.accessPersistentMetaData(PlayerMetaKeys.GRANTS)) {
817     *     int grants = access.get();
818     *     access.set(grants + 1);
819     * }
820     * }</pre>
821     *
822     * @param key Meta data key
823     * @param <T> Meta data type
824     * @return Meta data access. MUST be closed after being used
825     */
826    public @NonNull <T> MetaDataAccess<T> accessPersistentMetaData(final @NonNull MetaDataKey<T> key) {
827        return new PersistentMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey()));
828    }
829
830    /**
831     * Access keyed temporary meta data for this player. This returns a meta data
832     * access instance, that MUST be closed. It is meant to be used with try-with-resources,
833     * like such:
834     * <pre>{@code
835     * try (final MetaDataAccess<Integer> access = player.accessTemporaryMetaData(PlayerMetaKeys.GRANTS)) {
836     *     int grants = access.get();
837     *     access.set(grants + 1);
838     * }
839     * }</pre>
840     *
841     * @param key Meta data key
842     * @param <T> Meta data type
843     * @return Meta data access. MUST be closed after being used
844     */
845    public @NonNull <T> MetaDataAccess<T> accessTemporaryMetaData(final @NonNull MetaDataKey<T> key) {
846        return new TemporaryMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey()));
847    }
848
849    <T> void setPersistentMeta(
850            final @NonNull MetaDataKey<T> key,
851            final @NonNull T value
852    ) {
853        if (key.getType().getRawType().equals(Integer.class)) {
854            this.setPersistentMeta(key.toString(), Ints.toByteArray((int) (Object) value));
855        } else if (key.getType().getRawType().equals(Boolean.class)) {
856            this.setPersistentMeta(key.toString(), ByteArrayUtilities.booleanToBytes((boolean) (Object) value));
857        } else {
858            throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType()));
859        }
860    }
861
862    @SuppressWarnings("unchecked")
863    @Nullable
864    <T> T getPersistentMeta(final @NonNull MetaDataKey<T> key) {
865        final byte[] value = this.getPersistentMeta(key.toString());
866        if (value == null) {
867            return null;
868        }
869        final Object returnValue;
870        if (key.getType().getRawType().equals(Integer.class)) {
871            returnValue = Ints.fromByteArray(value);
872        } else if (key.getType().getRawType().equals(Boolean.class)) {
873            returnValue = ByteArrayUtilities.bytesToBoolean(value);
874        } else {
875            throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType()));
876        }
877        return (T) returnValue;
878    }
879
880    void setPersistentMeta(String key, byte[] value) {
881        boolean delete = hasPersistentMeta(key);
882        this.metaMap.put(key, value);
883        if (Settings.Enabled_Components.PERSISTENT_META) {
884            DBFunc.addPersistentMeta(getUUID(), key, value, delete);
885        }
886    }
887
888    /**
889     * Send a title to the player that fades in, in 10 ticks, stays for 50 ticks and fades
890     * out in 20 ticks
891     *
892     * @param title        Title text
893     * @param subtitle     Subtitle text
894     * @param replacements Variable replacements
895     */
896    public void sendTitle(
897            final @NonNull Caption title, final @NonNull Caption subtitle,
898            final @NonNull TagResolver... replacements
899    ) {
900        sendTitle(
901                title,
902                subtitle,
903                Settings.Titles.TITLES_FADE_IN,
904                Settings.Titles.TITLES_STAY,
905                Settings.Titles.TITLES_FADE_OUT,
906                replacements
907        );
908    }
909
910    /**
911     * Send a title to the player
912     *
913     * @param title        Title
914     * @param subtitle     Subtitle
915     * @param fadeIn       Fade in time (in ticks)
916     * @param stay         The title stays for (in ticks)
917     * @param fadeOut      Fade out time (in ticks)
918     * @param replacements Variable replacements
919     */
920    public void sendTitle(
921            final @NonNull Caption title, final @NonNull Caption subtitle,
922            final int fadeIn, final int stay, final int fadeOut,
923            final @NonNull TagResolver... replacements
924    ) {
925        final Component titleComponent = MiniMessage.miniMessage().deserialize(title.getComponent(this), replacements);
926        final Component subtitleComponent =
927                MiniMessage.miniMessage().deserialize(subtitle.getComponent(this), replacements);
928        final Title.Times times = Title.Times.times(
929                Duration.of(Settings.Titles.TITLES_FADE_IN * 50L, ChronoUnit.MILLIS),
930                Duration.of(Settings.Titles.TITLES_STAY * 50L, ChronoUnit.MILLIS),
931                Duration.of(Settings.Titles.TITLES_FADE_OUT * 50L, ChronoUnit.MILLIS)
932        );
933        getAudience().showTitle(Title
934                .title(titleComponent, subtitleComponent, times));
935    }
936
937    /**
938     * Method designed to send an ActionBar to a player.
939     *
940     * @param caption      Caption
941     * @param replacements Variable replacements
942     */
943    public void sendActionBar(
944            final @NonNull Caption caption,
945            final @NonNull TagResolver... replacements
946    ) {
947        String message;
948        try {
949            message = caption.getComponent(this);
950        } catch (final CaptionMap.NoSuchCaptionException exception) {
951            // This sends feedback to the player
952            message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey();
953            // And this also prints it to the console
954            exception.printStackTrace();
955        }
956        if (message.isEmpty()) {
957            return;
958        }
959        // Replace placeholders, etc
960        message = CaptionUtility.format(this, message)
961                .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&')
962                .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this));
963
964
965        final Component component = MiniMessage.miniMessage().deserialize(message, replacements);
966        getAudience().sendActionBar(component);
967    }
968
969    @Override
970    public void sendMessage(
971            final @NonNull Caption caption,
972            final @NonNull TagResolver... replacements
973    ) {
974        String message;
975        try {
976            message = caption.getComponent(this);
977        } catch (final CaptionMap.NoSuchCaptionException exception) {
978            // This sends feedback to the player
979            message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey();
980            // And this also prints it to the console
981            exception.printStackTrace();
982        }
983        if (message.isEmpty()) {
984            return;
985        }
986        // Replace placeholders, etc
987        message = CaptionUtility.format(this, message)
988                .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&')
989                .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this));
990        // Parse the message
991        final Component component = MiniMessage.miniMessage().deserialize(message, replacements);
992        if (!Objects.equal(component, this.getMeta("lastMessage"))
993                || System.currentTimeMillis() - this.<Long>getMeta("lastMessageTime") > 5000) {
994            setMeta("lastMessage", component);
995            setMeta("lastMessageTime", System.currentTimeMillis());
996            getAudience().sendMessage(component);
997        }
998    }
999
1000    /**
1001     * Sends a message to the command caller, when the future is resolved
1002     *
1003     * @param caption          Caption to send
1004     * @param asyncReplacement Async variable replacement
1005     * @return A Future to be resolved, after the message was sent
1006     * @since 7.1.0
1007     */
1008    public final CompletableFuture<Void> sendMessage(
1009            @NonNull Caption caption,
1010            CompletableFuture<@NonNull TagResolver> asyncReplacement
1011    ) {
1012        return sendMessage(caption, new CompletableFuture[]{asyncReplacement});
1013    }
1014
1015    /**
1016     * Sends a message to the command caller, when all futures are resolved
1017     *
1018     * @param caption           Caption to send
1019     * @param asyncReplacements Async variable replacements
1020     * @param replacements      Sync variable replacements
1021     * @return A Future to be resolved, after the message was sent
1022     * @since 7.1.0
1023     */
1024    public final CompletableFuture<Void> sendMessage(
1025            @NonNull Caption caption,
1026            CompletableFuture<@NonNull TagResolver>[] asyncReplacements,
1027            @NonNull TagResolver... replacements
1028    ) {
1029        return CompletableFuture.allOf(asyncReplacements).whenComplete((unused, throwable) -> {
1030            Set<TagResolver> resolvers = new HashSet<>(Arrays.asList(replacements));
1031            if (throwable != null) {
1032                sendMessage(
1033                        TranslatableCaption.of("errors.error"),
1034                        TagResolver.resolver(
1035                                "value", Tag.inserting(
1036                                        Component.text("Failed to resolve asynchronous caption replacements")
1037                                )
1038                        )
1039                );
1040                LOGGER.error("Failed to resolve asynchronous tagresolver(s) for " + caption, throwable);
1041            } else {
1042                for (final CompletableFuture<TagResolver> asyncReplacement : asyncReplacements) {
1043                    resolvers.add(asyncReplacement.join());
1044                }
1045            }
1046            sendMessage(caption, resolvers.toArray(TagResolver[]::new));
1047        });
1048    }
1049
1050    // Redefine from PermissionHolder as it's required from CommandCaller
1051    @Override
1052    public boolean hasPermission(@NonNull String permission) {
1053        return hasPermission(null, permission);
1054    }
1055
1056    boolean hasPersistentMeta(String key) {
1057        return this.metaMap.containsKey(key);
1058    }
1059
1060    /**
1061     * Check if the player is able to see the other player.
1062     * This does not mean that the other player is in line of sight of the player,
1063     * but rather that the player is permitted to see the other player.
1064     *
1065     * @param other Other player
1066     * @return {@code true} if the player is able to see the other player, {@code false} if not
1067     */
1068    public abstract boolean canSee(PlotPlayer<?> other);
1069
1070    public abstract void stopSpectating();
1071
1072    public boolean hasDebugMode() {
1073        return this.getAttribute("debug");
1074    }
1075
1076    @NonNull
1077    @Override
1078    public Locale getLocale() {
1079        if (this.locale == null) {
1080            this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE);
1081        }
1082        return this.locale;
1083    }
1084
1085    @Override
1086    public void setLocale(final @NonNull Locale locale) {
1087        if (!PlotSquared.get().getCaptionMap(TranslatableCaption.DEFAULT_NAMESPACE).supportsLocale(locale)) {
1088            this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE);
1089        } else {
1090            this.locale = locale;
1091        }
1092    }
1093
1094    @Override
1095    public int hashCode() {
1096        if (this.hash == 0 || this.hash == 485) {
1097            this.hash = 485 + this.getUUID().hashCode();
1098        }
1099        return this.hash;
1100    }
1101
1102    @Override
1103    public boolean equals(final Object obj) {
1104        if (!(obj instanceof final PlotPlayer<?> other)) {
1105            return false;
1106        }
1107        return this.getUUID().equals(other.getUUID());
1108    }
1109
1110    /**
1111     * Get the {@link Audience} that represents this plot player
1112     *
1113     * @return Player audience
1114     */
1115    public @NonNull
1116    abstract Audience getAudience();
1117
1118    /**
1119     * Get this player's {@link LockRepository}
1120     *
1121     * @return Lock repository instance
1122     */
1123    public @NonNull LockRepository getLockRepository() {
1124        return this.lockRepository;
1125    }
1126
1127    /**
1128     * Removes any effects present of the given type.
1129     *
1130     * @param name the name of the type to remove
1131     * @since 6.10.0
1132     */
1133    public abstract void removeEffect(@NonNull String name);
1134
1135    @FunctionalInterface
1136    public interface PlotPlayerConverter<BaseObject> {
1137
1138        PlotPlayer<?> convert(BaseObject object);
1139
1140    }
1141
1142}