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