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