diff --git a/src/main/java/world/bentobox/bentobox/api/user/User.java b/src/main/java/world/bentobox/bentobox/api/user/User.java index 70bd4781b..4d2c90d10 100644 --- a/src/main/java/world/bentobox/bentobox/api/user/User.java +++ b/src/main/java/world/bentobox/bentobox/api/user/User.java @@ -42,748 +42,801 @@ import world.bentobox.bentobox.database.objects.Players; import world.bentobox.bentobox.util.Util; /** - * Combines {@link Player}, {@link OfflinePlayer} and {@link CommandSender} to provide convenience methods related to - * localization and generic interactions. + * Combines {@link Player}, {@link OfflinePlayer} and {@link CommandSender} to + * provide convenience methods related to localization and generic interactions. *
- * Therefore, a User could usually be a Player, an OfflinePlayer or the server's console. - * Preliminary checks should be performed before trying to run methods that relies on a specific implementation. - *

- * It is good practice to use the User instance whenever possible instead of Player or CommandSender. + * Therefore, a User could usually be a Player, an OfflinePlayer or the server's + * console. Preliminary checks should be performed before trying to run methods + * that relies on a specific implementation.
+ *
+ * It is good practice to use the User instance whenever possible instead of + * Player or CommandSender. * * @author tastybento */ public class User implements MetaDataAble { - private static final Map users = new HashMap<>(); - - // Used for particle validation - private static final Map> VALIDATION_CHECK; - static { - Map> v = new EnumMap<>(Particle.class); - v.put(Particle.REDSTONE, Particle.DustOptions.class); - v.put(Particle.ITEM_CRACK, ItemStack.class); - v.put(Particle.BLOCK_CRACK, BlockData.class); - v.put(Particle.BLOCK_DUST, BlockData.class); - v.put(Particle.FALLING_DUST, BlockData.class); - v.put(Particle.BLOCK_MARKER, BlockData.class); - v.put(Particle.DUST_COLOR_TRANSITION, DustTransition.class); - v.put(Particle.VIBRATION, Vibration.class); - v.put(Particle.SCULK_CHARGE, Float.class); - v.put(Particle.SHRIEK, Integer.class); - v.put(Particle.LEGACY_BLOCK_CRACK, BlockData.class); - v.put(Particle.LEGACY_BLOCK_DUST, BlockData.class); - v.put(Particle.LEGACY_FALLING_DUST, BlockData.class); - VALIDATION_CHECK = Collections.unmodifiableMap(v); - } - - /** - * Clears all users from the user list - */ - public static void clearUsers() { - users.clear(); - } - - /** - * Gets an instance of User from a CommandSender - * @param sender - command sender, e.g. console - * @return user - user - */ - @NonNull - public static User getInstance(@NonNull CommandSender sender) { - if (sender instanceof Player p) { - return getInstance(p); - } - // Console - return new User(sender); - } - - /** - * Gets an instance of User from a Player object. - * @param player - the player - * @return user - user - */ - @NonNull - public static User getInstance(@NonNull Player player) { - if (users.containsKey(player.getUniqueId())) { - return users.get(player.getUniqueId()); - } - return new User(player); - } - - /** - * Gets an instance of User from a UUID. This will always return a user object. - * If the player is offline then the getPlayer value will be null. - * @param uuid - UUID - * @return user - user - */ - @NonNull - public static User getInstance(@NonNull UUID uuid) { - if (users.containsKey(uuid)) { - return users.get(uuid); - } - // Return a user instance - return new User(uuid); - } - - /** - * Gets an instance of User from an OfflinePlayer - * @param offlinePlayer offline Player - * @return user - * @since 1.3.0 - */ - @NonNull - public static User getInstance(@NonNull OfflinePlayer offlinePlayer) { - if (users.containsKey(offlinePlayer.getUniqueId())) { - return users.get(offlinePlayer.getUniqueId()); - } - return new User(offlinePlayer); - } - - /** - * Removes this player from the User cache and player manager cache - * @param player the player - */ - public static void removePlayer(Player player) { - if (player != null) { - users.remove(player.getUniqueId()); - BentoBox.getInstance().getPlayers().removePlayer(player); - } - } - - // ---------------------------------------------------- - - private static BentoBox plugin = BentoBox.getInstance(); - - @Nullable - private final Player player; - private OfflinePlayer offlinePlayer; - private final UUID playerUUID; - @Nullable - private final CommandSender sender; - - private Addon addon; - - private User(@Nullable CommandSender sender) { - player = null; - playerUUID = null; - this.sender = sender; - } - - private User(@NonNull Player player) { - this.player = player; - offlinePlayer = player; - sender = player; - playerUUID = player.getUniqueId(); - users.put(playerUUID, this); - } - - private User(@NonNull OfflinePlayer offlinePlayer) { - this.player = offlinePlayer.isOnline() ? offlinePlayer.getPlayer() : null; - this.playerUUID = offlinePlayer.getUniqueId(); - this.sender = offlinePlayer.isOnline() ? offlinePlayer.getPlayer() : null; - this.offlinePlayer = offlinePlayer; - } - - private User(UUID playerUUID) { - player = Bukkit.getPlayer(playerUUID); - this.playerUUID = playerUUID; - sender = player; - offlinePlayer = Bukkit.getOfflinePlayer(playerUUID); - } - - /** - * Used for testing - * @param p - plugin - */ - public static void setPlugin(BentoBox p) { - plugin = p; - } - - public Set getEffectivePermissions() { - return sender.getEffectivePermissions(); - } - - /** - * Get the user's inventory - * @return player's inventory - */ - @NonNull - public PlayerInventory getInventory() { - return Objects.requireNonNull(player, "getInventory can only be called for online players!").getInventory(); - } - - /** - * Get the user's location - * @return location - */ - @NonNull - public Location getLocation() { - return Objects.requireNonNull(player, "getLocation can only be called for online players!").getLocation(); - } - - /** - * Get the user's name - * @return player's name - */ - @NonNull - public String getName() { - return player != null ? player.getName() : plugin.getPlayers().getName(playerUUID); - } - - /** - * Get the user's display name - * @return player's display name if the player is online otherwise just their name - * @since 1.22.1 - */ - @NonNull - public String getDisplayName() { - return player != null ? player.getDisplayName() : plugin.getPlayers().getName(playerUUID); - } - - /** - * Check if the User is a player before calling this method. {@link #isPlayer()} - * @return the player - */ - @NonNull - public Player getPlayer() { - return Objects.requireNonNull(player, "User is not a player!"); - } - - /** - * @return true if this user is a player, false if not, e.g., console - */ - public boolean isPlayer() { - return player != null; - } - - /** - * Use {@link #isOfflinePlayer()} before calling this method - * @return the offline player - * @since 1.3.0 - */ - @NonNull - public OfflinePlayer getOfflinePlayer() { - return Objects.requireNonNull(offlinePlayer, "User is not an OfflinePlayer!"); - } - - /** - * @return true if this user is an OfflinePlayer, false if not, e.g., console - * @since 1.3.0 - */ - public boolean isOfflinePlayer() { - return offlinePlayer != null; - } - - @Nullable - public CommandSender getSender() { - return sender; - } - - public UUID getUniqueId() { - return playerUUID; - } - - /** - * @param permission permission string - * @return true if permission is empty or null or if the player has that permission or if the player is op. - */ - public boolean hasPermission(@Nullable String permission) { - return permission == null || permission.isEmpty() || isOp() || sender.hasPermission(permission); - } - - /** - * Removes permission from user - * @param name - Name of the permission to remove - * @return true if successful - * @since 1.5.0 - */ - public boolean removePerm(String name) { - for (PermissionAttachmentInfo p : player.getEffectivePermissions()) { - if (p.getPermission().equals(name) && p.getAttachment() != null) { - player.removeAttachment(p.getAttachment()); - break; - } - } - player.recalculatePermissions(); - return !player.hasPermission(name); - } - - /** - * Add a permission to user - * @param name - Name of the permission to attach - * @return The PermissionAttachment that was just created - * @since 1.5.0 - */ - public PermissionAttachment addPerm(String name) { - return player.addAttachment(plugin, name, true); - } - - public boolean isOnline() { - return player != null && player.isOnline(); - } - - /** - * Checks if user is Op - * @return true if user is Op - */ - public boolean isOp() { - if (sender != null) { - return sender.isOp(); - } - if (playerUUID != null && offlinePlayer != null) { - return offlinePlayer.isOp(); - } - return false; - } - - /** - * Get the maximum value of a numerical permission setting. - * If a player is given an explicit negative number then this is treated as "unlimited" and returned immediately. - * @param permissionPrefix the start of the perm, e.g., {@code plugin.mypermission} - * @param defaultValue the default value; the result may be higher or lower than this - * @return max value - */ - public int getPermissionValue(String permissionPrefix, int defaultValue) { - // If requester is console, then return the default value - if (!isPlayer()) return defaultValue; - - // If there is a dot at the end of the permissionPrefix, remove it - if (permissionPrefix.endsWith(".")) { - permissionPrefix = permissionPrefix.substring(0, permissionPrefix.length()-1); - } - - final String permPrefix = permissionPrefix + "."; - - List permissions = player.getEffectivePermissions().stream() - .filter(PermissionAttachmentInfo::getValue) // Must be a positive permission, not a negative one - .map(PermissionAttachmentInfo::getPermission) - .filter(permission -> permission.startsWith(permPrefix)) - .toList(); - - if (permissions.isEmpty()) return defaultValue; - - return iteratePerms(permissions, permPrefix, defaultValue); - - } - - private int iteratePerms(List permissions, String permPrefix, int defaultValue) { - int value = 0; - for (String permission : permissions) { - if (permission.contains(permPrefix + "*")) { - // 'Star' permission - return defaultValue; - } else { - String[] spl = permission.split(permPrefix); - if (spl.length > 1) { - if (!NumberUtils.isNumber(spl[1])) { - plugin.logError("Player " + player.getName() + " has permission: '" + permission + "' <-- the last part MUST be a number! Ignoring..."); - } else { - int v = Integer.parseInt(spl[1]); - if (v < 0) { - return v; - } - value = Math.max(value, v); - } - } - } - } - - return value; - } - - /** - * Gets a translation for a specific world - * @param world - world of translation - * @param reference - reference found in a locale file - * @param variables - variables to insert into translated string. Variables go in pairs, for example - * "[name]", "tastybento" - * @return Translated string with colors converted, or the reference if nothing has been found - * @since 1.3.0 - */ - public String getTranslation(World world, String reference, String... variables) { - // Get translation. - String addonPrefix = plugin.getIWM() - .getAddon(world).map(a -> a.getDescription().getName().toLowerCase(Locale.ENGLISH) + ".").orElse(""); - return Util.translateColorCodes(translate(addonPrefix, reference, variables)); - } - - /** - * Gets a translation of this reference for this user with colors converted. Translations may be overridden by Addons - * by using the same reference prefixed by the addon name (from the Addon Description) in lower case. - * @param reference - reference found in a locale file - * @param variables - variables to insert into translated string. Variables go in pairs, for example - * "[name]", "tastybento" - * @return Translated string with colors converted, or the reference if nothing has been found - */ - public String getTranslation(String reference, String... variables) { - // Get addonPrefix - String addonPrefix = addon == null ? "" : addon.getDescription().getName().toLowerCase(Locale.ENGLISH) + "."; - return Util.translateColorCodes(translate(addonPrefix, reference, variables)); - } - - /** - * Gets a translation of this reference for this user without colors translated. Translations may be overridden by Addons - * by using the same reference prefixed by the addon name (from the Addon Description) in lower case. - * @param reference - reference found in a locale file - * @param variables - variables to insert into translated string. Variables go in pairs, for example - * "[name]", "tastybento" - * @return Translated string or the reference if nothing has been found - * @since 1.17.4 - */ - public String getTranslationNoColor(String reference, String... variables) { - // Get addonPrefix - String addonPrefix = addon == null ? "" : addon.getDescription().getName().toLowerCase(Locale.ENGLISH) + "."; - return translate(addonPrefix, reference, variables); - } - - private String translate(String addonPrefix, String reference, String[] variables) { - // Try to get the translation for this specific addon - String translation = plugin.getLocalesManager().get(this, addonPrefix + reference); - - if (translation == null) { - // No luck, try to get the generic translation - translation = plugin.getLocalesManager().get(this, reference); - if (translation == null) { - // Nothing found. Replace vars (probably will do nothing) and return - return replaceVars(reference, variables); - } - } - - // If this is a prefix, just gather and return the translation - if (!reference.startsWith("prefixes.")) { - // Replace the prefixes - return replacePrefixes(translation, variables); - } - return translation; - } - - private String replacePrefixes(String translation, String[] variables) { - for (String prefix : plugin.getLocalesManager().getAvailablePrefixes(this)) { - String prefixTranslation = getTranslation("prefixes." + prefix); - // Replace the [gamemode] text variable - prefixTranslation = prefixTranslation.replace("[gamemode]", addon != null ? addon.getDescription().getName() : "[gamemode]"); - // Replace the [friendly_name] text variable - prefixTranslation = prefixTranslation.replace("[friendly_name]", isPlayer() ? plugin.getIWM().getFriendlyName(getWorld()) : "[friendly_name]"); - - // Replace the prefix in the actual message - translation = translation.replace("[prefix_" + prefix + "]", prefixTranslation); - } - - // Then replace variables - if (variables.length > 1) { - for (int i = 0; i < variables.length; i += 2) { - // Prevent a NPE if the substituting variable is null - if (variables[i + 1] != null) { - translation = translation.replace(variables[i], variables[i + 1]); - } - } - } - - // Then replace Placeholders, this will only work if this is a player - if (player != null) { - translation = plugin.getPlaceholdersManager().replacePlaceholders(player, translation); - } - return translation; - } - - private String replaceVars(String reference, String[] variables) { - - // Then replace variables - if (variables.length > 1) { - for (int i = 0; i < variables.length; i += 2) { - reference = reference.replace(variables[i], variables[i + 1]); - } - } - - // Then replace Placeholders, this will only work if this is a player - if (player != null) { - reference = plugin.getPlaceholdersManager().replacePlaceholders(player, reference); - } - - // If no translation has been found, return the reference for debug purposes. - return reference; - } - - /** - * Gets a translation of this reference for this user. - * @param reference - reference found in a locale file - * @param variables - variables to insert into translated string. Variables go in pairs, for example - * "[name]", "tastybento" - * @return Translated string with colors converted, or a blank String if nothing has been found - */ - public String getTranslationOrNothing(String reference, String... variables) { - String translation = getTranslation(reference, variables); - return translation.equals(reference) ? "" : translation; - } - - /** - * Send a message to sender if message is not empty. - * @param reference - language file reference - * @param variables - CharSequence target, replacement pairs - */ - public void sendMessage(String reference, String... variables) { - String message = getTranslation(reference, variables); - if (!ChatColor.stripColor(message).trim().isEmpty()) { - sendRawMessage(message); - } - } - - /** - * Sends a message to sender without any modification (colors, multi-lines, placeholders). - * @param message - the message to send - */ - public void sendRawMessage(String message) { - if (sender != null) { - sender.sendMessage(message); - } else { - // Offline player fire event - Bukkit.getPluginManager().callEvent(new OfflineMessageEvent(this.playerUUID, message)); - } - } - - /** - * Sends a message to sender if message is not empty and if the same wasn't sent within the previous Notifier.NOTIFICATION_DELAY seconds. - * @param reference - language file reference - * @param variables - CharSequence target, replacement pairs - * - * @see Notifier - */ - public void notify(String reference, String... variables) { - String message = getTranslation(reference, variables); - if (!ChatColor.stripColor(message).trim().isEmpty() && sender != null) { - plugin.getNotifier().notify(this, message); - } - } - - /** - * Sends a message to sender if message is not empty and if the same wasn't sent within the previous Notifier.NOTIFICATION_DELAY seconds. - * @param world - the world the translation should come from - * @param reference - language file reference - * @param variables - CharSequence target, replacement pairs - * - * @see Notifier - * @since 1.3.0 - */ - public void notify(World world, String reference, String... variables) { - String message = getTranslation(world, reference, variables); - if (!ChatColor.stripColor(message).trim().isEmpty() && sender != null) { - plugin.getNotifier().notify(this, message); - } - } - - /** - * Sets the user's game mode - * @param mode - GameMode - */ - public void setGameMode(GameMode mode) { - player.setGameMode(mode); - } - - /** - * Teleports user to this location. If the user is in a vehicle, they will exit first. - * @param location - the location - */ - public void teleport(Location location) { - player.teleport(location); - } - - /** - * Gets the current world this entity resides in - * @return World - world - */ - @NonNull - public World getWorld() { - Objects.requireNonNull(player, "Cannot be called on a non-player User!"); - return Objects.requireNonNull(player.getWorld(), "Player's world cannot be null!"); - } - - /** - * Closes the user's inventory - */ - public void closeInventory() { - player.closeInventory(); - } - - /** - * Get the user's locale - * @return Locale - */ - public Locale getLocale() { - if (sender instanceof Player && !plugin.getPlayers().getLocale(playerUUID).isEmpty()) { - return Locale.forLanguageTag(plugin.getPlayers().getLocale(playerUUID)); - } - return Locale.forLanguageTag(plugin.getSettings().getDefaultLanguage()); - } - - /** - * Forces an update of the user's complete inventory. - * Deprecated, but there is no current alternative. - */ - public void updateInventory() { - player.updateInventory(); - } - - /** - * Performs a command as the player - * @param command - command to execute - * @return true if the command was successful, otherwise false - */ - public boolean performCommand(String command) { - PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(getPlayer(), command); - Bukkit.getPluginManager().callEvent(event); - - // only perform the command, if the event wasn't cancelled by an other plugin: - if (!event.isCancelled()) { - return getPlayer().performCommand(event.getMessage()); - } - // Cancelled, but it was recognized, so return true - return true; - } - - /** - * Checks if a user is in one of the game worlds - * @return true if user is, false if not - */ - public boolean inWorld() { - return plugin.getIWM().inWorld(getLocation()); - } - - /** - * Spawn particles to the player. - * They are only displayed if they are within the server's view distance. - * @param particle Particle to display. - * @param dustOptions Particle.DustOptions for the particle to display. - * Cannot be null when particle is {@link Particle#REDSTONE}. - * @param x X coordinate of the particle to display. - * @param y Y coordinate of the particle to display. - * @param z Z coordinate of the particle to display. - */ - public void spawnParticle(Particle particle, @Nullable Object dustOptions, double x, double y, double z) - { - Class expectedClass = VALIDATION_CHECK.get(particle); - if (expectedClass == null) throw new IllegalArgumentException("Unexpected value: " + particle); - - if (!(expectedClass.isInstance(dustOptions))) { - throw new IllegalArgumentException("A non-null " + expectedClass.getSimpleName() + " must be provided when using Particle." + particle + " as particle."); - } - - // Check if this particle is beyond the viewing distance of the server - if (this.player != null - && this.player.getLocation().toVector().distanceSquared(new Vector(x, y, z)) < - (Bukkit.getServer().getViewDistance() * 256 * Bukkit.getServer().getViewDistance())) - { - if (particle.equals(Particle.REDSTONE)) - { - player.spawnParticle(particle, x, y, z, 1, 0, 0, 0, 1, dustOptions); - } - else if (dustOptions != null) - { - player.spawnParticle(particle, x, y, z, 1, dustOptions); - } - else - { - // This will never be called unless the value in VALIDATION_CHECK is null in the future - player.spawnParticle(particle, x, y, z, 1); - } - } - } - - - /** - * Spawn particles to the player. - * They are only displayed if they are within the server's view distance. - * Compatibility method for older usages. - * @param particle Particle to display. - * @param dustOptions Particle.DustOptions for the particle to display. - * Cannot be null when particle is {@link Particle#REDSTONE}. - * @param x X coordinate of the particle to display. - * @param y Y coordinate of the particle to display. - * @param z Z coordinate of the particle to display. - */ - public void spawnParticle(Particle particle, Particle.DustOptions dustOptions, double x, double y, double z) - { - this.spawnParticle(particle, (Object) dustOptions, x, y, z); - } - - - /** - * Spawn particles to the player. - * They are only displayed if they are within the server's view distance. - * @param particle Particle to display. - * @param dustOptions Particle.DustOptions for the particle to display. - * Cannot be null when particle is {@link Particle#REDSTONE}. - * @param x X coordinate of the particle to display. - * @param y Y coordinate of the particle to display. - * @param z Z coordinate of the particle to display. - */ - public void spawnParticle(Particle particle, Particle.DustOptions dustOptions, int x, int y, int z) - { - this.spawnParticle(particle, dustOptions, (double) x, (double) y, (double) z); - } - - - /* (non-Javadoc) - * @see java.lang.Object#hashCode() - */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((playerUUID == null) ? 0 : playerUUID.hashCode()); - return result; - } - - /* (non-Javadoc) - * @see java.lang.Object#equals(java.lang.Object) - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof User other)) { - return false; - } - if (playerUUID == null) { - return other.playerUUID == null; - } else return playerUUID.equals(other.playerUUID); - } - - /** - * Set the addon context when a command is executed - * @param addon - the addon executing the command - */ - public void setAddon(Addon addon) { - this.addon = addon; - } - - /** - * Get all the meta data for this user - * @return the metaData - * @since 1.15.4 - */ - @Override - public Optional> getMetaData() { - Players p = plugin - .getPlayers() - .getPlayer(playerUUID); - return Objects.requireNonNull(p, "Unknown player for " + playerUUID).getMetaData(); - } - - /** - * @param metaData the metaData to set - * @since 1.15.4 - */ - @Override - public void setMetaData(Map metaData) { - Players p = plugin - .getPlayers() - .getPlayer(playerUUID); - - Objects.requireNonNull(p, "Unknown player for " + playerUUID).setMetaData(metaData); - } + private static final Map users = new HashMap<>(); + + // Used for particle validation + private static final Map> VALIDATION_CHECK; + static { + Map> v = new EnumMap<>(Particle.class); + v.put(Particle.REDSTONE, Particle.DustOptions.class); + v.put(Particle.ITEM_CRACK, ItemStack.class); + v.put(Particle.BLOCK_CRACK, BlockData.class); + v.put(Particle.BLOCK_DUST, BlockData.class); + v.put(Particle.FALLING_DUST, BlockData.class); + v.put(Particle.BLOCK_MARKER, BlockData.class); + v.put(Particle.DUST_COLOR_TRANSITION, DustTransition.class); + v.put(Particle.VIBRATION, Vibration.class); + v.put(Particle.SCULK_CHARGE, Float.class); + v.put(Particle.SHRIEK, Integer.class); + v.put(Particle.LEGACY_BLOCK_CRACK, BlockData.class); + v.put(Particle.LEGACY_BLOCK_DUST, BlockData.class); + v.put(Particle.LEGACY_FALLING_DUST, BlockData.class); + VALIDATION_CHECK = Collections.unmodifiableMap(v); + } + + /** + * Clears all users from the user list + */ + public static void clearUsers() { + users.clear(); + } + + /** + * Gets an instance of User from a CommandSender + * + * @param sender - command sender, e.g. console + * @return user - user + */ + @NonNull + public static User getInstance(@NonNull CommandSender sender) { + if (sender instanceof Player p) { + return getInstance(p); + } + // Console + return new User(sender); + } + + /** + * Gets an instance of User from a Player object. + * + * @param player - the player + * @return user - user + */ + @NonNull + public static User getInstance(@NonNull Player player) { + if (users.containsKey(player.getUniqueId())) { + return users.get(player.getUniqueId()); + } + return new User(player); + } + + /** + * Gets an instance of User from a UUID. This will always return a user object. + * If the player is offline then the getPlayer value will be null. + * + * @param uuid - UUID + * @return user - user + */ + @NonNull + public static User getInstance(@NonNull UUID uuid) { + if (users.containsKey(uuid)) { + return users.get(uuid); + } + // Return a user instance + return new User(uuid); + } + + /** + * Gets an instance of User from an OfflinePlayer + * + * @param offlinePlayer offline Player + * @return user + * @since 1.3.0 + */ + @NonNull + public static User getInstance(@NonNull OfflinePlayer offlinePlayer) { + if (users.containsKey(offlinePlayer.getUniqueId())) { + return users.get(offlinePlayer.getUniqueId()); + } + return new User(offlinePlayer); + } + + /** + * Removes this player from the User cache and player manager cache + * + * @param player the player + */ + public static void removePlayer(Player player) { + if (player != null) { + users.remove(player.getUniqueId()); + BentoBox.getInstance().getPlayers().removePlayer(player); + } + } + + // ---------------------------------------------------- + + private static BentoBox plugin = BentoBox.getInstance(); + + @Nullable + private final Player player; + private OfflinePlayer offlinePlayer; + private final UUID playerUUID; + @Nullable + private final CommandSender sender; + + private Addon addon; + + private User(@Nullable CommandSender sender) { + player = null; + playerUUID = null; + this.sender = sender; + } + + private User(@NonNull Player player) { + this.player = player; + offlinePlayer = player; + sender = player; + playerUUID = player.getUniqueId(); + users.put(playerUUID, this); + } + + private User(@NonNull OfflinePlayer offlinePlayer) { + this.player = offlinePlayer.isOnline() ? offlinePlayer.getPlayer() : null; + this.playerUUID = offlinePlayer.getUniqueId(); + this.sender = offlinePlayer.isOnline() ? offlinePlayer.getPlayer() : null; + this.offlinePlayer = offlinePlayer; + } + + private User(UUID playerUUID) { + player = Bukkit.getPlayer(playerUUID); + this.playerUUID = playerUUID; + sender = player; + offlinePlayer = Bukkit.getOfflinePlayer(playerUUID); + } + + /** + * Used for testing + * + * @param p - plugin + */ + public static void setPlugin(BentoBox p) { + plugin = p; + } + + public Set getEffectivePermissions() { + return sender.getEffectivePermissions(); + } + + /** + * Get the user's inventory + * + * @return player's inventory + */ + @NonNull + public PlayerInventory getInventory() { + return Objects.requireNonNull(player, "getInventory can only be called for online players!").getInventory(); + } + + /** + * Get the user's location + * + * @return location + */ + @NonNull + public Location getLocation() { + return Objects.requireNonNull(player, "getLocation can only be called for online players!").getLocation(); + } + + /** + * Get the user's name + * + * @return player's name + */ + @NonNull + public String getName() { + return player != null ? player.getName() : plugin.getPlayers().getName(playerUUID); + } + + /** + * Get the user's display name + * + * @return player's display name if the player is online otherwise just their + * name + * @since 1.22.1 + */ + @NonNull + public String getDisplayName() { + return player != null ? player.getDisplayName() : plugin.getPlayers().getName(playerUUID); + } + + /** + * Check if the User is a player before calling this method. {@link #isPlayer()} + * + * @return the player + */ + @NonNull + public Player getPlayer() { + return Objects.requireNonNull(player, "User is not a player!"); + } + + /** + * @return true if this user is a player, false if not, e.g., console + */ + public boolean isPlayer() { + return player != null; + } + + /** + * Use {@link #isOfflinePlayer()} before calling this method + * + * @return the offline player + * @since 1.3.0 + */ + @NonNull + public OfflinePlayer getOfflinePlayer() { + return Objects.requireNonNull(offlinePlayer, "User is not an OfflinePlayer!"); + } + + /** + * @return true if this user is an OfflinePlayer, false if not, e.g., console + * @since 1.3.0 + */ + public boolean isOfflinePlayer() { + return offlinePlayer != null; + } + + @Nullable + public CommandSender getSender() { + return sender; + } + + public UUID getUniqueId() { + return playerUUID; + } + + /** + * @param permission permission string + * @return true if permission is empty or null or if the player has that + * permission or if the player is op. + */ + public boolean hasPermission(@Nullable String permission) { + return permission == null || permission.isEmpty() || isOp() || sender.hasPermission(permission); + } + + /** + * Removes permission from user + * + * @param name - Name of the permission to remove + * @return true if successful + * @since 1.5.0 + */ + public boolean removePerm(String name) { + for (PermissionAttachmentInfo p : player.getEffectivePermissions()) { + if (p.getPermission().equals(name) && p.getAttachment() != null) { + player.removeAttachment(p.getAttachment()); + break; + } + } + player.recalculatePermissions(); + return !player.hasPermission(name); + } + + /** + * Add a permission to user + * + * @param name - Name of the permission to attach + * @return The PermissionAttachment that was just created + * @since 1.5.0 + */ + public PermissionAttachment addPerm(String name) { + return player.addAttachment(plugin, name, true); + } + + public boolean isOnline() { + return player != null && player.isOnline(); + } + + /** + * Checks if user is Op + * + * @return true if user is Op + */ + public boolean isOp() { + if (sender != null) { + return sender.isOp(); + } + if (playerUUID != null && offlinePlayer != null) { + return offlinePlayer.isOp(); + } + return false; + } + + /** + * Get the maximum value of a numerical permission setting. If a player is given + * an explicit negative number then this is treated as "unlimited" and returned + * immediately. + * + * @param permissionPrefix the start of the perm, e.g., + * {@code plugin.mypermission} + * @param defaultValue the default value; the result may be higher or lower + * than this + * @return max value + */ + public int getPermissionValue(String permissionPrefix, int defaultValue) { + // If requester is console, then return the default value + if (!isPlayer()) + return defaultValue; + + // If there is a dot at the end of the permissionPrefix, remove it + if (permissionPrefix.endsWith(".")) { + permissionPrefix = permissionPrefix.substring(0, permissionPrefix.length() - 1); + } + + final String permPrefix = permissionPrefix + "."; + + List permissions = player.getEffectivePermissions().stream().filter(PermissionAttachmentInfo::getValue) // Must + // be + // a + // positive + // permission, + // not + // a + // negative + // one + .map(PermissionAttachmentInfo::getPermission).filter(permission -> permission.startsWith(permPrefix)) + .toList(); + + if (permissions.isEmpty()) + return defaultValue; + + return iteratePerms(permissions, permPrefix, defaultValue); + + } + + private int iteratePerms(List permissions, String permPrefix, int defaultValue) { + int value = 0; + for (String permission : permissions) { + if (permission.contains(permPrefix + "*")) { + // 'Star' permission + return defaultValue; + } else { + String[] spl = permission.split(permPrefix); + if (spl.length > 1) { + if (!NumberUtils.isNumber(spl[1])) { + plugin.logError("Player " + player.getName() + " has permission: '" + permission + + "' <-- the last part MUST be a number! Ignoring..."); + } else { + int v = Integer.parseInt(spl[1]); + if (v < 0) { + return v; + } + value = Math.max(value, v); + } + } + } + } + + return value; + } + + /** + * Gets a translation for a specific world + * + * @param world - world of translation + * @param reference - reference found in a locale file + * @param variables - variables to insert into translated string. Variables go + * in pairs, for example "[name]", "tastybento" + * @return Translated string with colors converted, or the reference if nothing + * has been found + * @since 1.3.0 + */ + public String getTranslation(World world, String reference, String... variables) { + // Get translation. + String addonPrefix = plugin.getIWM().getAddon(world) + .map(a -> a.getDescription().getName().toLowerCase(Locale.ENGLISH) + ".").orElse(""); + return Util.translateColorCodes(translate(addonPrefix, reference, variables)); + } + + /** + * Gets a translation of this reference for this user with colors converted. + * Translations may be overridden by Addons by using the same reference prefixed + * by the addon name (from the Addon Description) in lower case. + * + * @param reference - reference found in a locale file + * @param variables - variables to insert into translated string. Variables go + * in pairs, for example "[name]", "tastybento" + * @return Translated string with colors converted, or the reference if nothing + * has been found + */ + public String getTranslation(String reference, String... variables) { + // Get addonPrefix + String addonPrefix = addon == null ? "" : addon.getDescription().getName().toLowerCase(Locale.ENGLISH) + "."; + return Util.translateColorCodes(translate(addonPrefix, reference, variables)); + } + + /** + * Gets a translation of this reference for this user without colors translated. + * Translations may be overridden by Addons by using the same reference prefixed + * by the addon name (from the Addon Description) in lower case. + * + * @param reference - reference found in a locale file + * @param variables - variables to insert into translated string. Variables go + * in pairs, for example "[name]", "tastybento" + * @return Translated string or the reference if nothing has been found + * @since 1.17.4 + */ + public String getTranslationNoColor(String reference, String... variables) { + // Get addonPrefix + String addonPrefix = addon == null ? "" : addon.getDescription().getName().toLowerCase(Locale.ENGLISH) + "."; + return translate(addonPrefix, reference, variables); + } + + private String translate(String addonPrefix, String reference, String[] variables) { + // Try to get the translation for this specific addon + String translation = plugin.getLocalesManager().get(this, addonPrefix + reference); + + if (translation == null) { + // No luck, try to get the generic translation + translation = plugin.getLocalesManager().get(this, reference); + if (translation == null) { + // Nothing found. Replace vars (probably will do nothing) and return + return replaceVars(reference, variables); + } + } + + // If this is a prefix, just gather and return the translation + if (!reference.startsWith("prefixes.")) { + // Replace the prefixes + return replacePrefixes(translation, variables); + } + return translation; + } + + private String replacePrefixes(String translation, String[] variables) { + for (String prefix : plugin.getLocalesManager().getAvailablePrefixes(this)) { + String prefixTranslation = getTranslation("prefixes." + prefix); + // Replace the [gamemode] text variable + prefixTranslation = prefixTranslation.replace("[gamemode]", + addon != null ? addon.getDescription().getName() : "[gamemode]"); + // Replace the [friendly_name] text variable + prefixTranslation = prefixTranslation.replace("[friendly_name]", + isPlayer() ? plugin.getIWM().getFriendlyName(getWorld()) : "[friendly_name]"); + + // Replace the prefix in the actual message + translation = translation.replace("[prefix_" + prefix + "]", prefixTranslation); + } + + // Then replace variables + if (variables.length > 1) { + for (int i = 0; i < variables.length; i += 2) { + // Prevent a NPE if the substituting variable is null + if (variables[i + 1] != null) { + translation = translation.replace(variables[i], variables[i + 1]); + } + } + } + + // Then replace Placeholders, this will only work if this is a player + if (player != null) { + translation = plugin.getPlaceholdersManager().replacePlaceholders(player, translation); + } + return translation; + } + + private String replaceVars(String reference, String[] variables) { + + // Then replace variables + if (variables.length > 1) { + for (int i = 0; i < variables.length; i += 2) { + reference = reference.replace(variables[i], variables[i + 1]); + } + } + + // Then replace Placeholders, this will only work if this is a player + if (player != null) { + reference = plugin.getPlaceholdersManager().replacePlaceholders(player, reference); + } + + // If no translation has been found, return the reference for debug purposes. + return reference; + } + + /** + * Gets a translation of this reference for this user. + * + * @param reference - reference found in a locale file + * @param variables - variables to insert into translated string. Variables go + * in pairs, for example "[name]", "tastybento" + * @return Translated string with colors converted, or a blank String if nothing + * has been found + */ + public String getTranslationOrNothing(String reference, String... variables) { + String translation = getTranslation(reference, variables); + return translation.equals(reference) ? "" : translation; + } + + /** + * Send a message to sender if message is not empty. + * + * @param reference - language file reference + * @param variables - CharSequence target, replacement pairs + */ + public void sendMessage(String reference, String... variables) { + String message = getTranslation(reference, variables); + if (!ChatColor.stripColor(message).trim().isEmpty()) { + sendRawMessage(message); + } + } + + /** + * Sends a message to sender without any modification (colors, multi-lines, + * placeholders). + * + * @param message - the message to send + */ + public void sendRawMessage(String message) { + if (sender != null) { + sender.sendMessage(message); + } else { + // Offline player fire event + Bukkit.getPluginManager().callEvent(new OfflineMessageEvent(this.playerUUID, message)); + } + } + + /** + * Sends a message to sender if message is not empty and if the same wasn't sent + * within the previous Notifier.NOTIFICATION_DELAY seconds. + * + * @param reference - language file reference + * @param variables - CharSequence target, replacement pairs + * + * @see Notifier + */ + public void notify(String reference, String... variables) { + String message = getTranslation(reference, variables); + if (!ChatColor.stripColor(message).trim().isEmpty() && sender != null) { + plugin.getNotifier().notify(this, message); + } + } + + /** + * Sends a message to sender if message is not empty and if the same wasn't sent + * within the previous Notifier.NOTIFICATION_DELAY seconds. + * + * @param world - the world the translation should come from + * @param reference - language file reference + * @param variables - CharSequence target, replacement pairs + * + * @see Notifier + * @since 1.3.0 + */ + public void notify(World world, String reference, String... variables) { + String message = getTranslation(world, reference, variables); + if (!ChatColor.stripColor(message).trim().isEmpty() && sender != null) { + plugin.getNotifier().notify(this, message); + } + } + + /** + * Sets the user's game mode + * + * @param mode - GameMode + */ + public void setGameMode(GameMode mode) { + player.setGameMode(mode); + } + + /** + * Teleports user to this location. If the user is in a vehicle, they will exit + * first. + * + * @param location - the location + */ + public void teleport(Location location) { + player.teleport(location); + } + + /** + * Gets the current world this entity resides in + * + * @return World - world + */ + @NonNull + public World getWorld() { + Objects.requireNonNull(player, "Cannot be called on a non-player User!"); + return Objects.requireNonNull(player.getWorld(), "Player's world cannot be null!"); + } + + /** + * Closes the user's inventory + */ + public void closeInventory() { + player.closeInventory(); + } + + /** + * Get the user's locale + * + * @return Locale + */ + public Locale getLocale() { + if (sender instanceof Player && !plugin.getPlayers().getLocale(playerUUID).isEmpty()) { + return Locale.forLanguageTag(plugin.getPlayers().getLocale(playerUUID)); + } + return Locale.forLanguageTag(plugin.getSettings().getDefaultLanguage()); + } + + /** + * Forces an update of the user's complete inventory. Deprecated, but there is + * no current alternative. + */ + public void updateInventory() { + player.updateInventory(); + } + + /** + * Performs a command as the player + * + * @param command - command to execute + * @return true if the command was successful, otherwise false + */ + public boolean performCommand(String command) { + PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(getPlayer(), command); + Bukkit.getPluginManager().callEvent(event); + + // only perform the command, if the event wasn't cancelled by an other plugin: + if (!event.isCancelled()) { + return getPlayer().performCommand( + event.getMessage().startsWith("/") ? event.getMessage().substring(1) : event.getMessage()); + } + // Cancelled, but it was recognized, so return true + return true; + } + + /** + * Checks if a user is in one of the game worlds + * + * @return true if user is, false if not + */ + public boolean inWorld() { + return plugin.getIWM().inWorld(getLocation()); + } + + /** + * Spawn particles to the player. They are only displayed if they are within the + * server's view distance. + * + * @param particle Particle to display. + * @param dustOptions Particle.DustOptions for the particle to display. Cannot + * be null when particle is {@link Particle#REDSTONE}. + * @param x X coordinate of the particle to display. + * @param y Y coordinate of the particle to display. + * @param z Z coordinate of the particle to display. + */ + public void spawnParticle(Particle particle, @Nullable Object dustOptions, double x, double y, double z) { + Class expectedClass = VALIDATION_CHECK.get(particle); + if (expectedClass == null) + throw new IllegalArgumentException("Unexpected value: " + particle); + + if (!(expectedClass.isInstance(dustOptions))) { + throw new IllegalArgumentException("A non-null " + expectedClass.getSimpleName() + + " must be provided when using Particle." + particle + " as particle."); + } + + // Check if this particle is beyond the viewing distance of the server + if (this.player != null && this.player.getLocation().toVector().distanceSquared(new Vector(x, y, + z)) < (Bukkit.getServer().getViewDistance() * 256 * Bukkit.getServer().getViewDistance())) { + if (particle.equals(Particle.REDSTONE)) { + player.spawnParticle(particle, x, y, z, 1, 0, 0, 0, 1, dustOptions); + } else if (dustOptions != null) { + player.spawnParticle(particle, x, y, z, 1, dustOptions); + } else { + // This will never be called unless the value in VALIDATION_CHECK is null in the + // future + player.spawnParticle(particle, x, y, z, 1); + } + } + } + + /** + * Spawn particles to the player. They are only displayed if they are within the + * server's view distance. Compatibility method for older usages. + * + * @param particle Particle to display. + * @param dustOptions Particle.DustOptions for the particle to display. Cannot + * be null when particle is {@link Particle#REDSTONE}. + * @param x X coordinate of the particle to display. + * @param y Y coordinate of the particle to display. + * @param z Z coordinate of the particle to display. + */ + public void spawnParticle(Particle particle, Particle.DustOptions dustOptions, double x, double y, double z) { + this.spawnParticle(particle, (Object) dustOptions, x, y, z); + } + + /** + * Spawn particles to the player. They are only displayed if they are within the + * server's view distance. + * + * @param particle Particle to display. + * @param dustOptions Particle.DustOptions for the particle to display. Cannot + * be null when particle is {@link Particle#REDSTONE}. + * @param x X coordinate of the particle to display. + * @param y Y coordinate of the particle to display. + * @param z Z coordinate of the particle to display. + */ + public void spawnParticle(Particle particle, Particle.DustOptions dustOptions, int x, int y, int z) { + this.spawnParticle(particle, dustOptions, (double) x, (double) y, (double) z); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((playerUUID == null) ? 0 : playerUUID.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof User other)) { + return false; + } + if (playerUUID == null) { + return other.playerUUID == null; + } else + return playerUUID.equals(other.playerUUID); + } + + /** + * Set the addon context when a command is executed + * + * @param addon - the addon executing the command + */ + public void setAddon(Addon addon) { + this.addon = addon; + } + + /** + * Get all the meta data for this user + * + * @return the metaData + * @since 1.15.4 + */ + @Override + public Optional> getMetaData() { + Players p = plugin.getPlayers().getPlayer(playerUUID); + return Objects.requireNonNull(p, "Unknown player for " + playerUUID).getMetaData(); + } + + /** + * @param metaData the metaData to set + * @since 1.15.4 + */ + @Override + public void setMetaData(Map metaData) { + Players p = plugin.getPlayers().getPlayer(playerUUID); + + Objects.requireNonNull(p, "Unknown player for " + playerUUID).setMetaData(metaData); + } }