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