diff --git a/build.gradle b/build.gradle index b5420ca..701d5da 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 compileJava.options.encoding = 'UTF-8' group = 'dev.esophose' -version = '7.14' +version = '7.15' java { withJavadocJar() diff --git a/changelog.txt b/changelog.txt index 785a431..5292aa1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ +=== v7.15 === ++ Added additional hex color support ++ Fixed preset group permissions not working === v7.14 === + Added hex code support for 1.16 servers. Example: `prefix: '&7[#add8e6PlayerParticles&7] '` * Fixed typo in GUI material for crimson_spore particle diff --git a/src/main/java/dev/esophose/playerparticles/locale/EnglishLocale.java b/src/main/java/dev/esophose/playerparticles/locale/EnglishLocale.java index 401de29..3f56037 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/EnglishLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/EnglishLocale.java @@ -19,7 +19,7 @@ public class EnglishLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&cYou must have access to effects and styles to use this command!"); diff --git a/src/main/java/dev/esophose/playerparticles/locale/FrenchLocale.java b/src/main/java/dev/esophose/playerparticles/locale/FrenchLocale.java index c574b75..8444b6c 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/FrenchLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/FrenchLocale.java @@ -19,7 +19,7 @@ public class FrenchLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&cVous devez avoir accès aux effets et aux styles pour utiliser cette commande !"); diff --git a/src/main/java/dev/esophose/playerparticles/locale/GermanLocale.java b/src/main/java/dev/esophose/playerparticles/locale/GermanLocale.java index bec90e7..fcaf087 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/GermanLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/GermanLocale.java @@ -19,7 +19,7 @@ public class GermanLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&cSie müssen Zugriff auf Effekte und Stile haben, um diesen Befehl verwenden zu können!"); diff --git a/src/main/java/dev/esophose/playerparticles/locale/RussianLocale.java b/src/main/java/dev/esophose/playerparticles/locale/RussianLocale.java index fe04e47..3c382d4 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/RussianLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/RussianLocale.java @@ -19,7 +19,7 @@ public class RussianLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&cВы должны иметь доступ к эффектам и стилям, чтобы использовать эту команду!"); diff --git a/src/main/java/dev/esophose/playerparticles/locale/SimplifiedChineseLocale.java b/src/main/java/dev/esophose/playerparticles/locale/SimplifiedChineseLocale.java index 016654b..638a69b 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/SimplifiedChineseLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/SimplifiedChineseLocale.java @@ -19,7 +19,7 @@ public class SimplifiedChineseLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&c您必须有权使用效果和样式才能使用此命令!"); diff --git a/src/main/java/dev/esophose/playerparticles/locale/VietnameseLocale.java b/src/main/java/dev/esophose/playerparticles/locale/VietnameseLocale.java index 1ae6ca1..ca3f94c 100644 --- a/src/main/java/dev/esophose/playerparticles/locale/VietnameseLocale.java +++ b/src/main/java/dev/esophose/playerparticles/locale/VietnameseLocale.java @@ -19,7 +19,7 @@ public class VietnameseLocale implements Locale { public Map getDefaultLocaleStrings() { return new LinkedHashMap() {{ this.put("#0", "Plugin Message Prefix"); - this.put("prefix", "&7[&3PlayerParticles&7] "); + this.put("prefix", "&7[PlayerParticles&7] "); this.put("#1", "Command Description Messages"); this.put("command-error-missing-effects-or-styles", "&cBạn phải có quyền truy cập vào các hiệu ứng và phong cách để sử dụng lệnh này!"); diff --git a/src/main/java/dev/esophose/playerparticles/manager/LocaleManager.java b/src/main/java/dev/esophose/playerparticles/manager/LocaleManager.java index 983731a..43c45f0 100644 --- a/src/main/java/dev/esophose/playerparticles/manager/LocaleManager.java +++ b/src/main/java/dev/esophose/playerparticles/manager/LocaleManager.java @@ -12,22 +12,17 @@ import dev.esophose.playerparticles.locale.SimplifiedChineseLocale; import dev.esophose.playerparticles.locale.VietnameseLocale; import dev.esophose.playerparticles.manager.ConfigurationManager.Setting; import dev.esophose.playerparticles.particles.PPlayer; -import dev.esophose.playerparticles.util.NMSUtil; +import dev.esophose.playerparticles.util.HexUtils; import dev.esophose.playerparticles.util.StringPlaceholders; import java.io.File; import java.io.IOException; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; public class LocaleManager extends Manager { - private static final Pattern HEX_PATTERN = Pattern.compile("#([A-Fa-f0-9]){6}"); - private CommentedFileConfiguration locale; public LocaleManager(PlayerParticles playerParticles) { @@ -119,7 +114,7 @@ public class LocaleManager extends Manager { String message = this.locale.getString(messageKey); if (message == null) return ChatColor.RED + "Missing message in locale file: " + messageKey; - return ChatColor.translateAlternateColorCodes('&', stringPlaceholders.apply(message)); + return HexUtils.colorify(stringPlaceholders.apply(message)); } /** @@ -205,7 +200,7 @@ public class LocaleManager extends Manager { * @param messageKey The message key of the Locale to send */ public void sendSimpleMessage(CommandSender sender, String messageKey) { - this.sendMessage(sender, messageKey, StringPlaceholders.empty()); + this.sendSimpleMessage(sender, messageKey, StringPlaceholders.empty()); } /** @@ -215,7 +210,7 @@ public class LocaleManager extends Manager { * @param messageKey The message key of the Locale to send */ public void sendSimpleMessage(PPlayer pplayer, String messageKey) { - this.sendMessage(pplayer, messageKey, StringPlaceholders.empty()); + this.sendSimpleMessage(pplayer, messageKey, StringPlaceholders.empty()); } /** @@ -254,31 +249,6 @@ public class LocaleManager extends Manager { return message; } - /** - * Parses the colors in a message - * Parses & to the normal color code character - * Parses hex codes if the game version is 1.16 or higher - * - * @param message The message - * @return A color-replaced message - */ - private String parseColors(String message) { - String parsed = message; - - if (NMSUtil.getVersionNumber() >= 16) { - Matcher matcher = HEX_PATTERN.matcher(parsed); - while (matcher.find()) { - ChatColor hexColor = ChatColor.of(matcher.group()); - String before = parsed.substring(0, matcher.start()); - String after = parsed.substring(matcher.end()); - parsed = before + hexColor + after; - matcher = HEX_PATTERN.matcher(parsed); - } - } - - return ChatColor.translateAlternateColorCodes('&', parsed); - } - /** * Sends a message with placeholders and colors parsed to a CommandSender * @@ -286,11 +256,7 @@ public class LocaleManager extends Manager { * @param message The message */ private void sendParsedMessage(CommandSender sender, String message) { - if (NMSUtil.getVersionNumber() >= 16) { - sender.spigot().sendMessage(TextComponent.fromLegacyText(this.parseColors(this.parsePlaceholders(sender, message)))); - } else { - sender.sendMessage(this.parseColors(this.parsePlaceholders(sender, message))); - } + HexUtils.sendMessage(sender, this.parsePlaceholders(sender, message)); } } diff --git a/src/main/java/dev/esophose/playerparticles/particles/ParticleGroupPreset.java b/src/main/java/dev/esophose/playerparticles/particles/ParticleGroupPreset.java index 2d72061..1b58643 100644 --- a/src/main/java/dev/esophose/playerparticles/particles/ParticleGroupPreset.java +++ b/src/main/java/dev/esophose/playerparticles/particles/ParticleGroupPreset.java @@ -46,7 +46,7 @@ public class ParticleGroupPreset { */ public boolean canPlayerUse(PPlayer player) { // If this particle group has a permission, does the player have it? - if (!this.permission.isEmpty() && PlayerParticles.getInstance().getManager(PermissionManager.class).hasPermission(player, this.permission)) + if (!this.permission.isEmpty() && !PlayerParticles.getInstance().getManager(PermissionManager.class).hasPermission(player, this.permission)) return false; // If allowPermissionOverride is true, always let the player apply this group diff --git a/src/main/java/dev/esophose/playerparticles/util/HexUtils.java b/src/main/java/dev/esophose/playerparticles/util/HexUtils.java new file mode 100644 index 0000000..38cc2cd --- /dev/null +++ b/src/main/java/dev/esophose/playerparticles/util/HexUtils.java @@ -0,0 +1,355 @@ +package dev.esophose.playerparticles.util; + +import java.awt.Color; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.command.CommandSender; + +public final class HexUtils { + + private static final Pattern RAINBOW_PATTERN = Pattern.compile("<(rainbow|r)(:\\d*\\.?\\d+){0,2}>"); + private static final Pattern GRADIENT_PATTERN = Pattern.compile("<(gradient|g)(:#([A-Fa-f0-9]){6})*>"); + private static final List HEX_PATTERNS = Arrays.asList( + Pattern.compile("<#([A-Fa-f0-9]){6}>"), // <#FFFFFF> + Pattern.compile("&#([A-Fa-f0-9]){6}"), // &#FFFFFF + Pattern.compile("#([A-Fa-f0-9]){6}") // #FFFFFF + ); + + private static final Pattern STOP = Pattern.compile("<(gradient|g)(:#([A-Fa-f0-9]){6})*>|<(rainbow|r)(:\\d*\\.?\\d+){0,2}>|(&[a-f0-9r])|<#([A-Fa-f0-9]){6}>|&#([A-Fa-f0-9]){6}|#([A-Fa-f0-9]){6}|" + org.bukkit.ChatColor.COLOR_CHAR); + + private HexUtils() { + + } + + /** + * Sends a CommandSender a colored message + * + * @param sender The CommandSender to send to + * @param message The message to send + */ + public static void sendMessage(CommandSender sender, String message) { + sender.sendMessage(colorify(message)); + } + + /** + * Parses gradients, hex colors, and legacy color codes + * + * @param message The message + * @return A color-replaced message + */ + public static String colorify(String message) { + String parsed = message; + parsed = parseRainbow(parsed); + parsed = parseGradients(parsed); + parsed = parseHex(parsed); + parsed = parseLegacy(parsed); + return parsed; + } + + private static String parseRainbow(String message) { + String parsed = message; + + Matcher matcher = RAINBOW_PATTERN.matcher(parsed); + while (matcher.find()) { + StringBuilder parsedRainbow = new StringBuilder(); + + String match = matcher.group(); + int tagLength = match.startsWith(""); + String extraDataContent = match.substring(tagLength, indexOfClose); + + double[] extraData; + if (!extraDataContent.isEmpty()) { + extraDataContent = extraDataContent.substring(1); + extraData = Arrays.stream(extraDataContent.split(":")).mapToDouble(Double::parseDouble).toArray(); + } else { + extraData = new double[0]; + } + + float saturation = extraData.length > 0 ? (float) extraData[0] : 1.0F; + float brightness = extraData.length > 1 ? (float) extraData[1] : 1.0F; + + int stop = findStop(parsed, matcher.end()); + String content = parsed.substring(matcher.end(), stop); + Rainbow rainbow = new Rainbow(content.length(), saturation, brightness); + + for (char c : content.toCharArray()) + parsedRainbow.append(translateHex(rainbow.next())).append(c); + + String before = parsed.substring(0, matcher.start()); + String after = parsed.substring(stop); + parsed = before + parsedRainbow + after; + matcher = RAINBOW_PATTERN.matcher(parsed); + } + + return parsed; + } + + private static String parseGradients(String message) { + String parsed = message; + + Matcher matcher = GRADIENT_PATTERN.matcher(parsed); + while (matcher.find()) { + StringBuilder parsedGradient = new StringBuilder(); + + String match = matcher.group(); + int tagLength = match.startsWith(""); + String hexContent = match.substring(tagLength, indexOfClose); + List hexSteps = Arrays.stream(hexContent.split(":")).map(Color::decode).collect(Collectors.toList()); + + int stop = findStop(parsed, matcher.end()); + String content = parsed.substring(matcher.end(), stop); + Gradient gradient = new Gradient(hexSteps, content.length()); + + for (char c : content.toCharArray()) + parsedGradient.append(translateHex(gradient.next())).append(c); + + String before = parsed.substring(0, matcher.start()); + String after = parsed.substring(stop); + parsed = before + parsedGradient + after; + matcher = GRADIENT_PATTERN.matcher(parsed); + } + + return parsed; + } + + private static String parseHex(String message) { + String parsed = message; + + for (Pattern pattern : HEX_PATTERNS) { + Matcher matcher = pattern.matcher(parsed); + while (matcher.find()) { + String color = translateHex(cleanHex(matcher.group())); + String before = parsed.substring(0, matcher.start()); + String after = parsed.substring(matcher.end()); + parsed = before + color + after; + matcher = pattern.matcher(parsed); + } + } + + return parsed; + } + + private static String parseLegacy(String message) { + return ChatColor.translateAlternateColorCodes('&', message); + } + + /** + * Returns the index before the color changes + * + * @param content The content to search through + * @param searchAfter The index at which to search after + * @return the index of the color stop, or the end of the string index if none is found + */ + private static int findStop(String content, int searchAfter) { + Matcher matcher = STOP.matcher(content); + while (matcher.find()) { + if (matcher.start() > searchAfter) + return matcher.start(); + } + return content.length() - 1; + } + + private static String cleanHex(String hex) { + if (hex.startsWith("<")) { + return hex.substring(1, hex.length() - 1); + } else if (hex.startsWith("&")) { + return hex.substring(1); + } else { + return hex; + } + } + + /** + * Finds the closest hex or ChatColor value as the hex string + * + * @param hex The hex color + * @return The closest ChatColor value + */ + private static String translateHex(String hex) { + if (NMSUtil.getVersionNumber() >= 16) + return ChatColor.of(hex).toString(); + return translateHex(Color.decode(hex)); + } + + private static String translateHex(Color color) { + if (NMSUtil.getVersionNumber() >= 16) + return ChatColor.of(color).toString(); + + int minDist = Integer.MAX_VALUE; + ChatColor legacy = ChatColor.WHITE; + for (ChatColorHexMapping mapping : ChatColorHexMapping.values()) { + int r = mapping.getRed() - color.getRed(); + int g = mapping.getGreen() - color.getGreen(); + int b = mapping.getBlue() - color.getBlue(); + int dist = r * r + g * g + b * b; + if (dist < minDist) { + minDist = dist; + legacy = mapping.getChatColor(); + } + } + + return legacy.toString(); + } + + /** + * Maps hex codes to ChatColors + */ + private enum ChatColorHexMapping { + + BLACK(0x000000, ChatColor.BLACK), + DARK_BLUE(0x0000AA, ChatColor.DARK_BLUE), + DARK_GREEN(0x00AA00, ChatColor.DARK_GREEN), + DARK_AQUA(0x00AAAA, ChatColor.DARK_AQUA), + DARK_RED(0xAA0000, ChatColor.DARK_RED), + DARK_PURPLE(0xAA00AA, ChatColor.DARK_PURPLE), + GOLD(0xFFAA00, ChatColor.GOLD), + GRAY(0xAAAAAA, ChatColor.GRAY), + DARK_GRAY(0x555555, ChatColor.DARK_GRAY), + BLUE(0x5555FF, ChatColor.BLUE), + GREEN(0x55FF55, ChatColor.GREEN), + AQUA(0x55FFFF, ChatColor.AQUA), + RED(0xFF5555, ChatColor.RED), + LIGHT_PURPLE(0xFF55FF, ChatColor.LIGHT_PURPLE), + YELLOW(0xFFFF55, ChatColor.YELLOW), + WHITE(0xFFFFFF, ChatColor.WHITE); + + private final int r, g, b; + private final ChatColor chatColor; + + ChatColorHexMapping(int hex, ChatColor chatColor) { + this.r = (hex >> 16) & 0xFF; + this.g = (hex >> 8) & 0xFF; + this.b = hex & 0xFF; + this.chatColor = chatColor; + } + + public int getRed() { + return this.r; + } + + public int getGreen() { + return this.g; + } + + public int getBlue() { + return this.b; + } + + public ChatColor getChatColor() { + return this.chatColor; + } + + } + + /** + * Allows generation of a multi-part gradient with a fixed number of steps + */ + public static class Gradient { + + private final List colors; + private final int stepSize; + private int step, stepIndex; + + public Gradient(List colors, int totalColors) { + if (colors.size() < 2) + throw new IllegalArgumentException("Must provide at least 2 colors"); + + if (totalColors < 1) + throw new IllegalArgumentException("Must have at least 1 total color"); + + this.colors = colors; + this.stepSize = totalColors / (colors.size() - 1); + this.step = this.stepIndex = 0; + } + + public Color next() { + // Gradients will use the first color of the entire spectrum won't be available to preserve prettiness + if (NMSUtil.getVersionNumber() < 16) + return this.colors.get(0); + + Color color; + if (this.stepIndex + 1 < this.colors.size()) { + Color start = this.colors.get(this.stepIndex); + Color end = this.colors.get(this.stepIndex + 1); + float interval = (float) this.step / this.stepSize; + + color = getGradientInterval(start, end, interval); + } else { + color = this.colors.get(this.colors.size() - 1); + } + + this.step += 1; + if (this.step >= this.stepSize) { + this.step = 0; + this.stepIndex++; + } + + return color; + } + + /** + * Gets a color along a linear gradient between two colors + * + * @param start The start color + * @param end The end color + * @param interval The interval to get, between 0 and 1 inclusively + * @return A Color at the interval between the start and end colors + */ + public static Color getGradientInterval(Color start, Color end, float interval) { + if (0 > interval || interval > 1) + throw new IllegalArgumentException("Interval must be between 0 and 1 inclusively."); + + int r = (int) (end.getRed() * interval + start.getRed() * (1 - interval)); + int g = (int) (end.getGreen() * interval + start.getGreen() * (1 - interval)); + int b = (int) (end.getBlue() * interval + start.getBlue() * (1 - interval)); + + return new Color(r, g, b); + } + + } + + /** + * Allows generation of a rainbow gradient with a fixed numbef of steps + */ + public static class Rainbow { + + private final float hueStep, saturation, brightness; + private float hue; + + public Rainbow(int totalColors, float saturation, float brightness) { + if (totalColors < 1) + throw new IllegalArgumentException("Must have at least 1 total color"); + + if (0.0F > saturation || saturation > 1.0F) + throw new IllegalArgumentException("Saturation must be between 0.0 and 1.0"); + + if (0.0F > brightness || brightness > 1.0F) + throw new IllegalArgumentException("Saturation must be between 0.0 and 1.0"); + + this.hueStep = 1.0F / totalColors; + this.saturation = saturation; + this.brightness = brightness; + this.hue = 0; + } + + public Rainbow(int totalColors) { + this(totalColors, 1.0F, 1.0F); + } + + public Color next() { + Color color = Color.getHSBColor(this.hue, this.saturation, this.brightness); + this.hue += this.hueStep; + return color; + } + + } + +}