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