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 += area.getPlotsAbs(uuid).stream().filter(plot -> !DoneFlag.isDone(plot)).count();
395            } else {
396                count += area.getPlotsAbs(uuid).size();
397            }
398        }
399        return count;
400    }
401
402    public int getClusterCount(String world) {
403        int count = 0;
404        for (PlotArea area : this.plotAreaManager.getPlotAreasSet(world)) {
405            for (PlotCluster cluster : area.getClusters()) {
406                if (cluster.isOwner(getUUID())) {
407                    count++;
408                }
409            }
410        }
411        return count;
412    }
413
414    /**
415     * Get a {@link Set} of plots owned by this player.
416     *
417     * <p>
418     * Take a look at {@link PlotSquared} for more searching functions.
419     * See {@link #getPlotCount()} for the number of plots.
420     * </p>
421     *
422     * @return a {@link Set} of plots owned by the player
423     */
424    public Set<Plot> getPlots() {
425        return PlotQuery.newQuery().ownedBy(this).asSet();
426    }
427
428    /**
429     * Return the PlotArea this player is currently in, or null.
430     *
431     * @return Plot area the player is currently in, or {@code null}
432     */
433    public @Nullable PlotArea getPlotAreaAbs() {
434        return this.plotAreaManager.getPlotArea(getLocation());
435    }
436
437    public PlotArea getApplicablePlotArea() {
438        Plot plot = getCurrentPlot();
439        if (plot == null) {
440            return this.plotAreaManager.getApplicablePlotArea(getLocation());
441        }
442        return plot.getArea();
443    }
444
445    @Override
446    public @NonNull RequiredType getSuperCaller() {
447        return RequiredType.PLAYER;
448    }
449
450    /**
451     * Get this player's last recorded location or null if they don't any plot relevant location.
452     *
453     * @return The location
454     */
455    public @NonNull Location getLocation() {
456        Location location = getMeta("location");
457        if (location != null) {
458            return location;
459        }
460        return getLocationFull();
461    }
462
463    /////////////// PLAYER META ///////////////
464
465    ////////////// PARTIALLY IMPLEMENTED ///////////
466
467    /**
468     * Get this player's full location (including yaw/pitch)
469     *
470     * @return location
471     */
472    public abstract Location getLocationFull();
473
474    ////////////////////////////////////////////////
475
476    /**
477     * Get this player's UUID.
478     * <p>=== !IMPORTANT ===</p>
479     * The UUID is dependent on the mode chosen in the settings.yml and may not be the same as Bukkit has
480     * (especially if using an old version of Bukkit that does not support UUIDs)
481     *
482     * @return UUID
483     */
484    @Override
485    public @NonNull
486    abstract UUID getUUID();
487
488    public boolean canTeleport(final @NonNull Location location) {
489        Preconditions.checkNotNull(location, "Specified location cannot be null");
490        final Location current = getLocationFull();
491        teleport(location);
492        boolean result = getLocation().equals(location);
493        teleport(current);
494        return result;
495    }
496
497    /**
498     * Teleport this player to a location.
499     *
500     * @param location the target location
501     */
502    public void teleport(Location location) {
503        teleport(location, TeleportCause.PLUGIN);
504    }
505
506    /**
507     * Teleport this player to a location.
508     *
509     * @param location the target location
510     * @param cause    the cause of the teleport
511     */
512    public abstract void teleport(Location location, TeleportCause cause);
513
514    /**
515     * Kick this player to a location
516     *
517     * @param location the target location
518     */
519    public void plotkick(Location location) {
520        setMeta("kick", true);
521        teleport(location, TeleportCause.KICK);
522        deleteMeta("kick");
523    }
524
525    /**
526     * Set this compass target.
527     *
528     * @param location the target location
529     */
530    public abstract void setCompassTarget(Location location);
531
532    /**
533     * Set player data that will persist restarts.
534     * - Please note that this is not intended to store large values
535     * - For session only data use meta
536     *
537     * @param key metadata key
538     */
539    public void setAttribute(String key) {
540        setPersistentMeta("attrib_" + key, new byte[]{(byte) 1});
541    }
542
543    /**
544     * Retrieves the attribute of this player.
545     *
546     * @param key metadata key
547     * @return the attribute will be either {@code true} or {@code false}
548     */
549    public boolean getAttribute(String key) {
550        if (!hasPersistentMeta("attrib_" + key)) {
551            return false;
552        }
553        return getPersistentMeta("attrib_" + key)[0] == 1;
554    }
555
556    /**
557     * Remove an attribute from a player.
558     *
559     * @param key metadata key
560     */
561    public void removeAttribute(String key) {
562        removePersistentMeta("attrib_" + key);
563    }
564
565    /**
566     * Sets the local weather for this Player.
567     *
568     * @param weather the weather visible to the player
569     */
570    public abstract void setWeather(@NonNull PlotWeather weather);
571
572    /**
573     * Get this player's gamemode.
574     *
575     * @return the gamemode of the player.
576     */
577    public abstract @NonNull GameMode getGameMode();
578
579    /**
580     * Set this player's gameMode.
581     *
582     * @param gameMode the gamemode to set
583     */
584    public abstract void setGameMode(@NonNull GameMode gameMode);
585
586    /**
587     * Set this player's local time (ticks).
588     *
589     * @param time the time visible to the player
590     */
591    public abstract void setTime(long time);
592
593    /**
594     * Determines whether or not the player can fly.
595     *
596     * @return {@code true} if the player is allowed to fly
597     */
598    public abstract boolean getFlight();
599
600    /**
601     * Sets whether or not this player can fly.
602     *
603     * @param fly {@code true} if the player can fly, otherwise {@code false}
604     */
605    public abstract void setFlight(boolean fly);
606
607    /**
608     * Play music at a location for this player.
609     *
610     * @param location where to play the music
611     * @param id       the record item id
612     */
613    public abstract void playMusic(@NonNull Location location, @NonNull ItemType id);
614
615    /**
616     * Check if this player is banned.
617     *
618     * @return {@code true} if the player is banned, {@code false} otherwise.
619     */
620    public abstract boolean isBanned();
621
622    /**
623     * Kick this player from the game.
624     *
625     * @param message the reason for the kick
626     */
627    public abstract void kick(String message);
628
629    public void refreshDebug() {
630        final boolean debug = this.getAttribute("debug");
631        if (debug && !debugModeEnabled.contains(this)) {
632            debugModeEnabled.add(this);
633        } else if (!debug) {
634            debugModeEnabled.remove(this);
635        }
636    }
637
638    /**
639     * Called when this player quits.
640     */
641    public void unregister() {
642        Plot plot = getCurrentPlot();
643        if (plot != null && Settings.Enabled_Components.PERSISTENT_META && plot
644                .getArea() instanceof SinglePlotArea) {
645            PlotId id = plot.getId();
646            int x = id.getX();
647            int z = id.getY();
648            ByteBuffer buffer = ByteBuffer.allocate(14);
649            buffer.putShort((short) x);
650            buffer.putShort((short) z);
651            Location location = getLocation();
652            buffer.putInt(location.getX());
653            buffer.putShort((short) location.getY());
654            buffer.putInt(location.getZ());
655            setPersistentMeta("quitLocV2", buffer.array());
656        } else if (hasPersistentMeta("quitLocV2")) {
657            removePersistentMeta("quitLocV2");
658        }
659        if (plot != null) {
660            this.eventDispatcher.callLeave(this, plot);
661        }
662        if (Settings.Enabled_Components.BAN_DELETER && isBanned()) {
663            for (Plot owned : getPlots()) {
664                owned.getPlotModificationManager().deletePlot(null, null);
665                LOGGER.info("Plot {} was deleted + cleared due to {} getting banned", owned.getId(), getName());
666            }
667        }
668        if (PlotSquared.platform().expireManager() != null) {
669            PlotSquared.platform().expireManager().storeDate(getUUID(), System.currentTimeMillis());
670        }
671        PlotSquared.platform().playerManager().removePlayer(this);
672        PlotSquared.platform().unregister(this);
673
674        debugModeEnabled.remove(this);
675    }
676
677    /**
678     * Get the amount of clusters this player owns in the specific world.
679     *
680     * @param world world
681     * @return number of clusters owned
682     */
683    public int getPlayerClusterCount(String world) {
684        return PlotSquared.get().getClusters(world).stream()
685                .filter(cluster -> getUUID().equals(cluster.owner)).mapToInt(PlotCluster::getArea)
686                .sum();
687    }
688
689    /**
690     * Get the amount of clusters this player owns.
691     *
692     * @return the number of clusters this player owns
693     */
694    public int getPlayerClusterCount() {
695        final AtomicInteger count = new AtomicInteger();
696        this.plotAreaManager.forEachPlotArea(value -> count.addAndGet(value.getClusters().size()));
697        return count.get();
698    }
699
700    /**
701     * Return a {@code Set} of all plots this player owns in a certain world.
702     *
703     * @param world the world to retrieve plots from
704     * @return a {@code Set} of plots this player owns in the provided world
705     */
706    public Set<Plot> getPlots(String world) {
707        return PlotQuery.newQuery().inWorld(world).ownedBy(getUUID()).asSet();
708    }
709
710    public void populatePersistentMetaMap() {
711        if (Settings.Enabled_Components.PERSISTENT_META) {
712            DBFunc.getPersistentMeta(
713                    getUUID(), new RunnableVal<>() {
714                        @Override
715                        public void run(Map<String, byte[]> value) {
716                            try {
717                                PlotPlayer.this.metaMap = value;
718                                if (value.isEmpty()) {
719                                    return;
720                                }
721
722                                if (PlotPlayer.this.getAttribute("debug")) {
723                                    debugModeEnabled.add(PlotPlayer.this);
724                                }
725
726                                if (!Settings.Teleport.ON_LOGIN) {
727                                    return;
728                                }
729                                PlotAreaManager manager = PlotPlayer.this.plotAreaManager;
730
731                                if (!(manager instanceof SinglePlotAreaManager)) {
732                                    return;
733                                }
734                                PlotArea area = ((SinglePlotAreaManager) manager).getArea();
735                                boolean V2 = false;
736                                byte[] arr = PlotPlayer.this.getPersistentMeta("quitLoc");
737                                if (arr == null) {
738                                    arr = PlotPlayer.this.getPersistentMeta("quitLocV2");
739                                    if (arr == null) {
740                                        return;
741                                    }
742                                    V2 = true;
743                                    removePersistentMeta("quitLocV2");
744                                } else {
745                                    removePersistentMeta("quitLoc");
746                                }
747
748                                if (!getMeta("teleportOnLogin", true)) {
749                                    return;
750                                }
751                                ByteBuffer quitWorld = ByteBuffer.wrap(arr);
752                                final int plotX = quitWorld.getShort();
753                                final int plotZ = quitWorld.getShort();
754                                PlotId id = PlotId.of(plotX, plotZ);
755                                int x = quitWorld.getInt();
756                                int y = V2 ? quitWorld.getShort() : (quitWorld.get() & 0xFF);
757                                int z = quitWorld.getInt();
758                                Plot plot = area.getOwnedPlot(id);
759
760                                if (plot == null) {
761                                    return;
762                                }
763
764                                final Location location = Location.at(plot.getWorldName(), x, y, z);
765                                if (plot.isLoaded()) {
766                                    TaskManager.runTask(() -> {
767                                        if (getMeta("teleportOnLogin", true)) {
768                                            teleport(location, TeleportCause.LOGIN);
769                                            sendMessage(
770                                                    TranslatableCaption.of("teleport.teleported_to_plot"));
771                                        }
772                                    });
773                                } else if (!PlotSquared.get().isMainThread(Thread.currentThread())) {
774                                    if (getMeta("teleportOnLogin", true)) {
775                                        plot.teleportPlayer(
776                                                PlotPlayer.this,
777                                                result -> TaskManager.runTask(() -> {
778                                                    if (getMeta("teleportOnLogin", true)) {
779                                                        if (plot.isLoaded()) {
780                                                            teleport(location, TeleportCause.LOGIN);
781                                                            sendMessage(TranslatableCaption
782                                                                    .of("teleport.teleported_to_plot"));
783                                                        }
784                                                    }
785                                                })
786                                        );
787                                    }
788                                }
789                            } catch (Throwable e) {
790                                LOGGER.error("Error populating persistent meta for player {}", PlotPlayer.this.getName(), e);
791                            }
792                        }
793                    }
794            );
795        }
796    }
797
798    byte[] getPersistentMeta(String key) {
799        return this.metaMap.get(key);
800    }
801
802    Object removePersistentMeta(String key) {
803        final Object old = this.metaMap.remove(key);
804        if (Settings.Enabled_Components.PERSISTENT_META) {
805            DBFunc.removePersistentMeta(getUUID(), key);
806        }
807        return old;
808    }
809
810    /**
811     * Access keyed persistent meta data for this player. This returns a meta data
812     * access instance, that MUST be closed. It is meant to be used with try-with-resources,
813     * like such:
814     * <pre>{@code
815     * try (final MetaDataAccess<Integer> access = player.accessPersistentMetaData(PlayerMetaKeys.GRANTS)) {
816     *     int grants = access.get();
817     *     access.set(grants + 1);
818     * }
819     * }</pre>
820     *
821     * @param key Meta data key
822     * @param <T> Meta data type
823     * @return Meta data access. MUST be closed after being used
824     */
825    public @NonNull <T> MetaDataAccess<T> accessPersistentMetaData(final @NonNull MetaDataKey<T> key) {
826        return new PersistentMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey()));
827    }
828
829    /**
830     * Access keyed temporary meta data for this player. This returns a meta data
831     * access instance, that MUST be closed. It is meant to be used with try-with-resources,
832     * like such:
833     * <pre>{@code
834     * try (final MetaDataAccess<Integer> access = player.accessTemporaryMetaData(PlayerMetaKeys.GRANTS)) {
835     *     int grants = access.get();
836     *     access.set(grants + 1);
837     * }
838     * }</pre>
839     *
840     * @param key Meta data key
841     * @param <T> Meta data type
842     * @return Meta data access. MUST be closed after being used
843     */
844    public @NonNull <T> MetaDataAccess<T> accessTemporaryMetaData(final @NonNull MetaDataKey<T> key) {
845        return new TemporaryMetaDataAccess<>(this, key, this.lockRepository.lock(key.getLockKey()));
846    }
847
848    <T> void setPersistentMeta(
849            final @NonNull MetaDataKey<T> key,
850            final @NonNull T value
851    ) {
852        if (key.getType().getRawType().equals(Integer.class)) {
853            this.setPersistentMeta(key.toString(), Ints.toByteArray((int) (Object) value));
854        } else if (key.getType().getRawType().equals(Boolean.class)) {
855            this.setPersistentMeta(key.toString(), ByteArrayUtilities.booleanToBytes((boolean) (Object) value));
856        } else {
857            throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType()));
858        }
859    }
860
861    @SuppressWarnings("unchecked")
862    @Nullable
863    <T> T getPersistentMeta(final @NonNull MetaDataKey<T> key) {
864        final byte[] value = this.getPersistentMeta(key.toString());
865        if (value == null) {
866            return null;
867        }
868        final Object returnValue;
869        if (key.getType().getRawType().equals(Integer.class)) {
870            returnValue = Ints.fromByteArray(value);
871        } else if (key.getType().getRawType().equals(Boolean.class)) {
872            returnValue = ByteArrayUtilities.bytesToBoolean(value);
873        } else {
874            throw new IllegalArgumentException(String.format("Unknown meta data type '%s'", key.getType()));
875        }
876        return (T) returnValue;
877    }
878
879    void setPersistentMeta(String key, byte[] value) {
880        boolean delete = hasPersistentMeta(key);
881        this.metaMap.put(key, value);
882        if (Settings.Enabled_Components.PERSISTENT_META) {
883            DBFunc.addPersistentMeta(getUUID(), key, value, delete);
884        }
885    }
886
887    /**
888     * Send a title to the player that fades in, in 10 ticks, stays for 50 ticks and fades
889     * out in 20 ticks
890     *
891     * @param title        Title text
892     * @param subtitle     Subtitle text
893     * @param replacements Variable replacements
894     */
895    public void sendTitle(
896            final @NonNull Caption title, final @NonNull Caption subtitle,
897            final @NonNull TagResolver... replacements
898    ) {
899        sendTitle(
900                title,
901                subtitle,
902                Settings.Titles.TITLES_FADE_IN,
903                Settings.Titles.TITLES_STAY,
904                Settings.Titles.TITLES_FADE_OUT,
905                replacements
906        );
907    }
908
909    /**
910     * Send a title to the player
911     *
912     * @param title        Title
913     * @param subtitle     Subtitle
914     * @param fadeIn       Fade in time (in ticks)
915     * @param stay         The title stays for (in ticks)
916     * @param fadeOut      Fade out time (in ticks)
917     * @param replacements Variable replacements
918     */
919    public void sendTitle(
920            final @NonNull Caption title, final @NonNull Caption subtitle,
921            final int fadeIn, final int stay, final int fadeOut,
922            final @NonNull TagResolver... replacements
923    ) {
924        final Component titleComponent = MiniMessage.miniMessage().deserialize(title.getComponent(this), replacements);
925        final Component subtitleComponent =
926                MiniMessage.miniMessage().deserialize(subtitle.getComponent(this), replacements);
927        final Title.Times times = Title.Times.times(
928                Duration.of(Settings.Titles.TITLES_FADE_IN * 50L, ChronoUnit.MILLIS),
929                Duration.of(Settings.Titles.TITLES_STAY * 50L, ChronoUnit.MILLIS),
930                Duration.of(Settings.Titles.TITLES_FADE_OUT * 50L, ChronoUnit.MILLIS)
931        );
932        getAudience().showTitle(Title
933                .title(titleComponent, subtitleComponent, times));
934    }
935
936    /**
937     * Method designed to send an ActionBar to a player.
938     *
939     * @param caption      Caption
940     * @param replacements Variable replacements
941     */
942    public void sendActionBar(
943            final @NonNull Caption caption,
944            final @NonNull TagResolver... replacements
945    ) {
946        String message;
947        try {
948            message = caption.getComponent(this);
949        } catch (final CaptionMap.NoSuchCaptionException exception) {
950            // This sends feedback to the player
951            message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey();
952            // And this also prints it to the console
953            exception.printStackTrace();
954        }
955        if (message.isEmpty()) {
956            return;
957        }
958        // Replace placeholders, etc
959        message = CaptionUtility.format(this, message)
960                .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&')
961                .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this));
962
963
964        final Component component = MiniMessage.miniMessage().deserialize(message, replacements);
965        getAudience().sendActionBar(component);
966    }
967
968    @Override
969    public void sendMessage(
970            final @NonNull Caption caption,
971            final @NonNull TagResolver... replacements
972    ) {
973        String message;
974        try {
975            message = caption.getComponent(this);
976        } catch (final CaptionMap.NoSuchCaptionException exception) {
977            // This sends feedback to the player
978            message = NON_EXISTENT_CAPTION + ((TranslatableCaption) caption).getKey();
979            // And this also prints it to the console
980            exception.printStackTrace();
981        }
982        if (message.isEmpty()) {
983            return;
984        }
985        // Replace placeholders, etc
986        message = CaptionUtility.format(this, message)
987                .replace('\u2010', '%').replace('\u2020', '&').replace('\u2030', '&')
988                .replace("<prefix>", TranslatableCaption.of("core.prefix").getComponent(this));
989        // Parse the message
990        final Component component = MiniMessage.miniMessage().deserialize(message, replacements);
991        if (!Objects.equal(component, this.getMeta("lastMessage"))
992                || System.currentTimeMillis() - this.<Long>getMeta("lastMessageTime") > 5000) {
993            setMeta("lastMessage", component);
994            setMeta("lastMessageTime", System.currentTimeMillis());
995            getAudience().sendMessage(component);
996        }
997    }
998
999    /**
1000     * Sends a message to the command caller, when the future is resolved
1001     *
1002     * @param caption          Caption to send
1003     * @param asyncReplacement Async variable replacement
1004     * @return A Future to be resolved, after the message was sent
1005     * @since 7.1.0
1006     */
1007    public final CompletableFuture<Void> sendMessage(
1008            @NonNull Caption caption,
1009            CompletableFuture<@NonNull TagResolver> asyncReplacement
1010    ) {
1011        return sendMessage(caption, new CompletableFuture[]{asyncReplacement});
1012    }
1013
1014    /**
1015     * Sends a message to the command caller, when all futures are resolved
1016     *
1017     * @param caption           Caption to send
1018     * @param asyncReplacements Async variable replacements
1019     * @param replacements      Sync variable replacements
1020     * @return A Future to be resolved, after the message was sent
1021     * @since 7.1.0
1022     */
1023    public final CompletableFuture<Void> sendMessage(
1024            @NonNull Caption caption,
1025            CompletableFuture<@NonNull TagResolver>[] asyncReplacements,
1026            @NonNull TagResolver... replacements
1027    ) {
1028        return CompletableFuture.allOf(asyncReplacements).whenComplete((unused, throwable) -> {
1029            Set<TagResolver> resolvers = new HashSet<>(Arrays.asList(replacements));
1030            if (throwable != null) {
1031                sendMessage(
1032                        TranslatableCaption.of("errors.error"),
1033                        TagResolver.resolver(
1034                                "value", Tag.inserting(
1035                                        Component.text("Failed to resolve asynchronous caption replacements")
1036                                )
1037                        )
1038                );
1039                LOGGER.error("Failed to resolve asynchronous tagresolver(s) for " + caption, throwable);
1040            } else {
1041                for (final CompletableFuture<TagResolver> asyncReplacement : asyncReplacements) {
1042                    resolvers.add(asyncReplacement.join());
1043                }
1044            }
1045            sendMessage(caption, resolvers.toArray(TagResolver[]::new));
1046        });
1047    }
1048
1049    // Redefine from PermissionHolder as it's required from CommandCaller
1050    @Override
1051    public boolean hasPermission(@NonNull String permission) {
1052        return hasPermission(null, permission);
1053    }
1054
1055    boolean hasPersistentMeta(String key) {
1056        return this.metaMap.containsKey(key);
1057    }
1058
1059    /**
1060     * Check if the player is able to see the other player.
1061     * This does not mean that the other player is in line of sight of the player,
1062     * but rather that the player is permitted to see the other player.
1063     *
1064     * @param other Other player
1065     * @return {@code true} if the player is able to see the other player, {@code false} if not
1066     */
1067    public abstract boolean canSee(PlotPlayer<?> other);
1068
1069    public abstract void stopSpectating();
1070
1071    public boolean hasDebugMode() {
1072        return this.getAttribute("debug");
1073    }
1074
1075    @NonNull
1076    @Override
1077    public Locale getLocale() {
1078        if (this.locale == null) {
1079            this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE);
1080        }
1081        return this.locale;
1082    }
1083
1084    @Override
1085    public void setLocale(final @NonNull Locale locale) {
1086        if (!PlotSquared.get().getCaptionMap(TranslatableCaption.DEFAULT_NAMESPACE).supportsLocale(locale)) {
1087            this.locale = Locale.forLanguageTag(Settings.Enabled_Components.DEFAULT_LOCALE);
1088        } else {
1089            this.locale = locale;
1090        }
1091    }
1092
1093    @Override
1094    public int hashCode() {
1095        if (this.hash == 0 || this.hash == 485) {
1096            this.hash = 485 + this.getUUID().hashCode();
1097        }
1098        return this.hash;
1099    }
1100
1101    @Override
1102    public boolean equals(final Object obj) {
1103        if (!(obj instanceof final PlotPlayer<?> other)) {
1104            return false;
1105        }
1106        return this.getUUID().equals(other.getUUID());
1107    }
1108
1109    /**
1110     * Get the {@link Audience} that represents this plot player
1111     *
1112     * @return Player audience
1113     */
1114    public @NonNull
1115    abstract Audience getAudience();
1116
1117    /**
1118     * Get this player's {@link LockRepository}
1119     *
1120     * @return Lock repository instance
1121     */
1122    public @NonNull LockRepository getLockRepository() {
1123        return this.lockRepository;
1124    }
1125
1126    /**
1127     * Removes any effects present of the given type.
1128     *
1129     * @param name the name of the type to remove
1130     * @since 6.10.0
1131     */
1132    public abstract void removeEffect(@NonNull String name);
1133
1134    @FunctionalInterface
1135    public interface PlotPlayerConverter<BaseObject> {
1136
1137        PlotPlayer<?> convert(BaseObject object);
1138
1139    }
1140
1141}