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